Configure caching
Granit.Caching provides a typed cache abstraction (ICacheService<T>) that works
identically across three providers: in-memory for development, Redis for production,
and HybridCache (L1 memory + L2 Redis) for multi-pod Kubernetes deployments.
Built-in stampede protection and optional AES-256 encryption for GDPR-sensitive data.
Prerequisites
Section titled “Prerequisites”- A working Granit application
- Redis server (for production setups)
Step 1 — Install the package
Section titled “Step 1 — Install the package”Choose the package matching your deployment target:
dotnet add package Granit.Caching[DependsOn(typeof(GranitCachingModule))]public sealed class AppModule : GranitModule { }No additional configuration needed. Uses MemoryDistributedCache internally.
dotnet add package Granit.Caching.StackExchangeRedis[DependsOn(typeof(GranitCachingRedisModule))]public sealed class AppModule : GranitModule { }dotnet add package Granit.Caching.Hybrid[DependsOn(typeof(GranitCachingHybridModule))]public sealed class AppModule : GranitModule { }Hybrid pulls in Caching.StackExchangeRedis transitively.
Step 2 — Configure appsettings.json
Section titled “Step 2 — Configure appsettings.json”{ "Cache": { "KeyPrefix": "myapp" }}{ "Cache": { "KeyPrefix": "myapp", "DefaultAbsoluteExpirationRelativeToNow": "01:00:00", "DefaultSlidingExpiration": "00:20:00", "EncryptValues": true, "Encryption": { "Key": "<base64-aes256-key-from-vault>" }, "Redis": { "IsEnabled": true, "Configuration": "redis-service:6379", "InstanceName": "myapp:" } }}{ "Cache": { "KeyPrefix": "myapp", "EncryptValues": true, "Encryption": { "Key": "<base64-aes256-key-from-vault>" }, "Redis": { "IsEnabled": true, "Configuration": "redis-service:6379", "InstanceName": "myapp:" }, "Hybrid": { "LocalCacheExpiration": "00:00:30" } }}Step 3 — Use ICacheService in your code
Section titled “Step 3 — Use ICacheService in your code”String key (simple)
Section titled “String key (simple)”public sealed class UserService(ICacheService<UserCacheItem> cache){ public async Task<UserCacheItem> GetByIdAsync( Guid id, CancellationToken cancellationToken) { return await cache.GetOrAddAsync( id.ToString(), async ct => await LoadUserFromDatabaseAsync(id, ct), cancellationToken: cancellationToken); }}Typed key
Section titled “Typed key”public sealed class UserService(ICacheService<UserCacheItem, Guid> cache){ public async Task<UserCacheItem> GetByIdAsync( Guid id, CancellationToken cancellationToken) { return await cache.GetOrAddAsync( id, async ct => await LoadUserFromDatabaseAsync(id, ct), cancellationToken: cancellationToken); }}The business code is identical regardless of the provider (Memory, Redis, or Hybrid).
Step 4 — Customize cache key naming
Section titled “Step 4 — Customize cache key naming”Keys are built automatically using the format {KeyPrefix}:{CacheName}:{userKey}.
KeyPrefix | Cache item type | User key | Final key |
|---|---|---|---|
myapp | UserCacheItem | d4e5f6 | myapp:User:d4e5f6 |
myapp | PatientCacheItem | p-001 | myapp:Patient:p-001 |
The convention automatically strips the CacheItem suffix from the type name.
Use the [CacheName] attribute to override the convention:
[CacheName("Session")]public sealed class SessionData{ public string UserId { get; init; } = default!; public DateTimeOffset ExpiresAt { get; init; }}This produces keys like myapp:Session:abc instead of myapp:SessionData:abc.
Step 5 — Enable encryption for sensitive data
Section titled “Step 5 — Enable encryption for sensitive data”Global encryption
Section titled “Global encryption”Set EncryptValues: true in the Cache section to encrypt all cached values
with AES-256-CBC.
Per-type control
Section titled “Per-type control”Use [CacheEncrypted] to control encryption independently of the global flag:
// Always encrypted, even if EncryptValues = false[CacheEncrypted]public sealed class PatientCacheItem{ public string Name { get; init; } = default!; public DateOnly DateOfBirth { get; init; }}
// Never encrypted, even if EncryptValues = true[CacheEncrypted(false)]public sealed class PublicConfigCacheItem{ public string ThemeColor { get; init; } = default!;}
// Follows the global EncryptValues flagpublic sealed class UserSessionCacheItem{ public string Token { get; init; } = default!;}The attribute takes priority over the global EncryptValues flag.
Step 6 — Cache invalidation
Section titled “Step 6 — Cache invalidation”Remove a cached entry explicitly:
await cache.RemoveAsync("user-id", cancellationToken);Invalidation in multi-pod deployments
Section titled “Invalidation in multi-pod deployments”With HybridCache, RemoveAsync clears the L2 (Redis) and the local L1 cache of
the calling pod. Other pods’ L1 caches expire naturally within the
LocalCacheExpiration window (default: 30 seconds).
Configuration reference
Section titled “Configuration reference”CachingOptions (Cache section)
Section titled “CachingOptions (Cache section)”| Property | Type | Default | Description |
|---|---|---|---|
KeyPrefix | string | "dd" | Prefix for all cache keys |
DefaultAbsoluteExpirationRelativeToNow | TimeSpan? | 1h | Default absolute expiration |
DefaultSlidingExpiration | TimeSpan? | 20min | Default sliding expiration |
EncryptValues | bool | false | Enable AES-256 encryption globally |
RedisCachingOptions (Cache:Redis section)
Section titled “RedisCachingOptions (Cache:Redis section)”| Property | Type | Default | Description |
|---|---|---|---|
IsEnabled | bool | true | Disable Redis without changing the loaded module |
Configuration | string | "localhost:6379" | StackExchange.Redis connection string |
InstanceName | string | "dd:" | Redis key prefix for multi-app isolation |
HybridCachingOptions (Cache:Hybrid section)
Section titled “HybridCachingOptions (Cache:Hybrid section)”| Property | Type | Default | Description |
|---|---|---|---|
LocalCacheExpiration | TimeSpan | 30s | L1 cache TTL (bounds staleness across pods) |
Stampede protection
Section titled “Stampede protection”GetOrAddAsync guarantees the factory executes only once, even under heavy
concurrency (10+ simultaneous requests for the same missing key):
- Lock-free check (cache hit returns immediately)
- Acquire lock (
SemaphoreSlimstored in a boundedIMemoryCache, TTL 30s) - Double-check (another thread may have populated the cache)
- Execute the factory (guaranteed single execution)
With HybridCache, stampede protection is built into the .NET runtime — no
additional SemaphoreSlim needed.
Next steps
Section titled “Next steps”- Configure multi-tenancy — tenant-aware cache keys
- Add an endpoint — use cached data in your API endpoints
- Caching reference — full architecture and service registration details