[Home](../) › [Blog](../blog) › Five Ways To Add Caching To Your ASP.NET App

# Five Ways To Add Caching To Your ASP.NET App

> ASP.NET Core offers several approaches to improving application performance through caching. This article presents and compares them all.

Published: 2024-08-06 · Author: Metalama Team · Canonical: https://postsharp.net/blog/aspnet-caching

Caching has been a fundamental feature of ASP.NET since its inception. From response caching to tag caching and data source caching, there are many solutions available for each layer of your application. This article gives you an overview of these techniques and the challenges they present. We propose a solution to cache invalidation, robust cache key generation, and tools to avoid boilerplate code and keep your code clean.

## 1. Caching the HTTP response

[HTTP Response Caching](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/response) has been part of ASP.NET since version 1.0. In ASP.NET Core, it is implemented by the [Response Caching Middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/middleware).

It's an implementation of the [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111) internet standard. The protocol specifies request and response headers (such as `Cache-Control`) for cache control and defines conditions under which a response can be cached by any node in the internet infrastructure, i.e., not only the web server but also the browser or intermediate nodes such as a CDN or a corporate proxy.

### How to use Response Caching?

To use HTTP Response Caching in ASP.NET Core:

1. Add the middleware service using the `AddResponseCaching` method:

    ```cs
    builder.Services.AddResponseCaching();
    ```

2. Add the `[ResponseCache]` attribute to controller classes or methods:

    ```cs
    [ApiController]
    public class TimeController : ControllerBase
    {
        [Route("api/[controller]")]
        [HttpGet]
        [ResponseCache]
        public ContentResult GetTime() => Content(DateTime.Now.ToString());
    }
    ```

These two steps will cause the HTTP response of the page or API call to be cached in memory. Some settings allow the cached response to vary according to some HTTP headers or query string parameters. Please refer to the ASP.NET documentation for details.

The full source code of examples in this article is available on [GitHub](https://github.com/postsharp/TimelessDotNetEngineer/tree/main/src/aspire/caching).

### Limitations of Response Caching

The benefit of  Response Caching is also its inconvenience: it respects the cache control HTTP headers sent by the client. However, giving the client control over the server's performance can be dangerous. Additionally, some web frameworks like Blazor always set the `Cache-Control` header to `no-cache, no-store`, ultimately defeating this approach to caching.

Another limitation of Response Caching is that it only uses in-memory storage.

## 2. Output Caching

[Output Caching](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output) is an alternative to response caching that does not depend on the HTTP headers set by the client. It offers different storage options than local memory, like Redis. Additionally, Output Caching implements some advanced features such as [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede), [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem), and cache revalidation that go beyond the scope of this article.

1. The first thing to do is to add the output caching service to the application builder. There are several approaches according to the storage you want:

    - To use an in-memory store for your output cache, call the `AddOutputCache()` method:

        ```cs
        builder.Services.AddOutputCache();
        ```

    - Using a Redis cache is more challenging to configure, except if you let .NET Aspire do the wiring for you.

        In your _app host_, add a Redis component:

        ```cs
        builder.AddRedis("cache");
        ```

        Then, in your web app, call `AddRedisOutputCache`:

        ```cs
        builder.AddRedisOutputCache("cache");
        ```

2. Then, call `UseOutputCache` on the application.

    ```cs
    var app = builder.Build();

    // ...
    app.UseOutputCache();
    // ...
    ```

3. Enable output for your controller or page using one of the following techniques:

    - Call the `CacheOutput()` extension method after `MapGet(...)`:

        ```csharp
app.MapGet(
        "/weatherforecast-cached",
        ( WeatherForecastService forecastService )
            => forecastService.GetWeatherForecast() )
    .CacheOutput( policy => policy.Expire( TimeSpan.FromSeconds( 5 ) ) );
```

    - Add the `[CacheOutput]` attribute to the controller class, controller method, or controller delegate:

        ```csharp
app.MapGet(
    "/weatherforecast-cached-attribute",
    [OutputCache( Duration = 5 )]( WeatherForecastService forecastService )
        => forecastService.GetWeatherForecast() );
```

    - Add the `[CacheOutput]` attribute to the Razor page:

        ```csharp
@attribute [OutputCache( Duration = 5 )]
```

## 3. Caching Razor Tags

The first two approaches allowed you to cache the whole output of a page or API call. Instead of caching the whole page, you might want to cache just a part. In Razor, this is possible thanks to the `<cache>` tag helper.

For instance, consider a to-do list app displaying the weather forecast. This forecast varies only by the user's hometown and is refreshed every 15 minutes. However, the content of the to-do list can change at any time upon user action. Therefore, we choose to cache the weather forecast component.

```html
<cache vary-by="@Model.User.HomeTown" expires-after="@TimeSpan.FromMinutes(15)">
    <!-- Weather rendering goes here. -->
</cache>
```

For more details regarding this technique, see [cache tag helpers](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/cache-tag-helper).

<!--
<img src="https://postsharp.net/assets/images/2024/2024-06-aspire-caching/todo-9bfee2dad8.png" alt="The to-do list with a weather widget" style="max-width: 1000px">
-->

## 4. Caching the data source

The preceding techniques added caching to the very end of the server pipeline, by caching _our_ response to the client. Suppose that several pages use the same data source and that every call to this data source is expensive. By adding caching to the _calls_ to this data source, we would not only increase the performance of our own website but also save real money if we have to pay for that service per call.

.NET comes with two abstractions for caching:

- The [`IMemoryCache` service](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory) allows for caching objects in local memory. The advantage of a local cache is the absence of serialization and deserialization.

- The [`IDistributedCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed) service lets your app share a cache with multiple instances of the app. Using this service requires serialization and out-of-process communication, usually including network communication, so the cache operations may be slower here.

Let's see how we can cache an HTTP request using `IMemoryCache`:

1. Add the `IMemoryCache` service to your app:

    ```cs
    builder.Services.AddMemoryCache();
    ```

2. Where you need caching, call the `GetOrCreateAsync` extension method.

    ```csharp
public partial class WeatherApiClient( HttpClient httpClient, IMemoryCache cache )
{
    private async Task<WeatherForecast[]> GetWeatherAsync(
        string endpoint,
        int maxItems,
        CancellationToken cancellationToken )
    {
        var forecast = await cache.GetOrCreateAsync(
            CacheKeyFactory.GetWeather( endpoint ),
            async _ => await httpClient.GetFromJsonAsync<WeatherForecast[]>(
                endpoint,
                cancellationToken ) );

        return forecast!.Take( maxItems ).ToArray();
    }
```

One of the challenges of caching is cache invalidation. For instance, if we cache the to-do list, we must not forget to remove the list from the cache whenever this list is modified. This causes the challenge of producing consistent caching keys: the caching key generated in the _update_ method must exactly match the one used by the _get_ method. This is why we moved the cache key generation logic to a `CacheKeyFactory` class, which both the _update_ and the _get_ method would call.

```csharp
public static class CacheKeyFactory
{
    public static string GetWeather(string endpoint) =>  $"{nameof(GetWeather)}({endpoint})";
    public static string GetToDo(string endpoint, int id) =>  $"{nameof(GetToDo)}({endpoint}, {id})";
    
    public static string GetToDoList(string endpoint) =>  $"{nameof(GetToDo)}({endpoint})";
}
```

Still, implementing caching by hand results in some boilerplate code, making the business code harder to read. To avoid repetitive work _and_ keep your source clean, we can use source generation.

## 5. Caching method results using aspects

Instead of writing caching code manually, you can use a special kind of custom attribute called an _aspect_ to generate that code for you at build time. Tools that make this magic possible are called _aspect-oriented frameworks_. One of them, based on Roslyn, is [Metalama](https://www.postsharp.net/metalama).

Metalama comes with its own open-source [caching library](https://www.postsharp.net/metalama/marketplace?metalama-marketplace%5Bquery%5D=Metalama.Patterns.Caching), which makes caching method return values a no-brainer.

To cache the return value of a method according to its parameters, just add the `[Cache]` attribute:

```csharp
[Cache]
public async Task<IEnumerable<Todo>> GetTodosAsync(
    [NotCacheKey] CancellationToken cancellationToken = default )
    => await db.Todos.ToListAsync( cancellationToken );

[Cache]
public async Task<Todo?> GetTodoAsync(
    int id,
    [NotCacheKey] CancellationToken cancellationToken = default )
    => await db.Todos.FindAsync( id );
```

To remove a method return value from the cache when an _update_ method is executed, use the `[InvalidateCache]` attribute:

```csharp
[InvalidateCache( nameof(this.GetTodosAsync) )]
public async Task<Todo> AddTodoAsync( Todo todo, CancellationToken cancellationToken = default )
{
    var newEntry = db.Todos.Add( todo );
    await db.SaveChangesAsync( cancellationToken );

    return newEntry.Entity;
}
    
[InvalidateCache( nameof(this.GetTodosAsync), nameof(this.GetTodoAsync) )]
public async Task<bool> UpdateTodoAsync(
    int id,
    Todo todo,
    CancellationToken cancellationToken = default )
{
    var existingTodo = await this.GetTodoAsync( id, cancellationToken );

    if ( existingTodo is null )
    {
        return false;
    }

    existingTodo.IsCompleted = todo.IsCompleted;
    existingTodo.Title = todo.Title;
    db.Todos.Update( existingTodo );
    await db.SaveChangesAsync( cancellationToken );

    return true;
}
```

Metalama Caching offers the following benefits:

- Reduces the amount of repetitive code, saving you time and making your code cleaner and easier to read.
- Minimizes errors related to the consistent generation of cache keys.
- Provides robust support for cache invalidation.
- Offers several caching topologies, including in-memory, Redis, Redis with an in-memory L1, and more.

If you're interested in learning more about Metalama Caching, read [Simplify Your .NET Aspire Caching With Metalama](https://postsharp.net/blog/aspire-caching-metalama).

## Summary

Choosing the right level of caching can greatly improve the performance of your application and, at the same time, reduce its operating costs. However, implementing caching can have its own challenges. Doing cache invalidation properly is a [notoriously hard problem](https://martinfowler.com/bliki/TwoHardThings.html). Open-source libraries like Metalama Caching can reduce the boilerplate involved with caching and improve its robustness.

---

## Site navigation

- Index of the whole site: [llms.txt](../llms.txt)

