DamianH.HttpHybridCacheHandler 0.1.1

dotnet add package DamianH.HttpHybridCacheHandler --version 0.1.1
                    
NuGet\Install-Package DamianH.HttpHybridCacheHandler -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DamianH.HttpHybridCacheHandler" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DamianH.HttpHybridCacheHandler" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="DamianH.HttpHybridCacheHandler" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DamianH.HttpHybridCacheHandler --version 0.1.1
                    
#r "nuget: DamianH.HttpHybridCacheHandler, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DamianH.HttpHybridCacheHandler@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DamianH.HttpHybridCacheHandler&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=DamianH.HttpHybridCacheHandler&version=0.1.1
                    
Install as a Cake Tool

HttpHybridCache Handler

CI License .NET GitHub Stars

This repository contains two companion NuGet packages:

Package NuGet Downloads
DamianH.HttpHybridCacheHandler — RFC 9111 client-side HTTP caching handler for HttpClient NuGet Downloads
DamianH.FileDistributedCache — File-based IDistributedCache / IBufferDistributedCache for zero-infrastructure persistent caching NuGet Downloads

Project is new and it getting dog-fooded. If you try it out feedback would be appreciated.

Table of Contents

HttpHybridCacheHandler

FileDistributedCache

General

Features

Package: DamianH.HttpHybridCacheHandler — A caching DelegatingHandler for HttpClient that provides client-side HTTP caching based on RFC 9111.

Core Caching Capabilities

  • RFC 9111 Compliant: Full implementation of HTTP caching specification for client-side caching
  • HybridCache Integration: Leverages .NET's HybridCache for efficient L1 (memory) and L2 (distributed) caching
  • Transparent Operation: Works seamlessly with existing HttpClient code

Cache-Control Directives

Request Directives:

  • max-age: Control maximum acceptable response age
  • max-stale: Accept stale responses within specified staleness tolerance
  • min-fresh: Require responses to remain fresh for specified duration
  • no-cache: Force revalidation with origin server
  • no-store: Bypass cache completely
  • only-if-cached: Return cached responses or 504 if not cached

Response Directives:

  • max-age: Define response freshness lifetime
  • no-cache: Store but require validation before use
  • no-store: Prevent caching
  • public/private: Control cache visibility
  • must-revalidate: Enforce validation when stale

Advanced Features

  • Conditional Requests: Automatic ETag (If-None-Match) and Last-Modified (If-Modified-Since) validation
  • Vary Header Support: Content negotiation with multiple cache entries per resource
  • Freshness Calculation: Supports Expires header, Age header, and heuristic freshness (Last-Modified based)
  • Stale Response Handling:
    • stale-while-revalidate: Serve stale content while updating in background
    • stale-if-error: Serve stale content when origin is unavailable
  • Configurable Limits: Per-item content size limits (default 10MB)
  • Metrics: Built-in metrics via System.Diagnostics.Metrics for hit/miss rates and cache operations
  • Custom Cache Keys: Extensible cache key generation for advanced scenarios
  • Request Collapsing: Prevents cache stampede via HybridCache.GetOrCreateAsync automatic request coalescing

Installation

dotnet add package DamianH.HttpHybridCacheHandler

Quick Start

var services = new ServiceCollection();

services.AddHttpHybridCacheHandler(options =>
{
    options.FallbackCacheDuration = TimeSpan.FromMinutes(5);
    options.MaxCacheableContentSize = 10 * 1024 * 1024; // 10MB
    options.CompressionThreshold = 1024; // Compress cached content >1KB
});

services.AddHttpClient("MyClient")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        // Enable automatic decompression - server compression handled transparently
        AutomaticDecompression = DecompressionMethods.All,
        
        // DNS refresh every 5 minutes - critical for cloud/microservices
        PooledConnectionLifetime = TimeSpan.FromMinutes(5),
        
        // Close idle connections after 2 minutes
        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
        
        // Reasonable connection timeout
        ConnectTimeout = TimeSpan.FromSeconds(10)
    })
    .AddHttpMessageHandler(sp => sp.GetRequiredService<HttpHybridCacheHandler>());

var client = services.BuildServiceProvider()
    .GetRequiredService<IHttpClientFactory>()
    .CreateClient("MyClient");

var response = await client.GetAsync("https://api.example.com/data");

Handler Pipeline Configuration

Always use SocketsHttpHandler with AutomaticDecompression enabled (better performance, DNS refresh, and connection pooling than legacy HttpClientHandler):

.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    AutomaticDecompression = DecompressionMethods.All,
    PooledConnectionLifetime = TimeSpan.FromMinutes(5),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
})

AutomaticDecompression Explained

Two different compressions:

  1. Transport Compression (Server → Client)

    • Controlled by: AutomaticDecompression on SocketsHttpHandler
    • Purpose: Reduce network bandwidth
    • Result: Handler receives decompressed content
  2. Cache Storage Compression (This library)

    • Controlled by: CompressionThreshold in options
    • Purpose: Reduce cache storage size
    • Result: Content compressed before storing in cache

Example Flow:

Server sends: gzipped 512 bytes
    ↓
SocketsHttpHandler: auto-decompresses → 2048 bytes
    ↓
HttpHybridCacheHandler: receives decompressed content
    ↓
Our compression: compresses → 600 bytes
    ↓
Cache: stores 600 bytes (no Base64 overhead!)

Benefits:

  • Cache handler can inspect and validate response content
  • Cache-Control, ETag, and Last-Modified headers are readable
  • Enables intelligent caching decisions
  • Storage compression is optional and configurable

Handler Ordering

Pipeline structure:

HttpClient → [Outer Handlers] → HttpHybridCacheHandler → SocketsHttpHandler → Network
.AddHttpMessageHandler(sp => new HttpHybridCacheHandler(...))
.AddStandardResilienceHandler(options =>
{
    options.Retry.MaxRetryAttempts = 3;
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
});

Order: Polly (outer) → Cache → SocketsHttpHandler

Why: Cache hit = fast path, Polly never invoked. Cache miss + network failure = Polly retries.

With Authentication
.AddHttpMessageHandler(() => new AuthenticationHandler())
.AddHttpMessageHandler(sp => sp.GetRequiredService<HttpHybridCacheHandler>());

To include auth headers in cache key, configure VaryHeaders in the AddHttpHybridCacheHandler options.

Auth applied before caching, headers included in cache key via Vary.

Common Mistakes

Wrong: Not enabling AutomaticDecompression

new SocketsHttpHandler()  // Defaults to None!

Problem: Cache handler receives compressed content, can't inspect properly.

Correct: Explicitly enable decompression

new SocketsHttpHandler
{
    AutomaticDecompression = DecompressionMethods.All
}

Wrong: Using legacy HttpClientHandler

new HttpClientHandler()  // Legacy, less efficient

Correct: Use modern SocketsHttpHandler

new SocketsHttpHandler { /* ... */ }

Wrong: Cache handler after Polly

.AddStandardResilienceHandler()  // Outer
.AddHttpMessageHandler(sp => new HttpHybridCacheHandler(...))  // Inner - Wrong!

Correct: Cache handler before Polly

.AddHttpMessageHandler(sp => new HttpHybridCacheHandler(...))  // Inner - Correct!
.AddStandardResilienceHandler()  // Outer

Golden Rule: HttpHybridCacheHandler should receive decompressed, ready-to-use content.

Configuration Options

Cache Mode

The library supports two cache modes, following RFC 9111 semantics:

CacheMode.Private (Default)

Browser-like cache behavior suitable for client applications:

Use Cases:

  • HttpClient in web applications, APIs, background services
  • Scaled-out clients sharing cache (multiple instances, serverless/Lambda)
  • Per-user/per-tenant caching scenarios

Behavior:

  • Caches responses with Cache-Control: private
  • Uses max-age directive (ignores s-maxage)
  • Caches authenticated requests if marked private or max-age
  • Each cache key is client-specific (Vary headers applied)

Example:

new HttpHybridCacheHandlerOptions
{
    Mode = CacheMode.Private, // Shares cache across app instances via Redis L2
    FallbackCacheDuration = TimeSpan.FromMinutes(5)
}
CacheMode.Shared

Proxy/CDN-like cache behavior suitable for gateways:

Use Cases:

  • Reverse proxies (YARP, Envoy)
  • API gateways
  • Edge caches / CDN-like scenarios

Behavior:

  • Does NOT cache responses with Cache-Control: private
  • Prefers s-maxage over max-age
  • Only caches authenticated requests with public or s-maxage
  • Cache is shared across all clients/users

Example:

new HttpHybridCacheHandlerOptions
{
    Mode = CacheMode.Shared, // RFC 9111 shared cache semantics
    MaxCacheableContentSize = 50 * 1024 * 1024 // 50MB
}

HttpHybridCacheHandlerOptions

  • Mode: Cache mode determining caching behavior (default: CacheMode.Private). Use CacheMode.Shared for proxy/CDN scenarios
  • HeuristicFreshnessPercent: Heuristic freshness percentage for responses with Last-Modified but no explicit freshness info (default: 0.1 or 10%)
  • VaryHeaders: Headers to include in Vary-aware cache keys (default: Accept, Accept-Encoding, Accept-Language, User-Agent)
  • MaxCacheableContentSize: Maximum size in bytes for cacheable response content (default: 10 MB). Responses larger than this will not be cached
  • FallbackCacheDuration: Fallback cache duration for responses without explicit caching headers (default: TimeSpan.MinValue, meaning responses without caching headers are not cached)
  • CompressionThreshold: Minimum content size in bytes to enable compression (default: 1024 bytes). Set to 0 or negative value to disable compression
  • CompressibleContentTypes: Content types eligible for compression (default: text/*, application/json, application/json+*, application/xml, application/javascript, image/svg+xml)
  • CacheableContentTypes: Content types eligible for caching (default: text/*, application/json, application/json+*, application/xml, application/javascript, application/xhtml+xml, image/*)
  • ContentKeyPrefix: Prefix for content cache keys (default: "httpcache:content:"). Content is stored separately from metadata to avoid Base64 encoding overhead
  • IncludeDiagnosticHeaders: Whether to include diagnostic headers (X-Cache-Diagnostic, etc.) in responses (default: false)

Metrics

The handler emits the following counters via System.Diagnostics.Metrics under the meter named DamianH.HttpHybridCacheHandler:

Counter Description
cache.hits Number of cache hits (fresh, revalidated, stale-while-revalidate, stale-if-error)
cache.misses Number of cache misses (including cache errors and failed revalidations)
cache.stale Number of stale cache entries served (stale-while-revalidate or stale-if-error)
cache.size_exceeded Number of responses that exceeded MaxCacheableContentSize and were not cached

All counters include the following tags (following OpenTelemetry semantic conventions):

Tag Description Example
http.request.method HTTP method GET, HEAD
url.scheme URL scheme http, https
server.address Server hostname api.example.com
server.port Server port 443

Cache Behavior

Diagnostic Headers

When IncludeDiagnosticHeaders is enabled in options, the handler adds diagnostic information to responses:

  • X-Cache-Diagnostic: Indicates cache behavior for the request
    • HIT-FRESH: Served from cache, content is fresh
    • HIT-REVALIDATED: Served from cache after successful 304 revalidation
    • HIT-STALE-WHILE-REVALIDATE: Served stale while background revalidation occurs
    • HIT-STALE-IF-ERROR: Served stale due to backend error
    • HIT-ONLY-IF-CACHED: Served from cache with only-if-cached directive
    • MISS: Not in cache, fetched from backend
    • MISS-REVALIDATED: Cache entry was stale and resource changed
    • MISS-CACHE-ERROR: Cache operation failed, bypassed
    • MISS-ONLY-IF-CACHED: Not in cache with only-if-cached directive (504 Gateway Timeout)
    • BYPASS-METHOD: Request method not cacheable (POST, PUT, etc.)
    • BYPASS-NO-STORE: Request has no-store directive
  • X-Cache-Age: Age of cached content in seconds (only for cache hits)
  • X-Cache-MaxAge: Maximum age of cached content in seconds (only for cache hits)
  • X-Cache-Compressed: "true" if content was stored compressed (only for cache hits)

Example:

var options = new HttpHybridCacheHandlerOptions
{
    IncludeDiagnosticHeaders = true
};

Cacheable Responses

Only GET and HEAD requests are cached. Responses are cached when:

  • Status code is 200 OK
  • Cache-Control allows caching (not no-store, not no-cache without validation)
  • Content size is within MaxContentSize limit

Cache Key Generation

Cache keys are generated from:

  • HTTP method
  • Request URI
  • Vary header values from the response

Conditional Requests

When serving stale content, the handler automatically adds:

  • If-None-Match header with cached ETag
  • If-Modified-Since header with cached Last-Modified date

If the server responds with 304 Not Modified, the cached response is refreshed and served.

Samples

See the /samples directory for complete examples:

  • HttpClientFactorySample: Integration with IHttpClientFactory
  • YarpCachingProxySample: Building a caching reverse proxy with YARP
  • FusionCacheSample: Using FusionCache via its HybridCache adapter for enhanced caching features
  • FileDistributedCacheSample: File-based L2 cache with HttpHybridCacheHandler

FileDistributedCache Overview

A file-based IDistributedCache (and IBufferDistributedCache) implementation for .NET. Stores cache entries as individual files on the local filesystem — no external infrastructure required.

When to use FileDistributedCache vs Redis/SQL Server

Scenario FileDistributedCache Redis / SQL Server
Desktop apps (WPF, WinForms, MAUI) ✅ Ideal — local disk, no infrastructure Overkill
Mobile apps (MAUI, Xamarin) ✅ Ideal — device-local storage Not available
CLI tools and background agents ✅ Simple, self-contained Overkill
Single-instance services ✅ Good — persistent L2 with no dependencies Either works
Microservices / SOA (multi-instance) ❌ Not shared across instances ✅ Use Redis / SQL Server
Scaled-out web apps (load-balanced) ❌ Each instance has its own cache ✅ Use a shared cache

FileDistributedCache is the L2 backend for HybridCache when you need persistence across process restarts but don't have (or don't want) external cache infrastructure. For multi-instance services that need a shared cache, use Redis, SQL Server, or another distributed backend.

Key characteristics:

  • Zero infrastructure — no Redis, SQL Server, or other external dependencies
  • Persistent across restarts — cache survives process recycling
  • AOT compatible — fully trimming/AOT safe
  • IBufferDistributedCache support — implements the buffer-based interface for efficient HybridCache L2 integration (avoids byte[] allocations)
  • Background eviction — configurable periodic cleanup of expired entries, with optional soft limits on entry count and total size
  • Sliding expiration — last-access timestamps are updated on read for sliding window support
  • Concurrency safe — file-level locking with retry logic for concurrent access on Windows

FileDistributedCache Installation

dotnet add package DamianH.FileDistributedCache

FileDistributedCache Quick Start

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddFileDistributedCache(options =>
{
    options.CacheDirectory = Path.Combine(AppContext.BaseDirectory, "my-cache");
    options.MaxEntries = 10_000;
    options.MaxTotalSize = 500 * 1024 * 1024; // 500 MB
    options.EvictionInterval = TimeSpan.FromMinutes(5);
    options.DefaultAbsoluteExpiration = TimeSpan.FromHours(1);
});

var provider = services.BuildServiceProvider();
var cache = provider.GetRequiredService<IDistributedCache>();

// Set a value
await cache.SetStringAsync("key", "value", new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
    SlidingExpiration = TimeSpan.FromMinutes(10)
});

// Get a value
var value = await cache.GetStringAsync("key");

FileDistributedCache Configuration Options

Option Type Default Description
CacheDirectory string {TempPath}/DamianH.FileDistributedCache Directory where cache files are stored
MaxEntries int? null (unlimited) Soft limit on number of cache entries. Oldest by last access are evicted when exceeded
MaxTotalSize long? null (unlimited) Soft limit on total cached data size in bytes. Oldest by last access are evicted when exceeded
EvictionInterval TimeSpan 5 minutes How frequently the background eviction scan runs
DefaultSlidingExpiration TimeSpan? null Default sliding expiration applied when an entry is stored without one
DefaultAbsoluteExpiration TimeSpan? null Default absolute expiration (relative to now) applied when an entry has no explicit expiration

Using with HttpHybridCacheHandler

FileDistributedCache is designed as a drop-in L2 backend for .NET's HybridCache. When both packages are registered, HybridCache automatically discovers the IDistributedCache / IBufferDistributedCache service and uses it for L2 storage:

var builder = Host.CreateApplicationBuilder(args);

// Register file-based L2 cache — HybridCache picks it up automatically
builder.Services.AddFileDistributedCache(options =>
{
    options.CacheDirectory = Path.Combine(AppContext.BaseDirectory, "http-cache");
    options.DefaultAbsoluteExpiration = TimeSpan.FromHours(1);
});

// Register the HTTP caching handler
builder.Services.AddHttpHybridCacheHandler(options =>
{
    options.FallbackCacheDuration = TimeSpan.FromMinutes(5);
});

// Wire up an HttpClient with caching
builder.Services
    .AddHttpClient("CachedClient")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        AutomaticDecompression = DecompressionMethods.All,
        PooledConnectionLifetime = TimeSpan.FromMinutes(5),
    })
    .AddHttpMessageHandler(sp => sp.GetRequiredService<HttpHybridCacheHandler>());

var host = builder.Build();
var client = host.Services.GetRequiredService<IHttpClientFactory>().CreateClient("CachedClient");

// First request: fetched from network, stored in L1 (memory) + L2 (file)
// Subsequent requests: served from L1 or L2 — even across process restarts
var response = await client.GetAsync("https://api.example.com/data");

See samples/FileDistributedCacheSample for a complete working example.

Performance & Memory

The handler is designed for high-performance scenarios with several key optimizations:

Content/Metadata Separation Architecture

Eliminates Base64 overhead in distributed cache:

  • Metadata (small, ~1-2KB): Status code, headers, timestamps → Stored as JSON
  • Content (large, variable): Response body → Stored as raw byte[]
    • No Base64 encoding = 33% size savings
    • Content deduplication via SHA256 hash
    • Same content shared across cache entries (different Vary headers)

Trade-offs:

  • Two cache lookups (metadata + content) vs one lookup
  • Acceptable: L1 (memory) cache makes second lookup very fast (~microseconds)
  • Benefit: Zero Base64 overhead on all cached content

Memory Efficiency

  • Stampede Prevention (via HybridCache.GetOrCreateAsync): Multiple concurrent requests for the same resource are automatically collapsed into a single backend request
  • Automatic Deduplication: Only one request hits the backend while others await the cached result
  • Built-in HybridCache feature - no additional configuration needed

Efficient Caching

  • L1/L2 Strategy: Fast in-memory (L1) + optional distributed (L2) via HybridCache
  • Size Limits: Configurable per-item limits (default: 10MB) prevent memory issues
  • Conditional Requests: ETags and Last-Modified enable efficient 304 responses

Benchmark Results

See /benchmarks for comprehensive memory allocation benchmarks:

Response Size Allocations Gen2 (LOH) Notes
1-10KB ~10-20 KB 0 No LOH, optimal
10-85KB ~20-100 KB 0 No LOH, good
>85KB ~100KB+ >0 LOH expected, acceptable for reliability

Run benchmarks: cd benchmarks && .\run-memory-tests.ps1

Benchmarks

Run benchmarks to measure performance:

dotnet run --project benchmarks/Benchmarks.csproj -c Release

Contributing

Bug reports should be accompanied by a reproducible test case in a pull request.

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 28 3/1/2026