caching — Claude Code skill: .NET caching guide
Guides caching work in .NET 10 applications with HybridCache, response caching, and distributed cache patterns. Use it when reducing database reads or planning cache invalidation.
npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a claude-codeWhat caching does
This skill guides caching work in .NET 10 applications with HybridCache, which combines an in-memory L1 cache and a distributed L2 layer. It covers stampede protection, explicit expiration periods, and user-scoped keys for mutable user data.
For full HTTP responses, it describes output caching middleware with policies for expiration, route-value variation, and cache tags. Cache read operations rather than writes. After a mutation, remove an entry with cache.RemoveAsync() or evict response entries by tag.
The skill recommends HybridCache for new code because manual IDistributedCache cache-aside needs serialization and does not provide L1/L2 layering or built-in stampede protection. Redis can serve as the L2 backend in a distributed deployment.
What it helps with
- cache a database read with
HybridCache - invalidate a cache entry after updating data
- configure full HTTP response caching
- scope user data cache keys by user ID
- add Redis as an L2 distributed cache
Example prompts for the agent
- Add
HybridCachefor product reads and invalidate the entry after an update. - Configure output caching for the product list and evict it by tag after
PUT. - Review this
IDistributedCachecache-aside code and replace it withHybridCachewhere appropriate.
How to install caching
1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a claude-code2Manually: put the files in place
~/.claude/skills/caching/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/caching/
1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a codex2Manually: put the files in place
.agents/skills/caching/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a cursor2Manually: put the files in place
.cursor/skills/caching/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install codewithmukesh/dotnet-claude-kit caching2Manually: put the files in place
.github/skills/caching/SKILL.md1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a gemini-cli2Platform CLI
gemini skills install https://github.com/codewithmukesh/dotnet-claude-kit --path skills/cachingManually: put the files in place
.devin/skills/caching/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about caching
How do I install caching in Claude Code?
Run npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a claude-code, then run npx skills add codewithmukesh/dotnet-claude-kit --skill caching -a claude-code again. The skill file is placed at ~/.claude/skills/caching/SKILL.md.
What does the caching skill do?
It provides guidance for .NET 10 caching with HybridCache, output caching, response caching, and distributed cache patterns. It also covers invalidation and entry expiration.
Why does it recommend HybridCache for new code?
HybridCache combines local L1 and distributed L2 caching and includes stampede protection. Manual IDistributedCache cache-aside requires serialization and lacks that layered setup.
Source text
SKILL.md · MIT
---
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
1. **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.
2. **Cache reads, not writes** — Cache GET operations. Invalidate on mutations. Never cache POST/PUT/DELETE responses.
3. **Output caching for entire responses** — When the full HTTP response can be cached (public APIs, static data), use output caching middleware.
4. **Set explicit TTLs** — Every cached item needs an expiration. No unbounded caches.
## Patterns
### HybridCache (Recommended Default)
```csharp
// 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");
});
```
```csharp
// 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
```csharp
// 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)
```csharp
// 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 `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
```csharp
// 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
```csharp
// 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
```csharp
// 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 |
© codewithmukesh · MIT · codewithmukesh/dotnet-claude-kit