Open sandboxFocusImprove this doc

Calling auxiliary templates

Auxiliary templates are templates designed to be called from other templates. When an auxiliary template is called from a template, the code generated by the auxiliary template is expanded at the point where it is called.

There are two primary reasons why you may want to use auxiliary templates:

  • Code reuse: Moving repetitive code logic to an auxiliary template can reduce duplication. This aligns with the primary goal of Metalama, which is to streamline code writing.
  • Abstraction: Since template methods can be virtual, you can allow the users of your aspects to modify the templates.

There are two ways to call a template: the standard way, just like you would call any C# method, and the dynamic way, which addresses more advanced scenarios. Both approaches will be covered in the subsequent sections.

Creating auxiliary templates

To create an auxiliary template, follow these steps:

  1. Just like a normal template, create a method and annotate it with the [Template] custom attribute.

  2. If you are creating this method outside of an aspect or fabric type, ensure that this class implements the ITemplateProvider empty interface.

    Note

    This rule applies even if you want to create a helper class that contains only static methods. In this case, you cannot mark the class as static, but you can add a unique private constructor to prevent instantiation of the class.

  3. Most of the time, you will want auxiliary templates to be void, as explained below.

A template can invoke another template just like any other method. You can pass values to its compile-time and run-time parameters.

Warning

An important limitation to bear in mind is that templates can be invoked only as statements and not as part of an expression. We will revisit this restriction later in this article.

Example: simple auxiliary templates

The following example is a simple caching aspect. The aspect is intended to be used in different projects, and in some projects, we want to log a message in case of cache hit or miss. Therefore, we moved the logging logic to virtual auxiliary template methods, with an empty implementation by default. In CacheAndLog, we override the logging logic.

1using Metalama.Framework.Aspects;
2using System;
3using System.Collections.Concurrent;
4
5namespace Doc.AuxiliaryTemplate;
6
7internal class CacheAttribute : OverrideMethodAspect
8{
9    [Introduce( WhenExists = OverrideStrategy.Ignore )]
10    private readonly ConcurrentDictionary<string, object?> _cache = new();
11
12    // This method is the usual top-level template.
13    public override dynamic? OverrideMethod()
14    {
15        // Naive implementation of a caching key.
16        var cacheKey =
17            $"{meta.Target.Method.Name}({string.Join( ", ", meta.Target.Method.Parameters.ToValueArray() )})";
18
19        if ( this._cache.TryGetValue( cacheKey, out var returnValue ) )
20        {
21            this.LogCacheHit( cacheKey, returnValue );
22        }
23        else
24        {
25            this.LogCacheMiss( cacheKey );
26            returnValue = meta.Proceed();
27            this._cache.TryAdd( cacheKey, returnValue );
28        }
29
30        return returnValue;
31    }
32
33    // This method is an auxiliary template.
34
35    [Template]
36    protected virtual void LogCacheHit( string cacheKey, object? value ) { }
37
38    // This method is an auxiliary template.
39    [Template]
40    protected virtual void LogCacheMiss( string cacheKey ) { }
41}
42
43internal class CacheAndLogAttribute : CacheAttribute
44{
45    protected override void LogCacheHit( string cacheKey, object? value )
46    {
47        Console.WriteLine( $"Cache hit: {cacheKey} => {value}" );
48    }
49
50    protected override void LogCacheMiss( string cacheKey )
51    {
52        Console.WriteLine( $"Cache hit: {cacheKey}" );
53    }
54}
Source Code
1namespace Doc.AuxiliaryTemplate;
2



3public class SelfCachedClass
4{
5    [Cache]
6    public int Add( int a, int b ) => a + b;
7
8    [CacheAndLog]
















9    public int Rmove( int a, int b ) => a - b;
10}
Transformed Code
1using System;
2using System.Collections.Concurrent;
3
4namespace Doc.AuxiliaryTemplate;
5
6public class SelfCachedClass
7{
8    [Cache]
9    public int Add(int a, int b)
10    {
11        var cacheKey = $"Add({string.Join(", ", new object[] { a, b })})";
12        if (_cache.TryGetValue(cacheKey, out var returnValue))
13        {
14            string cacheKey_1 = cacheKey;
15            global::System.Object? value = returnValue;
16        }
17        else
18        {
19            string cacheKey_2 = cacheKey;
20            returnValue = a + b;
21            _cache.TryAdd(cacheKey, returnValue);
22        }
23
24        return (int)returnValue;
25    }
26
27    [CacheAndLog]
28    public int Rmove(int a, int b)
29    {
30        var cacheKey = $"Rmove({string.Join(", ", new object[] { a, b })})";
31        if (_cache.TryGetValue(cacheKey, out var returnValue))
32        {
33            string cacheKey_1 = cacheKey;
34            global::System.Object? value = returnValue;
35            Console.WriteLine($"Cache hit: {cacheKey_1} => {value}");
36        }
37        else
38        {
39            string cacheKey_2 = cacheKey;
40            Console.WriteLine($"Cache hit: {cacheKey_2}");
41            returnValue = a - b;
42            _cache.TryAdd(cacheKey, returnValue);
43        }
44
45        return (int)returnValue;
46    }
47
48    private readonly ConcurrentDictionary<string, object?> _cache = new();
49}

Using return statements in auxiliary templates

The behavior of return statements in auxiliary templates can sometimes be confusing compared to normal templates. Their nominal processing by the T# compiler is identical (indeed the T# compiler does not differentiate auxiliary templates from normal templates as their difference is only in usage): return statements in any template result in return statements in the output.

In a normal non-void C# method, all execution branches must end with a return <expression> statement. However, because auxiliary templates often generate snippets instead of complete method bodies, you don't always want every branch of the auxiliary template to end with a return statement.

To work around this situation, you can make the subtemplate void and call the meta.Return method, which will generate a return <expression> statement while making the C# compiler satisfied with your template.

Note

There is no way to explicitly interrupt the template processing other than playing with compile-time if, else and switch statements and ensuring that the control flow continues to the natural end of the template method.

Example: meta.Return

The following example is a variation of our previous caching example, but we abstract the entire caching logic instead of just the logging part. The aspect has two auxiliary templates: GetFromCache and AddToCache. The first template is problematic because the cache hit branch must have a return statement while the cache miss branch must continue the execution. Therefore, we designed GetFromCache as a void template and used meta.Return to generate the return statement.

1using Metalama.Framework.Aspects;
2using System.Collections.Concurrent;
3
4namespace Doc.AuxiliaryTemplate_Return;
5
6internal class CacheAttribute : OverrideMethodAspect
7{
8    [Introduce( WhenExists = OverrideStrategy.Ignore )]
9    private readonly ConcurrentDictionary<string, object?> _cache = new();
10
11    // This method is the usual top-level template.
12    public override dynamic? OverrideMethod()
13    {
14        // Naive implementation of a caching key.
15        var cacheKey =
16            $"{meta.Target.Method.Name}({string.Join( ", ", meta.Target.Method.Parameters.ToValueArray() )})";
17
18        this.GetFromCache( cacheKey );
19
20        var returnValue = meta.Proceed();
21
22        this.AddToCache( cacheKey, returnValue );
23
24        return returnValue;
25    }
26
27    // This method is an auxiliary template.
28
29    [Template]
30    protected virtual void GetFromCache( string cacheKey )
31    {
32        if ( this._cache.TryGetValue( cacheKey, out var returnValue ) )
33        {
34            meta.Return( returnValue );
35        }
36    }
37
38    // This method is an auxiliary template.
39    [Template]
40    protected virtual void AddToCache( string cacheKey, object? returnValue )
41    {
42        this._cache.TryAdd( cacheKey, returnValue );
43    }
44}
Source Code
1namespace Doc.AuxiliaryTemplate_Return;
2


3public class SelfCachedClass
4{
5    [Cache]
6    public int Add( int a, int b ) => a + b;
7}
Transformed Code
1using System.Collections.Concurrent;
2
3namespace Doc.AuxiliaryTemplate_Return;
4
5public class SelfCachedClass
6{
7    [Cache]
8    public int Add(int a, int b)
9    {
10        var cacheKey = $"Add({string.Join(", ", new object[] { a, b })})";
11        string cacheKey_1 = cacheKey;
12        if (_cache.TryGetValue(cacheKey_1, out var returnValue))
13        {
14            return (int)returnValue;
15        }
16
17        int returnValue_1;
18        returnValue_1 = a + b;
19        string cacheKey_2 = cacheKey;
20        global::System.Object? returnValue_2 = returnValue_1;
21        _cache.TryAdd(cacheKey_2, returnValue_2);
22        return returnValue_1;
23    }
24
25    private readonly ConcurrentDictionary<string, object?> _cache = new();
26}

Invoking generic templates

Auxiliary templates can be beneficial when you need to call a generic API from a foreach loop and the type parameter must be bound to a type that depends on the iterator variable.

For instance, suppose you want to generate a field-by-field implementation of the Equals method and you want to invoke the EqualityComparer<T>.Default.Equals method for each field or property of the target type. C# does not allow you to write EqualityComparer<field.Type>.Default.Equals, although this is what you would conceptually need.

In this situation, you can use an auxiliary template with a compile-time type parameter.

To invoke the template, use the meta.InvokeTemplate and specify the args parameter. For instance:

meta.InvokeTemplate( nameof(CompareFieldOrProperty), args:
new { TFieldOrProperty = fieldOrProperty.Type, fieldOrProperty, other = (IExpression) other! } );

This is illustrated by the following example:

Example: invoking a generic template

The following aspect implements the Equals method by comparing all fields or automatic properties. For the sake of the exercise, we want to call the EqualityComparer<T>.Default.Equals method with the proper value of T for each field or property. This is achieved by the use of an auxiliary template and the meta.InvokeTemplate method.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System.Collections.Generic;
5using System;
6using System.Linq;
7
8namespace Doc.StructurallyComparable;
9
10public class StructuralEquatableAttribute : TypeAspect
11{
12    [Introduce( Name = nameof(Equals), WhenExists = OverrideStrategy.Override )]
13    public bool EqualsImpl( object? other )
14    {
15        foreach ( var fieldOrProperty in meta.Target.Type.FieldsAndProperties.Where(
16                     t => t is { IsAutoPropertyOrField: true, IsImplicitlyDeclared: false } ) )
17        {
18            meta.InvokeTemplate(
19                nameof(this.CompareFieldOrProperty),
20                args: new
21                {
22                    TFieldOrProperty = fieldOrProperty.Type,
23                    fieldOrProperty,
24                    other = ExpressionFactory.Capture( other! )
25                } );
26        }
27
28        return true;
29    }
30
31    [Template]
32    private void CompareFieldOrProperty<[CompileTime] TFieldOrProperty>(
33        IFieldOrProperty fieldOrProperty,
34        IExpression other )
35    {
36        if ( !EqualityComparer<TFieldOrProperty>.Default.Equals(
37                fieldOrProperty.Value,
38                fieldOrProperty.With( other ).Value ) )
39        {
40            meta.Return( false );
41        }
42    }
43
44    [Introduce( Name = nameof(GetHashCode), WhenExists = OverrideStrategy.Override )]
45    public int GetHashCodeImpl()
46    {
47        var hashCode = new HashCode();
48
49        foreach ( var fieldOrProperty in meta.Target.Type.FieldsAndProperties.Where(
50                     t => t is { IsAutoPropertyOrField: true, IsImplicitlyDeclared: false } ) )
51        {
52            hashCode.Add( fieldOrProperty.Value );
53        }
54
55        return hashCode.ToHashCode();
56    }
57}
Source Code
1namespace Doc.StructurallyComparable;
2



3[StructuralEquatable]
4internal class WineBottle
5{
6    public string Cepage { get; init; }
7
8    public int Millesime { get; init; }
9
10    public WineProducer Vigneron { get; init; }
11}
12






13[StructuralEquatable]
14internal class WineProducer























15{
16    public string Name { get; init; }
17
18    public string Address { get; init; }
19}
Transformed Code
1using System;
2using System.Collections.Generic;
3
4namespace Doc.StructurallyComparable;
5
6[StructuralEquatable]
7internal class WineBottle
8{
9    public string Cepage { get; init; }
10
11    public int Millesime { get; init; }
12
13    public WineProducer Vigneron { get; init; }
14
15    public override bool Equals(object? other)
16    {
17        if (!EqualityComparer<string>.Default.Equals(Cepage, ((WineBottle)other).Cepage))
18        {
19            return false;
20        }
21
22        if (!EqualityComparer<int>.Default.Equals(Millesime, ((WineBottle)other).Millesime))
23        {
24            return false;
25        }
26
27        if (!EqualityComparer<WineProducer>.Default.Equals(Vigneron, ((WineBottle)other).Vigneron))
28        {
29            return false;
30        }
31
32        return true;
33    }
34
35    public override int GetHashCode()
36    {
37        var hashCode = new HashCode();
38        hashCode.Add(Cepage);
39        hashCode.Add(Millesime);
40        hashCode.Add(Vigneron);
41        return hashCode.ToHashCode();
42    }
43}
44
45[StructuralEquatable]
46internal class WineProducer
47{
48    public string Name { get; init; }
49
50    public string Address { get; init; }
51
52    public override bool Equals(object? other)
53    {
54        if (!EqualityComparer<string>.Default.Equals(Name, ((WineProducer)other).Name))
55        {
56            return false;
57        }
58
59        if (!EqualityComparer<string>.Default.Equals(Address, ((WineProducer)other).Address))
60        {
61            return false;
62        }
63
64        return true;
65    }
66
67    public override int GetHashCode()
68    {
69        var hashCode = new HashCode();
70        hashCode.Add(Name);
71        hashCode.Add(Address);
72        return hashCode.ToHashCode();
73    }
74}

Encapsulating a template invocation as a delegate

Calls to auxiliary templates can be encapsulated into an object of type TemplateInvocation, similar to the encapsulation of a method call into a delegate. The TemplateInvocation can be passed as an argument to another auxiliary template and invoked by the meta.InvokeTemplate method.

This technique is helpful when an aspect allows customizations of the generated code but when the customized template must call a given logic. For instance, a caching aspect may allow the customization to inject some try..catch, and therefore requires a mechanism for the customization to call the desired logic inside the try..catch.

Example: delegate-like invocation

The following code shows a base caching aspect named CacheAttribute that allows customizations to wrap the entire caching logic into arbitrary logic by overriding the AroundCaching template. This template must by contract invoke the TemplateInvocation it receives. The CacheAndRetryAttribute uses this mechanism to inject retry-on-exception logic.

1using Metalama.Framework.Aspects;
2using System;
3using System.Collections.Concurrent;
4
5namespace Doc.AuxiliaryTemplate_TemplateInvocation;
6
7public class CacheAttribute : OverrideMethodAspect
8{
9    [Introduce( WhenExists = OverrideStrategy.Ignore )]
10    private readonly ConcurrentDictionary<string, object?> _cache = new();
11
12    public override dynamic? OverrideMethod()
13    {
14        this.AroundCaching( new TemplateInvocation( nameof(this.CacheOrExecuteCore) ) );
15
16        // This should be unreachable.
17        return default;
18    }
19
20    [Template]
21    protected virtual void AroundCaching( TemplateInvocation templateInvocation )
22    {
23        meta.InvokeTemplate( templateInvocation );
24    }
25
26    [Template]
27    private void CacheOrExecuteCore()
28    {
29        // Naive implementation of a caching key.
30        var cacheKey =
31            $"{meta.Target.Method.Name}({string.Join( ", ", meta.Target.Method.Parameters.ToValueArray() )})";
32
33        if ( !this._cache.TryGetValue( cacheKey, out var returnValue ) )
34        {
35            returnValue = meta.Proceed();
36            this._cache.TryAdd( cacheKey, returnValue );
37        }
38
39        meta.Return( returnValue );
40    }
41}
42
43public class CacheAndRetryAttribute : CacheAttribute
44{
45    public bool IncludeRetry { get; set; }
46
47    protected override void AroundCaching( TemplateInvocation templateInvocation )
48    {
49        if ( this.IncludeRetry )
50        {
51            for ( var i = 0;; i++ )
52            {
53                try
54                {
55                    meta.InvokeTemplate( templateInvocation );
56                }
57                catch ( Exception ex ) when ( i < 10 )
58                {
59                    Console.WriteLine( ex.ToString() );
60                }
61            }
62        }
63        else
64        {
65            meta.InvokeTemplate( templateInvocation );
66        }
67    }
68}
Source Code
1namespace Doc.AuxiliaryTemplate_TemplateInvocation;
2



3public class SelfCachedClass
4{
5    [Cache]
6    public int Add( int a, int b ) => a + b;
7
8    [CacheAndRetry( IncludeRetry = true )]









































9    public int Rmove( int a, int b ) => a - b;
10}
Transformed Code
1using System;
2using System.Collections.Concurrent;
3
4namespace Doc.AuxiliaryTemplate_TemplateInvocation;
5
6public class SelfCachedClass
7{
8    [Cache]
9    public int Add(int a, int b)
10    {
11        {
12            var cacheKey = $"Add({string.Join(", ", new object[] { a, b })})";
13            if (!_cache.TryGetValue(cacheKey, out var returnValue))
14            {
15                returnValue = a + b;
16                _cache.TryAdd(cacheKey, returnValue);
17            }
18
19            return (int)returnValue;
20        }
21
22        return default;
23    }
24
25    [CacheAndRetry(IncludeRetry = true)]
26    public int Rmove(int a, int b)
27    {
28        for (var i = 0; ; i++)
29        {
30            try
31            {
32                {
33                    var cacheKey = $"Rmove({string.Join(", ", new object[] { a, b })})";
34                    if (!_cache.TryGetValue(cacheKey, out var returnValue))
35                    {
36                        returnValue = a - b;
37                        _cache.TryAdd(cacheKey, returnValue);
38                    }
39
40                    return (int)returnValue;
41                }
42            }
43            catch (Exception ex) when (i < 10)
44            {
45                Console.WriteLine(ex.ToString());
46            }
47        }
48
49        return default;
50    }
51
52    private readonly ConcurrentDictionary<string, object?> _cache = new();
53}

This example is contrived in two regards. First, it would make sense in this case to use two aspects. Second, the use of a protected method invoked by AroundCaching would be preferable in this case. The use of TemplateInvocation makes sense when the template to call is not a part of the same class — for instance, if the caching aspect accepts options that can be set from a fabric, and that would allow users to supply a different implementation of this logic without overriding the caching attribute itself.

Evaluating a template into an IStatement

If you want to use templates with facilities like SwitchStatementBuilder, you will need an IStatement. To wrap a template invocation into an IStatement, use StatementFactory.FromTemplate.

You can call UnwrapBlock to remove braces from the template output, which will return an IStatementList.

Example: SwitchExpressionBuilder

The following example generates an Execute method which has two arguments: a message name and an opaque argument. The aspect must be used on a class with one or many ProcessFoo methods, where Foo is the message name. The aspect generates a switch statement that dispatches the message to the proper method. We use the StatementFactory.FromTemplate method to pass templates to the SwitchStatementBuilder.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System;
5using System.Linq;
6
7namespace Doc.SwitchStatementBuilder_FullTemplate;
8
9public class DispatchAttribute : TypeAspect
10{
11    [Introduce]
12    public void Execute( string messageName, string args )
13    {
14        var switchBuilder = new SwitchStatementBuilder( ExpressionFactory.Capture( messageName ) );
15
16        var processMethods =
17            meta.Target.Type.Methods.Where(
18                m => m.Name.StartsWith( "Process", StringComparison.OrdinalIgnoreCase ) );
19
20        foreach ( var processMethod in processMethods )
21        {
22            var nameWithoutPrefix = processMethod.Name.Substring( "Process".Length );
23
24            switchBuilder.AddCase(
25                SwitchStatementLabel.CreateLiteral( nameWithoutPrefix ),
26                null,
27                StatementFactory.FromTemplate(
28                        nameof(this.Case),
29                        new { method = processMethod, args = ExpressionFactory.Capture( args ) } )
30                    .UnwrapBlock() );
31        }
32
33        switchBuilder.AddDefault(
34            StatementFactory.FromTemplate( nameof(this.DefaultCase) ).UnwrapBlock(),
35            false );
36
37        meta.InsertStatement( switchBuilder.ToStatement() );
38    }
39
40    [Template]
41    private void Case( IMethod method, IExpression args )
42    {
43        method.Invoke( args );
44    }
45
46    [Template]
47    private void DefaultCase()
48    {
49        throw new ArgumentOutOfRangeException();
50    }
51}
Source Code
1namespace Doc.SwitchStatementBuilder_FullTemplate;
2


3[Dispatch]
4public class FruitProcessor
5{
6    private void ProcessApple( string args ) { }
7
8    private void ProcessOrange( string args ) { }


9
10    private void ProcessPear( string args ) { }
11}
Transformed Code
1using System;
2
3namespace Doc.SwitchStatementBuilder_FullTemplate;
4
5[Dispatch]
6public class FruitProcessor
7{
8    private void ProcessApple(string args) { }
9
10    private void ProcessOrange(string args) { }
11
12    private void ProcessPear(string args) { }
13
14    public void Execute(string messageName, string args)
15    {
16        switch (messageName)
17        {
18            case "Apple":
19                ProcessApple(args);
20                break;
21            case "Orange":
22                ProcessOrange(args);
23                break;
24            case "Pear":
25                ProcessPear(args);
26                break;
27            default:
28                throw new ArgumentOutOfRangeException();
29        }
30    }
31}