You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Caching strategies for .NET 10 applications. Covers HybridCache (the default), output caching, response caching, and distributed cache patterns. Load this skill when implementing caching, optimizing read performance, reducing database load, or when the user mentions "cache", "HybridCache", "Redis", "output cache", "response cache", "distributed cache", "IMemoryCache", "cache invalidation", "stampede protection", or "cache-aside".
Caching
Core Principles
HybridCache is the default — .NET 9+ introduced HybridCache as the unified caching abstraction. It combines in-memory (L1) and distributed (L2) caching with stampede protection. See ADR-004.
Cache reads, not writes — Cache GET operations. Invalidate on mutations. Never cache POST/PUT/DELETE responses.
Output caching for entire responses — When the full HTTP response can be cached (public APIs, static data), use output caching middleware.
Set explicit TTLs — Every cached item needs an expiration. No unbounded caches.
Patterns
HybridCache (Recommended Default)
// Program.csbuilder.Services.AddHybridCache(options =>{options.DefaultEntryOptions=newHybridCacheEntryOptions{Expiration=TimeSpan.FromMinutes(5),LocalCacheExpiration=TimeSpan.FromMinutes(2)};});// Optional: Add Redis as the L2 distributed cachebuilder.Services.AddStackExchangeRedisCache(options =>{options.Configuration=builder.Configuration.GetConnectionString("Redis");});
// Usage in a handlerpublicclassGetProduct{publicrecordQuery(GuidId);publicrecordResponse(GuidId,stringName,decimalPrice);internalclassHandler(AppDbContextdb,HybridCachecache){publicasyncTask<Response?>Handle(Queryquery,CancellationTokenct){returnawaitcache.GetOrCreateAsync($"products:{query.Id}",async token =>awaitdb.Products.Where(p =>p.Id==query.Id).Select(p =>newResponse(p.Id,p.Name,p.Price)).FirstOrDefaultAsync(token),newHybridCacheEntryOptions{Expiration=TimeSpan.FromMinutes(10)},cancellationToken:ct);}}}
Cache Invalidation
// Invalidate on mutationpublicclassUpdateProduct{internalclassHandler(AppDbContextdb,HybridCachecache){publicasyncTask<Result>Handle(Commandcommand,CancellationTokenct){varproduct=awaitdb.Products.FindAsync([command.Id],ct);if(productisnull)returnResult.Failure("Product not found");product.Update(command.Name,command.Price);awaitdb.SaveChangesAsync(ct);// Invalidate the cached entryawaitcache.RemoveAsync($"products:{command.Id}",ct);returnResult.Success();}}}
Output Caching (Full Response Caching)
// Program.csbuilder.Services.AddOutputCache(options =>{options.AddBasePolicy(b =>b.NoCache());// Don't cache by defaultoptions.AddPolicy("ProductList", b =>b.Expire(TimeSpan.FromMinutes(5)).Tag("products"));options.AddPolicy("ProductById", b =>b.Expire(TimeSpan.FromMinutes(10)).SetVaryByRouteValue("id").Tag("products"));});app.UseOutputCache();// Apply to endpointsgroup.MapGet("/",ListProducts).CacheOutput("ProductList");group.MapGet("/{id:guid}",GetProduct).CacheOutput("ProductById");// Invalidate by tag on mutationsgroup.MapPut("/{id:guid}",async(Guidid,UpdateProductRequestrequest,IOutputCacheStorestore,CancellationTokenct)=>{// ... update logic ...awaitstore.EvictByTagAsync("products",ct);returnTypedResults.NoContent();});
Cache-Aside Pattern (Legacy)
Prefer HybridCache for all new code. Manual IDistributedCache cache-aside lacks stampede
protection, requires manual serialization, and has no L1/L2 layering. Use only when
integrating with existing code that already uses IDistributedCache directly.
Anti-patterns
Don't Cache Without Expiration
// BAD — cache lives forever, stale data guaranteedawaitcache.SetStringAsync(key,value);// GOOD — always set TTLawaitcache.SetStringAsync(key,value,newDistributedCacheEntryOptions{AbsoluteExpirationRelativeToNow=TimeSpan.FromMinutes(10)});
Don't Cache Mutable User-Specific Data
// BAD — caching user's cart with a global keyawaitcache.GetOrCreateAsync("shopping-cart", ...);// GOOD — include user ID in keyawaitcache.GetOrCreateAsync($"shopping-cart:{userId}", ...);
Don't Build Your Own Stampede Protection
// BAD — manual lock to prevent cache stampedeprivatestaticreadonlySemaphoreSlimLock=new(1,1);awaitLock.WaitAsync();try{/* check cache, populate if missing */}finally{Lock.Release();}// GOOD — HybridCache has built-in stampede protectionawaithybridCache.GetOrCreateAsync(key,factory);