Skip to main content

caching-patterns-dotnet

Apply in-memory, distributed, and hybrid caching in .NET with explicit expiration, invalidation, L1+L2 layering, and stampede-control patterns.

Source facts

Repository
visionarycoder/vc.Agentic
Last source activity
November 9, 2026 at 06:22
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
caching-patterns-dotnet
description
Apply in-memory, distributed, and hybrid caching in .NET with explicit expiration, invalidation, L1+L2 layering, and stampede-control patterns.
metadata
{"metadata-version":"1.0.0","title":"Caching Patterns for .NET","document-type":"skill","repository":"vc.Agentic","language":"Standard Technical English","updated":"2026-11-08","collection":"optional"}
# Caching Patterns for .NET Agent applies caching only when the cache improves latency or load without hiding correctness defects. ## When to Use | Condition | Use | |---|---| | Agent reduces repeated read cost | Use this skill | | Agent caches reference data, feature data, or external responses | Use this skill | | Agent chooses `IMemoryCache`, `IDistributedCache`, or `HybridCache` | Use this skill | | Agent adds expiration or invalidation strategy | Use this skill | | Agent implements L1+L2 cache layering | Use this skill | ## When Not to Use | Condition | Use | |---|---| | Source of truth changes every request | Use direct read path | | Defect is N+1 query or poor SQL shape | Fix query first | | Write path or transaction consistency is main bottleneck | Use non-cache optimization | ## Required Inputs | Input | Required | Notes | |---|---|---| | Cached data | Yes | Value, projection, or expensive computation | | Staleness budget | Yes | Minutes, seconds, or zero | | Cache scope | Yes | Process-local, shared across instances, or automatic L1+L2 | | Invalidation trigger | No | Expiration, write success, or event | | Stampede protection | No | Required for hot keys or expensive misses | ## Caching Solution Decision Matrix Agent selects the caching implementation by deployment scope, .NET version, and complexity tolerance. | Scenario | .NET Version | Deployment | Recommended Solution | Rationale | |---|---|---|---|---| | Single-instance API | Any | Single process | `IMemoryCache` | Simplest, lowest latency, no network cost | | Multi-instance service | .NET 9+ | Scaled-out | `HybridCache` | Automatic L1+L2, built-in stampede protection, tag-based invalidation | | Multi-instance service | .NET 8 or earlier | Scaled-out | `IDistributedCache` + manual L1 | Manual L1+L2 layering, explicit stampede control | | Read-heavy distributed | .NET 9+ | Scaled-out | `HybridCache` with Redis backend | Automatic L1 in-process + L2 Redis, transparent operation | | Read-heavy distributed | .NET 8 or earlier | Scaled-out | Custom L1+L2 wrapper over `IMemoryCache` + Redis | Explicit two-tier implementation | | High-consistency writes | Any | Any | `IDistributedCache` only (no L1) | Avoids L1 staleness on write-heavy workloads | | Hot key with expensive source | .NET 9+ | Any | `HybridCache` | Built-in per-key stampede protection | | Hot key with expensive source | .NET 8 or earlier | Any | `IDistributedCache` + `SemaphoreSlim` per key | Manual single-flight pattern | | Reference data (static) | Any | Any | `IMemoryCache` or `HybridCache` with long TTL | Data rarely changes; cache at startup (seeding) | | Rapidly changing data | Any | Any | Short TTL cache or cache-bypass | Stale data risk outweighs cache benefit unless non-critical | | Session state | Any | Scaled-out | `IDistributedCache` (Redis) | Per-user data; survives instance failure | ## L1 + L2 Caching Strategies Agent applies L1 (in-process) and L2 (distributed) cache layering when read latency matters and data is shared across instances. ### HybridCache (Recommended for .NET 9+) `HybridCache` provides automatic L1+L2 layering with no manual coordination. **Characteristics:** - L1: In-process memory cache (lowest latency) - L2: Distributed backend (Redis, SQL Server, etc.) - Stampede protection: Built-in per-key locking - Serialization: Automatic with `System.Text.Json` or custom serializers - Invalidation: Tag-based removal across L1 and L2 **When to use:** - Multi-instance .NET 9+ services - Read-heavy workloads with moderate write rates - Hot keys requiring stampede protection - Teams preferring framework-managed complexity **Pattern:** ```csharp // Agent registers HybridCache with Redis L2 backend services.AddHybridCache(options => { options.DefaultEntryOptions = new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(5), LocalCacheExpiration = TimeSpan.FromMinutes(1) }; }) .AddDistributedMemoryCache(); // or .AddStackExchangeRedisCache() // Agent uses HybridCache with automatic L1+L2 coordination var product = await hybridCache.GetOrCreateAsync( $"product:{productId}", async cancel => await LoadProductAsync(productId, cancel), tags: ["products"], cancellationToken: cancellationToken); ``` ### Manual L1 + L2 (.NET 8 and earlier) Agent implements explicit L1+L2 layering when `HybridCache` is unavailable. **Pattern:** ```csharp // Agent checks L1 first (fastest) if (!memoryCache.TryGetValue(key, out TValue value)) { // Agent checks L2 second var serialized = await distributedCache.GetStringAsync(key); if (serialized != null) { value = JsonSerializer.Deserialize<TValue>(serialized); // Agent backfills L1 memoryCache.Set(key, value, TimeSpan.FromMinutes(1)); } else { // Agent fetches from source value = await LoadFromSourceAsync(); // Agent writes to both layers await distributedCache.SetStringAsync(key, JsonSerializer.Serialize(value), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }); memoryCache.Set(key, value, TimeSpan.FromMinutes(1)); } } ``` **L1 vs L2 Expiration Guidelines:** - L1 expiration: 10-20% of L2 expiration (reduces cross-instance staleness) - L2 expiration: Matches source data staleness tolerance - Write invalidation: Removes both L1 and L2 entries ## Cache Invalidation and Write Consistency Strategies Agent selects invalidation strategy based on read-after-write freshness requirements and write frequency. ### Invalidate-on-Write (Cache-Aside) Agent removes cache entry after writing to source. Next read repopulates from source. **When to use:** - Read-heavy workloads with infrequent writes - Acceptable for readers to see stale data briefly after write - Simpler write path without cache coordination **Pattern:** ```csharp // Agent writes to source await database.UpdateProductAsync(product); // Agent invalidates cache await cache.RemoveAsync($"product:{product.Id}"); // or with HybridCache tags await hybridCache.RemoveByTagAsync("products"); ``` **Trade-offs:** - ✅ Simple write path - ✅ Cache and source cannot diverge permanently - ⚠️ Cache miss on next read after write - ⚠️ Brief window where readers see stale data ### Write-Through Agent updates cache and source together. Returns only after both succeed. **When to use:** - Read-after-write freshness is required - Read-heavy paths where cache miss cost is high - Write frequency is low to moderate **Pattern:** ```csharp // Agent updates source and cache atomically await database.UpdateProductAsync(product); await cache.SetAsync($"product:{product.Id}", product, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }); ``` **Trade-offs:** - ✅ Immediate read-after-write consistency - ✅ No cache miss after write - ⚠️ Higher write latency - ⚠️ More complex write coordination **Reference:** [Write-through caching with Azure Managed Redis and Azure SQL Database](https://learn.microsoft.com/en-us/azure/architecture/databases/architecture/write-through-caching-azure-sql-managed-redis) ### Cache Seeding Agent pre-populates cache with reference data at startup. **When to use:** - Static or rarely changing reference data (product catalogs, pricing, configuration) - Predictable working set - Reducing startup latency matters less than runtime performance **Pattern:** ```csharp // Agent seeds cache during application startup public async Task SeedCacheAsync(IDistributedCache cache) { var products = await database.GetAllProductsAsync(); foreach (var product in products) { await cache.SetAsync($"product:{product.Id}", JsonSerializer.SerializeToUtf8Bytes(product), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) }); } } ``` **Trade-offs:** - ✅ Eliminates cold-start cache misses - ✅ Predictable cache hit rate from startup - ⚠️ High load on source at startup - ⚠️ Not suitable for large or dynamic datasets **Reference:** [Caching Guidance - Determine how to cache data effectively](https://learn.microsoft.com/en-us/azure/architecture/best-practices/caching#determine-how-to-cache-data-effectively) ## Microsoft Learn Integration Agent uses Microsoft Learn MCP and Azure Architecture Center for current documentation and API reference when implementing caching patterns. **Azure Architecture Center:** - [Caching Guidance](https://learn.microsoft.com/en-us/azure/architecture/best-practices/caching) — Comprehensive best practices covering: - Private vs shared caching strategies - Cache-aside and write-through patterns - Data expiration and eviction policies - Concurrency management - Dynamic data caching considerations - Cache seeding strategies **Recommended MCP queries:** - `HybridCache .NET 9` — Official HybridCache guidance - `IDistributedCache Redis` — Redis distributed cache setup - `IMemoryCache expiration` — Memory cache lifetime management - `cache stampede protection` — Concurrency control patterns - `Azure Cache for Redis` — Azure managed Redis configuration - `cache-aside pattern` — Cache population and invalidation patterns ## Workflow 1. Agent identifies the cached data and stale-data budget. 2. Agent selects caching solution using the Decision Matrix (§ Caching Solution Decision Matrix). 3. Agent implements L1+L2 layering when deployment is multi-instance (§ L1 + L2 Caching Strategies). 4. Agent adds explicit expiration, invalidation, and stampede control. 5. Agent verifies hit, miss, expiry, and cache-outage behavior. Test: Run `dotnet build [project].csproj`. Pass: Zero compile errors. Zero cache entry without explicit freshness rule in changed scope. ## Rule Matrix | Rule | Agent verifies | Detection pattern | Fix | |---|---|---|---| | CACHE-001 | Cache scope matches deployment scope | Process-local cache used for shared multi-instance data | Use `HybridCache` (.NET 9+) or `IDistributedCache` | | CACHE-002 | Every entry has explicit freshness rule | Entry added with no absolute or sliding expiration | Add expiration policy | | CACHE-003 | Mutable data has invalidation plan | Writes succeed and stale entry remains | Remove or update cache on write | | CACHE-004 | Key format stays centralized | Inline string keys appear across files | Move key generation to helper | | CACHE-005 | Cache-aside path is explicit | Cache read and fill logic is duplicated | Centralize miss handling or use `HybridCache.GetOrCreateAsync` | | CACHE-006 | Expensive misses have stampede control | Hot-key miss fans out to many source calls | Use `HybridCache` or add per-key lock | | CACHE-007 | Cache outage does not become service outage | Cache exception aborts source fallback | Fall back to source path | | CACHE-008 | Telemetry exposes hit and miss behavior | No counters or logs for cache results | Add structured hit, miss, and eviction telemetry | | CACHE-009 | L1 expiration is shorter than L2 | L1 and L2 use same expiration in manual layering | Set L1 to 10-20% of L2 duration | | CACHE-010 | HybridCache used for .NET 9+ multi-instance | Manual L1+L2 implementation in .NET 9+ project | Replace with `HybridCache` | | CACHE-011 | Tag-based invalidation used when available | Manual key enumeration for bulk invalidation | Use `HybridCache` tags or key prefix patterns | ## Pattern Matrix | Scenario | Use | Avoid | |---|---|---| | Single instance API | `IMemoryCache` with explicit expiration | Distributed cache without need | | Multi-instance .NET 9+ | `HybridCache` with Redis or SQL backend | Manual L1+L2 implementation | | Multi-instance .NET 8 or earlier | `IDistributedCache` + manual L1 wrapper | Process-local cache for shared data | | Mutable reference data | Expiration plus write invalidation | No invalidation path | | Expensive hot key | `HybridCache` (.NET 9+) or cache-aside with `SemaphoreSlim` | Unbounded concurrent misses | | Write-heavy shared data | `IDistributedCache` only (no L1) | L1 cache with high staleness risk | | Tag-based bulk invalidation | `HybridCache` with tags | Manual key iteration | ## Verification Matrix | Test | Run | Pass | |---|---|---| | Compile verification | `dotnet build [project].csproj` | Zero compile errors | | Scope verification | Search changed files for inline cache keys or entries with no expiration | Zero unauthorized matches | | Decision matrix alignment | Compare selected cache type against Decision Matrix | Selection matches deployment and .NET version | | L1+L2 expiration ratio | Check L1 and L2 expiration settings in manual implementations | L1 expiration ≤ 20% of L2 expiration | | Behavior verification | Run `dotnet test [test-project].csproj --filter Cache` when tests exist | Zero failing tests | ## Verification Checklist Agent verifies: - [ ] Cache scope matches deployment scope - [ ] Caching solution follows Decision Matrix recommendation - [ ] .NET 9+ multi-instance services use `HybridCache` when applicable - [ ] Every entry has explicit expiration or invalidation - [ ] Mutable data has cache update or removal path - [ ] L1 expiration is 10-20% of L2 expiration in manual L1+L2 implementations - [ ] Expensive misses use stampede control when load warrants it - [ ] Telemetry exposes hit, miss, and cache-outage behavior - [ ] Tag-based invalidation used when supported ## Common Pitfalls | Pitfall | Fix | |---|---| | Shared data uses process-local cache | Use `HybridCache` (.NET 9+) or `IDistributedCache` | | Manual L1+L2 in .NET 9+ projects | Replace with `HybridCache` | | Cache key strings are copied inline | Centralize key helper | | Write path skips invalidation | Remove or update cache entry, use tags when available | | Cache outage aborts source fallback | Use source fallback | | L1 and L2 have same expiration | Set L1 to 10-20% of L2 duration | | No stampede protection on hot keys | Use `HybridCache` or add per-key `SemaphoreSlim` | ## Additional source coverage The selected version is the preferred starting point by the recorded date/context basis. Distinct source coverage is retained in the scoped references; do not apply superseded or conflicting defaults automatically. - [caching-patterns-dotnet-alternative-1](references/contexts/caching-patterns-dotnet-alternative-1/guide.md) - [caching-patterns-dotnet-alternative-2](references/contexts/caching-patterns-dotnet-alternative-2/guide.md)
View on GitHub