Open sandboxFocusImprove this doc

Generating run-time code

Templates use the dynamic type to represent types unknown to the template developer. For example, an aspect may not know the return type of the methods to which it is applied in advance. The return type is represented by the dynamic type.

dynamic? OverrideMethod()
{
    return default;
}

All dynamic compile-time code is transformed into strongly-typed run-time code. That is, we use dynamic when the expression type is unknown to the template developer, but the type is always known when the template is applied.

In a template, it is not possible to generate code that employs dynamic typing at run time.

Dynamic code

The meta API exposes some properties of the dynamic type and some methods returning dynamic values. These members are compile-time, but they produce a C# expression that can be used in the run-time code of the template. Because these members return a dynamic value, they can be utilized anywhere in your template. The code will not be validated when the template is compiled but when the template is applied.

For instance, meta.This returns a dynamic object that represents the expression this. Because meta.This is dynamic, you can write meta.This._logger in your template, which will translate to this._logger. This will work even if your template does not contain a member named _logger because meta.This returns a dynamic; therefore, any field or method referenced on the right hand of the meta.This expression will not be validated when the template is compiled (or in the IDE) but when the template is expanded, in the context of a specific target declaration.

Here are a few examples of APIs that return a dynamic:

  • Equivalents to the this or base keywords:
    • meta.This, equivalent to the this keyword, allows calling arbitrary instance members of the target type.
    • meta.Base, equivalent to the base keyword, allows calling arbitrary instance members of the base of the target type.
    • meta.ThisType allows calling arbitrary static members of the target type.
    • meta.BaseType allows calling arbitrary static members of the base of the target type.
  • IExpression.Value allows getting or setting the value of a compile-time expression in run-time code. It is implemented, for instance, by:
    • meta.Target.Field.Value, meta.Target.Property.Value, or meta.Target.FieldOrProperty.Value allow getting or setting the value of the target field or property.
    • meta.Target.Parameter.Value allows getting or setting the value of the target parameter.
    • meta.Target.Method.Parameters[*].Value allows getting or setting the value of a target method's parameter.
Warning

Due to the limitations of the C# language, you cannot use extension methods on the right part of a dynamic expression. In this case, you must call the extension method in the traditional way, by specifying its type name on the left and passing the dynamic expression as an argument. An alternative approach is to cast the dynamic expression to a specified type if it is well-known.

Using dynamic expressions

You can write any dynamic code on the left of a dynamic expression. As with any dynamically typed code, the syntax of the code is validated, but not the existence of the invoked members.

// Translates into: this.OnPropertyChanged( "X" );
meta.This.OnPropertyChanged( "X" );

You can combine dynamic code and compile-time expressions. In the following snippet, OnPropertyChanged is dynamically resolved but meta.Property.Name evaluates into a string:

// Translated into: this.OnPropertyChanged( "MyProperty" );
meta.This.OnPropertyChanged( meta.Property.Name );

Dynamic expressions can appear anywhere in an expression. In the following example, it is part of a string concatenation expression:

// Translates into: Console.WriteLine( "p = " + p );
Console.WriteLine( "p = " + meta.Target.Parameters["p"].Value );

Example: dynamic member

In the following aspect, the logging aspect uses meta.This, which returns a dynamic object, to access the type being enhanced. The aspect assumes that the target type defines a field named _logger and that the type of this field has a method named WriteLine.

1using Metalama.Framework.Aspects;
2
3namespace Doc.DynamicTrivial;
4
5internal class LogAttribute : OverrideMethodAspect
6{
7    public override dynamic? OverrideMethod()
8    {
9        meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );
10
11        return meta.Proceed();
12    }
13}
Source Code
1using System;
2using System.IO;
3
4namespace Doc.DynamicTrivial;
5
6internal class Program
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void Foo() { }
12
13    private static void Main()



14    {
15        new Program().Foo();
16    }
17}
Transformed Code
1using System;
2using System.IO;
3
4namespace Doc.DynamicTrivial;
5
6internal class Program
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void Foo()
12    {
13        _logger.WriteLine("Executing Program.Foo().");
14    }
15
16    private static void Main()
17    {
18        new Program().Foo();
19    }
20}
Executing Program.Foo().

Assignment of dynamic members

When the expression is writable, the dynamic member can be used on the right hand of an assignment:

// Translates into: this.MyProperty = 5;
meta.Property.Value = 5;

Dynamic local variables

When the template is expanded, dynamic variables are transformed into var variables. Therefore, all dynamic variables must be initialized.

Generating calls to the code model

When you have a Metalama.Framework.Code representation of a declaration, you may want to access it from your generated run-time code. You can do this by using one of the following methods or properties:

By default, when used with an instance member, all the methods and properties above generate calls for the current (this) instance. To specify a different instance, use the With method.

Example: invoking members

The following example is a variation of the previous one. The aspect no longer assumes the logger field is named _logger. Instead, it looks for any field of type TextWriter. Because it does not know the field's name upfront, the aspect must use the IExpression.Value property to get an expression allowing it to access the field. This property returns a dynamic object, but we cast it to TextWriter because we know its actual type. When the template is expanded, Metalama recognizes that the cast is redundant and simplifies it. However, the cast is useful in the T# template to get as much strongly-typed code as we can.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System.IO;
4using System.Linq;
5
6namespace Doc.DynamicCodeModel;
7
8internal class LogAttribute : OverrideMethodAspect
9{
10    public override dynamic? OverrideMethod()
11    {
12        var loggerField = meta.Target.Type.FieldsAndProperties
13            .Where( x => x.Type.Is( typeof(TextWriter) ) )
14            .Single();
15
16        ((TextWriter) loggerField.Value!).WriteLine( $"Executing {meta.Target.Method}." );
17
18        return meta.Proceed();
19    }
20}
Source Code
1using System;
2using System.IO;
3
4namespace Doc.DynamicCodeModel;
5
6internal class Program
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void Foo() { }
12
13    private static void Main()



14    {
15        new Program().Foo();
16    }
17}
Transformed Code
1using System;
2using System.IO;
3
4namespace Doc.DynamicCodeModel;
5
6internal class Program
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void Foo()
12    {
13        _logger.WriteLine("Executing Program.Foo().");
14    }
15
16    private static void Main()
17    {
18        new Program().Foo();
19    }
20}
Executing Program.Foo().

Converting run-time expressions into compile-time IExpression

Instead of using techniques like parsing to generate IExpression objects, it can be convenient to write the expression in T#/C# and to convert it. This allows you to have expressions that depend on compile-time conditions and control flows.

Two approaches are available depending on the situation:

  • When the expression is dynamic, you can simply use an explicit cast to IExpression. For instance:

    var thisParameter = meta.Target.Method.IsStatic
    ? meta.Target.Method.Parameters.First()
    : (IExpression) meta.This;
    

    This also works when the cast is implicit, for instance:

    IExpression baseCall;
    
    if (meta.Target.Method.IsOverride)
    {
        baseCall = (IExpression) meta.Base.Clone();
    }
    else
    {
        baseCall = (IExpression) meta.Base.MemberwiseClone();
    }
    

    This template generates either var clone = (TargetType) base.Clone(); or var clone = (TargetType) this.MemberwiseClone(); depending on the condition.

  • Otherwise, use the ExpressionFactory.Capture method.

You can use the WithType and WithNullability extension methods to modify the return type of the returned IExpression.

Generating run-time arrays

The first way to generate a run-time array is to declare a variable of array type and to use a statement to set each element, for instance:

var args = new object[2];
args[0] = "a";
args[1] = DateTime.Now;
MyRunTimeMethod( args );

To generate an array of variable length, you can use the ArrayBuilder class.

For instance:

var arrayBuilder = new ArrayBuilder();
arrayBuilder.Add( "a" );
arrayBuilder.Add( DateTime.Now );
MyRunTimeMethod( arrayBuilder.ToValue() );

This will generate the following code:

MyRunTimeMethod( new object[] { "a", DateTime.Now });

Generating interpolated strings

Instead of generating a string as an array separately and using string.Format, you can generate an interpolated string using the InterpolatedStringBuilder class.

The following example shows how an InterpolatedStringBuilder can be used to implement the ToString method automatically.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3using System.Linq;
4
5namespace Doc.ToString;
6
7internal class ToStringAttribute : TypeAspect
8{
9    [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
10    public string IntroducedToString()
11    {
12        var stringBuilder = new InterpolatedStringBuilder();
13        stringBuilder.AddText( "{ " );
14        stringBuilder.AddText( meta.Target.Type.Name );
15        stringBuilder.AddText( " " );
16
17        var fields = meta.Target.Type.FieldsAndProperties
18            .Where( f => !f.IsImplicitlyDeclared && !f.IsStatic )
19            .ToList();
20
21        var i = meta.CompileTime( 0 );
22
23        foreach ( var field in fields )
24        {
25            if ( i > 0 )
26            {
27                stringBuilder.AddText( ", " );
28            }
29
30            stringBuilder.AddText( field.Name );
31            stringBuilder.AddText( "=" );
32            stringBuilder.AddExpression( field.Value );
33
34            i++;
35        }
36
37        stringBuilder.AddText( " }" );
38
39        return stringBuilder.ToValue();
40    }
41}
Source Code
1namespace Doc.ToString;
2

3[ToString]
4internal class Foo
5{
6    private int _x;
7
8    public string? Y { get; set; }
9}
Transformed Code
1namespace Doc.ToString;
2
3[ToString]
4internal class Foo
5{
6    private int _x;
7
8    public string? Y { get; set; }
9
10    public override string ToString()
11    {
12        return $"{{ Foo _x={_x}, Y={Y} }}";
13    }
14}

Generating expressions using a StringBuilder-like API

It is sometimes easier to generate the run-time code as simple text instead of using a complex meta API. In this situation, you can use the ExpressionBuilder class. It offers convenient methods like AppendLiteral, AppendTypeName, or AppendExpression. The AppendVerbatim method must be used for anything else, such as keywords or punctuation.

When you are done building the expression, call the ToExpression method. It will return an IExpression object. The IExpression.Value property is dynamic and can be used in run-time code.

Note

A major benefit of ExpressionBuilder is that it can be used in a compile-time method that is not a template.

Example: ExpressionBuilder

The following example uses an ExpressionBuilder to build a pattern comparing an input value to several forbidden values. Notice the use of AppendLiteral, AppendExpression, and AppendVerbatim.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3using System;
4
5namespace Doc.ExpressionBuilder_;
6
7public class NotInAttribute : ContractAspect
8{
9    private readonly int[] _forbiddenValues;
10
11    public NotInAttribute( params int[] forbiddenValues )
12    {
13        this._forbiddenValues = forbiddenValues;
14    }
15
16    public override void Validate( dynamic? value )
17    {
18        // Build the expression.
19        var expressionBuilder = new ExpressionBuilder();
20        expressionBuilder.AppendExpression( value );
21        expressionBuilder.AppendVerbatim( " is ( " );
22
23        var requiresOr = meta.CompileTime( false );
24
25        foreach ( var forbiddenValue in this._forbiddenValues )
26        {
27            if ( requiresOr )
28            {
29                expressionBuilder.AppendVerbatim( " or " );
30            }
31            else
32            {
33                requiresOr = true;
34            }
35
36            expressionBuilder.AppendLiteral( forbiddenValue );
37        }
38
39        expressionBuilder.AppendVerbatim( ")" );
40        var condition = expressionBuilder.ToExpression();
41
42        // Use the expression in run-time code.
43        if ( condition.Value )
44        {
45            throw new ArgumentOutOfRangeException();
46        }
47    }
48}
Source Code
1namespace Doc.ExpressionBuilder_;
2


3public class Customer
4{
5    [NotIn( 0, 1, 100 )]
6    public int Id { get; set; }
7}
Transformed Code
1using System;
2
3namespace Doc.ExpressionBuilder_;
4
5public class Customer
6{
7    private int _id;
8
9    [NotIn(0, 1, 100)]
10    public int Id
11    {
12        get
13        {
14            return _id;
15        }
16
17        set
18        {
19            if (value is (0 or 1 or 100))
20            {
21                throw new ArgumentOutOfRangeException();
22            }
23
24            _id = value;
25        }
26    }
27}

Generating statements using a StringBuilder-like API

StatementBuilder is to statements what ExpressionBuilder is to expressions. Note that it also allows you to generate blocks thanks to its BeginBlock and EndBlock methods.

Warning

Do not forget the trailing semicolon at the end of the statement.

When you are done, call the ToStatement method. You can inject the returned IStatement in run-time code by calling the InsertStatement method in the template.

Parsing C# expressions and statements

If you already have a string representing an expression or a statement, you can turn it into an IExpression or IStatement using the ExpressionFactory.Parse or StatementFactory.Parse method, respectively.

Example: parsing expressions

The _logger field is accessed through a parsed expression in the following example.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3
4namespace Doc.ParseExpression;
5
6internal class LogAttribute : OverrideMethodAspect
7{
8    public override dynamic? OverrideMethod()
9    {
10        var logger = ExpressionFactory.Parse( "this._logger" );
11
12        logger.Value?.WriteLine( $"Executing {meta.Target.Method}." );
13
14        return meta.Proceed();
15    }
16}
Source Code
1using System;
2using System.IO;
3
4namespace Doc.ParseExpression;
5
6internal class Program
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void Foo() { }
12
13    private static void Main()



14    {
15        new Program().Foo();
16    }
17}
Transformed Code
1using System;
2using System.IO;
3
4namespace Doc.ParseExpression;
5
6internal class Program
7{
8    private TextWriter _logger = Console.Out;
9
10    [Log]
11    private void Foo()
12    {
13        _logger?.WriteLine("Executing Program.Foo().");
14    }
15
16    private static void Main()
17    {
18        new Program().Foo();
19    }
20}
Executing Program.Foo().

Generating switch statements

You can use the SwitchStatementBuilder class to generate switch statements. Note that it is limited to constant and default labels, i.e., patterns are not supported. Tuple matching is supported.

Example: SwitchStatementBuilder

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.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System;
5using System.Linq;
6
7namespace Doc.SwitchStatementBuilder_;
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            var invokeExpression = (IExpression) processMethod.Invoke( args )!;
24
25            switchBuilder.AddCase(
26                SwitchStatementLabel.CreateLiteral( nameWithoutPrefix ),
27                null,
28                StatementFactory.FromExpression( invokeExpression ).AsList() );
29        }
30
31        switchBuilder.AddDefault(
32            StatementFactory.FromTemplate( nameof(this.DefaultCase) ).UnwrapBlock(),
33            false );
34
35        meta.InsertStatement( switchBuilder.ToStatement() );
36    }
37
38    [Template]
39    private void DefaultCase()
40    {
41        throw new ArgumentOutOfRangeException();
42    }
43}
Source Code
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3using System;
4using System.Linq;
5
6namespace Doc.SwitchStatementBuilder_;
7
8[Dispatch]
9public class FruitProcessor
10{
11    private void ProcessApple( string args ) { }
12
13    private void ProcessOrange( string args ) { }


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

Converting compile-time values to run-time values

You can utilize meta.RunTime(expression) to convert the result of a compile-time expression into a run-time value. The compile-time expression will be evaluated at compile time, and its value will be converted into syntax representing that value. Conversions are possible for the following compile-time types:

Example: conversions

The following aspect converts the subsequent build-time values into a run-time expression: a List<string>, a Guid, and a System.Type.

1using Metalama.Framework.Aspects;
2using System;
3using System.Linq;
4
5namespace Doc.ConvertToRunTime;
6
7internal class ConvertToRunTimeAspect : OverrideMethodAspect
8{
9    public override dynamic? OverrideMethod()
10    {
11        var parameterNamesCompileTime = meta.Target.Parameters.Select( p => p.Name ).ToList();
12        var parameterNames = meta.RunTime( parameterNamesCompileTime );
13
14        var buildTime = meta.RunTime(
15            meta.CompileTime( new Guid( "13c139ea-42f5-4726-894d-550406357978" ) ) );
16
17        var parameterType = meta.RunTime( meta.Target.Parameters[0].Type.ToType() );
18
19        return null;
20    }
21}
Source Code
1using System;
2
3namespace Doc.ConvertToRunTime;

4
5internal class Foo
6{
7    [ConvertToRunTimeAspect]
8    private void Bar( string a, int c, DateTime e )
9    {
10        Console.WriteLine( $"Method({a}, {c}, {e})" );
11    }


12}
Transformed Code
1using System;
2using System.Collections.Generic;
3
4namespace Doc.ConvertToRunTime;
5
6internal class Foo
7{
8    [ConvertToRunTimeAspect]
9    private void Bar(string a, int c, DateTime e)
10    {
11        var parameterNames = new List<string>
12        {
13            "a",
14            "c",
15            "e"
16        };
17        var buildTime = new Guid(331430378, 17141, 18214, 137, 77, 85, 4, 6, 53, 121, 120);
18        var parameterType = typeof(string);
19    }
20}

Converting custom objects

You can have classes that exist both at compile and run time. To allow Metalama to convert a compile-time value to a run-time value, your class must implement the IExpressionBuilder interface. The ToExpression() method must generate a C# expression that, when evaluated, returns a value that is structurally equivalent to the current value. Note that your implementation of IExpressionBuilder is not a template, so you will have to use the ExpressionBuilder class to generate your code.

Example: custom converter

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System.Collections.Generic;
5using System.Linq;
6
7namespace Doc.CustomSyntaxSerializer;
8
9public class MemberCountAspect : TypeAspect
10{
11    // Introduces a method that returns a dictionary of method names with the number of overloads
12    // of this method.
13    [Introduce]
14    public Dictionary<string, MethodOverloadCount> GetMethodOverloadCount()
15    {
16        var dictionary = meta.Target.Type.Methods
17            .GroupBy( m => m.Name )
18            .Select( g => new MethodOverloadCount( g.Key, g.Count() ) )
19            .ToDictionary( m => m.Name, m => m );
20
21        return dictionary;
22    }
23}
24
25// This class is both compile-time and run-time.
26// It implements IExpressionBuilder to convert its compile-time value to an expression that results
27// in the equivalent run-time value.
28public class MethodOverloadCount : IExpressionBuilder
29{
30    public MethodOverloadCount( string name, int count )
31    {
32        this.Name = name;
33        this.Count = count;
34    }
35
36    public string Name { get; }
37
38    public int Count { get; }
39
40    public IExpression ToExpression()
41    {
42        var builder = new ExpressionBuilder();
43        builder.AppendVerbatim( "new " );
44        builder.AppendTypeName( typeof(MethodOverloadCount) );
45        builder.AppendVerbatim( "(" );
46        builder.AppendLiteral( this.Name );
47        builder.AppendVerbatim( ", " );
48        builder.AppendLiteral( this.Count );
49        builder.AppendVerbatim( ")" );
50
51        return builder.ToExpression();
52    }
53}
Source Code
1namespace Doc.CustomSyntaxSerializer;
2


3[MemberCountAspect]
4public class TargetClass
5{
6    public void Method1() { }
7
8    public void Method1( int a ) { }
9
10    public void Method2() { }
11}
Transformed Code
1using System.Collections.Generic;
2
3namespace Doc.CustomSyntaxSerializer;
4
5[MemberCountAspect]
6public class TargetClass
7{
8    public void Method1() { }
9
10    public void Method1(int a) { }
11
12    public void Method2() { }
13
14    public Dictionary<string, MethodOverloadCount> GetMethodOverloadCount()
15    {
16        return new Dictionary<string, MethodOverloadCount>
17        {
18            {
19                "Method1",
20                new MethodOverloadCount("Method1", 2)
21            },
22            {
23                "Method2",
24                new MethodOverloadCount("Method2", 1)
25            }
26        };
27    }
28}