ASP.NET Core caching options: IMemoryCache, Redis, HybridCache, output caching, HTTP and CDN

IMemoryCache: lives in process memory.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ======================================================================
// Q: Which caching options exist in ASP.NET Core, and when do you use each?
//
// - IMemoryCache: lives in process memory. For a single instance and small data
//   that rarely changes (settings, country lists).
// - IDistributedCache (Redis): external, shared by all instances. For multiple
//   instances, sessions, and data that must be identical everywhere.
// - HybridCache (.NET 9): L1 in memory + L2 in Redis. The new default: stampede
//   protection and tags for invalidation.
// - Output caching: the whole HTTP response, on the server. For public pages or
//   APIs (catalog) that are the same for everyone.
// - HTTP caching (Cache-Control, ETag): browser, proxy. You avoid the whole
//   request or send 304 Not Modified.
// - CDN (Azure Front Door): edge servers. Static assets, product images.
// Output caching does not cache authenticated requests by default. In B2B every
// customer has their own prices, so the response is not the same for everyone.
// There you cache the shared data (catalog) and compute the price per customer
// separately.
// ======================================================================

// HybridCache: automatically uses Redis as L2 if it is registered
builder.Services.AddStackExchangeRedisCache(o =>
    o.Configuration = builder.Configuration.GetConnectionString("Redis"));
builder.Services.AddHybridCache(o => o.DefaultEntryOptions = new HybridCacheEntryOptions
{
    Expiration = TimeSpan.FromMinutes(10),            // L2 (Redis)
    LocalCacheExpiration = TimeSpan.FromMinutes(1)    // L1 (memory)
});

public class CatalogService(HybridCache cache, AppDbContext db)
{
    public ValueTask<ProductDto?> GetProductAsync(string sku, CancellationToken ct) =>
        cache.GetOrCreateAsync(
            $"product:{sku}",
            async token => await db.Products.AsNoTracking()
                .Where(p => p.Sku == sku)
                .Select(p => new ProductDto(p.Sku, p.Name, p.ListPrice))
                .FirstOrDefaultAsync(token),
            tags: ["catalog"],
            cancellationToken: ct);
}

// Output caching for a public catalog
builder.Services.AddOutputCache(o => o.AddPolicy("catalog", b => b
    .Expire(TimeSpan.FromMinutes(5))
    .SetVaryByQuery("category", "page")
    .Tag("catalog")));

app.UseOutputCache();
app.MapGet("/api/catalog", GetCatalog).CacheOutput("catalog");