Replicant
A wrapper for HttpClient that caches to disk. Cached files, over the max specified, are deleted based on the last access times.
Install / Use
/learn @SimonCropp/ReplicantREADME
<img src="/src/icon.png" height="30px"> Replicant
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:
NuGet package
https://nuget.org/packages/Replicant/
Usage
Default instance
There is a default static instance:
<!-- snippet: DefaultInstance --><a id='snippet-DefaultInstance'></a>
var content = await HttpCache.Default.DownloadAsync("https://httpbin.org/status/200");
<sup><a href='/src/Tests/HttpCacheTests.cs#L87-L91' title='Snippet source file'>snippet source</a> | <a href='#snippet-DefaultInstance' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->This caches to {Temp}/Replicant.
Construction
An instance of HttpCache should be long running.
<!-- snippet: Construction --><a id='snippet-Construction'></a>
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();
<sup><a href='/src/Tests/HttpCacheTests.cs#L25-L40' title='Snippet source file'>snippet source</a> | <a href='#snippet-Construction' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Dependency injection
Add HttpCache as a singleton when using dependency injection.
<!-- snippet: DependencyInjection --><a id='snippet-DependencyInjection'></a>
var services = new ServiceCollection();
services.AddSingleton(_ => new HttpCache(cacheDirectory));
using var provider = services.BuildServiceProvider();
var httpCache = provider.GetRequiredService<HttpCache>();
NotNull(httpCache);
<sup><a href='/src/Tests/HttpCacheTests.cs#L48-L57' title='Snippet source file'>snippet source</a> | <a href='#snippet-DependencyInjection' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Using HttpClient with HttpClientFactory.
<!-- snippet: DependencyInjectionWithHttpFactory --><a id='snippet-DependencyInjectionWithHttpFactory'></a>
var services = new ServiceCollection();
services.AddHttpClient();
services.AddSingleton(_ =>
{
var clientFactory = _.GetRequiredService<IHttpClientFactory>();
return new HttpCache(cacheDirectory, clientFactory.CreateClient);
});
using var provider = services.BuildServiceProvider();
var httpCache = provider.GetRequiredService<HttpCache>();
NotNull(httpCache);
<sup><a href='/src/Tests/HttpCacheTests.cs#L65-L79' title='Snippet source file'>snippet source</a> | <a href='#snippet-DependencyInjectionWithHttpFactory' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->DelegatingHandler
ReplicantHandler can be used as a DelegatingHandler in the HttpClient pipeline:
<!-- snippet: ReplicantHandlerUsage --><a id='snippet-ReplicantHandlerUsage'></a>
var handler = new ReplicantHandler(cacheDirectory)
{
InnerHandler = new HttpClientHandler()
};
using var client = new HttpClient(handler);
var response = await client.GetAsync("https://example.com");
<sup><a href='/src/Tests/CachingHandlerTests.cs#L26-L35' title='Snippet source file'>snippet source</a> | <a href='#snippet-ReplicantHandlerUsage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->DelegatingHandler with HttpClientFactory
ReplicantHandler integrates with HttpClientFactory using AddHttpMessageHandler:
<a id='snippet-HttpClientFactoryUsage'></a>
var services = new ServiceCollection();
services.AddHttpClient("CachedClient")
.AddHttpMessageHandler(() => new ReplicantHandler(cacheDirectory));
<sup><a href='/src/Tests/CachingHandlerTests.cs#L40-L46' title='Snippet source file'>snippet source</a> | <a href='#snippet-HttpClientFactoryUsage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->To share a single cache (and purge timer) across multiple named clients, register a ReplicantCache as a singleton:
<a id='snippet-HttpClientFactorySharedCacheUsage'></a>
var services = new ServiceCollection();
services.AddReplicantCache(cacheDirectory);
services.AddHttpClient("CachedClient")
.AddReplicantCaching();
<sup><a href='/src/Tests/CachingHandlerTests.cs#L51-L58' title='Snippet source file'>snippet source</a> | <a href='#snippet-HttpClientFactorySharedCacheUsage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->HybridCache support
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):
<a id='snippet-DistributedCacheUsage'></a>
var services = new ServiceCollection();
services.AddReplicantDistributedCache(cacheDirectory);
services.AddHybridCache();
<sup><a href='/src/Tests/DistributedCacheTests.cs#L25-L31' title='Snippet source file'>snippet source</a> | <a href='#snippet-DistributedCacheUsage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Single cache per directory
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).
Get a string
<!-- snippet: string --><a id='snippet-string'></a>
var content = await httpCache.StringAsync("https://httpbin.org/json");
<sup><a href='/src/Tests/HttpCacheTests.cs#L287-L291' title='Snippet source file'>snippet source</a> | <a href='#snippet-string' title='Start of snippet'>anchor</a></sup> <a id='snippet-string-1'></a>
var lines = new List<string>();
await foreach (var line in httpCache.LinesAsync("https://httpbin.org/json"))
{
lines.Add(line);
}
<sup><a href='/src/Tests/HttpCacheTests.cs#L302-L310' title='Snippet source file'>snippet source</a> | <a href='#snippet-string-1' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Get bytes
<!-- snippet: bytes --><a id='snippet-bytes'></a>
var bytes = await httpCache.BytesAsync("https://httpbin.org/json");
<sup><a href='/src/Tests/HttpCacheTests.cs#L321-L325' title='Snippet source file'>snippet source</a> | <a href='#snippet-bytes' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Get a stream
<!-- snippet: stream --><a id='snippet-stream'></a>
using var stream = await httpCache.StreamAsync("https://httpbin.org/json");
<sup><a href='/src/Tests/HttpCacheTests.cs#L336-L340' title='Snippet source file'>snippet source</a> | <a href='#snippet-stream' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Download to a file
<!-- snippet: ToFile --><a id='snippet-ToFile'></a>
await httpCache.ToFileAsync("https://httpbin.org/json", targetFile);
<sup><a href='/src/Tests/HttpCacheTests.cs#L353-L357' title='Snippet source file'>snippet source</a> | <a href='#snippet-ToFile' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Download to a stream
<!-- snippet: ToStream --><a id='snippet-ToStream'></a>
await httpCache.ToStreamAsync("https://httpbin.org/json", targetStream);
<sup><a href='/src/Tests/HttpCacheTests.cs#L374-L378' title='Snippet source file'>snippet source</a> | <a href='#snippet-ToStream' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Manually add an item to the cache
<!-- snippet: AddItem --><a id='snippet-AddItem'></a>
using var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("the content")
};
await httpCache.AddItemAsync(uri, response);
<sup><a href='/src/Tests/HttpCacheTests.cs#L439-L447' title='Snippet source file'>snippet source</a> | <a href='#snippet-AddItem' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->Use stale item on error
If an error occurs when re-validating a potentially stale item, then the cached item can be used as a fallback.
<!-- snippet: staleIfError --><a id='snippet-staleIfError'></a>
var content = httpCache.StringAsync(uri, staleIfError: true);
<sup><a href='/src/Tests/HttpCacheTests.cs#L524-L528' title='Snippet source file'>snippet source</a> | <a href='#snippet-staleIfError' title='Start of snippet'>an
