caching
codewithmukesh/dotnet-claude-kit
HybridCache and output caching strategies for .NET 10 applications with stampede protection.
What is caching?
Implements caching patterns for .NET 10 using HybridCache (unified L1/L2 abstraction), output caching for full HTTP responses, and cache invalidation on mutations. Use when optimizing read performance, reducing database load, or implementing distributed caching with Redis.
- HybridCache with built-in stampede protection and L1/L2 layering
- Output caching middleware for full HTTP response caching
- Cache invalidation patterns on data mutations
- Redis integration as distributed L2 cache backend
- Cache-aside pattern for legacy IDistributedCache integration
- User-scoped and tag-based cache invalidation
How to install caching
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill caching- HybridCache service registered in dependency injection
- Optional: StackExchangeRedis NuGet package for distributed caching
- Optional: Redis server for L2 distributed cache
How to use caching
- 1.Register HybridCache in Program.cs with default TTL options
- 2.Optionally add Redis as L2 distributed cache backend
- 3.Inject HybridCache into handlers and call GetOrCreateAsync with cache key and factory function
- 4.Set explicit Expiration and LocalCacheExpiration timeouts for each cached item
- 5.Call cache.RemoveAsync() in mutation handlers to invalidate entries
- 6.For full response caching, use AddOutputCache() with policies and apply .CacheOutput() to endpoints
- 7.Use output cache tags to invalidate multiple entries at once via EvictByTagAsync()
Use cases
- Caching product data with automatic expiration and Redis backing
- Output caching for public API endpoints with tag-based invalidation
- Preventing cache stampede during high-concurrency reads
- Invalidating cached entries when database records are updated
- Distributing cache across multiple servers using Redis
- Backend developers building .NET 10 applications
- Teams deploying multi-server distributed systems
- Developers optimizing read-heavy workloads
- Engineers implementing cache invalidation strategies
caching FAQ
Use HybridCache for all new code. It provides built-in stampede protection, L1/L2 layering, and automatic serialization. Manual IDistributedCache cache-aside is only for integrating with existing code.
Set explicit TTLs based on data freshness requirements. Use shorter TTLs (2-5 minutes) for frequently-changing data and longer TTLs (10-30 minutes) for stable reference data. Never cache without expiration.
HybridCache has built-in stampede protection via GetOrCreateAsync(). Do not implement manual locks; they are unnecessary and less efficient.
Yes, but include the user ID in the cache key (e.g., 'shopping-cart:{userId}') to prevent cross-user data leakage.
Use output caching for full HTTP responses that can be cached identically for all users (public APIs, static data). Use HybridCache for granular data caching within handlers.
Full instructions (SKILL.md)
Source of truth, from codewithmukesh/dotnet-claude-kit.
name: caching description: > 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
HybridCacheas 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.cs
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
// Optional: Add Redis as the L2 distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
// Usage in a handler
public class GetProduct
{
public record Query(Guid Id);
public record Response(Guid Id, string Name, decimal Price);
internal class Handler(AppDbContext db, HybridCache cache)
{
public async Task<Response?> Handle(Query query, CancellationToken ct)
{
return await cache.GetOrCreateAsync(
$"products:{query.Id}",
async token => await db.Products
.Where(p => p.Id == query.Id)
.Select(p => new Response(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(token),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10)
},
cancellationToken: ct);
}
}
}
Cache Invalidation
// Invalidate on mutation
public class UpdateProduct
{
internal class Handler(AppDbContext db, HybridCache cache)
{
public async Task<Result> Handle(Command command, CancellationToken ct)
{
var product = await db.Products.FindAsync([command.Id], ct);
if (product is null) return Result.Failure("Product not found");
product.Update(command.Name, command.Price);
await db.SaveChangesAsync(ct);
// Invalidate the cached entry
await cache.RemoveAsync($"products:{command.Id}", ct);
return Result.Success();
}
}
}
Output Caching (Full Response Caching)
// Program.cs
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(b => b.NoCache()); // Don't cache by default
options.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 endpoints
group.MapGet("/", ListProducts).CacheOutput("ProductList");
group.MapGet("/{id:guid}", GetProduct).CacheOutput("ProductById");
// Invalidate by tag on mutations
group.MapPut("/{id:guid}", async (Guid id, UpdateProductRequest request,
IOutputCacheStore store, CancellationToken ct) =>
{
// ... update logic ...
await store.EvictByTagAsync("products", ct);
return TypedResults.NoContent();
});
Cache-Aside Pattern (Legacy)
Prefer HybridCache for all new code. Manual
IDistributedCachecache-aside lacks stampede protection, requires manual serialization, and has no L1/L2 layering. Use only when integrating with existing code that already usesIDistributedCachedirectly.
Anti-patterns
Don't Cache Without Expiration
// BAD — cache lives forever, stale data guaranteed
await cache.SetStringAsync(key, value);
// GOOD — always set TTL
await cache.SetStringAsync(key, value, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
Don't Cache Mutable User-Specific Data
// BAD — caching user's cart with a global key
await cache.GetOrCreateAsync("shopping-cart", ...);
// GOOD — include user ID in key
await cache.GetOrCreateAsync($"shopping-cart:{userId}", ...);
Don't Build Your Own Stampede Protection
// BAD — manual lock to prevent cache stampede
private static readonly SemaphoreSlim Lock = new(1, 1);
await Lock.WaitAsync();
try { /* check cache, populate if missing */ }
finally { Lock.Release(); }
// GOOD — HybridCache has built-in stampede protection
await hybridCache.GetOrCreateAsync(key, factory);
Decision Guide
| Scenario | Recommendation |
|---|---|
| General data caching | HybridCache (GetOrCreateAsync) |
| Full HTTP response | Output caching with .CacheOutput() |
| Frequently read, rarely written | HybridCache with longer TTL |
| User-specific data | HybridCache with user-scoped key |
| Cache invalidation on write | cache.RemoveAsync() or output cache tags |
| Distributed deployment | HybridCache + Redis L2 backend |
| Single-server deployment | HybridCache with in-memory only |
Related skills
More from codewithmukesh/dotnet-claude-kit and the wider catalog.

checkpoint
Mid-session save point: commit progress and write a handoff note before risky changes or task switches.

ci-cd
CI/CD pipelines for .NET with GitHub Actions and Azure DevOps YAML workflows.

clean-architecture
4-layer .NET architecture with dependency inversion, domain-driven design, and use-case handlers.

code-review
MCP-powered multi-dimensional code review for .NET projects with blast-radius prioritization

configuration
Options pattern, secrets management, and environment-based configuration for .NET 10 applications.

container-publish
Containerize .NET 10 apps without writing a Dockerfile using SDK-native publishing.