Caching
Arc4u lets you declare one or more named caches in appsettings.json, then use them through a single
interface, ICache. The store behind a name (process memory, Redis, SQL Server or a Dapr state
store) is a configuration choice, so the same code can use memory on a developer machine and Redis in production.
This guide builds on the named cache and
cache context concepts; see the
concepts overview for the rest of the framework.
What it solves
.NET already provides IMemoryCache and IDistributedCache. Arc4u does not replace them: the memory, Redis and SQL Server
providers wrap the standard IDistributedCache implementations. On top of them, Arc4u adds:
- Several caches by name. Declare a small cache in memory and a shared one in Redis, and pick each by name in code.
- Configuration instead of code. The kind and the settings of every cache come from the
Cachingsection. - Typed values. You put and get objects, not
byte[]. The value is serialized for you, even in the memory cache, with anIObjectSerialization(System.Text.Json). - One interface for all stores, so a cache can change kind without changing the calling code.
flowchart LR
App["Your code"] --> Ctx["ICacheContext"]
Ctx -->|"Default or name"| Cache["ICache"]
Cache --> Ser["IObjectSerialization"]
Cache --> Store["Memory / Redis / SQL Server"]
Cache -.->|"Dapr client instead"| Dapr["Dapr state store"]
Note
HybridCache (the .NET hybrid cache) is not implemented. The
Arc4u 9.0 roadmap (an open issue) lists "Upgrade Caching to use
HybridCache". The 9.0 code does not reference
HybridCache: caches are ICache instances as described on this page.
Packages involved
| Package | Use it for |
|---|---|
Arc4u.Caching |
ICacheContext, ICache, the Caching configuration section and AddCacheContext. Every other caching package references it. |
Arc4u.Caching.Memory |
The Memory kind: a cache in the memory of the process. |
Arc4u.Caching.Redis |
The Redis and RedisSentinel kinds: a shared cache in Redis. |
Arc4u.Caching.SqlServer |
The Sql kind: a cache in a SQL Server table. The project is in the src/Arc4u.Caching.Sql folder. |
Arc4u.Caching.Dapr |
The Dapr kind: a cache in a Dapr state store. |
Arc4u.Serializer |
The IObjectSerialization contract. |
Arc4u.Serializer.JSon |
The JSON serializers: plain and compressed. |
Arc4u.Caching references Arc4u.Serializer. Each provider package references Arc4u.Caching. The serializer
implementations are not referenced by the providers: install Arc4u.Serializer.JSon yourself, except for the Dapr kind,
which does not use it.
Install
Install the core package, one package per kind you use, and the JSON serializer. For example, for a memory and a Redis cache:
dotnet add package Arc4u.Caching --prerelease
dotnet add package Arc4u.Caching.Memory --prerelease
dotnet add package Arc4u.Caching.Redis --prerelease
dotnet add package Arc4u.Serializer.JSon --prerelease
The caches log through the Arc4u logger. Register it with AddILogger() (see Diagnostics);
without it the first cache created fails with InvalidOperationException: Bad Arc4u usage..
Configuration
AddCacheContext(configuration, sectionName = "Caching") reads the section to register the options of each cache, and
CacheContext, the ICacheContext implementation, reads the same section to create the caches.
The examples below use the default name Caching.
appsettings.json
{
"Caching": {
"Default": "Volatile",
"Caches": [
{
"Name": "Volatile",
"Kind": "Memory",
"IsAutoStart": true,
"Settings": {
"SizeLimitInMB": 100
}
},
{
"Name": "Shared",
"Kind": "Redis",
"IsAutoStart": true,
"Settings": {
"ConnectionString": "localhost:6379",
"InstanceName": "MyApp-"
}
}
]
}
}
| Key | Type | Default | Description |
|---|---|---|---|
Caching:Default |
string |
empty | Name of the cache returned by ICacheContext.Default. When it is empty, no cache is created at all. |
Caching:Caches |
array | empty | The declared caches. |
Caching:Caches:n:Name |
string |
empty | Name used to get the cache from ICacheContext. Also the name of its options. |
Caching:Caches:n:Kind |
string |
empty | Memory, Redis, RedisSentinel, Sql or Dapr, case-insensitive. Any other value is a custom kind and must match, with the same casing, the key of the keyed ICache service that implements it. |
Caching:Caches:n:IsAutoStart |
bool |
false |
true creates and initializes the cache when the ICacheContext is first resolved; false does it on the first access to the cache. |
Caching:Caches:n:Settings |
object | none | Settings of the kind. The keys are in the page of each kind. |
The section replaces the format of the 8.x documentation: Memory.Settings, SizeLimit, SizeLimitInMegaBytes and a
Principal block are not read by Arc4u 9.
Code
AddCacheContext registers ICacheContext and the options of each declared cache. Each Arc4u.Caching.* package
registers its cache implementations with its own extension: AddMemoryCacheKind(), AddRedisCacheKinds() (Redis and
RedisSentinel), AddSqlCacheKind() and AddDaprCacheKind(). Register the serializer yourself.
// Program.cs
using Arc4u.Caching;
using Arc4u.Caching.Memory;
using Arc4u.Caching.Redis;
using Arc4u.Dependency;
using Arc4u.Serializer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddILogger();
builder.Services.AddCacheContext(builder.Configuration);
// The serializer used by the caches (the Memory, Redis and Sql kinds).
builder.Services.AddSingleton<IObjectSerialization, JsonSerialization>();
// The ICache implementations of the kinds you use.
builder.Services.AddMemoryCacheKind();
builder.Services.AddRedisCacheKinds();
var app = builder.Build();
app.Run();
The constants CacheContext.Memory, Redis, RedisSentinel, Sql and Dapr hold the kind names; the extensions register each implementation as a keyed ICache service with that key. The
[Export] attributes on the cache classes let the Arc4u code generator write the same registrations
(see Dependency injection).
Common scenarios
Use the default cache
Inject ICacheContext and use Default, the cache named by Caching:Default:
using Arc4u.Caching;
public class Catalog(ICacheContext caches)
{
public async Task<Product?> GetAsync(string id)
{
var product = await caches.Default.GetAsync<Product>(id);
if (product is null)
{
product = new Product(id, "Widget");
await caches.Default.PutAsync(id, TimeSpan.FromMinutes(10), product);
}
return product;
}
}
public record Product(string Id, string Name);
Get returns default when the key does not exist; TryGetValue returns false for a missing key, so it tells a stored 0
or false apart from no value. Put without a timeout keeps the value until it is removed or evicted by the store; with a
timeout, the value expires after it. Every kind behaves the same way:
PutandPutAsyncrejectnullwithArgumentNullExceptionand store the default of a value type (0,false).- An error of the store or of the serializer is thrown as
DataCacheException, with the original exception asInnerException, byGetandPut.TryGetValueandRemovereturnfalseinstead.
Use several caches
Get another cache by name with the indexer. Exist tells whether a name is declared, without creating the cache.
using Arc4u.Caching;
public class Sessions(ICacheContext caches)
{
public Task SaveAsync(string id, string data)
{
// A shared cache for values that other instances of the service must see.
return caches["Shared"].PutAsync(id, TimeSpan.FromHours(1), data);
}
public bool IsShared => caches.Exist("Shared");
}
The indexer throws InvalidOperationException when no cache has that name.
Expire values
using Arc4u.Caching;
public static class Expiration
{
public static async Task RunAsync(ICache cache)
{
// Absolute: the value disappears 5 minutes after it was written.
await cache.PutAsync("report", TimeSpan.FromMinutes(5), "value");
// Sliding: the 5 minutes restart each time the value is read.
await cache.PutAsync("token", TimeSpan.FromMinutes(5), "value", isSlided: true);
}
}
The Dapr cache does not support the sliding expiration and throws NotSupportedException.
Use a different kind per environment
Because the kind is configuration, keep the cache name and change the kind and its settings in an environment file such as
appsettings.Production.json. Register the keyed ICache of every kind you may use.
{
"Caching": {
"Default": "Volatile",
"Caches": [
{
"Name": "Volatile",
"Kind": "Redis",
"IsAutoStart": true,
"Settings": {
"ConnectionString": "redis:6379",
"InstanceName": "MyApp-"
}
}
]
}
}
.NET configuration merges array items by index, so this entry overrides the first cache of the base file: repeat its
Name. Keys of the base file that the override does not repeat stay in place. In containers, replace : by __ in environment
variable names (Caching__Caches__0__Kind).
Choose a provider
| Kind | Package | Shared between instances | Survives a restart | Sliding expiration | Serializer |
|---|---|---|---|---|---|
Memory |
Arc4u.Caching.Memory |
No | No | Yes | IObjectSerialization |
Redis, RedisSentinel |
Arc4u.Caching.Redis |
Yes | Depends on the Redis persistence | Yes | IObjectSerialization |
Sql |
Arc4u.Caching.SqlServer |
Yes | Yes | Yes | IObjectSerialization |
Dapr |
Arc4u.Caching.Dapr |
Depends on the state store | Depends on the state store | No | The Dapr client |
Extensibility points
| Service | Default implementation | Replace it to |
|---|---|---|
| ICache (keyed by kind) | MemoryCache, RedisCache, RedisSentinelCache, SqlCache, DaprCache |
Add another store. Register your class with AddKeyedTransient<ICache, MyCache>("MyKind") and use "MyKind" as Kind. |
| IObjectSerialization | JsonSerialization and its compressed variants |
Change the format or the JSON options. See Serialization. |
| ICacheContext | CacheContext | Change how caches are created. AddCacheContext uses TryAddSingleton, so register yours first. |
A custom kind is not covered by AddCacheContext, which only registers the options of the five known kinds: read
your own options yourself. Deriving from BaseDistributeCache<T> gives you the serializer handling,
the expiration and the tracing on top of an IDistributedCache.
Troubleshooting
InvalidOperationException: Bad Arc4u usage. when the cache context is created
The caches log with the Arc4u logger. Call builder.Services.AddILogger() (the order relative to AddCacheContext does not matter).
InvalidOperationException: Section 'Caching' not found in configuration.
AddCacheContext requires the section. Add the Caching section to the configuration.
InvalidOperationException: There is no cache configured with the name ...
The name is not declared in Caching:Caches, or the cache could not be created. Check Exist(name), then the log of
CacheContext: Cannot resolve an ICache instance with the name: <Kind> means that no keyed ICache is registered for
that Kind. Default fails the same way when Caching:Default is empty or is not a declared name.
CacheNotInitializedException
The cache exists but could not finish its initialization. For the Memory, Redis and Sql kinds, the message says that an
IObjectSerialization cannot be resolved: register one (see Serialization).