Skip to content

reactiveui/Fusillade

NuGet Stats Build Code Coverage


Fusillade: An opinionated HTTP library for .NET apps

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.

Install

  • Package Manager: Install-Package fusillade
  • .NET CLI: dotnet add package fusillade

Optional (examples below): Akavache for caching.

  • .NET CLI: dotnet add package Akavache.SystemTextJson

Quick start

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.

Core ideas

1) Request de-duplication

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.

2) Concurrency limiting and prioritization

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);

3) Speculative background fetching with byte budgets

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

Caching and offline

Fusillade can optionally cache responses (body bytes + headers) and replay them when offline.

There are two ways to wire caching:

  1. Provide a cacheResultFunc to RateLimitedHttpMessageHandler, which gets called with the response and a unique request key when a response is received.

  2. Set NetCache.RequestCache with an implementation of IRequestCache. Fusillade will invoke Save and Fetch automatically.

IRequestCache

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.

Simple Akavache-based cache

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);
}

Offline replay

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)));

Dependency injection and Splat integration

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.

Advanced configuration

  • 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.

Usage recipes

  • 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

FAQ

  • 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.

Contribute

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.

What’s with the name?

“Fusillade” is a synonym for Volley 🙂

Sponsor this project

 

Packages

 
 
 

Contributors 18

Languages