Decorator Pattern — Cross-Cutting Concerns
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 IFusionCacheSerializer {
+SerializeAsync()
+DeserializeAsync()
}
class EncryptingFusionCacheSerializer {
-inner : IFusionCacheSerializer
-encryptor : ICacheValueEncryptor
+SerializeAsync()
+DeserializeAsync()
}
class ILocalizationOverrideStore {
+GetOverridesAsync()
+SetOverrideAsync()
}
class CachedLocalizationOverrideStore {
-inner : ILocalizationOverrideStore
-cache : IFusionCache
+GetOverridesAsync()
+SetOverrideAsync()
}
EncryptingFusionCacheSerializer --> IFusionCacheSerializer : decorates
CachedLocalizationOverrideStore --> ILocalizationOverrideStore : decorates
Implementation in Granit
Section titled “Implementation in Granit”| Decorator | File | Target | Added responsibilities |
|---|---|---|---|
EncryptingFusionCacheSerializer | src/Granit.Caching/Internal/EncryptingFusionCacheSerializer.cs | IFusionCacheSerializer | AES-256-GCM authenticated encryption of L2 (Redis) cache values |
CachedLocalizationOverrideStore | src/Granit.Localization/Internal/CachedLocalizationOverrideStore.cs | ILocalizationOverrideStore | FusionCache with per-tenant invalidation |
Custom variant — Conditional encryption: EncryptingFusionCacheSerializer
wraps the inner serializer and applies AES-256-GCM authenticated encryption to all
values written to L2 (Redis). L1 (in-process) stores unencrypted objects. Per-type
encryption is resolved via CacheEncryptionResolver using the [CacheEncrypted] attribute.
Rationale
Section titled “Rationale”Separating concerns (encryption, caching) from business 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 IFusionCache -- the decorator is transparentIFusionCache cache = serviceProvider .GetRequiredService<IFusionCache>();
PatientDto patient = await cache.GetOrSetAsync( $"patient:{patientId}", async (ctx, ct) => await db.Patients.FindAsync([patientId], ct), cancellationToken: cancellationToken);
// Behind the scenes:// 1. Check L1 (in-process memory)// 2. If miss -> check L2 (Redis, decrypted via EncryptingFusionCacheSerializer)// 3. If miss -> native stampede protection (only one factory call)// 4. Execute the factory// 5. Store in L1 + serialize -> encrypt -> store in L2 (Redis)