Metalama//Conceptual documentation/Creating aspects/Writing T# templates/Writing compile-time code
Open sandboxFocus

Writing compile-time code

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

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

The meta pseudo-keyword

The entry point of the compile-time API is the meta static class, which you can consider as a pseudo-keyword that opens the door to the meta side of meta-programming. The meta class is the entry point to the meta-model and the members of this class can be invoked only in the context of a template.

The meta static class exposes to the following members:

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

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{
6    public 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    }
22}
Source Code
1using System;
2
3namespace Doc.SimpleLogging
4{
5    internal class Foo
6    {
7        [SimpleLog]
8        public void Method1()
9        {
10            Console.WriteLine( "Hello, world." );
11        }









12    }
13}
Transformed Code
1using System;
2
3namespace Doc.SimpleLogging
4{
5    internal 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    }
22}

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 to a compile-time value. If you need to initialize the compile-time variable to 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

You cannot assign a compile-time variable 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.

Note

You may not have a compile-time if inside a block whose execution depends on a run-time condition, including a run-time if, else, for, foreach, while, switch, catch or finally.

Example

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

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.CompileTimeIf
5{
6    internal 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( $"Invoking {meta.Target.Method.ToDisplayString()} on instance {meta.This.ToString()}." );
17            }
18
19            return meta.Proceed();
20        }
21    }
22}
Source Code
1using System;
2
3namespace Doc.CompileTimeIf
4{
5    internal 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    }
19}
Transformed Code
1using System;
2
3namespace Doc.CompileTimeIf
4{
5    internal class Foo
6    {
7        [CompileTimeIf]
8        public void InstanceMethod()
9        {
10            Console.WriteLine($"Invoking Foo.InstanceMethod() on instance {base.ToString()}.");
11            Console.WriteLine("InstanceMethod");
12            return;
13        }
14
15        [CompileTimeIf]
16        public static void StaticMethod()
17        {
18            Console.WriteLine("Invoking Foo.StaticMethod()");
19            Console.WriteLine("StaticMethod");
20            return;
21        }
22    }
23}

Compile-time foreach

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

Note

It is not allowed to have a compile-time foreach inside a block whose execution depends on a run-time condition, including a run-time if, else, for, foreach, while, switch, catch or finally.

Example

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{
8    internal 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    }
20}
Source Code
1using System;
2
3namespace Doc.CompileTimeForEach
4{
5    internal 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    }
13}
Transformed Code
1using System;
2
3namespace Doc.CompileTimeForEach
4{
5    internal 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            return;
14        }
15    }
16}

No compile-time for, while and goto

It is not possible to create compile-time for or while loops. goto statements are forbidden 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 approach above is not possible, you can try to 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, which makes it possible for 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. You may sometimes need to call the meta.RunTime method to tip the T# compiler that you want a run-time expression instead of a compile-time one.

Accessing aspect members

Aspect members are compile-time and can be accessed from templates. For instance, an aspect custom attribute can define a property that can be set when the custom attribute is applied to a target declaration and then read from the aspect compile-time code.

There are a few exceptions to this rule:

  • aspect members whose signature contains a run-time-only type cannot be accessed from a template.
  • aspect members annotated with the [Template] attribute (or overriding members that are, such as OverrideMethod) cannot be invoked from a template.
  • aspect members annotated with the [Introduce] or [InterfaceMember] attribute are considered run-time (see Introducing members and Implementing interfaces).

Example

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.

1using Metalama.Framework.Aspects;
2using System;
3using System.Threading;
4
5namespace Doc.Retry
6{
7    internal 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    }
27}
Source Code
1using System;
2

3namespace Doc.Retry
4{
5    internal 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    }
19}
Transformed Code
1using System;
2using System.Threading;
3
4namespace Doc.Retry
5{
6    internal 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    }
44}

Custom 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. You would typically use this attribute on:
    • a method or field of an aspect;
    • a type (class, struct, record, ...);
    • an assembly, using [assembly: CompileTime].
  • Code that can either execute at 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 call only 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. Make sure 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>

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