Open sandboxFocusImprove this doc

Generating run-time expressions

In Metalama, expressions are compile-time objects that implement the IExpression interface.

Expressions represent C# syntax—not their result. For instance, 1+1 and 2 are two different expressions, although they evaluate to the same value at run time.

This article covers different ways to create IExpression objects.

Two-way convertibility between IExpression and dynamic

As noted in Dynamic typing in templates, all dynamic objects in a template actually implement the IExpression interface, so it's safe to cast a dynamic into an IExpression in a template. An expression can be converted back to a dynamic either by using a cast or the Value property.

Therefore, IExpression objects are compile-time objects that represent run-time syntax:

  • When typed as the compile-time IExpression, an expression can be used in compile-time APIs.
  • When typed as dynamic, expressions can be used in run-time APIs.

Capturing a C# expression into an IExpression

The simplest way to create an expression in a T# template is to write plain C# code and then capture its syntax. The ExpressionFactory.Capture method captures the C# syntax tree of an expression into an IExpression object without evaluating it.

// Defines a run-time local variable.
var now = DateTime.Now;

// Captures the reference to the local variable "now".
var expression1 = ExpressionFactory.Capture( now );

// Captures the expression "DateTime.Now".
var expression2 = ExpressionFactory.Capture( DateTime.Now );

Capturing a dynamic expression

Warning

When the compile-time type of the expression to capture is dynamic, it must be explicitly cast to IExpression to work around limitations of the C# language.

Example:

// A compile-time object representing a method.
IMethod method;

// Invokes the method and stores the result in a run-time local variable.
// At compile time, `result` is dynamic.
var result = method.Invoke();

// Captures the reference to the local variable `result`.
// The `(IExpression)` cast is necessary.
var expression = ExpressionFactory.Capture( (IExpression) result );

Generating expressions using a StringBuilder-like API

When you need to construct complex expressions programmatically or dynamically, the ExpressionBuilder class provides a text-based approach. It offers convenient methods like AppendLiteral, AppendTypeName, and AppendExpression. The AppendVerbatim method must be used for anything else, such as keywords or punctuation.

When you're done building the expression, call the ToExpression method. It returns 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 isn't a template.

Warning

Your aspect must not assume that the target code has any required using directives. Make sure to write fully namespace-qualified type names. Metalama will simplify the code and add the relevant using directives when asked to produce pretty-formatted code. The best way to ensure type names are fully qualified is to use the AppendTypeName method.

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}

Example: using ExpressionBuilder in BuildAspect

Since ExpressionBuilder works outside of templates, it's essential for generating expressions in contexts like BuildAspect. The following example introduces two fields: a static counter and an instance ID initialized using Interlocked.Increment. The initializer expression must be built programmatically because BuildAspect isn't a template.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System.Threading;
5
6namespace Doc.ExpressionBuilderToExpression;
7
8public class AutoIncrementIdAspect : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        // Introduce the static counter field.
13        var counterField = builder.IntroduceField(
14            "_nextId",
15            typeof(int),
16            buildField: f => f.IsStatic = true );
17
18        // Build the initializer expression: Interlocked.Increment(ref _nextId)
19        var initializerBuilder = new ExpressionBuilder();
20        initializerBuilder.AppendTypeName( typeof(Interlocked) );
21        initializerBuilder.AppendVerbatim( ".Increment(ref " );
22        initializerBuilder.AppendVerbatim( counterField.Declaration.Name );
23        initializerBuilder.AppendVerbatim( ")" );
24
25        // Introduce the instance ID field with the computed initializer.
26        builder.IntroduceField(
27            "_id",
28            typeof(int),
29            buildField: f =>
30            {
31                f.Writeability = Writeability.ConstructorOnly;
32                f.InitializerExpression = initializerBuilder.ToExpression();
33            } );
34    }
35}
36
Source Code
1namespace Doc.ExpressionBuilderToExpression;
2


3[AutoIncrementIdAspect]
4internal class Foo
5{
6    public void DoSomething() { }
7}
8
Transformed Code
1using System.Threading;
2
3namespace Doc.ExpressionBuilderToExpression;
4
5[AutoIncrementIdAspect]
6internal class Foo
7{
8    public void DoSomething() { }
9
10    private readonly int _id = Interlocked.Increment(ref _nextId);
11    private static int _nextId;
12}
13

Parsing string-based C# expressions

If you already have a string representing an expression or statement, you can convert it into an IExpression using ExpressionFactory.Parse.

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 run-time arrays

You can generate run-time arrays using two approaches:

Using statements (multi-line approach)

The straightforward way is to declare an array variable and set each element using statements:

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

The limitation of this approach is that it requires multiple statements and can't be used where a single expression is needed.

Using ArrayBuilder (expression approach)

To generate an array as a single expression, use the ArrayBuilder class. This is particularly useful when passing arrays as method arguments or in other contexts where expressions are required.

For instance:

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

This generates 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 is { IsImplicitlyDeclared: false, IsStatic: false } )
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}

Converting compile-time values to run-time values

Use meta.RunTime(expression) to convert the result of a compile-time expression into a run-time expression. The compile-time expression is evaluated at compile time, and its value is 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-time 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's structurally equivalent to the current value. Note that your implementation of IExpressionBuilder isn't a template, so you'll need 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}