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);
});
}
}
Topics
36
API Documentation with Swagger/OpenAPI
API Versioning
Authentication
Authorization
Background Tasks & Hosted Services
Blazor Integration with ASP.NET Core
Caching
Configuration & Options Pattern
Content Negotiation & Output Formatters
Controllers vs Minimal APIs
CORS (Cross-Origin Resource Sharing)
Dependency Injection
Endpoint Metadata, Route Constraints & Templates
Error Handling
File Uploads & Streaming Large Files
Filters
gRPC Services
Health Checks
Hosting Models: Kestrel, IIS & Reverse Proxies
HTTPS, Certificates & Transport Security
Localization & Globalization
Logging
Model Binding & Validation
MVC Views, Razor Syntax & Tag Helpers
Output Caching & Response Caching
Rate Limiting
Razor Pages
Request Pipeline & Middleware
Response Compression & Caching Headers
Routing
Security Headers, Antiforgery & CSRF Protection
Sessions, Cookies & TempData
SignalR & Real-Time Communication
Static Files, wwwroot & Content Delivery
Testing ASP.NET Core Applications
WebSockets
Caching
15 questions found
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.
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.
Diagnostics & Performance;Configuration & Options Pattern
What is IDistributedCache, and how does it differ from IMemoryCache in a horizontally-scaled ASP.NET Core deployment?
IntermediateIDistributedCache 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.
Diagnostics & Performance;Configuration & Options Pattern
What is response caching middleware, and how does it differ from application-level data caching?
AdvancedResponse 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.
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?
IntermediateOutput 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.
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?
AdvancedA 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.
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?
IntermediateHybridCache 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.
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?
AdvancedStrategies 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.
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?
IntermediateAddStackExchangeRedisCache() 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.
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?
AdvancedCache-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.
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?
IntermediateCaching 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.
Authentication;ASP.NET Core Middleware & Request Pipeline
Showing 1–10 of 15