Skip to content

Granit.Caching

Granit.Caching provides a typed, provider-swappable cache layer. Same ICacheService<T> interface whether you’re using in-memory for dev, Redis for production, or HybridCache (L1 memory + L2 Redis) for multi-pod Kubernetes deployments. Built-in stampede protection, AES-256 encryption for GDPR-sensitive data, and convention-based key naming.

  • DirectoryGranit.Caching/ Base abstractions, in-memory default, AES encryption
    • DirectoryGranit.Caching.StackExchangeRedis/ Redis L2 provider, health checks
      • Granit.Caching.Hybrid HybridCache L1+L2 (memory + Redis)
PackageRoleDepends on
Granit.CachingICacheService<T>, stampede protection, encryptionGranit.Core
Granit.Caching.StackExchangeRedisRedis provider, health checkGranit.Caching
Granit.Caching.HybridHybridCache L1+L2Granit.Caching.StackExchangeRedis, Granit.Timing
graph TD
    C[Granit.Caching] --> CO[Granit.Core]
    R[Granit.Caching.StackExchangeRedis] --> C
    H[Granit.Caching.Hybrid] --> R
    H --> T[Granit.Timing]
[DependsOn(typeof(GranitCachingModule))]
public class AppModule : GranitModule { }

No configuration needed. Uses MemoryDistributedCache internally.

The main abstraction — inject it with your cache item type:

public class PatientCacheItem
{
public Guid Id { get; set; }
public string FullName { get; set; } = string.Empty;
public DateOnly DateOfBirth { get; set; }
}
public class PatientService(
ICacheService<PatientCacheItem, Guid> cache,
AppDbContext db)
{
public async Task<PatientCacheItem?> GetAsync(
Guid id, CancellationToken cancellationToken)
{
return await cache.GetOrAddAsync(id, async ct =>
{
var patient = await db.Patients
.Where(p => p.Id == id)
.Select(p => new PatientCacheItem
{
Id = p.Id,
FullName = $"{p.FirstName} {p.LastName}",
DateOfBirth = p.DateOfBirth
})
.FirstOrDefaultAsync(ct)
.ConfigureAwait(false);
return patient!;
}, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
}
public interface ICacheService<TCacheItem>
{
Task<TCacheItem?> GetAsync(string key, CancellationToken cancellationToken = default);
Task<TCacheItem> GetOrAddAsync(string key,
Func<CancellationToken, Task<TCacheItem>> factory,
DistributedCacheEntryOptions? options = null,
CancellationToken cancellationToken = default);
Task SetAsync(string key, TCacheItem value,
DistributedCacheEntryOptions? options = null,
CancellationToken cancellationToken = default);
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
Task RefreshAsync(string key, CancellationToken cancellationToken = default);
}

ICacheService<TCacheItem, TKey> adds overloads accepting TKey instead of string (converted via ToString()).

Cache keys follow the pattern: {KeyPrefix}:{CacheName}:{userKey}

PartSourceExample
KeyPrefixCachingOptions.KeyPrefixmyapp
CacheNameType name (strips CacheItem suffix) or [CacheName]Patient
userKeyCaller-providedd4e5f6a7-...

Result: myapp:Patient:d4e5f6a7-...

Override the convention with [CacheName]:

[CacheName("PatientSummary")]
public class PatientSummaryCacheItem { /* ... */ }
// Key: myapp:PatientSummary:{userKey}

GetOrAddAsync guarantees one factory execution under concurrent requests:

sequenceDiagram
    participant R1 as Request 1
    participant R2 as Request 2
    participant C as CacheService
    participant DB as Database

    R1->>C: GetOrAddAsync("patient:123")
    R2->>C: GetOrAddAsync("patient:123")
    C->>C: Cache miss
    C->>C: Acquire SemaphoreSlim
    Note over R2,C: R2 waits (lock held)
    C->>DB: Execute factory
    DB-->>C: PatientCacheItem
    C->>C: Store in cache
    C->>C: Release lock
    C-->>R1: PatientCacheItem
    C->>C: Double-check → cache hit
    C-->>R2: PatientCacheItem (from cache)

SemaphoreSlim instances are stored in a dedicated IMemoryCache with SizeLimit=10,000 and auto-cleanup after 30 seconds.

AES-256-CBC encryption for GDPR-sensitive cached data. Each encryption operation generates a random IV (16 bytes).

[CacheEncrypted] // Always encrypt this type
public class PatientCacheItem { /* ... */ }
[CacheEncrypted(false)] // Never encrypt (override global setting)
public class CountryCacheItem { /* ... */ }

Priority: [CacheEncrypted] attribute > CachingOptions.EncryptValues global flag.

Two-tier cache for multi-pod Kubernetes deployments:

TierBackendLatencyScope
L1In-memory per pod< 1 msPod-local
L2Redis (shared)~ 2 msCluster-wide
Pod A: GetOrAddAsync("patient:123")
L1 miss → L2 miss → DB query → store L1 (30s) + L2 (1h)
Pod B: GetOrAddAsync("patient:123")
L1 miss → L2 hit → ~2ms
RemoveAsync on Pod A:
Clears L2 + L1 on Pod A
Pod B L1: expires within LocalCacheExpiration (30s)

The LocalCacheExpiration setting bounds the stale-data window between pods.

builder.Services.AddHealthChecks()
.AddGranitRedisHealthCheck(degradedThreshold: TimeSpan.FromMilliseconds(100));
LatencyStatusEffect
< 100 msHealthyNormal
>= 100 msDegradedPod stays in load balancer
UnreachableUnhealthyPod removed from load balancer

Tagged ["readiness"] — integrates with Kubernetes readiness probes.

{
"Cache": {
"KeyPrefix": "myapp",
"DefaultAbsoluteExpirationRelativeToNow": "01:00:00",
"DefaultSlidingExpiration": "00:20:00",
"EncryptValues": false,
"Encryption": {
"Key": "<base64-aes256-key>"
},
"Redis": {
"IsEnabled": true,
"Configuration": "redis-service:6379",
"InstanceName": "myapp:"
},
"Hybrid": {
"LocalCacheExpiration": "00:00:30"
}
}
}
PropertyDefaultDescription
KeyPrefix"dd"Global key prefix
DefaultAbsoluteExpirationRelativeToNow01:00:00Default absolute TTL
DefaultSlidingExpiration00:20:00Default sliding TTL
EncryptValuesfalseGlobal encryption flag
Encryption.KeyAES-256 key (base64, 32 bytes)
Redis.IsEnabledtrueEnable/disable Redis module
Redis.Configuration"localhost:6379"StackExchange.Redis connection string
Redis.InstanceName"dd:"Redis key prefix (app isolation)
Hybrid.LocalCacheExpiration00:00:30L1 memory TTL (stale-data window)
CategoryKey typesPackage
ModuleGranitCachingModule, GranitCachingRedisModule, GranitCachingHybridModule
AbstractionsICacheService<T>, ICacheService<T, TKey>, ICacheValueEncryptorGranit.Caching
Attributes[CacheName], [CacheEncrypted]Granit.Caching
OptionsCachingOptions, CacheEncryptionOptions, RedisCachingOptions, HybridCachingOptions
ExtensionsAddGranitCaching(), AddGranitCachingRedis(), AddGranitCachingHybrid(), AddGranitRedisHealthCheck()