-memory store by default. For distributed scenarios, you must configure a distributed cache provider.
var builder = WebApplication.CreateBuilder(args);
// Register output caching services
builder.Services.AddOutputCaching(options =>
{
// Optional: Configure global defaults
options.MaximumBodySize = 1024 * 1024; // 1MB limit
});
// For distributed caching (e.g., Redis)
builder.Services.AddStackExchangeRedisCache(redisOptions =>
{
redisOptions.Configuration = builder.Configuration.GetConnectionString("Redis");
});
2. Middleware Pipeline Configuration
Middleware order is critical. UseOutputCaching must be placed after routing and authentication middleware to ensure policies can evaluate user context, but before endpoint execution.
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// Place output caching after auth to support VaryByUser
app.UseOutputCaching();
app.MapControllers();
app.MapEndpoints();
app.Run();
3. Endpoint Configuration
Apply caching to endpoints using the CacheOutput extension. You can use the default policy or define named policies.
// Minimal API with default policy
app.MapGet("/api/products", async (IProductService service) =>
{
var products = await service.GetAllAsync();
return Results.Ok(products);
})
.CacheOutput(); // Uses default policy
// MVC Controller
[HttpGet("categories")]
[OutputCache(PolicyName = "ShortDuration")]
public async Task<IActionResult> GetCategories()
{
// Implementation
}
4. Policy Definition and Vary Strategies
Define policies to control duration, storage, and cache key variation. Variation is essential to prevent serving stale or incorrect data across different contexts.
builder.Services.AddOutputCaching(options =>
{
options.AddBasePolicy(builder => builder
.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("VaryByQuery", policy => policy
.VaryByQuery("category", "page")
.Expire(TimeSpan.FromMinutes(10)));
options.AddPolicy("VaryByUser", policy => policy
.VaryByUser(isAuthenticated: true)
.Expire(TimeSpan.FromMinutes(15)));
options.AddPolicy("DistributedCache", policy => policy
.Expire(TimeSpan.FromHours(1))
.SetDistributedCacheProvider());
});
5. Cache Invalidation with Tags
Tags enable programmatic invalidation of cached responses without waiting for expiration. This is vital for data consistency.
// Apply tag to endpoint
app.MapGet("/api/products/{id}", async (int id, IProductService service) =>
{
var product = await service.GetByIdAsync(id);
return Results.Ok(product);
})
.CacheOutput(policy => policy.Tag($"product:{id}"));
// Invalidate tag on update
app.MapPut("/api/products/{id}", async (int id, ProductDto dto, IProductService service) =>
{
await service.UpdateAsync(id, dto);
// Invalidate specific product cache
var cacheService = app.Services.GetRequiredService<IOutputCacheStore>();
await cacheService.EvictByTagAsync($"product:{id}", CancellationToken.None);
return Results.NoContent();
});
Architecture Decisions
- In-Memory vs. Distributed: Use in-memory caching for single-instance deployments or edge caching where data staleness tolerance is high. For scaled-out deployments, configure
IDistributedCache to ensure cache coherence across nodes. The OutputCaching infrastructure abstracts the storage provider, allowing seamless switching.
- Pipeline Placement: Placing
UseOutputCaching after UseAuthentication allows the use of VaryByUser. If placed before authentication, user-specific variations cannot be resolved, leading to security risks where user A receives user B's cached response.
- Key Generation: The cache key is composed of a base key (derived from the request path and method) and vary components. Understanding this structure is necessary for debugging cache misses and optimizing storage usage.
Pitfall Guide
Production experience reveals recurring patterns of misuse that degrade performance or introduce data integrity issues.
-
Incorrect Middleware Ordering:
- Mistake: Placing
UseOutputCaching before UseAuthentication.
- Impact:
VaryByUser policies fail to function. Authenticated users may receive cached responses intended for anonymous users or other authenticated users, causing data leakage.
- Fix: Always position output caching middleware after authentication and authorization middleware.
-
Caching Non-Idempotent Methods:
- Mistake: Applying
CacheOutput to POST, PUT, or DELETE endpoints without strict validation.
- Impact: Output caching defaults to caching only
GET and HEAD requests. Forcing caching on mutating methods can lead to stale responses for subsequent reads and violate HTTP semantics.
- Fix: Restrict caching to read-only endpoints. If caching POST results is required, use explicit
CacheOutput configuration with caution and ensure idempotency.
-
Unbounded Cache Growth:
- Mistake: Using
VaryByQuery or VaryByHeader with high-cardinality values (e.g., timestamps, unique IDs) without limits.
- Impact: Memory exhaustion or distributed cache thrashing. The cache store fills with unique entries that are rarely reused, degrading performance.
- Fix: Validate input cardinality. Use
VaryByRouteValues only for low-cardinality parameters. Implement cache size limits and eviction policies.
-
Ignoring Cache Invalidation:
- Mistake: Relying solely on expiration for data that changes frequently.
- Impact: Users experience stale data for the duration of the TTL, leading to business logic errors.
- Fix: Implement tag-based invalidation. Evict tags immediately after write operations. Use background workers for complex invalidation scenarios.
-
Confusing OutputCaching with ResponseCaching:
- Mistake: Using
ResponseCache attributes alongside OutputCaching.
- Impact: Conflicting behaviors.
ResponseCaching relies on HTTP headers and may not integrate with the new policy engine.
- Fix: Migrate entirely to
OutputCaching. Remove ResponseCaching middleware and attributes to avoid overhead and confusion.
-
Missing UseOutputCaching Middleware:
- Mistake: Registering services but forgetting the middleware call.
- Impact: No caching occurs despite endpoint configuration. Developers waste time debugging endpoint logic.
- Fix: Verify pipeline configuration in
Program.cs. Use integration tests to assert cache headers or hit rates.
-
Serializing Large Payloads:
- Mistake: Caching endpoints returning massive JSON payloads without size limits.
- Impact: High memory pressure and increased serialization/deserialization latency.
- Fix: Configure
MaximumBodySize in options. Compress responses. Consider pagination or field selection to reduce payload size before caching.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single-instance, low traffic | In-Memory OutputCaching | Zero infrastructure cost; low latency. | None |
| Multi-instance, high traffic | Distributed Cache (Redis) | Cache coherence across nodes; scalability. | Infrastructure cost for Redis. |
| User-specific dashboards | VaryByUser policy | Ensures data isolation; personalization. | Increased cache storage usage. |
| Real-time data feeds | Short TTL or No-Cache | Prevents stale data; accuracy priority. | Higher backend load. |
| Static content/Reference data | Long TTL + Tag Invalidation | Maximize hit ratio; instant invalidation on update. | Minimal backend load. |
Configuration Template
Copy this template into Program.cs for a production-ready setup with Redis, policies, and tag support.
var builder = WebApplication.CreateBuilder(args);
// 1. Services
builder.Services.AddOutputCaching(options =>
{
options.MaximumBodySize = 1024 * 1024 * 5; // 5MB limit
options.AddBasePolicy(policy => policy
.Expire(TimeSpan.FromMinutes(10)));
options.AddPolicy("ApiDefault", policy => policy
.VaryByQuery("page", "pageSize")
.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("UserSensitive", policy => policy
.VaryByUser(isAuthenticated: true)
.Expire(TimeSpan.FromMinutes(15)));
});
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("RedisCache");
options.InstanceName = "MyApp_";
});
// 2. Pipeline
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// Middleware placement
app.UseOutputCaching();
// 3. Endpoints
app.MapGet("/data", () => Results.Ok(new { Timestamp = DateTime.UtcNow }))
.CacheOutput("ApiDefault");
app.Run();
Quick Start Guide
- Add Services: Insert
builder.Services.AddOutputCaching(); in Program.cs.
- Add Middleware: Insert
app.UseOutputCaching(); after authentication middleware.
- Cache Endpoint: Append
.CacheOutput(); to your endpoint definition.
- Run: Execute the application. Subsequent requests to the endpoint will return cached responses.
- Verify: Inspect response headers for
X-Cache or monitor metrics to confirm caching behavior.