A wrapper for HttpClient that caches to disk. Cached files, over the max specified, are deleted based on the last access times.
See Milestones for release notes.
Headers/Responses respected in caching decisions:
https://nuget.org/packages/Replicant/
There is a default static instance:
var content = await HttpCache.Default.DownloadAsync("https://httpbin.org/status/200");This caches to {Temp}/Replicant.
An instance of HttpCache should be long running.
var httpCache = new HttpCache(
cacheDirectory,
// omit for default new HttpClient()
new HttpClient
{
Timeout = TimeSpan.FromSeconds(30)
},
// omit for the default of 1000
maxEntries: 10000);
// Dispose when finished
await httpCache.DisposeAsync();Add HttpCache as a singleton when using dependency injection.
var services = new ServiceCollection();
services.AddSingleton(_ => new HttpCache(diPath));
using var provider = services.BuildServiceProvider();
var httpCache = provider.GetRequiredService<HttpCache>();
NotNull(httpCache);Using HttpClient with HttpClientFactory.
var services = new ServiceCollection();
services.AddHttpClient();
services.AddSingleton(
_ =>
{
var clientFactory = _.GetRequiredService<IHttpClientFactory>();
return new HttpCache(diPath, clientFactory.CreateClient);
});
using var provider = services.BuildServiceProvider();
var httpCache = provider.GetRequiredService<HttpCache>();
NotNull(httpCache);ReplicantHandler can be used as a DelegatingHandler in the HttpClient pipeline:
var handler = new ReplicantHandler(cacheDirectory)
{
InnerHandler = new HttpClientHandler()
};
using var client = new HttpClient(handler);
var response = await client.GetAsync("https://example.com");ReplicantHandler integrates with HttpClientFactory using AddHttpMessageHandler:
var services = new ServiceCollection();
services.AddHttpClient("CachedClient")
.AddHttpMessageHandler(() => new ReplicantHandler(cacheDirectory));To share a single cache (and purge timer) across multiple named clients, register a ReplicantCache as a singleton:
var services = new ServiceCollection();
services.AddReplicantCache(cacheDirectory);
services.AddHttpClient("CachedClient")
.AddReplicantCaching();Replicant can serve as a disk-based L2 cache for HybridCache. Register ReplicantDistributedCache as the IDistributedCache backend, and HybridCache will automatically use it for its L2 layer (with in-memory L1 handled by HybridCache itself):
var services = new ServiceCollection();
services.AddReplicantDistributedCache(cacheDirectory);
services.AddHybridCache();Only one cache instance (HttpCache, ReplicantCache, or ReplicantHandler with its own directory) can exist per cache directory at any time. Creating a second instance for the same directory will throw an InvalidOperationException. This prevents multiple purge timers from running against the same files.
To share a cache across multiple handlers or consumers, use a single ReplicantCache instance (see above).
var content = await httpCache.StringAsync("https://httpbin.org/json");var lines = new List<string>();
await foreach (var line in httpCache.LinesAsync("https://httpbin.org/json"))
{
lines.Add(line);
}var bytes = await httpCache.BytesAsync("https://httpbin.org/json");using var stream = await httpCache.StreamAsync("https://httpbin.org/json");await httpCache.ToFileAsync("https://httpbin.org/json", targetFile);await httpCache.ToStreamAsync("https://httpbin.org/json", targetStream);using var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("the content")
};
await httpCache.AddItemAsync(uri, response);If an error occurs when re-validating a potentially stale item, then the cached item can be used as a fallback.
var content = httpCache.StringAsync(uri, staleIfError: true);The HttpRequestMessage used can be customized using a callback.
var content = await httpCache.StringAsync(
uri,
modifyRequest: message =>
{
message.Headers.Add("Key1", "Value1");
message.Headers.Add("Key2", "Value2");
});An instance of the HttpResponseMessage can be created from a cached item:
using var response = await httpCache.ResponseAsync("https://httpbin.org/status/200");graph TD
IHttpCache["IHttpCache<br/>(interface)"]
HttpCache["HttpCache<br/>implements IHttpCache"]
ReplicantHandler["ReplicantHandler<br/>extends DelegatingHandler"]
ReplicantCache["ReplicantCache"]
CacheSession["CacheSession"]
CacheStore["CacheStore"]
FilePair["FilePair<br/>(struct)"]
Timestamp["Timestamp<br/>(struct)"]
MetaData["MetaData"]
Result["Result<br/>(struct)"]
DeriveCacheStatus["DeriveCacheStatus"]
CacheStatus["CacheStatus<br/>(enum)"]
ReplicantDistributedCache["ReplicantDistributedCache<br/>implements IDistributedCache"]
HybridCache["HybridCache<br/>(L1 memory + L2 disk)"]
HttpCache -->|implements| IHttpCache
HttpCache -->|owns| CacheStore
HttpCache -->|creates per call| CacheSession
HttpCache -->|returns| Result
ReplicantHandler -->|owns or references| CacheSession
ReplicantHandler -->|can share| ReplicantCache
ReplicantCache -->|owns| CacheStore
CacheSession -->|uses| CacheStore
CacheSession -->|uses| DeriveCacheStatus
DeriveCacheStatus -->|returns| CacheStatus
CacheStore -->|manages| FilePair
CacheStore -->|uses| Timestamp
CacheStore -->|serializes| MetaData
Result -->|holds| FilePair
Timestamp -->|encodes| FilePair
HybridCache -->|L2 backend| ReplicantDistributedCache
HttpCache is the standalone API that owns an HttpClient. ReplicantHandler is a DelegatingHandler that plugs into an existing HttpClient pipeline. Both delegate to CacheSession which orchestrates the cache protocol using CacheStore for disk I/O. When multiple handlers need to share a cache, a single ReplicantCache is registered as a singleton. ReplicantDistributedCache implements IDistributedCache for use as a disk-based L2 backend with HybridCache.
graph TD
Request[Incoming HTTP Request]
IsGetOrHead{GET or HEAD?}
Passthrough[Pass through to server]
CacheExists{Cached file<br/>exists for URI?}
SendNew[Send request to server]
IsSuccess{Response 2xx?}
ThrowException[Throw exception]
IsNoStoreNew{Cache-Control:<br/>no-store?}
ReturnDirect[Return response directly<br/>nothing cached]
Store[Store response to disk]
ReturnCached[Return content from cache]
IsExpired{Expired?<br/>file last-write-time<br/>vs now}
CacheHit[Cache hit<br/>return cached content]
SendConditional["Send conditional request<br/>If-Modified-Since: {last-modified}<br/>If-None-Match: {etag}"]
IsNetworkError{Network error?}
IsStaleIfError{staleIfError<br/>enabled?}
ReturnStale[Return stale<br/>cached content]
IsNoStore{Cache-Control:<br/>no-store?}
IsNoCache{Cache-Control:<br/>no-cache?}
StoreRevalidate[Store response<br/>always revalidate next time]
IsNotModified{HTTP 304<br/>Not Modified?}
IsSuccessRevalidate{HTTP 2xx?}
IsStaleIfErrorRevalidate{staleIfError<br/>enabled?}
Request --> IsGetOrHead
IsGetOrHead -->|No| Passthrough
IsGetOrHead -->|Yes| CacheExists
CacheExists -->|No| SendNew
SendNew --> IsSuccess
IsSuccess -->|No| ThrowException
IsSuccess -->|Yes| IsNoStoreNew
IsNoStoreNew -->|Yes| ReturnDirect
IsNoStoreNew -->|No| Store
Store --> ReturnCached
CacheExists -->|Yes| IsExpired
IsExpired -->|Not expired| CacheHit
IsExpired -->|Expired| SendConditional
SendConditional --> IsNetworkError
IsNetworkError -->|Yes| IsStaleIfError
IsStaleIfError -->|Yes| ReturnStale
IsStaleIfError -->|No| ThrowException
IsNetworkError -->|No| IsNoStore
IsNoStore -->|Yes| ReturnDirect
IsNoStore -->|No| IsNoCache
IsNoCache -->|Yes| StoreRevalidate
StoreRevalidate --> ReturnCached
IsNoCache -->|No| IsNotModified
IsNotModified -->|Yes| CacheHit
IsNotModified -->|No| IsSuccessRevalidate
IsSuccessRevalidate -->|Yes| Store
IsSuccessRevalidate -->|No| IsStaleIfErrorRevalidate
IsStaleIfErrorRevalidate -->|Yes| ReturnStale
IsStaleIfErrorRevalidate -->|No| ThrowException
When storing a response, the cache expiry is derived from response headers in this order:
Expiresheader — used as the absolute expiry timeCache-Control: max-age— expiry = now + max-age- Neither present — no expiry, file last-write-time set to min date (always revalidate)
The expiry is persisted as the cached file's last-write-time in the filesystem.
When a cached entry has expired, a conditional request is sent with:
If-Modified-Since— from theLast-Modifiedvalue stored in the cache filenameIf-None-Match— from theETagvalue stored in the cache filename (if present)
If the server responds 304 Not Modified, the cached content is reused without re-downloading.
Cyborg designed by Symbolon from The Noun Project.