# Fusillade
**Repository Path**: mirrors_reactiveui/Fusillade
## Basic Information
- **Project Name**: Fusillade
- **Description**: An opinionated HTTP library for Mobile Development
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-09-25
- **Last Updated**: 2026-09-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
[](https://www.nuget.org/packages/fusillade)  [](https://codecov.io/gh/reactiveui/fusillade)
## Fusillade: An opinionated HTTP library for .NET apps
Fusillade helps you write efficient, resilient networked apps by composing `HttpMessageHandler` instances for `HttpClient`. It focuses on:
- Request de-duplication for relevant HTTP methods
- Priority-aware concurrency limiting through Punchclock
- Request prioritization for predictable UX
- Speculative background fetching with byte-budget limits
- Optional response caching and offline replay
Design inspirations include Android's Volley and Picasso.
Supported targets: `net8.0`, `net9.0`, `net10.0`, `net11.0`, `net462`, `net472`, `net48`, and `net481`.
## V6.0.x Breaking Changes
Fusillade `6.0.x` moved the primary package from a System.Reactive-based dependency stack to `ReactiveUI.Primitives`.
The main `fusillade` package:
- Uses `ReactiveUI.Primitives` and the lean `punchclock` package.
- Does not require `System.Reactive`.
- Keeps the public Fusillade API handler-first: `HttpMessageHandler`, `HttpClient`, `Task`, `CancellationToken`, and `IRequestCache`.
- Keeps the namespace as `Fusillade`.
The new `fusillade.reactive` package:
- Uses `ReactiveUI.Primitives.Reactive` and `Punchclock.Reactive`.
- Is intended for System.Reactive-first applications.
- Uses the namespace `Fusillade.Reactive`.
- Mirrors the `fusillade` API surface so Rx applications can migrate with minimal code changes.
If your application only uses `HttpClient` handlers, install `fusillade`. If your application also relies on System.Reactive conventions through the surrounding queue or reactive infrastructure, install `fusillade.reactive`.
### Package Selection
| Scenario | Package | Namespace | Queue type |
| --- | --- | --- | --- |
| New code or non-Rx applications | `fusillade` | `Fusillade` | `Punchclock.OperationQueue` |
| Existing System.Reactive applications | `fusillade.reactive` | `Fusillade.Reactive` | `Punchclock.Reactive.OperationQueue` |
| R3 applications | `fusillade` plus `ReactiveUI.Primitives` and `R3` | `Fusillade`, `ReactiveUI.Primitives.R3Bridge` | `Punchclock.OperationQueue` |
### Migration From System.Reactive To V6
For the lean `fusillade` package:
```csharp
using Fusillade;
using Punchclock;
NetCache.OperationQueue = new OperationQueue(maxConcurrency: 6);
using var client = new HttpClient(NetCache.UserInitiated, disposeHandler: false);
var json = await client.GetStringAsync("https://example.com/api/items");
```
For the System.Reactive-compatible package:
```csharp
using Fusillade.Reactive;
using Punchclock.Reactive;
using System.Reactive.Linq;
NetCache.OperationQueue = new OperationQueue(maxConcurrency: 6);
using var client = new HttpClient(NetCache.UserInitiated, disposeHandler: false);
IObservable json = Observable.FromAsync(
ct => client.GetStringAsync("https://example.com/api/items", ct));
```
If both packages are referenced in the same project, use aliases at the boundary to avoid namespace ambiguity:
```csharp
using LeanNetCache = Fusillade.NetCache;
using RxNetCache = Fusillade.Reactive.NetCache;
using var leanClient = new HttpClient(LeanNetCache.Background, disposeHandler: false);
using var rxClient = new HttpClient(RxNetCache.Background, disposeHandler: false);
```
### R3 Source Generator Bridge
Do not reference the ReactiveUI.Primitives R3 bridge generator package directly. The generator is packed as an analyzer by `ReactiveUI.Primitives` and emits bridge methods when the consuming project references the required R3 symbols. Add a direct `ReactiveUI.Primitives` package reference in the consuming project so the analyzer is present.
```bash
dotnet add package fusillade
dotnet add package ReactiveUI.Primitives
dotnet add package R3
```
```csharp
using Fusillade;
using ReactiveUI.Primitives.R3Bridge;
R3.Observable selectedUris = GetSelectedUris();
// Generated when R3 and ReactiveUI.Primitives are both referenced.
System.IObservable primitivesUris = selectedUris.AsPrimitivesSignal();
// Convert back to R3 at a boundary if the rest of the app remains R3-first.
R3.Observable r3Uris = primitivesUris.AsR3Observable();
using var client = new HttpClient(NetCache.Background, disposeHandler: false);
```
Keep bridge calls at package or application boundaries. Keep the internal pipeline in one reactive model after conversion.
## Install
```bash
dotnet add package fusillade
```
For System.Reactive-first applications:
```bash
dotnet add package fusillade.reactive
```
## Quick Start
Create `HttpClient` instances by selecting the right handler from `NetCache`.
```csharp
using Fusillade;
using var client = new HttpClient(NetCache.UserInitiated, disposeHandler: false);
var json = await client.GetStringAsync("https://httpbin.org/get");
```
Available built-ins:
- `NetCache.UserInitiated`: foreground work the user is waiting for.
- `NetCache.Background`: background work that should not block UI work.
- `NetCache.Speculative`: background prefetching with a byte budget.
- `NetCache.Offline`: fetch from cache only.
By default, requests are processed four at a time through an `OperationQueue`.
## Core Ideas
### Request De-Duplication
Fusillade de-duplicates concurrent `GET`, `HEAD`, and `OPTIONS` requests for the same resource. If multiple callers request the same URL concurrently through the same `RateLimitedHttpMessageHandler`, one network request is made and the callers receive independent response instances.
```csharp
using Fusillade;
var handler = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.UserInitiated);
using var client = new HttpClient(handler);
var first = client.GetAsync("https://example.com/profile/42");
var second = client.GetAsync("https://example.com/profile/42");
using var firstResponse = await first;
using var secondResponse = await second;
```
### Concurrency Limiting And Prioritization
All rate-limited work is scheduled through an `OperationQueue`. The effective priority is the selected `Priority` value plus the optional `priority` offset.
```csharp
using Fusillade;
using Punchclock;
var queue = new OperationQueue(maxConcurrency: 2);
var handler = new RateLimitedHttpMessageHandler(
handler: new HttpClientHandler(),
basePriority: Priority.Explicit,
priority: 500,
maxBytesToRead: null,
operationQueue: queue);
using var client = new HttpClient(handler);
```
Priority values:
```csharp
var explicitPriority = (int)Priority.Explicit; // 0
var speculative = (int)Priority.Speculative; // 10
var background = (int)Priority.Background; // 20
var userInitiated = (int)Priority.UserInitiated; // 100
```
### Speculative Background Fetching
Use `NetCache.Speculative` for prefetching. Reset its byte budget when a new prefetch window starts.
```csharp
using Fusillade;
NetCache.Speculative.ResetLimit(5 * 1024 * 1024);
using var prefetch = new HttpClient(NetCache.Speculative, disposeHandler: false);
_ = prefetch.GetStringAsync("https://example.com/likely-next-screen");
```
Stop further speculative work:
```csharp
NetCache.Speculative.ResetLimit(-1);
```
Clear the byte limit:
```csharp
NetCache.Speculative.ResetLimit();
```
## Public API Reference
The `fusillade.reactive` package mirrors these APIs under `Fusillade.Reactive` and uses `Punchclock.Reactive.OperationQueue` where the lean package uses `Punchclock.OperationQueue`.
### `NetCache`
`NetCache` owns the shared handlers and app-wide queue/cache configuration.
```csharp
using Fusillade;
using Punchclock;
NetCache.OperationQueue = new OperationQueue(maxConcurrency: 6);
NetCache.RequestCache = new MemoryRequestCache();
using var foreground = new HttpClient(NetCache.UserInitiated, disposeHandler: false);
using var background = new HttpClient(NetCache.Background, disposeHandler: false);
using var speculative = new HttpClient(NetCache.Speculative, disposeHandler: false);
using var offline = new HttpClient(NetCache.Offline, disposeHandler: false);
```
Public members:
```csharp
public static LimitingHttpMessageHandler Speculative { get; set; }
public static HttpMessageHandler UserInitiated { get; set; }
public static HttpMessageHandler Background { get; set; }
public static HttpMessageHandler Offline { get; set; }
public static OperationQueue OperationQueue { get; set; }
public static IRequestCache? RequestCache { get; set; }
```
### `RateLimitedHttpMessageHandler`
`RateLimitedHttpMessageHandler` performs prioritization, de-duplication, optional byte limiting, and optional cache saving.
```csharp
using Fusillade;
using Punchclock;
var handler = new RateLimitedHttpMessageHandler(
handler: new HttpClientHandler(),
basePriority: Priority.Background,
priority: 25,
maxBytesToRead: 10 * 1024 * 1024,
operationQueue: new OperationQueue(maxConcurrency: 4),
cacheResultFunc: async (request, response, key, ct) =>
{
var bytes = await response.Content.ReadAsByteArrayAsync(ct);
await File.WriteAllBytesAsync($"{key}.bin", bytes, ct);
});
using var client = new HttpClient(handler);
```
Generate the same cache key Fusillade uses internally:
```csharp
using Fusillade;
using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/data");
var key = RateLimitedHttpMessageHandler.UniqueKeyForRequest(request);
```
Public members:
```csharp
public RateLimitedHttpMessageHandler(
HttpMessageHandler? handler,
Priority basePriority,
int priority = 0,
long? maxBytesToRead = null,
OperationQueue? operationQueue = null,
Func? cacheResultFunc = null);
public static string UniqueKeyForRequest(HttpRequestMessage request);
public override void ResetLimit(long? maxBytesToRead);
```
### `LimitingHttpMessageHandler`
`LimitingHttpMessageHandler` is the base class for handlers that can limit the total number of response bytes read.
```csharp
using Fusillade;
LimitingHttpMessageHandler handler = NetCache.Speculative;
handler.ResetLimit(1_000_000);
handler.ResetLimit();
```
Public members:
```csharp
public void ResetLimit();
public abstract void ResetLimit(long? maxBytesToRead);
```
### `IRequestCache`
Implement `IRequestCache` when you want `NetCache.RequestCache`, `RateLimitedHttpMessageHandler`, and `OfflineHttpMessageHandler` to share the same cache.
```csharp
using System.Collections.Concurrent;
using Fusillade;
public sealed class MemoryRequestCache : IRequestCache
{
private readonly ConcurrentDictionary _responses = new();
public async Task SaveAsync(
HttpRequestMessage request,
HttpResponseMessage response,
string key,
CancellationToken ct)
{
if (response.Content is null)
{
return;
}
_responses[key] = await response.Content.ReadAsByteArrayAsync(ct);
}
public Task FetchAsync(
HttpRequestMessage request,
string key,
CancellationToken ct)
{
_responses.TryGetValue(key, out var bytes);
return Task.FromResult(bytes);
}
}
```
Public members:
```csharp
public interface IRequestCache
{
Task SaveAsync(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct);
Task FetchAsync(HttpRequestMessage request, string key, CancellationToken ct);
}
```
### `OfflineHttpMessageHandler`
`OfflineHttpMessageHandler` serves cached data without touching the network. It returns `200 OK` when a cached body is found and `503 Service Unavailable` when no cached body exists.
Use the shared `NetCache.RequestCache`:
```csharp
using Fusillade;
NetCache.RequestCache = new MemoryRequestCache();
using var offline = new HttpClient(NetCache.Offline, disposeHandler: false);
var cached = await offline.GetStringAsync("https://example.com/data");
```
Or pass a cache lookup directly:
```csharp
using Fusillade;
var handler = new OfflineHttpMessageHandler(
retrieveBodyFunc: async (request, key, ct) =>
{
var path = Path.Combine("cache", $"{key}.bin");
return File.Exists(path)
? await File.ReadAllBytesAsync(path, ct)
: null;
});
using var offline = new HttpClient(handler);
```
Public member:
```csharp
public OfflineHttpMessageHandler(
Func>? retrieveBodyFunc);
```
### `Priority`
`Priority` defines the base scheduling priorities used by `RateLimitedHttpMessageHandler`.
```csharp
using Fusillade;
var userWork = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.UserInitiated);
var backgroundWork = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Background);
var prefetchWork = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Speculative);
var customWork = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Explicit, priority: 250);
```
Public values:
```csharp
public enum Priority
{
Explicit = 0,
Speculative = 10,
Background = 20,
UserInitiated = 100,
}
```
### Splat Builder Integration
Call `CreateFusilladeNetCache` when using Splat's builder pipeline. This initializes the shared `NetCache` instances from the current resolver.
```csharp
using Splat.Builder;
var app = AppBuilder.CreateSplatBuilder().Build();
app.CreateFusilladeNetCache();
```
Public member:
```csharp
public static IAppInstance CreateFusilladeNetCache(this IAppInstance builder);
```
## Caching And Offline
Fusillade can optionally cache response bodies and replay them when offline.
There are two supported cache paths:
1. Pass `cacheResultFunc` to `RateLimitedHttpMessageHandler`.
2. Set `NetCache.RequestCache` to an `IRequestCache` implementation.
When caching through `RateLimitedHttpMessageHandler`, Fusillade buffers the response content and forwards the buffered response to your cache callback. The key is generated by `RateLimitedHttpMessageHandler.UniqueKeyForRequest(request)`.
### File-Based Cache Example
```csharp
using Fusillade;
var cacheDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"fusillade-cache");
Directory.CreateDirectory(cacheDirectory);
var cachingHandler = new RateLimitedHttpMessageHandler(
new HttpClientHandler(),
Priority.UserInitiated,
cacheResultFunc: async (request, response, key, ct) =>
{
var data = await response.Content.ReadAsByteArrayAsync(ct);
var path = Path.Combine(cacheDirectory, $"{key}.bin");
await File.WriteAllBytesAsync(path, data, ct);
});
using var client = new HttpClient(cachingHandler);
var fresh = await client.GetStringAsync("https://httpbin.org/get");
```
```csharp
using Fusillade;
public sealed class FileRequestCache(string cacheDirectory) : IRequestCache
{
public async Task SaveAsync(
HttpRequestMessage request,
HttpResponseMessage response,
string key,
CancellationToken ct)
{
var bytes = await response.Content.ReadAsByteArrayAsync(ct);
var path = Path.Combine(cacheDirectory, $"{key}.bin");
await File.WriteAllBytesAsync(path, bytes, ct);
}
public Task FetchAsync(
HttpRequestMessage request,
string key,
CancellationToken ct)
{
var path = Path.Combine(cacheDirectory, $"{key}.bin");
return File.Exists(path)
? File.ReadAllBytesAsync(path, ct)
: Task.FromResult(null);
}
}
```
## Usage Recipes
### Image Gallery Or Avatars
Use `RateLimitedHttpMessageHandler` for visible images and `NetCache.Background` for preloading. De-duplication prevents duplicate downloads for the same URL while the first request is still in flight.
```csharp
using Fusillade;
using var visibleImages = new HttpClient(NetCache.UserInitiated, disposeHandler: false);
using var preloadImages = new HttpClient(NetCache.Background, disposeHandler: false);
var avatar = await visibleImages.GetByteArrayAsync("https://example.com/users/42/avatar.png");
_ = preloadImages.GetByteArrayAsync("https://example.com/users/43/avatar.png");
```
### Boot-Time Warmup
On app start or resume, reset `NetCache.Speculative` to a sensible byte budget and queue likely-next requests.
```csharp
using Fusillade;
NetCache.Speculative.ResetLimit(2 * 1024 * 1024);
using var warmup = new HttpClient(NetCache.Speculative, disposeHandler: false);
_ = warmup.GetAsync("https://example.com/bootstrap/menu");
_ = warmup.GetAsync("https://example.com/bootstrap/profile");
```
### Offline-First Views
Populate `NetCache.RequestCache` during online sessions and switch to `NetCache.Offline` when network access is unavailable.
```csharp
using Fusillade;
NetCache.RequestCache = new MemoryRequestCache();
using var online = new HttpClient(NetCache.UserInitiated, disposeHandler: false);
_ = await online.GetStringAsync("https://example.com/data");
using var offline = new HttpClient(NetCache.Offline, disposeHandler: false);
var cached = await offline.GetStringAsync("https://example.com/data");
```
## FAQ
### How many requests run at once?
The default `OperationQueue` processes four requests at a time. Override it with `NetCache.OperationQueue` or pass an explicit queue to `RateLimitedHttpMessageHandler`.
### Which methods are de-duplicated?
`GET`, `HEAD`, and `OPTIONS`.
### How are cache keys generated?
`RateLimitedHttpMessageHandler.UniqueKeyForRequest(request)` generates keys from the request URI, method, selected headers, referrer, user agent, and authorization header. Treat keys as implementation details; persist and reuse the key passed to your cache callback.
### Can I cancel a request?
Use `CancellationToken` with normal `HttpClient` APIs. If several callers share a de-duplicated request, the underlying request is cancelled only after every joined caller has cancelled.
### Should I dispose `HttpClient` instances that use `NetCache` handlers?
Use `new HttpClient(NetCache.UserInitiated, disposeHandler: false)` for shared `NetCache` handlers. This lets you dispose the client without disposing the process-wide handler.
## Contribute
Fusillade is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We welcome contributors of all experience levels.
- Answer questions on StackOverflow: https://stackoverflow.com/questions/tagged/fusillade
- Share knowledge and mentor the next generation of developers
- Donations: https://reactiveui.net/donate and Corporate Sponsorships: https://reactiveui.net/sponsorship
- Ask your employer to support open-source: https://github.com/github/balanced-employee-ip-agreement
- Improve documentation and examples
- Contribute features and bugfixes via PRs
## What's With The Name?
"Fusillade" is a synonym for Volley.