Caching is one of the most effective strategies for boosting your ASP.NET Core application’s performance. By storing and reusing frequently accessed data, you can significantly reduce server load, decrease latency, and create a snappier user experience. But when it comes to implementation, ASP.NET Core offers a couple of primary approaches: caching at the middleware level and caching at the controller level.

Choosing the right strategy depends on your specific needs. Do you need a broad, application-wide caching rule, or do you require precise control over individual endpoints? Let’s break down the differences to help you make an informed decision.

Understanding the Core Concepts

Before we compare, it’s crucial to understand the two main players in this context: Response Caching Middleware and the [ResponseCache] attribute.

  • Response Caching Middleware: This is a component you register in your application’s request processing pipeline. It inspects incoming requests and outgoing responses to decide whether to serve a response from its cache or to cache a new response based on HTTP headers. It’s the engine that performs the actual server-side caching.
  • [ResponseCache] Attribute: This is a filter attribute you apply to your controller actions or globally. Its primary job is to set the appropriate HTTP Cache-Control header on the response. These headers instruct clients (like browsers) and intermediate proxies on how they should cache the response.

A common point of confusion is thinking the [ResponseCache] attribute alone handles server-side caching. It does not. It only sets headers. To get server-side response caching, you must have the middleware enabled.

Caching at the Middleware Level

Setting up caching at the middleware level involves configuring the ResponseCachingMiddleware in your Program.cs file. This approach gives you a central place to define your application’s caching behavior.

How to Implement It

First, you need to add the response caching services to the dependency injection container and then add the middleware to the request pipeline.

// Program.cs

var builder = WebApplication.CreateBuilder(args);

// 1. Add Response Caching services
builder.Services.AddResponseCaching();

// Add other services like controllers
builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// 2. Add the middleware to the pipeline
// Important: Place it before UseAuthorization and MapControllers
app.UseResponseCaching();

app.UseAuthorization();

app.MapControllers();

app.Run();

With this setup, the middleware is now active. It will inspect Cache-Control, Vary, and other headers set by your application (often via the [ResponseCache] attribute) to determine how to cache responses on the server.

Pros:

  • Centralized Configuration: All caching logic is in one place, making it easy to manage.
  • Global Application: It applies to all endpoints that have the appropriate caching headers.
  • Server-Side Caching: It effectively reduces the workload on your server by storing and serving responses directly from memory.

Cons:

  • Less Granularity: It acts as an engine but doesn’t define the rules for individual endpoints. You still need to configure caching parameters elsewhere, typically with controller attributes.

Callout: A Note on the New Output Caching

With the release of .NET 7, Microsoft introduced a new, more powerful caching mechanism called Output Caching. While Response Caching is tied to HTTP caching headers, Output Caching gives you complete server-side control, independent of what headers are present. It offers features like locking to prevent cache stampedes and easier programmatic cache invalidation. For new applications, it’s often the better choice. We’ll focus on the classic Response Caching here, but it’s essential to know that Output Caching is the modern successor.

Caching at the Controller Level

This is the most common and flexible way to implement response caching. By using the [ResponseCache] attribute directly on your controller classes or action methods, you get fine-grained control over how each endpoint is cached.

How to Implement It

You can apply the attribute to an individual action method or to the entire controller, in which case it applies to all actions within that controller.

Here’s an example of applying it to a specific API endpoint:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // This response will be cached for 60 seconds.
    // The cache will vary based on the 'id' query parameter.
    [HttpGet("{id}")]
    [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "id" })]
    public IActionResult GetProductById(int id)
    {
        // Simulate fetching a product from a database
        var product = new { Id = id, Name = $"Product {id}", Timestamp = DateTime.UtcNow };
        return Ok(product);
    }

    [HttpGet("latest")]
    public IActionResult GetLatestProducts()
    {
        // This endpoint is not cached
        var products = new[] { "Product A", "Product B" };
        return Ok(products);
    }
}

In this example:

  • Duration = 60: Sets the Cache-Control: public, max-age=60 header. The response can be cached by clients and proxies for 60 seconds.
  • Location = ResponseCacheLocation.Any: Allows the response to be cached by the client browser and any intermediate proxies.
  • VaryByQueryKeys = new[] { "id" }: This is a crucial server-side instruction. It tells the Response Caching Middleware to create a unique cache entry for each different value of the id query parameter. A request to /api/products/1 will be cached separately from /api/products/2.

Pros:

  • High Granularity: You have precise control over the caching behavior of each action method.
  • Declarative and Clean: The caching rules are right next to the code they affect, making it easy to understand an endpoint’s behavior.
  • Flexible: You can easily override controller-level attributes at the action-level for specific cases.

Cons:

  • Repetitive: If you have many endpoints with the same caching policy, you might find yourself repeating the same attribute configuration. This can be mitigated using Cache Profiles.

Using Cache Profiles for Cleaner Code

To avoid repeating yourself, you can define named CacheProfile settings in Program.cs and reference them in your [ResponseCache] attributes.

// Program.cs
builder.Services.AddControllers(options =>
{
    options.CacheProfiles.Add("Default60",
        new CacheProfile()
        {
            Duration = 60,
            Location = ResponseCacheLocation.Any
        });
});

// ProductsController.cs
[HttpGet("{id}")]
[ResponseCache(CacheProfileName = "Default60", VaryByQueryKeys = new[] { "id" })]
public IActionResult GetProductById(int id)
{
    // ...
}

Middleware vs. Controller: Which One Should You Use?

The answer is: you use them together.

They serve different purposes that complement each other:

  • Middleware is the engine. It provides the server-side infrastructure to store and serve cached responses. Without app.UseResponseCaching(), the server won’t store anything, regardless of the attributes you use.
  • Controller attributes are the configuration. They tell the middleware how to cache a specific response by setting the necessary HTTP headers and providing server-side instructions like VaryByQueryKeys.

Think of it like this: The middleware owns the warehouse (the cache), and the controller attribute puts a shipping label on the box (the response) with instructions on how long to store it and for whom.

FeatureMiddleware (UseResponseCaching)Controller ([ResponseCache])
PurposeEnables and performs server-side caching.Configures caching headers and server-side policies.
ScopeGlobal (entire request pipeline).Specific (Controller or Action method).
GranularityLow (it’s either on or off).High (duration, location, variance per endpoint).
Primary ControlOn/Off switch for server-side caching.Defines the caching rules for an endpoint.
Typical UseAlways enabled if server-side caching is needed.Applied to endpoints serving cacheable data.

Conclusion

Understanding the distinct roles of middleware and controller-level attributes is key to effectively implementing caching in ASP.NET Core. They are not competing approaches but rather two parts of the same system.

For a robust caching strategy:

  1. Enable the Response Caching Middleware in your Program.cs to create the server-side caching capability.
  2. Apply [ResponseCache] attributes to your controllers and actions to define the specific caching rules for each endpoint.
  3. Use Cache Profiles to keep your code DRY (Don’t Repeat Yourself) when you have common caching patterns.
  4. Consider Output Caching for new projects built on .NET 7+ for a more powerful and flexible server-side caching solution.

By combining these tools, you can build a highly performant and scalable application that intelligently serves cached content, reduces server costs, and delivers a superior experience to your users.

References

This video provides a great visual explanation of how to set up response caching in your ASP.NET Core applications.

http://googleusercontent.com/youtube_content/046knd1DFtB4/46knd1DFtB4.jpg