caching について
.NET 開発者向けのキャッシュ運用スキルです。HybridCache、出力キャッシュ、無効化の方法を、期限設定や一般的な落とし穴への注意とともに説明します。
主な機能
- 01HybridCache の推奨方法
- 02キャッシュ無効化パターン
- 03HTTP レスポンスの出力キャッシュ
- 04TTL とユーザー別キー
活用シーン
- 01頻繁に読み込むデータのキャッシュ
- 02データ変更後のキャッシュ消去
- 03HTTP レスポンス全体のキャッシュ
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill cachingインストール後、Claude Code で/cachingを入力して読み込まれたか確認します。
詳細
キャッシュ
基本原則
- HybridCache がデフォルトです — .NET 9+ では、統合キャッシュ抽象化として
HybridCacheが導入されました。インメモリ(L1)キャッシュと分散(L2)キャッシュを、スタンピード保護と組み合わせます。ADR-004 を参照してください。 - 書き込みではなく読み取りをキャッシュします — GET 操作をキャッシュします。変更時には無効化します。POST/PUT/DELETE のレスポンスは決してキャッシュしないでください。
- レスポンス全体には出力キャッシュを使用します — HTTP レスポンス全体をキャッシュできる場合(公開 API、静的データ)は、出力キャッシュミドルウェアを使用します。
- TTL を明示的に設定します — キャッシュするすべての項目に有効期限が必要です。無制限のキャッシュは使用しないでください。
パターン
HybridCache(推奨デフォルト)
// 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);
}
}
}
キャッシュの無効化
// 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();
}
}
}
出力キャッシュ(レスポンス全体のキャッシュ)
// 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();
});
キャッシュ・アサイドパターン(レガシー)
新しいコードではすべて HybridCache を優先してください。
IDistributedCacheを手動で使うキャッシュ・アサイドでは、スタンピード保護がなく、シリアル化を手動で行う必要があり、L1/L2 の階層化もありません。IDistributedCacheをすでに直接使用している既存コードとの統合時にのみ使用してください。
アンチパターン
有効期限なしでキャッシュしない
// 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)
});
変更可能なユーザー固有データをキャッシュしない
// 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}", ...);
独自のスタンピード保護を実装しない
// 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);
選択ガイド
| シナリオ | 推奨事項 |
|---|---|
| 一般的なデータのキャッシュ | HybridCache(GetOrCreateAsync) |
| HTTP レスポンス全体 | .CacheOutput() による出力キャッシュ |
| 頻繁に読み取られ、書き込みがまれなデータ | TTL を長めに設定した HybridCache |
| ユーザー固有のデータ | ユーザー単位のキーを使用する HybridCache |
| 書き込み時のキャッシュ無効化 | cache.RemoveAsync() または出力キャッシュタグ |
| 分散デプロイ | Redis L2 バックエンドを使用する HybridCache |
| 単一サーバーのデプロイ | インメモリのみで使用する HybridCache |
コメント
Markdown 対応