Metalama / / Conceptual documentation / Creating aspects / Writing T# templates / Generating run-time code
Open sandbox

Generating run-time code

Dynamic typing

Templates use the dynamic type to represent types unknown to the template developer. For instance, an aspect may not know in advance the return type of the methods to which it is applied. 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. When the template is expanded, dynamic variables are transformed into var variables. Therefore, all dynamic variables must be initialized.

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

Converting compile-time values to run-time values

You can use 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 that represents that value. Conversions are possible for the following compile-time types:

Example

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

using Metalama.Framework.Aspects;
using System;
using System.Linq;

namespace Doc.ConvertToRunTime
{
    internal class ConvertToRunTimeAspect : OverrideMethodAspect
    {
        public override dynamic? OverrideMethod()
        {
            var parameterNamesCompileTime = meta.Target.Parameters.Select( p => p.Name ).ToList();
            var parameterNames = meta.RunTime( parameterNamesCompileTime );
            var buildTime = meta.RunTime( meta.CompileTime ( new Guid( "13c139ea-42f5-4726-894d-550406357978" ) ) );
            var parameterType = meta.RunTime( meta.Target.Parameters[0].Type.ToType() );

            return null;
        }
    }
}
using System;

namespace Doc.ConvertToRunTime
{
    internal class Foo
    {
        [ConvertToRunTimeAspect]
        private void Bar( string a, int c, DateTime e )
        {
            Console.WriteLine( $"Method({a}, {c}, {e})" );
        }
    }
}
using System;
using System.Collections.Generic;

namespace Doc.ConvertToRunTime
{
    internal class Foo
    {
        [ConvertToRunTimeAspect]
        private void Bar(string a, int c, DateTime e)
        {
            var parameterNames = new List<string>
    {
        "a",
        "c",
        "e"
    };
            var buildTime = new Guid(331430378, 17141, 18214, 137, 77, 85, 4, 6, 53, 121, 120);
            var parameterType = typeof(string);
            return;
        }
    }
}

Dynamic code

The meta API exposes some properties of 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 used 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 to get or set 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 to get or set the value of the target field or property.
    • meta.Target.Parameter.Value allows to get or set the value of the target parameter.
    • meta.Target.Method.Parameters[*].Value allows you to get or set the value of a target method's parameter.
    • ExpressionFactory.ToExpression(*).Value allows to get or set the value of an arbitrary IField, IProperty, or IParameter

Abilities

You can also 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" );

          

Some more complex expressions also expose dynamic. For instance, meta.Target.Parameters["p"].Value refers to the p parameter of the target method and compiles simply into the syntax p.

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

          

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;

          

You can combine dynamic code and compile-time expressions:

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

          

Example

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 _loggerhe and that the type of this field has a method named WriteLine.

using Metalama.Framework.Aspects;

namespace Doc.DynamicTrivial
{
    internal class LogAttribute : OverrideMethodAspect
    {
        public override dynamic? OverrideMethod()
        {
            meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );

            return meta.Proceed();
        }
    }
}
using System;
using System.IO;

namespace Doc.DynamicTrivial
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo() { }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
using System;
using System.IO;

namespace Doc.DynamicTrivial
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo()
        {
            this._logger.WriteLine("Executing Program.Foo().");
            return;
        }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
Executing Program.Foo().

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 the Invokers property exposed by the IMethod, IFieldOrProperty or IEvent interfaces.

For details, see the documentation of the Metalama.Framework.Code.Invokers namespace.

Example

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 Invokers.Final.GetValue to get an expression allowing it to access the field. Invokers.Final.GetValue returns a dynamic object.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using System.IO;
using System.Linq;

namespace Doc.DynamicCodeModel
{
    internal class LogAttribute : OverrideMethodAspect
    {
        public override dynamic? OverrideMethod()
        {
            var loggerField = meta.Target.Type.FieldsAndProperties.Where( x => x.Type.Is( typeof(TextWriter) ) )
                .Single();

            loggerField.Value!.WriteLine( $"Executing {meta.Target.Method}." );

            return meta.Proceed();
        }
    }
}
using System;
using System.IO;

namespace Doc.DynamicCodeModel
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo() { }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
using System;
using System.IO;

namespace Doc.DynamicCodeModel
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo()
        {
            this._logger.WriteLine("Executing Program.Foo().");
            return;
        }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
Executing Program.Foo().

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 a 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.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code.SyntaxBuilders;
using System.Linq;

namespace Doc.ToString
{
    internal class ToStringAttribute : TypeAspect
    {
        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
        public string IntroducedToString()
        {
            var stringBuilder = new InterpolatedStringBuilder();
            stringBuilder.AddText( "{ " );
            stringBuilder.AddText( meta.Target.Type.Name );
            stringBuilder.AddText( " " );

            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsImplicitlyDeclared && !f.IsStatic ).ToList();

            var i = meta.CompileTime( 0 );

            foreach ( var field in fields )
            {
                if ( i > 0 )
                {
                    stringBuilder.AddText( ", " );
                }

                stringBuilder.AddText( field.Name );
                stringBuilder.AddText( "=" );
                stringBuilder.AddExpression( field.Value );

                i++;
            }

            stringBuilder.AddText( " }" );

            return stringBuilder.ToValue();
        }
    }
}
namespace Doc.ToString
{
    [ToString]
    internal class Foo
    {
        private int _x;

        public string? Y { get; set; }
    }
}
namespace Doc.ToString
{
    [ToString]
    internal class Foo
    {
        private int _x;

        public string? Y { get; set; }

        public override string ToString()
        {
            return $"{{ Foo _x={_x}, Y={Y} }}";
        }
    }
}

Parsing C# code

Sometimes it is easier to generate the run-time code as a simple text instead of using a complex meta API. If you want to use C# code represented as a string in your code, use the ExpressionFactory.Parse method. This method returns an IExpression, which is a compile-time object that you can use anywhere in compile-time code. The IExpression interface exposes the run-time expression in the Value property.

Note

The string expression is inserted as is without validation or transformation. Always specify the full namespace of any declaration used in a text expression.

Note

Instead of the traditional StringBuilder you can use ExpressionBuilder to build an expression. It offers convenient methods like AppendLiteral, AppendTypeName or AppendExpression. To add a statement to the generated code, use StatementBuilder to create the statement and then meta.InsertStatement from the template at the place where the statement should be inserted.

Example

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

using Metalama.Framework.Aspects;
using Metalama.Framework.Code.SyntaxBuilders;

namespace Doc.ParseExpression
{
    internal class LogAttribute : OverrideMethodAspect
    {
        public override dynamic? OverrideMethod()
        {
            var logger = ExpressionFactory.Parse( "this._logger" );

            logger.Value?.WriteLine( $"Executing {meta.Target.Method}." );

            return meta.Proceed();
        }
    }
}
using System;
using System.IO;

namespace Doc.ParseExpression
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo() { }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
using System;
using System.IO;

namespace Doc.ParseExpression
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo()
        {
            this._logger?.WriteLine("Executing Program.Foo().");
            return;
        }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
Executing Program.Foo().

Capturing run-time expressions into compile-time objects

If you want to manipulate a run-time expression as a compile-time object, use the Capture method. This allows you to have expressions that depend on compile-time conditions and control flows. The Capture method returns an IExpression, the same interface returned by Parse. The IExpression is a compile-time object you can use anywhere in compile-time code. It exposes the run-time expression in the Value property.

The following example is taken from the clone aspect. It declares a local variable named clone, but the expression assigned to the variable depends on whether the Clone method is an override.

            IExpression baseCall;

if (meta.Target.Method.IsOverride)
{
    ExpressionFactory.Capture(meta.Base.Clone(), out baseCall);
}
else
{
    ExpressionFactory.Capture(meta.Base.MemberwiseClone(), out baseCall);
}

// Define a local variable of the same type as the target type.
var clone = meta.Cast(meta.Target.Type, baseCall);

          

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

Note

The weird syntax of Capture, which gives the result as an out parameter instead of a return value, is due to the viral nature of dynamic. In C#, the return type of ExpressionFactory.Caputre(someDynamicExpression) would be dynamic, while Metalama would require it to be IExpression.

Converting custom objects from compile-time to run-time values

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

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Code.SyntaxBuilders;
using System.Collections.Generic;
using System.Linq;

namespace Doc.CustomSyntaxSerializer
{
    public class MemberCountAspect : TypeAspect
    {
        // Introduces a method that returns a dictionary of method names with the number of overloads
        // of this method.
        [Introduce]
        public Dictionary<string, MethodOverloadCount> GetMethodOverloadCount()
        {
            var dictionary = meta.Target.Type.Methods
                .GroupBy( m => m.Name )
                .Select( g => new MethodOverloadCount( g.Key, g.Count() ) )
                .ToDictionary( m => m.Name, m => m );

            return dictionary;
        }
    }

    // This class is both compile-time and run-time.
    // It implements IExpressionBuilder to convert its compile-time value to an expression that results
    // in the equivalent run-time value.
    public class MethodOverloadCount : IExpressionBuilder
    {
        public MethodOverloadCount( string name, int count )
        {
            this.Name = name;
            this.Count = count;
        }

        public string Name { get; }

        public int Count { get; }

        public IExpression ToExpression()
        {
            var builder = new ExpressionBuilder();
            builder.AppendVerbatim( "new " );
            builder.AppendTypeName( typeof(MethodOverloadCount) );
            builder.AppendVerbatim( "(" );
            builder.AppendLiteral( this.Name );
            builder.AppendVerbatim( ", " );
            builder.AppendLiteral( this.Count );
            builder.AppendVerbatim( ")" );

            return builder.ToExpression();
        }
    }
}
namespace Doc.CustomSyntaxSerializer
{
    [MemberCountAspect]
    public class TargetClass
    {
        public void Method1() { }

        public void Method1( int a ) { }

        public void Method2() { }
    }
}
using System.Collections.Generic;

namespace Doc.CustomSyntaxSerializer
{
    [MemberCountAspect]
    public class TargetClass
    {
        public void Method1() { }

        public void Method1(int a) { }

        public void Method2() { }

        public Dictionary<string, MethodOverloadCount> GetMethodOverloadCount()
        {
            return new Dictionary<string, MethodOverloadCount>
    {
        {
            "Method1",
            new MethodOverloadCount("Method1", 2)
        },
        {
            "Method2",
            new MethodOverloadCount("Method2", 1)
        }
    };
        }
    }
}