Open sandboxFocusImprove this doc

Writing compile-time code

Compile-time expressions are expressions that contain a call to a compile-time method, a reference to a compile-time local variable, or a compile-time aspect member. These expressions are executed at compile time when the aspect is applied to a target.

Compile-time statements, such as if, foreach, or meta.DebugBreak(), are also executed at compile time.

The meta pseudo-keyword

The meta static class serves as the entry point for the compile-time API. It can be considered a pseudo-keyword that provides access to the meta side of meta-programming. The meta class is the entry point to the meta-model, and its members can only be invoked within the context of a template.

The meta static class exposes the following members:

  • The meta.Proceed method, which invokes the target method or accessor being intercepted. This can be either the next aspect applied on the same target or the target source implementation itself.
  • The meta.Target property, which provides access to the declaration to which the template is applied.
  • The meta.Target.Parameters property, which provides access to the current method or accessor parameters.
  • The meta.This property, which represents the this instance. Used in conjunction with meta.Base, meta.ThisType, and meta.BaseType properties, meta.This enables your template to access members of the target class using dynamic code.
  • The meta.Tags property, which provides access to an arbitrary dictionary passed to the advice factory method. See Sharing state with advice for more details.
  • The meta.CompileTime method, which coerces a neutral expression into a compile-time expression.
  • The meta.RunTime method, which converts the result of a compile-time expression into a run-time value.

Example: simple logging

The following code writes a message to the system console before and after the method execution. The text includes the name of the target method.

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.SimpleLogging;
5
6public class SimpleLogAttribute : OverrideMethodAspect
7{
8    public override dynamic? OverrideMethod()
9    {
10        Console.WriteLine( $"Entering {meta.Target.Method}" );
11
12        try
13        {
14            return meta.Proceed();
15        }
16        finally
17        {
18            Console.WriteLine( $"Leaving {meta.Target.Method}" );
19        }
20    }
21}
Source Code
1using System;
2
3namespace Doc.SimpleLogging;
4
5internal class Foo
6{
7    [SimpleLog]
8    public void Method1()
9    {
10        Console.WriteLine( "Hello, world." );
11    }
12}
Transformed Code
1using System;
2
3namespace Doc.SimpleLogging;
4
5internal class Foo
6{
7    [SimpleLog]
8    public void Method1()
9    {
10        Console.WriteLine("Entering Foo.Method1()");
11        try
12        {
13            Console.WriteLine("Hello, world.");
14            return;
15        }
16        finally
17        {
18            Console.WriteLine("Leaving Foo.Method1()");
19        }
20    }
21}

Compile-time language constructs

Compile-time local variables

Local variables are run-time by default. To declare a compile-time local variable, you must initialize it with a compile-time value. If you need to initialize the compile-time variable with a literal value such as 0 or "text", use the meta.CompileTime method to convert the literal into a compile-time value.

Examples:

  • In var i = 0;, i is a run-time variable.
  • In var i = meta.CompileTime(0);, i is a compile-time variable.
  • In var parameters = meta.Target.Parameters;, parameters is a compile-time variable.
Note

A compile-time variable cannot be assigned from a block whose execution depends on a run-time condition, including a run-time if, else, for, foreach, while, switch, catch, or finally.

Compile-time if

If the condition of an if statement is a compile-time expression, the if statement will be interpreted at compile-time.

Example: if

In the following example, the aspect prints a different string for static methods than for instance methods.

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.CompileTimeIf;
5
6internal class CompileTimeIfAttribute : OverrideMethodAspect
7{
8    public override dynamic? OverrideMethod()
9    {
10        if ( meta.Target.Method.IsStatic )
11        {
12            Console.WriteLine( $"Invoking {meta.Target.Method.ToDisplayString()}" );
13        }
14        else
15        {
16            Console.WriteLine(
17                $"Invoking {meta.Target.Method.ToDisplayString()} on instance {meta.This.ToString()}." );
18        }
19
20        return meta.Proceed();
21    }
22}
Source Code
1using System;
2
3namespace Doc.CompileTimeIf;
4
5internal class Foo
6{
7    [CompileTimeIf]
8    public void InstanceMethod()
9    {
10        Console.WriteLine( "InstanceMethod" );
11    }
12

13    [CompileTimeIf]
14    public static void StaticMethod()
15    {
16        Console.WriteLine( "StaticMethod" );
17    }
18}
Transformed Code
1using System;
2
3namespace Doc.CompileTimeIf;
4
5internal class Foo
6{
7    [CompileTimeIf]
8    public void InstanceMethod()
9    {
10        Console.WriteLine($"Invoking Foo.InstanceMethod() on instance {this.ToString()}.");
11        Console.WriteLine("InstanceMethod");
12    }
13
14    [CompileTimeIf]
15    public static void StaticMethod()
16    {
17        Console.WriteLine("Invoking Foo.StaticMethod()");
18        Console.WriteLine("StaticMethod");
19    }
20}

Compile-time foreach

If the expression of a foreach statement is a compile-time expression, the foreach statement will be interpreted at compile-time.

Example: foreach

The following aspect uses a foreach loop to print the value of each parameter of the method to which it is applied.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4using System.Linq;
5
6namespace Doc.CompileTimeForEach;
7
8internal class CompileTimeForEachAttribute : OverrideMethodAspect
9{
10    public override dynamic? OverrideMethod()
11    {
12        foreach ( var p in meta.Target.Parameters.Where( p => p.RefKind != RefKind.Out ) )
13        {
14            Console.WriteLine( $"{p.Name} = {p.Value}" );
15        }
16
17        return meta.Proceed();
18    }
19}
Source Code
1using System;
2
3namespace Doc.CompileTimeForEach;
4
5internal class Foo
6{
7    [CompileTimeForEach]
8    private void Bar( int a, string b )
9    {
10        Console.WriteLine( $"Hello, world! a={a}, b='{b}'." );
11    }
12}
Transformed Code
1using System;
2
3namespace Doc.CompileTimeForEach;
4
5internal class Foo
6{
7    [CompileTimeForEach]
8    private void Bar(int a, string b)
9    {
10        Console.WriteLine($"a = {a}");
11        Console.WriteLine($"b = {b}");
12        Console.WriteLine($"Hello, world! a={a}, b='{b}'.");
13    }
14}

No compile-time for and goto

Compile-time for loops are not supported. goto statements are also not allowed in templates. If you need a compile-time for, you can use the following construct:

foreach ( int i in meta.CompileTime( Enumerable.Range( 0, n ) ) )

If the above approach is not feasible, you can move your logic to a compile-time aspect function (not a template method), have this function return an enumerable, and use the return value in a foreach loop in the template method.

nameof expressions

nameof expressions in compile-time code are always pre-compiled into compile-time expressions, enabling compile-time code to reference run-time types.

typeof expressions

When typeof(Foo) is used with a run-time-only type Foo, a mock System.Type object is returned. This object can be used in run-time expressions or as an argument of Metalama compile-time methods. However, most members of this fake System.Type cannot be evaluated at compile time and will throw an exception. In some cases, you may need to call the meta.RunTime method to indicate to the T# compiler that you want a run-time expression instead of a compile-time one.

Aspect properties

Many aspects have properties that can be set when the aspect is instantiated — for instance as a custom attribute. The scope of these properties is generally run-time-or-compile-time. When you read these properties from a template, they will be replaced by their compile-time value.

Example: aspect property

The following example shows a simple Retry aspect. The maximum number of attempts can be configured by setting a property of the custom attribute. This property is compile-time. As you can see, when the template is expanded, the property reference is replaced by its value.

1using Metalama.Framework.Aspects;
2using System;
3using System.Threading;
4
5namespace Doc.Retry;
6
7internal class RetryAttribute : OverrideMethodAspect
8{
9    public int MaxAttempts { get; set; } = 5;
10
11    public override dynamic? OverrideMethod()
12    {
13        for ( var i = 0;; i++ )
14        {
15            try
16            {
17                return meta.Proceed();
18            }
19            catch ( Exception e ) when ( i < this.MaxAttempts )
20            {
21                Console.WriteLine( $"{e.Message}. Retrying in 100 ms." );
22                Thread.Sleep( 100 );
23            }
24        }
25    }
26}
Source Code
1using System;
2
3namespace Doc.Retry;

4
5internal class Foo
6{
7    [Retry]
8    private void RetryDefault()
9    {
10        throw new InvalidOperationException();
11    }




12








13    [Retry( MaxAttempts = 10 )]
14    private void RetryTenTimes()
15    {
16        throw new InvalidOperationException();
17    }




18}
Transformed Code
1using System;
2using System.Threading;
3
4namespace Doc.Retry;
5
6internal class Foo
7{
8    [Retry]
9    private void RetryDefault()
10    {
11        for (var i = 0; ; i++)
12        {
13            try
14            {
15                throw new InvalidOperationException();
16                return;
17            }
18            catch (Exception e) when (i < 5)
19            {
20                Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
21                Thread.Sleep(100);
22            }
23        }
24    }
25
26    [Retry(MaxAttempts = 10)]
27    private void RetryTenTimes()
28    {
29        for (var i = 0; ; i++)
30        {
31            try
32            {
33                throw new InvalidOperationException();
34                return;
35            }
36            catch (Exception e) when (i < 10)
37            {
38                Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
39                Thread.Sleep(100);
40            }
41        }
42    }
43}

Compile-time types and methods

If you want to share compile-time code between aspects or aspect methods, you can create your own types and methods that execute at compile time.

  • Compile-time code must be annotated with the [CompileTime] custom attribute. This attribute is typically used on:
    • A method or field of an aspect;
    • A type (class, struct, record, ...);
    • The whole project, using [assembly: CompileTime].
  • Code that can execute at either compile or run time must be annotated with the [RunTimeOrCompileTime] custom attribute.

Calling other packages from compile-time code

By default, compile-time code can only call the following APIs:

  • .NET Standard 2.0 (all libraries)
  • Metalama.Framework

For advanced scenarios, the following packages are also included by default:

  • Metalama.Framework.Sdk
  • Microsoft.CodeAnalysis.CSharp

To make another package available in compile-time code:

  1. Make sure that this package targets .NET Standard 2.0.
  2. Ensure that the package is included in the project.
  3. Edit your .csproj or Directory.Build.props file and add the following:
<ItemGroup>
 <MetalamaCompileTimePackage Include="MyPackage"/>
</ItemGroup>

Once this configuration is done, MyPackage can be used both in run-time and compile-time code.

Warning

You must also specify MetalamaCompileTimePackage in each project that uses the aspects.