Caching

15 questions found

What is IMemoryCache, and when is it appropriate for an ASP.NET Core application?

Beginner
IMemoryCache is a built-in in-process cache storing key-value pairs directly in the application's own memory, extremely fast since it requires no network round-trip, but not shared across multiple server instances and lost on application restart -- appropriate for single-instance applications or data that's cheap to recompute if lost.
builder.Services.AddMemoryCache();

public class ProductService(IMemoryCache cache) {
    public async Task<Product> GetProductAsync(int id) {
        return await cache.GetOrCreateAsync($"product-{id}", async entry => {
            entry.SlidingExpiration = TimeSpan.FromMinutes(10);
            return await _repository.GetByIdAsync(id);
        });
    }
}
Real-world example A single-instance internal reporting tool caches expensive aggregate query results in IMemoryCache for 10 minutes, dramatically reducing database load for a report viewed repeatedly by the same handful of users.

Common follow-ups: What's the memory pressure risk of unbounded IMemoryCache growth?;How does IMemoryCache behave differently across multiple load-balanced instances?

Diagnostics & Performance;Configuration & Options Pattern

What is IDistributedCache, and how does it differ from IMemoryCache in a horizontally-scaled ASP.NET Core deployment?

Intermediate
IDistributedCache is an abstraction over an external, shared cache store (commonly Redis) that all instances of a scaled-out application read from and write to consistently, ensuring cache coherency across multiple servers -- unlike IMemoryCache, where each instance maintains its own separate, unsynchronized cache, meaning a value cached by one instance wouldn't be visible to another.
builder.Services.AddStackExchangeRedisCache(options => {
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

public async Task<Product?> GetProductAsync(int id) {
    var cached = await _distributedCache.GetStringAsync($"product-{id}");
    return cached is not null ? JsonSerializer.Deserialize<Product>(cached) : null;
}
Real-world example An API running behind a load balancer with 10 instances uses Redis-backed IDistributedCache so a product cached by instance A is immediately visible to instances B through J, avoiding 10x redundant database queries for the same data.

Common follow-ups: Why does IDistributedCache only support byte[]/string, requiring manual serialization?;What's the latency trade-off of a network-based cache versus in-process memory access?

Diagnostics & Performance;Configuration & Options Pattern

What is response caching middleware, and how does it differ from application-level data caching?

Advanced
Response caching (via UseResponseCaching() and [ResponseCache] attributes) caches entire HTTP responses at the HTTP layer based on cache-control headers, serving cached output directly without re-invoking the controller action at all -- distinct from application-level data caching (IMemoryCache/IDistributedCache), which caches specific pieces of data used within business logic, potentially still requiring the full request pipeline to execute.
builder.Services.AddResponseCaching();
app.UseResponseCaching();

[ResponseCache(Duration = 60, VaryByQueryKeys = new[] { "category" })]
[HttpGet]
public IActionResult GetProducts(string category) => Ok(_products.Where(p => p.Category == category));
Real-world example A public product catalog API caches entire GET /products?category=electronics responses for 60 seconds using response caching middleware, avoiding the full controller execution (including any data-layer caching logic) entirely for repeated identical requests within that window.

Common follow-ups: How does VaryByQueryKeys affect what counts as a 'unique' cached response?;How does response caching interact with a CDN sitting in front of the API?

ASP.NET Core Middleware & Request Pipeline;Content Negotiation & Output Formatters

How does the newer Output Caching middleware (introduced in .NET 7) differ from Response Caching middleware?

Intermediate
Output Caching (UseOutputCache(), CacheOutput() policies) caches the response server-side (unlike Response Caching, which relies on client/proxy cache-control headers and doesn't actually store responses server-side by default), supports programmatic cache invalidation via tags, and offers a more flexible policy-based configuration model (varying by query string, header, or custom logic) -- generally the more powerful and recommended choice for server-side output caching going forward.
builder.Services.AddOutputCache(options => {
    options.AddPolicy("ProductsPolicy", policy => policy.Expire(TimeSpan.FromMinutes(5)).Tag("products"));
});
app.UseOutputCache();

app.MapGet("/products", () => GetProducts()).CacheOutput("ProductsPolicy");

// Invalidate all cached responses tagged 'products' when data changes:
await outputCacheStore.EvictByTagAsync("products", default);
Real-world example An e-commerce API uses Output Caching with tag-based invalidation, so updating any product immediately evicts all cached product-listing responses via EvictByTagAsync, rather than waiting for a fixed TTL to naturally expire potentially stale cached data.

Common follow-ups: Why is tag-based invalidation not available with Response Caching middleware?;Can Output Caching be configured to use a distributed store like Redis instead of in-memory?

ASP.NET Core Middleware & Request Pipeline;Configuration & Options Pattern

What is the 'cache stampede' (thundering herd) problem, and how would you mitigate it in an ASP.NET Core application?

Advanced
A cache stampede occurs when a popular cache entry expires and many concurrent requests simultaneously experience a cache miss, all racing to recompute and repopulate the same expensive value at once, overwhelming the backing data source -- mitigated via a per-key lock/semaphore ensuring only one request recomputes while others wait, or by using IMemoryCache.GetOrCreateAsync's built-in per-key coordination, or the newer HybridCache API which provides built-in stampede protection out of the box.
private static readonly SemaphoreSlim _lock = new(1, 1);

public async Task<Product> GetProductAsync(int id) {
    if (_cache.TryGetValue($"product-{id}", out Product cached)) return cached;
    await _lock.WaitAsync();
    try {
        if (_cache.TryGetValue($"product-{id}", out cached)) return cached;
        var product = await _repository.GetByIdAsync(id);
        _cache.Set($"product-{id}", product, TimeSpan.FromMinutes(10));
        return product;
    } finally { _lock.Release(); }
}
Real-world example A viral news article's cache entry expiring under heavy simultaneous traffic previously caused 500 concurrent database queries for the same article; adding per-key locking reduces this to exactly one database query while other requests briefly wait for the now-cached result.

Common follow-ups: How does HybridCache handle this automatically without manual locking code?;What is 'stale-while-revalidate' as an alternative mitigation approach?

Diagnostics & Performance;Concurrency (asyncio/threading/multiprocessing)

How does HybridCache (introduced in .NET 9) unify in-memory and distributed caching, and what problems does it solve?

Intermediate
HybridCache provides a single API layering a fast local in-memory cache in front of a distributed cache like Redis, automatically handling cache stampede protection (request coalescing for concurrent identical cache misses) and serialization -- giving you both the speed of local caching and the consistency of a distributed cache without hand-rolling the two-tier coordination logic yourself.
builder.Services.AddHybridCache();

public class ProductService(HybridCache cache) {
    public async Task<Product> GetProductAsync(int id) {
        return await cache.GetOrCreateAsync($"product-{id}", async ct => {
            return await _repository.GetByIdAsync(id, ct);
        }, new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(10) });
    }
}
Real-world example A team previously hand-rolling a two-tier cache (check IMemoryCache, fall back to Redis, populate both on miss) replaces dozens of lines of custom coordination logic with a single HybridCache.GetOrCreateAsync call after upgrading to .NET 9.

Common follow-ups: How does HybridCache handle stampede protection across multiple server instances, not just within one process?;What serialization options does HybridCache support?

Diagnostics & Performance;Configuration & Options Pattern

What are common cache invalidation strategies for ensuring cached data doesn't become stale, and why is invalidation considered a genuinely hard problem?

Advanced
Strategies include TTL-based expiration (simplest, but potential staleness until expiry), explicit invalidation (removing/updating a cache entry immediately when underlying data changes, requiring every mutation code path to remember to invalidate correctly), and tag-based bulk invalidation (Output Caching's EvictByTagAsync or a custom tag-tracking scheme). It's hard because every place data can change must remember to invalidate the right entries, and distributed deployments make coordinating invalidation across multiple cache instances even trickier.
public async Task UpdateProductAsync(Product product) {
    await _repository.UpdateAsync(product);
    await _cache.RemoveAsync($"product-{product.Id}");  // must remember this on EVERY write path!
    await _outputCacheStore.EvictByTagAsync("products", default);
}
Real-world example A bug where stale product prices were shown for hours was traced to a bulk-update script that updated the database directly, bypassing the normal service method that would have invalidated the corresponding cache entries.

Common follow-ups: Why is 'there are only two hard things in computer science: cache invalidation and naming things' such a common joke?;How do cache tags simplify bulk invalidation compared to tracking individual keys?

Diagnostics & Performance;Entity Framework Core & Data Access

How do you configure Redis as the backing distributed cache for both IDistributedCache and Output Caching in the same application?

Intermediate
AddStackExchangeRedisCache() registers IDistributedCache backed by Redis, while a separate output caching store configuration (via a Redis-backed IOutputCacheStore implementation) lets Output Caching also persist to the same Redis instance rather than the default in-memory store, ensuring both application-level data caching and HTTP output caching benefit from the same shared, distributed backing store across multiple server instances.
builder.Services.AddStackExchangeRedisCache(options => {
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

builder.Services.AddOutputCache(options => {
    options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(5)));
});
// Redis-backed output cache store configured via a corresponding provider package
Real-world example A horizontally-scaled API ensures both its manually-cached data (IDistributedCache) and its automatic output-cached responses share the same Redis instance, so cache coherency holds consistently across every caching mechanism the app uses.

Common follow-ups: What are the trade-offs of a shared Redis instance for both purposes versus separate instances?;How does connection pooling work for StackExchange.Redis under high concurrency?

Configuration & Options Pattern;Diagnostics & Performance

How do HTTP caching headers (Cache-Control, ETag) set by an ASP.NET Core API interact with browser and CDN caching, complementing server-side caching?

Advanced
Cache-Control directives (max-age, public/private) tell browsers and CDNs how long a response can be reused without re-requesting, while ETag provides a version identifier clients can send via If-None-Match, letting the server respond with a lightweight 304 Not Modified if unchanged -- these HTTP-layer mechanisms reduce load even before a request reaches your application's server-side caching logic, forming a complementary, multi-layered caching strategy from browser to CDN to server.
[HttpGet("{id}")]
public IActionResult GetProduct(int id) {
    var product = _service.GetProduct(id);
    var etag = ComputeETag(product);
    if (Request.Headers.IfNoneMatch == etag) return StatusCode(304);
    Response.Headers.ETag = etag;
    Response.Headers.CacheControl = "public, max-age=300";
    return Ok(product);
}
Real-world example A public API serving rarely-changing reference data sets long Cache-Control max-age values and ETags, letting a CDN and client browsers serve repeat requests without the request ever reaching the origin server or its own caching layers.

Common follow-ups: How does a CDN's cache differ from browser caching in terms of who benefits?;What's the risk of overly aggressive Cache-Control settings for frequently-changing data?

Content Negotiation & Output Formatters;Hosting Models: Kestrel IIS & Reverse Proxies

What is the risk of caching sensitive or user-specific data with an incorrectly scoped cache key, and how do you prevent cross-user data leakage?

Intermediate
Caching user-specific data (like another user's personal dashboard) under a shared or predictable cache key risks serving one user's private data to a different user -- a critical security bug -- so cache keys for personalized data must always incorporate the user's identity, and Output Caching's VaryByHeader/VaryByValue options must correctly account for authentication state to avoid this exact class of vulnerability.
// DANGEROUS: same cache key regardless of which user is requesting
var cacheKey = "user-dashboard";  // BUG!

// CORRECT: key includes user identity
var cacheKey = $"user-dashboard-{userId}";

// Output Caching: vary by an auth-derived value
app.MapGet("/dashboard", GetDashboard).CacheOutput(policy => policy.SetVaryByHeader("Authorization"));
Real-world example A critical security incident occurs when a response-caching misconfiguration caches a personalized dashboard response without varying by user identity, causing one user's private data to be served to the next user who happened to hit the same cached response.

Common follow-ups: How does VaryByHeader specifically help prevent this class of bug?;What data should never be cached server-side regardless of key scoping?

Authentication;ASP.NET Core Middleware & Request Pipeline

Showing 1–10 of 15