DEV Community

Gael Fraiteur for PostSharp Technologies

Posted on • Originally published at blog.postsharp.net on

Simplify Your .NET Aspire Caching With Metalama

As you may already know, .NET Aspire makes it very easy to build distributed cloud-native applications. It manages all the wiring between the different services of your application and other components like databases, messaging, and caching. One of the services you can use with .NET Aspire straight out of the box is Redis, a great solution for distributed caching. There are many points in your app where you can add caching (and one of the ways is to use Polly). In this article, we will see how to cache method results without boilerplate code thanks to a special kind of custom attribute called aspect. We’ll see how Metalama can generate the caching boilerplate code for you based on this custom attribute.

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 );

Enter fullscreen mode Exit fullscreen mode

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.

In this article, we demonstrate how to configure both .NET Aspire and Metalama to add caching with minimal effort, keeping your code clean of boilerplate. You’ll see, this is really a piece of cake.

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.

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<Projects.TodoList_Api>( "todolist-api" )
    .WithReference( db )
    .WithReference( cache );

Enter fullscreen mode Exit fullscreen mode

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.

  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.

  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.

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;
}

Enter fullscreen mode Exit fullscreen mode

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

.NET Aspire and Metalama make caching a real piece of cake. With just a few method calls and custom attributes, you can improve the responsiveness of your distributed cloud-native application, keeping it robust, scalable, and maintainable at the same time.

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

Top comments (0)