Simplify Your .NET Aspire Caching With Metalama

Simplify Your .NET Aspire Caching With Metalama

.NET Aspire manages the wiring between the services of your application and other components like databases, messaging, and caching. One of the services it supports out of the box is Redis, for distributed caching. There are many points in your app where you can add caching (one way is to use Polly). Here’s how to cache method results without boilerplate code, using a special kind of custom attribute called an aspect that generates the caching code for you.

Metalama is a free code generation framework for C#. It helps you write and maintain less code by eliminating boilerplate, generating it dynamically during compilation. It is built upon the concept of aspect, which encapsulates a code pattern. Most of the time, aspects are also custom attributes, but special kinds of custom attributes because they act like code templates and modify the code they are applied to.

Thanks to Metalama’s [Cache] aspect, adding caching to a method is as easy as applying the [Cache] custom attribute:

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

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

Under the hood, the [Cache] aspect:

  • pulls the necessary services into your class,
  • intercepts the method calls and adds the caching behavior,
  • generates cache keys for you,
  • serializes and deserializes the cached objects,
  • handles “weird” types to cache like IEnumerable or streams.

.NET Aspire gives you distributed caching; Metalama adds it to your methods without the boilerplate. A few attributes replace the hand-written cache-key and invalidation code.

The example app

Before we start, a few words about the example app. It is a to-do list app with an ASP.NET Core Minimal API backend and a Blazor front-end, orchestrated using .NET Aspire. The app stores the tasks using SQL Server, with an Entity Framework front-end.

To improve the app’s responsiveness and relieve the load from the database, we’d like to cache the to-do list, as it is more often displayed than updated. However, we can’t use HTTP response caching because we need to invalidate the cache when the data is updated.

{}

The full source code of examples in this article is available on .

Setting up the project

Step 1. Create a solution with the .NET Aspire template

To follow this tutorial, you will need an IDE supporting the .NET Aspire workload and an OCI-compliant container runtime, such as Docker Desktop or Podman.

If you’re not familiar with .NET Aspire, we recommend trying out the .NET Aspire Starter Application first.

The template will create several projects:

  • The app host project, containing the service orchestration. You can think of this project as the bootstrapper of other projects.
  • The web API project,
  • The web front-end project,
  • A service default project that contains shared service configuration,
  • A data project shared between the API and front-end projects.

Step 2. Set up .NET Aspire app host

Firstly, we need .NET Aspire to run and route the Redis cache server. To allow the Redis cache server to be orchestrated by .NET Aspire, we need to add it to the app host.

  1. Add Aspire.Hosting.Redis NuGet package to the app host project.

  2. Add the Redis cache to your app host and reference it in the app that will use the caching. In our case, we’ll use the caching in the API app.

var cache = builder
    .AddRedis( "cache" );

var api = builder
    .AddProject<TodoList_Api>( "todolist-api" )
    .WithReference( db )
    .WithReference( cache );

The name "cache" will be used among the .NET Aspire orchestrated apps to reference the Redis cache added in this step.

Step 3. Set up the app

With the app host prepared, we can proceed to set up the app that’s going to use the cache.

  1. To allow Metalama Caching with Redis to be used, add the following NuGet packages. In our example, we use the cache in the API project, so we add these packages into it.

    PackageDescription
    Aspire.StackExchange.RedisAllows using the Redis resource configured in the app host.
    Metalama.Patterns.Caching.AspectsAllows Metalama Caching to be used in your source code.
    Metalama.Patterns.Caching.Backends.RedisAllows Metalama Caching to use Redis as the storage.
  2. In the API project, we use the AddRedisClient to add the Redis client, an object of type IConnectionMultiplexer, to our service collection. The "cache" argument is the name of the Redis service previously defined in the app host.

    builder.AddRedisClient( "cache" );
    
  3. Finally, we call AddMetalamaCaching. By default, an in-memory caching service will be created, so we must not forget calling the Redis method. It implicitly consumes the IConnectionMultiplexer service added by .NET Aspire.

    builder.Services.AddMetalamaCaching( caching => caching.WithBackend( backend => backend.Redis() ) );
    

Step 4. Add the caching to your code

You can now add caching to your methods.

Caching a method

As we’ve shown in the introduction, adding caching to a method is now as easy as adding the [Cache] attribute.

A few points will require your attention.

  • First, you must ensure that the parameters of a cached method generate a meaningful caching key. The ToString() method is used by default, and Metalama offers several strategies to customize it.

  • Then, you might want to check that all return values are safely JSON-serializable. Metalama transparently handles special types like streams, enumerables, or enumerators.

Invalidating the cache

Now that we’ve set the reading methods to be cached, they will always return the same result even if we add or modify todos. We must now remove items from the cache when the underlying data has changed, a problem called cache invalidation.

The simplest approach is to use the [InvalidateCache] aspect, as you can see in the DeleteTodoAsync() method.

[InvalidateCache( nameof(this.GetTodosAsync), nameof(this.GetTodoAsync) )]
public async Task<bool> DeleteTodoAsync( int id, CancellationToken cancellationToken = default )
{
    var todo = await this.GetTodoAsync( id, cancellationToken );

    if ( todo is null )
    {
        return false;
    }

    db.Todos.Remove( todo );
    await db.SaveChangesAsync( cancellationToken );

    return true;
}

This aspect matches the method arguments with the cache keys and invalidates the proper cache items when a to-do item gets deleted.

As you probably know, cache invalidation is the second hardest problem in computer science, so there’s way more to cache invalidation in Metalama Caching besides the [InvalidateCache] aspect: imperative invalidation, and even cache dependencies.

Summary

Together, .NET Aspire and Metalama let you add caching with a few attributes instead of hand-written cache-key and invalidation code, improving the responsiveness of your distributed application while keeping the caching logic out of your business code.

This article was first published on https://postsharp.net/blog under the title Simplify Your .NET Aspire Caching With Metalama.

Discover Metalama, the leading code generation and validation toolkit for C#

  • Write and maintain less code by eliminating boilerplate, generating it dynamically during compilation, typically reducing code lines and bugs by 15%.
  • Validate your codebase against your own rules in real-time to enforce adherence to your architecture, patterns, and conventions. No need to wait for code reviews.
  • Excel with large, complex, or old codebases. Metalama does not require you to change your architecture. Beyond getting started, it's at scale that it really shines.

Discover Metalama Free Edition