Extension: Caching¶
Overview¶
CleanArchitecture.Extensions.Caching provides cache abstractions, deterministic key generation, and a MediatR query caching behavior. It is provider-agnostic and can target in-memory or distributed caches without leaking infrastructure concerns into handlers.
When to use¶
- You want transparent query caching without embedding cache calls in handlers.
- You need deterministic, namespace-aware cache keys.
- You want to start with memory caching in development and switch to distributed cache in production.
Prereqs and compatibility¶
- Target framework:
net10.0. - Dependencies: MediatR
13.1.0,Microsoft.Extensions.Caching.*10.0.0.
Install¶
dotnet add src/Application/Application.csproj package CleanArchitecture.Extensions.Caching
dotnet add src/Infrastructure/Infrastructure.csproj package CleanArchitecture.Extensions.Caching
Register services¶
using CleanArchitecture.Extensions.Caching;
using CleanArchitecture.Extensions.Caching.Options;
builder.Services.AddCleanArchitectureCaching(options =>
{
options.DefaultNamespace = "MyApp";
options.MaxEntrySizeBytes = 256 * 1024;
}, behaviorOptions =>
{
behaviorOptions.DefaultTtl = TimeSpan.FromMinutes(5);
});
Add the MediatR behavior¶
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
cfg.AddCleanArchitectureCachingPipeline();
});
How query caching works¶
QueryCachingBehavior<TRequest, TResponse> applies cache-aside semantics:
- The default predicate caches requests that opt in via
ICacheableQueryor[CacheableQuery]. - The cache key uses the request type name as the resource and a SHA256 hash of the request payload.
- Cache hits short-circuit the handler; cache misses store the handler result.
Opt-in a query by marker interface or attribute:
using CleanArchitecture.Extensions.Caching;
using CleanArchitecture.Extensions.Caching.Abstractions;
[CacheableQuery]
public record GetTodosQuery : IRequest<TodosVm>;
// or
public record GetUserQuery(int Id) : IRequest<UserDto>, ICacheableQuery;
Configure request selection and TTLs via QueryCachingBehaviorOptions:
builder.Services.AddCleanArchitectureCaching(
configureQueryCaching: options =>
{
options.CachePredicate = request => request is ICacheableQuery;
options.DefaultTtl = TimeSpan.FromMinutes(2);
options.TtlByRequestType[typeof(GetUserQuery)] = TimeSpan.FromSeconds(30);
options.CacheNullValues = false;
});
Cache keys and scopes¶
- Key format:
{namespace}:{tenant?}:{resource}:{hash}. DefaultCacheKeyFactoryhashes the request payload as JSON (deterministic SHA256).ICacheScopesupplies the namespace and optional tenant segment.
If you customize keys, keep them deterministic and stable across versions. For user-scoped data, include user context in the hash or namespace.
Choose a cache adapter¶
The default ICache implementation is MemoryCacheAdapter.
Note
The memory adapter is process-local. In a multi-instance deployment, use a distributed cache.
To use a distributed cache, register IDistributedCache and swap the adapter:
using CleanArchitecture.Extensions.Caching.Adapters;
using Microsoft.Extensions.Caching.StackExchangeRedis;
builder.Services.AddCleanArchitectureCaching();
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "<redis-connection-string>";
});
builder.Services.AddSingleton<ICache, DistributedCacheAdapter>();
Serialization¶
The default serializer is SystemTextJsonCacheSerializer. Replace it when needed:
using CleanArchitecture.Extensions.Caching.Serialization;
builder.Services.AddSingleton<ICacheSerializer>(sp =>
new SystemTextJsonCacheSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)));
When multiple serializers are registered, set CachingOptions.PreferredSerializer to a content type or serializer type name.
Stampede protection and entry options¶
CachingOptions.StampedePolicycontrols locking, timeouts, and jitter.CachingOptions.DefaultEntryOptionsdefines expiration, priority, and size hints.
builder.Services.AddCleanArchitectureCaching(options =>
{
options.StampedePolicy = new CacheStampedePolicy
{
EnableLocking = true,
LockTimeout = TimeSpan.FromSeconds(3),
Jitter = TimeSpan.FromMilliseconds(50)
};
});
Invalidation guidance¶
Caching is read-through; invalidation is explicit. On command success or domain events, remove keys:
Keep key conventions stable and consider bumping the namespace for breaking DTO changes.
Multitenancy integration¶
If you use multitenancy, install the adapter and call AddCleanArchitectureMultitenancyCaching to include tenant IDs in cache keys:
dotnet add src/Infrastructure/Infrastructure.csproj package CleanArchitecture.Extensions.Multitenancy.Caching
builder.Services.AddCleanArchitectureCaching();
builder.Services.AddCleanArchitectureMultitenancyCaching();
Observability¶
QueryCachingBehaviorlogs cache hits and misses atDebuglevel.- Adapters log warnings on oversized payloads or deserialization failures.
Troubleshooting¶
- Cache is never hit: ensure the request type matches the cache predicate and the behavior is registered.
- Missing tenant in keys: install
CleanArchitecture.Extensions.Multitenancy.Cachingand callAddCleanArchitectureMultitenancyCachingafter caching registration. - Large payloads: raise
MaxEntrySizeBytesor skip caching viaResponseCachePredicate.
Samples and tests¶
See the caching tests under tests/ for behavior coverage and usage patterns.