HTTPClient Cache
GitHub Overview
thomasgalliker/HttpClient.Caching
Caching extensions for .NET HttpClient
Topics
Star History
Cache Library
HTTPClient Cache
Overview
HTTPClient Cache refers to a collection of libraries that add caching functionality to System.Net.Http.HttpClient in .NET Framework/.NET Core. Compliant with RFC2616 HTTP caching standards, it enables efficient caching of HTTP responses. Through DelegatingHandler patterns and integration with IMemoryCache, caching functionality can be added to existing HttpClient code with minimal changes.
Details
HTTP caching in .NET environments exists through multiple approaches including HttpClient.Caching library, Marvin.HttpCache library, and ASP.NET Core's built-in caching features. These leverage HTTP headers (Cache-Control, ETag, Last-Modified, etc.) to automate conditional GET requests and response caching.
Key Libraries and Approaches
- HttpClient.Caching: Lightweight caching extension library by Thomas Galliker
- Marvin.HttpCache: Comprehensive RFC2616 implementation by Kevin Dockx
- ASP.NET Core Response Caching: Framework-built server-side caching
- Custom DelegatingHandler: Custom implementation integrated with IMemoryCache
- Polly: Resilience improvement through integration with cache policies
Technical Features
- RFC2616 Compliance: Complete implementation of standard HTTP caching specifications
- Conditional Requests: Efficient revalidation using ETag and Last-Modified
- DelegatingHandler Integration: Transparent integration into HttpClient pipeline
- Memory Cache: High-speed in-memory caching via IMemoryCache
- Disk Cache: Persistent disk-based caching capability
Pros and Cons
Pros
- Standards Compliance: Leverages established caching standards through RFC2616
- Transparency: Minimal impact on existing HttpClient code
- High Performance: Significant speed improvements through reduced network calls
- Flexibility: Support for custom cache policies and storage implementations
- ASP.NET Integration: Scalable solutions through deep framework integration
- Debug Support: Integration with existing HttpClient logging and tracing features
Cons
- Complexity: Difficulty in understanding and debugging HTTP caching specifications
- Memory Consumption: Increased memory usage due to large response data
- Consistency: Cache invalidation challenges in distributed environments
- Configuration Complexity: Difficulty in selecting and tuning appropriate cache strategies
Reference Links
- HttpClient.Caching GitHub
- Marvin.HttpCache GitHub
- ASP.NET Core Response Caching
- .NET Caching Documentation
Code Examples
Using HttpClient.Caching Library
using System;
using System.Net.Http;
using System.Threading.Tasks;
using HttpClientCaching;
public class HttpClientCachingExample
{
public async Task BasicCachingExample()
{
// Create InMemoryCacheHandler
var cacheHandler = new InMemoryCacheHandler()
{
InnerHandler = new HttpClientHandler()
};
// Configure HttpClient with cache handler
using var httpClient = new HttpClient(cacheHandler);
// First request (retrieved from cache)
var response1 = await httpClient.GetAsync("https://api.example.com/data");
var content1 = await response1.Content.ReadAsStringAsync();
// Second request (retrieved from cache, no network call)
var response2 = await httpClient.GetAsync("https://api.example.com/data");
var content2 = await response2.Content.ReadAsStringAsync();
Console.WriteLine($"First request: {content1.Length} characters");
Console.WriteLine($"Second request: {content2.Length} characters");
Console.WriteLine($"Same content: {content1 == content2}");
}
}
Custom DelegatingHandler Cache Implementation
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
public class CachingDelegatingHandler : DelegatingHandler
{
private readonly IMemoryCache _cache;
private readonly ILogger<CachingDelegatingHandler> _logger;
private readonly TimeSpan _defaultCacheDuration;
public CachingDelegatingHandler(
IMemoryCache cache,
ILogger<CachingDelegatingHandler> logger,
TimeSpan? defaultCacheDuration = null)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_defaultCacheDuration = defaultCacheDuration ?? TimeSpan.FromMinutes(5);
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Only cache GET requests
if (request.Method != HttpMethod.Get)
{
return await base.SendAsync(request, cancellationToken);
}
var cacheKey = GenerateCacheKey(request);
// Try to retrieve from cache
if (_cache.TryGetValue(cacheKey, out HttpResponseMessage cachedResponse))
{
_logger.LogInformation("Cache hit for {RequestUri}", request.RequestUri);
return CloneResponse(cachedResponse);
}
// Cache miss - execute actual request
_logger.LogInformation("Cache miss for {RequestUri}", request.RequestUri);
var response = await base.SendAsync(request, cancellationToken);
// Cache successful responses
if (response.IsSuccessStatusCode)
{
var cacheDuration = GetCacheDuration(response);
var responseToCache = await CloneResponseAsync(response);
_cache.Set(cacheKey, responseToCache, cacheDuration);
_logger.LogInformation("Cached response for {RequestUri} for {Duration}",
request.RequestUri, cacheDuration);
}
return response;
}
private string GenerateCacheKey(HttpRequestMessage request)
{
return $"HttpCache:{request.Method}:{request.RequestUri}";
}
private TimeSpan GetCacheDuration(HttpResponseMessage response)
{
// Get expiration from Cache-Control header
if (response.Headers.CacheControl?.MaxAge.HasValue == true)
{
return response.Headers.CacheControl.MaxAge.Value;
}
// Use default cache duration
return _defaultCacheDuration;
}
private async Task<HttpResponseMessage> CloneResponseAsync(HttpResponseMessage original)
{
var cloned = new HttpResponseMessage(original.StatusCode)
{
Content = new ByteArrayContent(await original.Content.ReadAsByteArrayAsync()),
ReasonPhrase = original.ReasonPhrase,
RequestMessage = original.RequestMessage,
Version = original.Version
};
// Copy headers
foreach (var header in original.Headers)
{
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (original.Content != null)
{
foreach (var header in original.Content.Headers)
{
cloned.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
return cloned;
}
private HttpResponseMessage CloneResponse(HttpResponseMessage original)
{
// Simplified clone (async clone recommended for actual implementation)
return original;
}
}
HttpClientFactory Integration in ASP.NET Core
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Caching.Memory;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Add memory cache
services.AddMemoryCache();
// Configure HttpClientFactory
services.AddHttpClient("CachedClient")
.AddHttpMessageHandler<CachingDelegatingHandler>();
// Register custom cache handler
services.AddTransient<CachingDelegatingHandler>();
// Named HttpClient configuration example
services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
})
.AddHttpMessageHandler<CachingDelegatingHandler>();
}
}
// Service usage example
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("CachedClient");
}
public async Task<string> GetDataAsync()
{
var response = await _httpClient.GetAsync("data");
return await response.Content.ReadAsStringAsync();
}
}
Conditional Requests with ETag
public class ETagCachingHandler : DelegatingHandler
{
private readonly IMemoryCache _cache;
public ETagCachingHandler(IMemoryCache cache)
{
_cache = cache;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Method != HttpMethod.Get)
{
return await base.SendAsync(request, cancellationToken);
}
var cacheKey = request.RequestUri.ToString();
// Check cached ETag
if (_cache.TryGetValue($"{cacheKey}:etag", out string cachedETag))
{
// Add If-None-Match header
request.Headers.IfNoneMatch.ParseAdd(cachedETag);
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.NotModified)
{
// 304 Not Modified - get response from cache
if (_cache.TryGetValue(cacheKey, out HttpResponseMessage cachedResponse))
{
return cachedResponse;
}
}
else if (response.IsSuccessStatusCode)
{
// Cache new response
var etag = response.Headers.ETag?.Tag;
if (!string.IsNullOrEmpty(etag))
{
var clonedResponse = await CloneResponseAsync(response);
_cache.Set(cacheKey, clonedResponse);
_cache.Set($"{cacheKey}:etag", etag);
}
}
return response;
}
private async Task<HttpResponseMessage> CloneResponseAsync(HttpResponseMessage original)
{
// Response cloning implementation
var content = await original.Content.ReadAsByteArrayAsync();
var cloned = new HttpResponseMessage(original.StatusCode)
{
Content = new ByteArrayContent(content)
};
foreach (var header in original.Headers)
{
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return cloned;
}
}
Resilient Caching with Polly Integration
using Polly;
using Polly.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
public void ConfigureServicesWithPolly(IServiceCollection services)
{
services.AddMemoryCache();
// Combining retry policy with caching
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
services.AddHttpClient("ResilientCachedClient")
.AddHttpMessageHandler<CachingDelegatingHandler>()
.AddPolicyHandler(retryPolicy);
}
// Usage example
public class ResilientApiService
{
private readonly HttpClient _httpClient;
public ResilientApiService(IHttpClientFactory factory)
{
_httpClient = factory.CreateClient("ResilientCachedClient");
}
public async Task<ApiData> GetApiDataAsync()
{
var response = await _httpClient.GetAsync("api/data");
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiData>(json);
}
}
WebRequest CachePolicy Configuration
using System.Net;
using System.Net.Cache;
public class WebRequestCacheExample
{
public void ConfigureWebRequestCache()
{
// Set global default cache policy
HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.Default);
// Or set policy for specific requests
var request = (HttpWebRequest)WebRequest.Create("https://api.example.com/data");
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);
// Completely disable cache
var noCache = new RequestCachePolicy(RequestCacheLevel.BypassCache);
request.CachePolicy = noCache;
}
public async Task<string> GetWithCachePolicy()
{
var request = (HttpWebRequest)WebRequest.Create("https://api.example.com/data");
// Retrieve from cache unless expired
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheOnly);
try
{
using var response = (HttpWebResponse)await request.GetResponseAsync();
using var reader = new StreamReader(response.GetResponseStream());
return await reader.ReadToEndAsync();
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.CacheEntryNotFound)
{
// Handle case when not in cache
return "No cached data available";
}
}
}