PluginBench
Skill
Pass
Audit score 90

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
Prerequisites
  • HybridCache service registered in dependency injection
  • Optional: StackExchangeRedis NuGet package for distributed caching
  • Optional: Redis server for L2 distributed cache
Claude Code
Cursor
Windsurf
Cline

How to use caching

  1. 1.Register HybridCache in Program.cs with default TTL options
  2. 2.Optionally add Redis as L2 distributed cache backend
  3. 3.Inject HybridCache into handlers and call GetOrCreateAsync with cache key and factory function
  4. 4.Set explicit Expiration and LocalCacheExpiration timeouts for each cached item
  5. 5.Call cache.RemoveAsync() in mutation handlers to invalidate entries
  6. 6.For full response caching, use AddOutputCache() with policies and apply .CacheOutput() to endpoints
  7. 7.Use output cache tags to invalidate multiple entries at once via EvictByTagAsync()

Use cases

Good for
  • 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
Who it's for
  • Backend developers building .NET 10 applications
  • Teams deploying multi-server distributed systems
  • Developers optimizing read-heavy workloads
  • Engineers implementing cache invalidation strategies

caching FAQ

Should I use HybridCache or manual IDistributedCache?

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.

What TTL should I set for cached items?

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.

How do I prevent cache stampede?

HybridCache has built-in stampede protection via GetOrCreateAsync(). Do not implement manual locks; they are unnecessary and less efficient.

Can I cache user-specific data?

Yes, but include the user ID in the cache key (e.g., 'shopping-cart:{userId}') to prevent cross-user data leakage.

When should I use output caching instead of HybridCache?

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

  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)

// 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 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 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

ScenarioRecommendation
General data cachingHybridCache (GetOrCreateAsync)
Full HTTP responseOutput caching with .CacheOutput()
Frequently read, rarely writtenHybridCache with longer TTL
User-specific dataHybridCache with user-scoped key
Cache invalidation on writecache.RemoveAsync() or output cache tags
Distributed deploymentHybridCache + Redis L2 backend
Single-server deploymentHybridCache with in-memory only