Decorator
Definition
Section titled “Definition”The Decorator pattern dynamically adds responsibilities to an object without modifying its class. Each decorator wraps the original object and enriches its behavior (serialization, encryption, caching, anti-stampede protection).
Diagram
Section titled “Diagram”classDiagram
class IDistributedCache {
+GetAsync()
+SetAsync()
}
class DistributedCacheService {
-cache : IDistributedCache
-encryptor : ICacheValueEncryptor
-semaphore : SemaphoreSlim
+GetOrAddAsync()
}
class ILocalizationOverrideStore {
+GetOverridesAsync()
+SetOverrideAsync()
}
class CachedLocalizationOverrideStore {
-inner : ILocalizationOverrideStore
-memoryCache : IMemoryCache
+GetOverridesAsync()
+SetOverrideAsync()
}
DistributedCacheService --> IDistributedCache : decorates
CachedLocalizationOverrideStore --> ILocalizationOverrideStore : decorates
Implementation in Granit
Section titled “Implementation in Granit”| Decorator | File | Target | Added responsibilities |
|---|---|---|---|
DistributedCacheService | src/Granit.Caching/DistributedCacheService.cs | IDistributedCache | JSON serialization, ICacheValueEncryptor encryption, double-check locking anti-stampede |
CachedLocalizationOverrideStore | src/Granit.Localization/CachedLocalizationOverrideStore.cs | ILocalizationOverrideStore | In-memory cache with per-tenant invalidation |
Custom variant — Conditional encryption: DistributedCacheService
applies AES-256-CBC encryption only if the target type carries the
[CacheEncrypted] attribute or if the configuration requires it.
Rationale
Section titled “Rationale”Separating concerns (serialization, encryption, anti-stampede) from cache logic allows testing and configuring them independently. The localization decorator avoids hitting the database on every translation resolution.
Usage example
Section titled “Usage example”// The consumer uses ICacheService<T> -- the decorator is transparentICacheService<PatientDto> cache = serviceProvider .GetRequiredService<ICacheService<PatientDto>>();
PatientDto patient = await cache.GetOrAddAsync( $"patient:{patientId}", async ct => await db.Patients.FindAsync([patientId], ct), cancellationToken);
// Behind the scenes:// 1. Check IDistributedCache (Redis)// 2. If miss -> SemaphoreSlim (anti-stampede)// 3. Double-check after lock// 4. Execute the factory// 5. Serialize to JSON -> encrypt (if [CacheEncrypted]) -> store in Redis