Skip to content

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.

  • A working Granit application
  • Redis server (for production setups)

Choose the package matching your deployment target:

Terminal window
dotnet add package Granit.Caching
[DependsOn(typeof(GranitCachingModule))]
public sealed class AppModule : GranitModule { }

No additional configuration needed. Uses MemoryDistributedCache internally.

{
"Cache": {
"KeyPrefix": "myapp"
}
}
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);
}
}
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).

Keys are built automatically using the format {KeyPrefix}:{CacheName}:{userKey}.

KeyPrefixCache item typeUser keyFinal key
myappUserCacheItemd4e5f6myapp:User:d4e5f6
myappPatientCacheItemp-001myapp: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”

Set EncryptValues: true in the Cache section to encrypt all cached values with AES-256-CBC.

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 flag
public sealed class UserSessionCacheItem
{
public string Token { get; init; } = default!;
}

The attribute takes priority over the global EncryptValues flag.

Remove a cached entry explicitly:

await cache.RemoveAsync("user-id", cancellationToken);

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).

PropertyTypeDefaultDescription
KeyPrefixstring"dd"Prefix for all cache keys
DefaultAbsoluteExpirationRelativeToNowTimeSpan?1hDefault absolute expiration
DefaultSlidingExpirationTimeSpan?20minDefault sliding expiration
EncryptValuesboolfalseEnable AES-256 encryption globally
PropertyTypeDefaultDescription
IsEnabledbooltrueDisable Redis without changing the loaded module
Configurationstring"localhost:6379"StackExchange.Redis connection string
InstanceNamestring"dd:"Redis key prefix for multi-app isolation

HybridCachingOptions (Cache:Hybrid section)

Section titled “HybridCachingOptions (Cache:Hybrid section)”
PropertyTypeDefaultDescription
LocalCacheExpirationTimeSpan30sL1 cache TTL (bounds staleness across pods)

GetOrAddAsync guarantees the factory executes only once, even under heavy concurrency (10+ simultaneous requests for the same missing key):

  1. Lock-free check (cache hit returns immediately)
  2. Acquire lock (SemaphoreSlim stored in a bounded IMemoryCache, TTL 30s)
  3. Double-check (another thread may have populated the cache)
  4. Execute the factory (guaranteed single execution)

With HybridCache, stampede protection is built into the .NET runtime — no additional SemaphoreSlim needed.