Open sandboxFocusImprove this doc

Caching example, step 1: getting started

At first glance, caching appears simple. The aspect generates the cache key, performs a cache lookup, and returns the result if it exists. If it doesn't exist, the method runs, and the return value gets stored in the cache. The following example compares the source code, without caching logic, to the transformed code, with caching logic.

1public class Calculator
2{
3    public int InvocationCounts { get; private set; }
4
5    public int Add( int a, int b )
6    {
7        Console.WriteLine( "Thinking..." );
8        this.InvocationCounts++;
9
10        return a + b;
11    }
12}

Infrastructure Code

The aspect relies on the ICache interface, which has only two methods used by the aspect.

1public interface ICache
2{
3    bool TryGetValue( string key, out object? value );
4
5    void TryAdd( string key, object? value );
6}

A typical implementation of this interface would use MemoryCache.

Aspect Code

The aspect itself is straightforward:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code.SyntaxBuilders;
4
5public class CacheAttribute : OverrideMethodAspect
6{
7    [IntroduceDependency]
    Warning CS0649: Field 'CacheAttribute._cache' is never assigned to, and will always have its default value null

8    private readonly ICache _cache;
9
10    public override dynamic? OverrideMethod()
11    {
12        // Builds the caching string.
13        var cacheKey = CacheKeyBuilder.GetCachingKey().ToValue();
14
15        // Cache lookup.
16        if ( this._cache.TryGetValue( cacheKey, out object value ) )
17        {
18            // Cache hit.
19            return value;
20        }
21
22        // Cache miss. Go and invoke the method.
23        var result = meta.Proceed();
24
25        // Add to cache.
26        this._cache.TryAdd( cacheKey, result );
27
28        return result;
29    }
30}

As usual, the aspect class inherits the abstract class OverrideMethodAspect, which, in turn, derives from the System.Attribute class. This makes CacheAttribute a custom attribute. The OverrideMethod method acts like a template. Most of its code gets injected into the target method, the one to which we add the [ReportAndSwallowExceptionsAttribute] custom attribute. Inside the OverrideMethod implementation, the call to meta.Proceed() has a unique meaning. When the aspect applies to the target, the call to meta.Proceed() stands in for the original implementation, with a few syntactic alterations that capture the return value.

The CacheKeyBuilder class hides the complexity of creating the cache key. It is a compile-time class.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3
4[CompileTime]
5internal static class CacheKeyBuilder
6{
7    public static InterpolatedStringBuilder GetCachingKey()
8    {
9        var stringBuilder = new InterpolatedStringBuilder();
10        stringBuilder.AddText( meta.Target.Type.ToString() );
11        stringBuilder.AddText( "." );
12        stringBuilder.AddText( meta.Target.Method.Name );
13        stringBuilder.AddText( "(" );
14
15        foreach ( var p in meta.Target.Parameters )
16        {
17            if ( p.Index > 0 )
18            {
19                stringBuilder.AddText( ", " );
20            }
21
22            // We have to add the parameter type to avoid ambiguities
23            // between different overloads of the same method.
24            stringBuilder.AddText( "(" );
25            stringBuilder.AddText( p.Type.ToString() );
26            stringBuilder.AddText( ")" );
27
28            stringBuilder.AddText( "{" );
29            stringBuilder.AddExpression( p );
30            stringBuilder.AddText( "}" );
31        }
32
33        stringBuilder.AddText( ")" );
34
35        return stringBuilder;
36    }
37}

As its name suggests, the InterpolatedStringBuilder class helps build interpolated strings. The meta.Target property exposes the context into which the template applies. meta.Target.Type is the current type, meta.Target.Method is the current method, and so on. The GetCachingKey method creates an interpolated string for the current context. Parameters get represented at compile time using the IParameter interface. The parameter.Value expression returns a dynamic object that represents a run-time expression. In this case, it's the name of the parameter.

Upon receiving the InterpolatedStringBuilder instance from the CacheKeyBuilder class, the aspect converts it to a run-time interpolated string by calling the ToValue method. This method returns a dynamic object representing the interpolated string. It can be cast to a string, and we use it as the caching key.