Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / T# Templates / Writing Compile-Time Code

Writing compile-time code

Compile-time expressions are expressions that either contain a call to a compile-time method, or contain 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, such as if, foreach or meta.DebugBreak(), that are executed at compile time.

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, it 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.
  • 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).

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

using Metalama.Framework.Aspects;
using System;

namespace Doc.CompileTimeIf
{
    internal class CompileTimeIfAttribute : OverrideMethodAspect
    {
        public override dynamic? OverrideMethod()
        {
            if ( meta.Target.Method.IsStatic )
            {
                Console.WriteLine( $"Invoking {meta.Target.Method.ToDisplayString()}" );
            }
            else
            {
                Console.WriteLine( $"Invoking {meta.Target.Method.ToDisplayString()} on instance {meta.This.ToString()}." );
            }

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

namespace Doc.CompileTimeIf
{
    internal class Foo
    {
        [CompileTimeIf]
        public void InstanceMethod()
        {
            Console.WriteLine( "InstanceMethod" );
        }

        [CompileTimeIf]
        public static void StaticMethod()
        {
            Console.WriteLine( "StaticMethod" );
        }
    }
}
using System;

namespace Doc.CompileTimeIf
{
    internal class Foo
    {
        [CompileTimeIf]
        public void InstanceMethod()
        {
            Console.WriteLine($"Invoking Foo.InstanceMethod() on instance {base.ToString()}.");
            Console.WriteLine("InstanceMethod");
            return;
        }

        [CompileTimeIf]
        public static void StaticMethod()
        {
            Console.WriteLine("Invoking Foo.StaticMethod()");
            Console.WriteLine("StaticMethod");
            return;
        }
    }
}

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.

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

namespace Doc.CompileTimeForEach
{
    internal class CompileTimeForEachAttribute : OverrideMethodAspect
    {
        public override dynamic? OverrideMethod()
        {
            foreach ( var p in meta.Target.Parameters.Where( p => p.RefKind != RefKind.Out ) )
            {
                Console.WriteLine( $"{p.Name} = {p.Value}" );
            }

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

namespace Doc.CompileTimeForEach
{
    internal class Foo
    {
        [CompileTimeForEach]
        private void Bar( int a, string b )
        {
            Console.WriteLine( $"Hello, world! a={a}, b='{b}'." );
        }
    }
}
using System;

namespace Doc.CompileTimeForEach
{
    internal class Foo
    {
        [CompileTimeForEach]
        private void Bar(int a, string b)
        {
            Console.WriteLine($"a = {a}");
            Console.WriteLine($"b = {b}");
            Console.WriteLine($"Hello, world! a={a}, b='{b}'.");
            return;
        }
    }
}

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 expression, 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, the members of this fake System.Type, for instance Type.Name, cannot be evaluated at design time. You may sometimes need to call the meta.RunTime method to tip the C# 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.

using Metalama.Framework.Aspects;
using System;
using System.Threading;

namespace Doc.Retry
{
    internal class RetryAttribute : OverrideMethodAspect
    {
        public int MaxAttempts { get; set; } = 5;

        public override dynamic? OverrideMethod()
        {
            for ( var i = 0;; i++ )
            {
                try
                {
                    return meta.Proceed();
                }
                catch ( Exception e ) when ( i < this.MaxAttempts )
                {
                    Console.WriteLine( $"{e.Message}. Retrying in 100 ms." );
                    Thread.Sleep( 100 );
                }
            }
        }
    }
}
using System;

namespace Doc.Retry
{
    internal class Foo
    {
        [Retry]
        private void RetryDefault()
        {
            throw new InvalidOperationException();
        }

        [Retry( MaxAttempts = 10 )]
        private void RetryTenTimes()
        {
            throw new InvalidOperationException();
        }
    }
}
using System;
using System.Threading;

namespace Doc.Retry
{
    internal class Foo
    {
        [Retry]
        private void RetryDefault()
        {
            for (var i = 0; ; i++)
            {
                try
                {
                    throw new InvalidOperationException();
                    return;
                }
                catch (Exception e) when (i < 5)
                {
                    Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
                    Thread.Sleep(100);
                }
            }
        }

        [Retry(MaxAttempts = 10)]
        private void RetryTenTimes()
        {
            for (var i = 0; ; i++)
            {
                try
                {
                    throw new InvalidOperationException();
                    return;
                }
                catch (Exception e) when (i < 10)
                {
                    Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
                    Thread.Sleep(100);
                }
            }
        }
    }
}

Custom compile-time types and methods

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

  • Compile-time code must be annotated with a [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 run either at compile time or at run time must be annotated with the [<xref:Metalama.Framework.Aspects.CompileTimeOrRunTimeAttribute?text=CompileTimeOrRunTime>] 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 packages 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.