Open sandboxFocusImprove this doc

Getting started with Metalama Caching

If you have a time-consuming method that consistently returns the same value when called with identical arguments, caching this method can significantly enhance your application's performance. With Metalama Caching, this process can be as straightforward as adding the [Cache] attribute from the Metalama.Patterns.Caching.Aspects package.

Before you can utilize the [Cache] aspect, your projects require some setup. The approach will depend on your project's architecture: with or without dependency injection.

Warning

The fallback strategy to generate the cache key of a parameter is to use the ToString method. However, the default implementation of the ToString method does not return a unique string for custom classes and structs (the default implementation of ToString for records is more likely to be correct). After completing the initial steps of this getting started guide, it is crucial to provide a cache key implementation for all parameter types of a cached method. For details, see Customizing cache keys.

With dependency injection

If your project is designed for the .NET Core dependency injection framework (Microsoft.Extensions.DependencyInjection), follow these steps:

  1. Add the Metalama.Patterns.Caching.Aspects package into your project.

  2. In your application setup logic, while adding services to the IServiceCollection, include a call to the AddMetalamaCaching extension method. This action will add an instance of the ICachingService interface, which is consumed by the [Cache] aspect.

    1using Metalama.Documentation.Helpers.ConsoleApp;
    2using Metalama.Patterns.Caching.Building;
    3using Microsoft.Extensions.DependencyInjection;
    4
    5namespace Doc.GettingStarted;
    6
    7internal static class Program
    8{
    9    public static void Main()
    10    {
    11        var builder = ConsoleApp.CreateBuilder();
    12
    13        // Add the caching service.
    14        builder.Services.AddMetalamaCaching();
    15
    16        // Add other components as usual, then run the application.
    17        builder.Services.AddConsoleMain<ConsoleMain>();
    18        builder.Services.AddSingleton<CloudCalculator>();
    19
    20        using var app = builder.Build();
    21        app.Run();
    22    }
    23}
    

The [Cache] aspect is now available to all objects instantiated by the dependency injection container.

Note

If your project uses a different dependency injection framework, you may need to create an adapter for this framework. Then create an instance of the ICachingService interface using the CachingService.Create method. For details about DI adapters, see Injecting dependencies into aspects.

Example: setting up caching with dependency injection

In this example, we demonstrate how to add logging to a self-hosted .NET Core application. This application consists of two services, the primary service called MainService and a hypothetical CloudCalculator, which performs complex and slow computations.

Program.Main calls the AddMetalamaCaching extension method. This action makes ICachingService available to CloudCalculator, which can use the [Cache] aspect. Note how the ICachingService interface is automatically pulled into the CloudCalculator class.

Finally, MainService calls CloudCalculator as usual. It calls the CloudCalculator.Add three times with the same parameters and displays the actual number of operations performed at the end.

Source Code
1using Metalama.Patterns.Caching.Aspects;
2using System;

3

4namespace Doc.GettingStarted;

5
6public sealed class CloudCalculator
7{
8    public int OperationCount { get; private set; }
9
10    [Cache]
11    public int Add( int a, int b )
12    {
13        Console.WriteLine( "Doing some very hard work." );
14










15        this.OperationCount++;
16
17        return a + b;
18    }
19}
Transformed Code
1using Metalama.Patterns.Caching;
2using Metalama.Patterns.Caching.Aspects;
3using Metalama.Patterns.Caching.Aspects.Helpers;
4using System;
5using System.Reflection;
6
7namespace Doc.GettingStarted;
8
9public sealed class CloudCalculator
10{
11    public int OperationCount { get; private set; }
12
13    [Cache]
14    public int Add(int a, int b)
15    {
16        static object? Invoke(object? instance, object?[] args)
17        {
18            return ((CloudCalculator)instance).Add_Source((int)args[0], (int)args[1]);
19        }
20
21        return _cachingService.GetFromCacheOrExecute<int>(_cacheRegistration_Add, this, new object[] { a, b }, Invoke);
22    }
23
24    private int Add_Source(int a, int b)
25    {
26        Console.WriteLine("Doing some very hard work.");
27
28        this.OperationCount++;
29
30        return a + b;
31    }
32
33    private static readonly CachedMethodMetadata _cacheRegistration_Add;
34    private ICachingService _cachingService;
35
36    static CloudCalculator()
37    {
38        _cacheRegistration_Add = CachedMethodMetadata.Register(typeof(CloudCalculator).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(int), typeof(int) }, null).ThrowIfMissing("CloudCalculator.Add(int, int)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, false);
39    }
40
41    public CloudCalculator(ICachingService? cachingService = null)
42    {
43        this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
44    }
45}
1using System;
2using Metalama.Documentation.Helpers.ConsoleApp;
3
4namespace Doc.GettingStarted;
5
6public sealed class ConsoleMain : IConsoleMain
7{
8    private readonly CloudCalculator _cloudCalculator;
9
10    public ConsoleMain( CloudCalculator cloudCalculator )
11    {
12        this._cloudCalculator = cloudCalculator;
13    }
14
15    public void Execute()
16    {
17        for ( var i = 0; i < 3; i++ )
18        {
19            var value = this._cloudCalculator.Add( 1, 1 );
20            Console.WriteLine( $"CloudCalculator returned {value}." );
21        }
22
23        Console.WriteLine(
24            $"In total, CloudCalculator performed {this._cloudCalculator.OperationCount} operation(s)." );
25    }
26}
Doing some very hard work.
CloudCalculator returned 2.
CloudCalculator returned 2.
CloudCalculator returned 2.
In total, CloudCalculator performed 1 operation(s).
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.GettingStarted;
6
7internal static class Program
8{
9    public static void Main()
10    {
11        var builder = ConsoleApp.CreateBuilder();
12
13        // Add the caching service.
14        builder.Services.AddMetalamaCaching();
15
16        // Add other components as usual, then run the application.
17        builder.Services.AddConsoleMain<ConsoleMain>();
18        builder.Services.AddSingleton<CloudCalculator>();
19
20        using var app = builder.Build();
21        app.Run();
22    }
23}

Without dependency injection

If your project does not use dependency injection, the global default instance of the ICachingService will be used. It is exposed as the CachingService.Default property.

Follow these steps to configure your project:

  1. Add the Metalama.Patterns.Caching.Aspects package into your project.

  2. Create a file named, for instance, CachingConfiguration.cs, and add the following code:

    1using Metalama.Patterns.Caching.Aspects;
    2
    3// Disable dependency injection.
    4[assembly: CachingConfiguration( UseDependencyInjection = false )]
    

    This code disables dependency injection for the whole project. You can also add this attribute to individual types to disable dependency injection specifically for these types. You can also configure your project using fabrics and use the amender.Outgoing.ConfigureCaching method and modify the UseDependencyInjection property for selected namespaces or types.

  3. Initialize the CachingService.Default property from your initialization code (typically Program.Main). Call the CachingService.Create method to get a new instance of the service.

    Important

    The caching service must be initialized before any cached method is called for the first time.

When dependency injection is disabled, we can also cache static methods. Observe the [Cache] in the static CloudCalculator implementation.

Source Code
1using Metalama.Patterns.Caching.Aspects;
2using System;

3

4namespace Doc.GettingStarted_NoDI;

5
6public static class CloudCalculator
7{
8    public static int OperationCount { get; private set; }
9
10    [Cache]
11    public static int Add( int a, int b )
12    {
13        Console.WriteLine( "Doing some very hard work." );
14










15        OperationCount++;
16
17        return a + b;
18    }
19}
Transformed Code
1using Metalama.Patterns.Caching;
2using Metalama.Patterns.Caching.Aspects;
3using Metalama.Patterns.Caching.Aspects.Helpers;
4using System;
5using System.Reflection;
6
7namespace Doc.GettingStarted_NoDI;
8
9public static class CloudCalculator
10{
11    public static int OperationCount { get; private set; }
12
13    [Cache]
14    public static int Add(int a, int b)
15    {
16        static object? Invoke(object? instance, object?[] args)
17        {
18            return Add_Source((int)args[0], (int)args[1]);
19        }
20
21        return ((ICachingService)CachingService.Default).GetFromCacheOrExecute<int>(_cacheRegistration_Add, null, new object[] { a, b }, Invoke);
22    }
23
24    private static int Add_Source(int a, int b)
25    {
26        Console.WriteLine("Doing some very hard work.");
27
28        OperationCount++;
29
30        return a + b;
31    }
32
33    private static readonly CachedMethodMetadata _cacheRegistration_Add;
34
35    static CloudCalculator()
36    {
37        _cacheRegistration_Add = CachedMethodMetadata.Register(typeof(CloudCalculator).GetMethod("Add", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(int), typeof(int) }, null).ThrowIfMissing("CloudCalculator.Add(int, int)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, false);
38    }
39}
1using Metalama.Patterns.Caching.Aspects;
2
3// Disable dependency injection.
4[assembly: CachingConfiguration( UseDependencyInjection = false )]
Doing some very hard work.
CloudCalculator returned 2.
CloudCalculator returned 2.
CloudCalculator returned 2.
In total, CloudCalculator performed 1 operation(s).
1using Metalama.Patterns.Caching;
2using System;
3
4namespace Doc.GettingStarted_NoDI;
5
6internal static class Program
7{
8    public static void Main()
9    {
10        // Set up the default caching service.
11        CachingService.Default = CachingService.Create();
12
13        // Execute the program.
14        for ( var i = 0; i < 3; i++ )
15        {
16            var value = CloudCalculator.Add( 1, 1 );
17            Console.WriteLine( $"CloudCalculator returned {value}." );
18        }
19
20        Console.WriteLine(
21            $"In total, CloudCalculator performed {CloudCalculator.OperationCount} operation(s)." );
22    }
23}

What's next

So far, so good. However, if your cached methods have more complex parameters than intrinsic types like int or string (and a dozen of other well-known types), Metalama Caching will use the ToString method to represent the parameter in the caching key. This approach may not always be appropriate. In the next article, we will discuss how to customize the caching key.