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 it generates expands at the call site.

There are two primary reasons to use auxiliary templates:

  • Code reuse: Moving repetitive code logic to an auxiliary template reduces duplication, aligning with Metalama's goal of streamlining code writing.
  • Abstraction: Since template methods can be virtual, users of your aspects can customize templates.

You can call a template in two ways: the standard way, like calling any C# method, and the dynamic way, which addresses more advanced scenarios. Both approaches are covered in the following sections.

Creating auxiliary templates

To create an auxiliary template, follow these steps:

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

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

    Note

    This rule applies even if you want to create a helper class containing only static methods. In this case, you can't mark the class as static, but you can add a private constructor to prevent instantiation.

  3. Most of the time, you'll 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 for use in different projects, and in some projects, we want to log a message on 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 (the T# compiler doesn't differentiate auxiliary templates from normal templates; 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, make the auxiliary template void and call the meta.Return method, which generates a return <expression> statement while satisfying the C# compiler.

Note

There's 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 are 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 invoke the EqualityComparer<T>.Default.Equals method for each field or property of the target type. C# doesn't allow you to write EqualityComparer<field.Type>.Default.Equals, although this is what you'd conceptually need.

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

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

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

Instead of using nameof() to reference a template, you can assign a stable identifier using the Id property and reference the template by that identifier. This is useful when templates are defined in a separate assembly where nameof() is not available.

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 this 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 using 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( t => t is
16                 {
17                     IsAutoPropertyOrField: true, IsImplicitlyDeclared: false
18                 } ) )
19        {
20            meta.InvokeTemplate(
21                nameof(this.CompareFieldOrProperty),
22                args: new
23                {
24                    TFieldOrProperty = fieldOrProperty.Type,
25                    fieldOrProperty,
26                    other = ExpressionFactory.Capture( other! )
27                } );
28        }
29
30        return true;
31    }
32
33    [Template]
34    private void CompareFieldOrProperty<[CompileTime] TFieldOrProperty>(
35        IFieldOrProperty fieldOrProperty,
36        IExpression other )
37    {
38        if ( !EqualityComparer<TFieldOrProperty>.Default.Equals(
39                fieldOrProperty.Value,
40                fieldOrProperty.WithObject( other ).Value ) )
41        {
42            meta.Return( false );
43        }
44    }
45
46    [Introduce( Name = nameof(GetHashCode), WhenExists = OverrideStrategy.Override )]
47    public int GetHashCodeImpl()
48    {
49        var hashCode = new HashCode();
50
51        foreach ( var fieldOrProperty in meta.Target.Type.FieldsAndProperties.Where( t => t is
52                 {
53                     IsAutoPropertyOrField: true, IsImplicitlyDeclared: false
54                 } ) )
55        {
56            hashCode.Add( fieldOrProperty.Value );
57        }
58
59        return hashCode.ToHashCode();
60    }
61}
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 encapsulating a method call into a delegate. The TemplateInvocation can be passed as an argument to another auxiliary template and invoked using the meta.InvokeTemplate method.

This technique is helpful when an aspect allows customizations of the generated code but the customized template must call given logic. For instance, a caching aspect may allow customization to inject try..catch, requiring 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 ways. First, it would make sense in this case to use two aspects. Second, using a protected method invoked by AroundCaching would be preferable. Using TemplateInvocation makes sense when the template to call isn't part of the same class—for instance, if the caching aspect accepts options that can be set from a fabric, allowing 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'll 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: SwitchStatementBuilder

The following example generates an Execute method with two arguments: a message name and an opaque argument. The aspect must be used on a class with one or more 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( m => m.Name.StartsWith(
18                                                "Process",
19                                                StringComparison.OrdinalIgnoreCase ) );
20
21        foreach ( var processMethod in processMethods )
22        {
23            var nameWithoutPrefix = processMethod.Name.Substring( "Process".Length );
24
25            switchBuilder.AddCase(
26                SwitchStatementLabel.CreateLiteral( nameWithoutPrefix ),
27                null,
28                StatementFactory.FromTemplate(
29                        nameof(this.Case),
30                        new { method = processMethod, args = ExpressionFactory.Capture( args ) } )
31                    .UnwrapBlock() );
32        }
33
34        switchBuilder.AddDefault(
35            StatementFactory.FromTemplate( nameof(this.DefaultCase) ).UnwrapBlock(),
36            false );
37
38        meta.InsertStatement( switchBuilder.ToStatement() );
39    }
40
41    [Template]
42    private void Case( IMethod method, IExpression args )
43    {
44        method.Invoke( args );
45    }
46
47    [Template]
48    private void DefaultCase()
49    {
50        throw new ArgumentOutOfRangeException();
51    }
52}
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}