HTTPClient Cache

.NETC#HTTP cacheRFC2616DelegatingHandlerASP.NET Coreperformance

GitHub Overview

thomasgalliker/HttpClient.Caching

Caching extensions for .NET HttpClient

Stars24
Watchers1
Forks7
Created:November 18, 2018
Language:C#
License:MIT License

Topics

cachecachinghttpclientmemorycache

Star History

thomasgalliker/HttpClient.Caching Star History
Data as of: 10/22/2025, 08:06 AM

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

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