
Fusillade helps you write efficient, resilient networked apps by composing HttpMessageHandlers for HttpClient. It focuses on:
- Request de-duplication for relevant HTTP methods
- Concurrency limiting via a priority-aware operation queue
- Request prioritization for predictable UX
- Speculative background fetching with byte-budget limits
- Optional caching of responses and an offline replay handler
Design inspirations include Android's Volley and Picasso.
Supported targets: library is built for .NET Standard 2.0 and is used from modern .NET (e.g., .NET 8/9), Xamarin/Mono, and .NET for iOS/Android/Mac Catalyst apps.
- Package Manager: Install-Package fusillade
- .NET CLI: dotnet add package fusillade
Optional (examples below): Akavache for caching.
- .NET CLI: dotnet add package Akavache.SystemTextJson
Create HttpClient instances by picking the right handler from NetCache:
using Fusillade;
using System.Net.Http;
// Highest priority: the user is waiting now
var client = new HttpClient(NetCache.UserInitiated);
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 (no network)
By default, requests are processed four at a time via an operation queue.
Fusillade de-duplicates concurrent requests for the same resource when the method is GET, HEAD, or OPTIONS. If multiple callers request the same URL concurrently, only one on-the-wire request is made; the others join the same in-flight response.
This happens transparently in RateLimitedHttpMessageHandler.
All work is scheduled through an OperationQueue (default parallelism is 4). Each handler has an effective priority:
- Priority.UserInitiated (100)
- Priority.Background (20)
- Priority.Speculative (10)
- Priority.Explicit (custom base with offset)
Higher numbers run before lower ones. You can set a custom base (Explicit) and an offset to fit your scenario.
using Fusillade;
using Punchclock;
using System.Net.Http;
// Custom queue with 2 concurrent slots
var queue = new OperationQueue(2);
var handler = new RateLimitedHttpMessageHandler(
new HttpClientHandler(),
basePriority: Priority.Explicit,
priority: 500, // higher runs earlier
opQueue: queue);
var client = new HttpClient(handler);
Use NetCache.Speculative for prefetching scenarios. Limit the total number of bytes fetched; once the limit is reached, further speculative requests are canceled.
// Reset byte budget to 5 MB (e.g., on app resume)
NetCache.Speculative.ResetLimit(5 * 1024 * 1024);
var prefetch = new HttpClient(NetCache.Speculative);
_ = prefetch.GetStringAsync("https://example.com/expensive-data");
To stop speculative fetching immediately:
NetCache.Speculative.ResetLimit(-1); // any further requests will be canceled
Fusillade can optionally cache responses (body bytes + headers) and replay them when offline.
There are two ways to wire caching:
-
Provide a cacheResultFunc to RateLimitedHttpMessageHandler, which gets called with the response and a unique request key when a response is received.
-
Set NetCache.RequestCache with an implementation of IRequestCache. Fusillade will invoke Save and Fetch automatically.
public interface IRequestCache
{
Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct);
Task<byte[]> Fetch(HttpRequestMessage request, string key, CancellationToken ct);
}
- Save is called once the handler has fully buffered the body (as ByteArrayContent) and cloned headers.
- Fetch should return the previously saved body bytes for the key (or null if not found).
Keys are generated by RateLimitedHttpMessageHandler.UniqueKeyForRequest(request). Treat the key as an implementation detail; persist what you receive and return it during Fetch.
using Akavache;
using Akavache.SystemTextJson;
using Fusillade;
using System.Net.Http;
// Initialize a simple in-memory Akavache cache
var database = CacheDatabase.CreateBuilder().WithSerializerSystemTextJson().Build();
var blobCache = new InMemoryBlobCache(database.Serializer);
// Option A: Provide a cacheResultFunc directly
var cachingHandler = new RateLimitedHttpMessageHandler(
new HttpClientHandler(),
Priority.UserInitiated,
cacheResultFunc: async (rq, resp, key, ct) =>
{
var data = await resp.Content.ReadAsByteArrayAsync(ct);
await blobCache.Insert(key, data);
});
var client = new HttpClient(cachingHandler);
var fresh = await client.GetStringAsync("https://httpbin.org/get");
// Option B: Implement IRequestCache and set NetCache.RequestCache
NetCache.RequestCache = new MyRequestCache(blobCache);
// Example IRequestCache wrapper over Akavache
class MyRequestCache : IRequestCache
{
private readonly IBlobCache _cache;
public MyRequestCache(IBlobCache cache) => _cache = cache;
public async Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct)
{
var bytes = await response.Content.ReadAsByteArrayAsync(ct);
await _cache.Insert(key, bytes);
}
public Task<byte[]> Fetch(HttpRequestMessage request, string key, CancellationToken ct)
=> _cache.Get(key);
}
Use OfflineHttpMessageHandler to serve cached data only (no network). This handler asks IRequestCache (or your custom retrieveBodyFunc) for the cached body and returns:
- 200 OK with the cached body, or
- 503 Service Unavailable if not found
// Use NetCache.Offline after setting NetCache.RequestCache
var offline = new HttpClient(NetCache.Offline);
var data = await offline.GetStringAsync("https://httpbin.org/get");
// Or construct explicitly
var offlineExplicit = new HttpClient(new OfflineHttpMessageHandler(
async (rq, key, ct) => await blobCache.Get(key)));
If you use Splat, you can initialize NetCache to use your container’s services via the provided extension:
using Splat.Builder;
var app = AppBuilder.CreateSplatBuilder().Build();
app.CreateFusilladeNetCache();
You can also register a platform-specific HttpMessageHandler (e.g., NSUrlSessionHandler on iOS, AndroidMessageHandler on Android) in your container beforehand; NetCache will pick it up as the inner HTTP handler.
- Custom OperationQueue: override NetCache.OperationQueue with your own queue to control concurrency for the entire app.
using Punchclock;
NetCache.OperationQueue = new OperationQueue(maxConcurrency: 6);
- Custom priorities: compose RateLimitedHttpMessageHandler with Priority.Explicit and an offset to place certain pipelines ahead or behind the defaults.
var urgent = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Explicit, priority: 1_000);
var slow = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Explicit, priority: -50);
- Deduplication scope: deduplication is per-HttpMessageHandler instance via an in-memory in-flight map. Multiple handlers mean multiple scopes.
-
Image gallery / avatars
- Use RateLimitedHttpMessageHandler for GETs
- De-dup prevents duplicate downloads for the same URL
- Use Background for preloading next images; switch to UserInitiated for visible images
-
Boot-time warmup
- On app start/resume, set NetCache.Speculative.ResetLimit to a sensible budget
- Queue speculative GETs for likely-next screens to reduce perceived latency
-
Offline-first data views
- Populate cache during online sessions using cacheResultFunc or IRequestCache
- When network is unavailable, point HttpClient to NetCache.Offline
-
How many requests run at once?
- Default is 4 (OperationQueue with concurrency 4). Override via NetCache.OperationQueue or pass a custom queue to a handler.
-
Which methods are de-duplicated?
- GET, HEAD, and OPTIONS.
-
How are cache keys generated?
- Via RateLimitedHttpMessageHandler.UniqueKeyForRequest(request). Treat this as an implementation detail; persist and reuse as given.
-
Can I cancel a request?
- Use CancellationToken in HttpClient APIs; dedup ensures the underlying request cancels only when all dependents cancel.
Fusillade is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We ❤ our contributors and welcome new 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
“Fusillade” is a synonym for Volley 🙂