The fastest database query is the one you never make. If an API endpoint returns data that rarely changes (like a list of Countries or an e-commerce Product Catalog), querying SQL every time 10,000 users load the homepage is an extreme waste of server resources. We fix this using Caching.
Response Caching uses the [ResponseCache] attribute to instruct the Client's Browser or intermediary proxies (like Cloudflare) to store the data locally. It is highly dependent on HTTP Headers (Cache-Control).
// Tells the browser: "Do not ask the server for this exact URL again for 60 seconds."
[HttpGet("catalog")]
[ResponseCache(Duration = 60)]
public IActionResult GetCatalog()
{
var data = _db.Catalogs.ToList();
return Ok(data);
}
Output Caching intercepts the HTTP Response on the Server-Side. The first user to hit the endpoint pays the database penalty. The server then caches the exact JSON string in RAM. For the next 60 seconds, if anyone else hits that endpoint, ASP.NET Core instantly returns the cached JSON string, bypassing the Controller and the Database completely.
var builder = WebApplication.CreateBuilder(args);
// Add the memory caching service
builder.Services.AddOutputCache();
builder.Services.AddControllers();
var app = builder.Build();
// Activate middleware (MUST be placed before MapControllers!)
app.UseOutputCache();
app.MapControllers();
// The server holds this exact JSON string in RAM for 120 seconds.
[HttpGet("countries")]
[OutputCache(Duration = 120)]
public IActionResult GetCountries()
{
// First user triggers this. Next 10,000 users never even execute this method.
var countries = _db.Countries.ToList();
return Ok(countries);
}
What if the data changes *before* the 120 seconds expire? We don't want to serve stale data. We can dynamically evict the cache using Tagging.
// The Get endpoint tags the cached data with the label "product_list"
[HttpGet]
[OutputCache(PolicyName = "BaseCache", Tags = new[] { "product_list" })]
public IActionResult GetProducts() { ... }
// The POST endpoint creates a new product.
// Using DI, we inject IOutputCacheStore to instantly delete the old cache!
[HttpPost]
public async Task<IActionResult> CreateProduct(ProductDto dto, IOutputCacheStore cache)
{
_db.Products.Add(dto);
await _db.SaveChangesAsync();
// VIOLENCE! Instantly evicts any cache tagged with "product_list".
// The very next GET request will be forced to hit the database to get fresh data.
await cache.EvictByTagAsync("product_list", default);
return Ok();
}
Q: "Why does Output Caching automatically disable itself if the client sends an 'Authorization: Bearer' token header, and how do we override it?"
Architect Answer: "Security. By default, ASP.NET Core refuses to cache Authenticated responses. Imagine if User A requests their private bank statement, and the server caches that JSON. If User B hits the exact same URL a second later, the server might accidentally serve User A's private banking data to User B! To cache authenticated data safely, you must create a custom OutputCache policy that VaryBy(userid) or specifically override the default policy. However, as an architect, I strongly recommend NEVER caching private user-specific JSON data via OutputCaching at the network level; rely on an explicit Redis Distributed Cache instead using `IDistributedCache`."