Open sandboxFocusImprove this doc

Generating run-time statements

In T# templates, statements are instructions that perform actions in the generated code, such as variable declarations, assignments, method calls, loops, and conditionals.

Most of the time, you can write statements directly in your template, and Metalama uses inference to automatically detect whether a statement should execute at compile-time or be included in the generated run-time code. This approach works transparently in most cases. However, when you need to dynamically generate statements based on compile-time logic—such as building a switch statement with a variable number of cases—you need to use special APIs.

To dynamically add statements to the generated code, use InsertStatement, which accepts an IStatement or IExpression object (since most C# expressions can also be used as statements).

This article shows how to dynamically add statements to the generated code.

Generating statements using a StringBuilder-like API

When you need to construct statements programmatically or generate complex statements dynamically, use the StatementBuilder class. This is the statement equivalent of ExpressionBuilder. It lets you generate both individual statements and blocks of statements using its BeginBlock and EndBlock methods.

Warning

Don't forget the trailing semicolon at the end of the statement.

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

Note

A major benefit of StatementBuilder is that it can be used in compile-time helper methods that aren't templates.

Example: using StatementBuilder in a compile-time helper

The following example demonstrates a reusable compile-time helper method that builds null-check statements. Since the helper is marked with [CompileTime] and isn't a template, it must use StatementBuilder to construct the statement. The aspect then calls this helper for each non-nullable reference parameter and inserts the resulting statements using InsertStatement.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System;
5using System.Linq;
6
7namespace Doc.StatementBuilderHelper;
8
9// Compile-time helper class - NOT a template, so must use StatementBuilder.
10[CompileTime]
11public static class ValidationStatementHelper
12{
13    // Builds: if (paramName == null) throw new ArgumentNullException(nameof(paramName));
14    public static IStatement BuildNullCheck( IParameter parameter )
15    {
16        var builder = new StatementBuilder();
17        builder.AppendVerbatim( "if (" );
18        builder.AppendExpression( parameter );
19        builder.AppendVerbatim( " == null) throw new " );
20        builder.AppendTypeName( typeof(ArgumentNullException) );
21        builder.AppendVerbatim( "(nameof(" );
22        builder.AppendExpression( parameter );
23        builder.AppendVerbatim( "));" );
24
25        return builder.ToStatement();
26    }
27}
28
29// Aspect that uses the helper.
30public class NullGuardAttribute : OverrideMethodAspect
31{
32    public override dynamic? OverrideMethod()
33    {
34        foreach ( var p in meta.Target.Parameters.Where(
35                      p => p.Type.IsReferenceType == true && p.Type.IsNullable != true ) )
36        {
37            meta.InsertStatement( ValidationStatementHelper.BuildNullCheck( p ) );
38        }
39
40        return meta.Proceed();
41    }
42}
43
Source Code
1namespace Doc.StatementBuilderHelper;
2
3internal class Foo
4{
5    [NullGuard]
6    public void DoSomething( string name, object? optional, int count )
7    {
8    }
9}


10
Transformed Code
1namespace Doc.StatementBuilderHelper;
2
3internal class Foo
4{
5    [NullGuard]
6    public void DoSomething(string name, object? optional, int count)
7    {
8        if (name == null)
9            throw new global::System.ArgumentNullException(nameof(name));
10    }
11}
12

Parsing C# statements

Just as you can parse C# expressions using ExpressionFactory.Parse, you can parse statements from strings using the StatementFactory.Parse method.

Warning

Don't forget the trailing semicolon at the end of the statement.

Defining local variables

In T# templates, local variable declarations are automatically classified as either compile-time or run-time according to the value they're assigned:

  • Variables assigned to compile-time values (like var field = meta.Target.Field;) are compile-time variables.
  • Variables assigned to run-time expressions (like var x = 0;) become run-time local variables in the generated code.

However, when you need to programmatically define local variables — for example, creating a variable within a compile-time foreach loop — you must use the DefineLocalVariable method. This method allows dynamic creation of local variables whose number or names are determined at compile-time.

When using DefineLocalVariable, you don't need to worry about generating unique names. Metalama automatically appends a numerical suffix to the variable name to ensure uniqueness within the target lexical scope.

Example: rolling back field changes upon exception

The following aspect saves the value of all fields and automatic properties into a local variable before an operation is executed and rolls back these changes upon exception.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System.Collections.Generic;
4using System.Linq;
5
6namespace Doc.LocalVariableTransaction;
7
8public class TransactedMethodAttribute : OverrideMethodAspect
9{
10    public override dynamic? OverrideMethod()
11    {
12        var fieldsAndProperties = meta.Target.Type.FieldsAndProperties.Where( f => f is
13            {
14                IsAutoPropertyOrField: true,
15                IsStatic: false,
16                Writeability: Writeability.All,
17                IsImplicitlyDeclared: false
18            } )
19            .OrderBy( f => f.Name );
20
21        var variables = new List<(IExpression Variable, IFieldOrProperty FieldOrProperty)>();
22
23        foreach ( var fieldAndProperty in fieldsAndProperties )
24        {
25            var variable = meta.DefineLocalVariable(
26                fieldAndProperty.Name,
27                fieldAndProperty );
28
29            variables.Add( (variable, fieldAndProperty) );
30        }
31
32        try
33        {
34            return meta.Proceed();
35        }
36        catch
37        {
38            foreach ( var pair in variables )
39            {
40                pair.FieldOrProperty.Value = pair.Variable.Value;
41            }
42
43            throw;
44        }
45    }
46}
Source Code
1using System;
2
3namespace Doc.LocalVariableTransaction;
4
5public class Account
6{
7    private decimal _amount;
8
9    public DateTime LastOperation { get; private set; }
10
11    public decimal Amount
12    {
13        get => this._amount;
14        private set
15        {
16            if ( value < 0 )
17            {
18                throw new ArgumentOutOfRangeException();
19            }
20
21            this._amount = value;
22        }
23    }
24
25    [TransactedMethod]
26    public void Withdraw( decimal amount )
27    {
28        this.LastOperation = DateTime.Now;
29        this.Amount -= amount;




30    }
31}
Transformed Code
1using System;
2
3namespace Doc.LocalVariableTransaction;
4
5public class Account
6{
7    private decimal _amount;
8
9    public DateTime LastOperation { get; private set; }
10
11    public decimal Amount
12    {
13        get => this._amount;
14        private set
15        {
16            if (value < 0)
17            {
18                throw new ArgumentOutOfRangeException();
19            }
20
21            this._amount = value;
22        }
23    }
24
25    [TransactedMethod]
26    public void Withdraw(decimal amount)
27    {
28        var _amount_1 = _amount;
29        var LastOperation_1 = LastOperation;
30        try
31        {
32            this.LastOperation = DateTime.Now;
33            this.Amount -= amount;
34            return;
35        }
36        catch
37        {
38            _amount = _amount_1;
39            LastOperation = LastOperation_1;
40            throw;
41        }
42    }
43}

Generating switch statements

Use the SwitchStatementBuilder class to generate switch statements. Note that it's limited to constant and default labels; patterns aren't supported. Tuple matching is supported.

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.

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( 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            var invokeExpression = (IExpression) processMethod.Invoke( args )!;
25
26            switchBuilder.AddCase(
27                SwitchStatementLabel.CreateLiteral( nameWithoutPrefix ),
28                null,
29                StatementFactory.FromExpression( invokeExpression ).AsList() );
30        }
31
32        switchBuilder.AddDefault(
33            StatementFactory.FromTemplate( nameof(this.DefaultCase) ).UnwrapBlock(),
34            false );
35
36        meta.InsertStatement( switchBuilder.ToStatement() );
37    }
38
39    [Template]
40    private void DefaultCase()
41    {
42        throw new ArgumentOutOfRangeException();
43    }
44}
Source Code
1namespace Doc.SwitchStatementBuilder_;
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_;
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}