fusillade 6.0.0
Prefix Reserveddotnet add package fusillade --version 6.0.0
NuGet\Install-Package fusillade -Version 6.0.0
<PackageReference Include="fusillade" Version="6.0.0" />
<PackageVersion Include="fusillade" Version="6.0.0" />
<PackageReference Include="fusillade" />
paket add fusillade --version 6.0.0
#r "nuget: fusillade, 6.0.0"
#:package fusillade@6.0.0
#addin nuget:?package=fusillade&version=6.0.0
#tool nuget:?package=fusillade&version=6.0.0
<br /> <a href="https://github.com/reactiveui/fusillade"> <img width="120" height="120" src="https://raw.githubusercontent.com/reactiveui/styleguide/master/logo_fusillade/main.png"> </a>
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.Primitivesand the leanpunchclockpackage. - Does not require
System.Reactive. - Keeps the public Fusillade API handler-first:
HttpMessageHandler,HttpClient,Task,CancellationToken, andIRequestCache. - Keeps the namespace as
Fusillade.
The new fusillade.reactive package:
- Uses
ReactiveUI.Primitives.ReactiveandPunchclock.Reactive. - Is intended for System.Reactive-first applications.
- Uses the namespace
Fusillade.Reactive. - Mirrors the
fusilladeAPI 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:
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:
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<string> 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:
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.
dotnet add package fusillade
dotnet add package ReactiveUI.Primitives
dotnet add package R3
using Fusillade;
using ReactiveUI.Primitives.R3Bridge;
R3.Observable<Uri> selectedUris = GetSelectedUris();
// Generated when R3 and ReactiveUI.Primitives are both referenced.
System.IObservable<Uri> primitivesUris = selectedUris.AsPrimitivesSignal();
// Convert back to R3 at a boundary if the rest of the app remains R3-first.
R3.Observable<Uri> 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
dotnet add package fusillade
For System.Reactive-first applications:
dotnet add package fusillade.reactive
Quick Start
Create HttpClient instances by selecting the right handler from NetCache.
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.
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.
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:
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.
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:
NetCache.Speculative.ResetLimit(-1);
Clear the byte limit:
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.
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:
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.
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:
using Fusillade;
using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/data");
var key = RateLimitedHttpMessageHandler.UniqueKeyForRequest(request);
Public members:
public RateLimitedHttpMessageHandler(
HttpMessageHandler? handler,
Priority basePriority,
int priority = 0,
long? maxBytesToRead = null,
OperationQueue? operationQueue = null,
Func<HttpRequestMessage, HttpResponseMessage, string, CancellationToken, Task>? 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.
using Fusillade;
LimitingHttpMessageHandler handler = NetCache.Speculative;
handler.ResetLimit(1_000_000);
handler.ResetLimit();
Public members:
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.
using System.Collections.Concurrent;
using Fusillade;
public sealed class MemoryRequestCache : IRequestCache
{
private readonly ConcurrentDictionary<string, byte[]> _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<byte[]?> FetchAsync(
HttpRequestMessage request,
string key,
CancellationToken ct)
{
_responses.TryGetValue(key, out var bytes);
return Task.FromResult<byte[]?>(bytes);
}
}
Public members:
public interface IRequestCache
{
Task SaveAsync(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct);
Task<byte[]?> 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:
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:
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:
public OfflineHttpMessageHandler(
Func<HttpRequestMessage, string, CancellationToken, Task<byte[]?>>? retrieveBodyFunc);
Priority
Priority defines the base scheduling priorities used by RateLimitedHttpMessageHandler.
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:
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.
using Splat.Builder;
var app = AppBuilder.CreateSplatBuilder().Build();
app.CreateFusilladeNetCache();
Public member:
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:
- Pass
cacheResultFunctoRateLimitedHttpMessageHandler. - Set
NetCache.RequestCacheto anIRequestCacheimplementation.
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
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");
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<byte[]?> FetchAsync(
HttpRequestMessage request,
string key,
CancellationToken ct)
{
var path = Path.Combine(cacheDirectory, $"{key}.bin");
return File.Exists(path)
? File.ReadAllBytesAsync(path, ct)
: Task.FromResult<byte[]?>(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.
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.
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.
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.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
| .NET Framework | net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 is compatible. net48 is compatible. net481 is compatible. |
-
.NETFramework 4.6.2
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
- System.Net.Http (>= 4.3.4)
-
.NETFramework 4.7.2
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
- System.Net.Http (>= 4.3.4)
-
.NETFramework 4.8
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
- System.Net.Http (>= 4.3.4)
-
.NETFramework 4.8.1
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
- System.Net.Http (>= 4.3.4)
-
net10.0
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
-
net11.0
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
-
net8.0
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
-
net9.0
- punchclock (>= 7.1.0)
- ReactiveUI.Primitives (>= 5.7.0)
- Splat (>= 20.0.0)
NuGet packages (10)
Showing the top 5 NuGet packages that depend on fusillade:
| Package | Downloads |
|---|---|
|
BD.Common.Mvvm
次元超越 Mvvm 库 |
|
|
ModSink.Common
This library contains most of the logic for ModSink. |
|
|
XamBasePacket
Includes all needed classes models and libs to start developing Xamarin application. |
|
|
Apizr.Integrations.Fusillade
Refit based web api client management, but resilient (retry, connectivity, cache, auth, log, priority...) |
|
|
Xablu.WebApiClient
The Xablu WebApiClient is a C# HTTP library which aims to simplify consuming of Web API services in .NET projects. |
GitHub repositories (4)
Showing the top 4 popular GitHub repositories that depend on fusillade:
| Repository | Stars |
|---|---|
|
BeyondDimension/SteamTools
🛠「Watt Toolkit」是一个开源跨平台的多功能 Steam 工具箱。
|
|
|
reactiveui/ReactiveUI.Samples
This repository contains ReactiveUI samples.
|
|
|
HTBox/crisischeckin
Crisischeckin Humanitarian Toolbox repository
|
|
|
Respawnsive/Apizr
Refit based web api client management, but resilient (retry, connectivity, cache, auth, log, priority, etc...)
|
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.0 | 96 | 6/27/2026 |
| 5.0.0 | 188 | 5/23/2026 |
| 4.0.3 | 1,796 | 12/7/2025 |
| 4.0.1 | 363 | 11/30/2025 |
| 3.0.1 | 1,238 | 9/3/2025 |
| 2.6.30 | 30,326 | 5/2/2024 |
| 2.6.1 | 11,174 | 9/21/2023 |
| 2.4.67 | 24,221 | 2/1/2023 |
| 2.4.62 | 3,218 | 11/24/2022 |
| 2.4.47 | 4,654 | 6/22/2022 |
| 2.4.1 | 14,901 | 12/13/2021 |
| 2.3.1 | 33,391 | 1/22/2021 |
| 2.2.9 | 3,361 | 11/27/2020 |
| 2.2.1 | 2,739 | 10/23/2020 |
| 2.1.9 | 13,783 | 7/29/2020 |
| 2.1.1 | 29,680 | 5/10/2020 |
| 2.0.5 | 101,706 | 4/2/2019 |
| 2.0.2 | 4,041 | 2/19/2019 |
| 2.0.1 | 5,836 | 2/5/2019 |
| 1.0.0 | 52,929 | 9/5/2017 |