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}
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}
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}
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}
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}
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}
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 asOverrideMethod
) 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}
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}
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:
- Make sure that this package targets .NET Standard 2.0.
- Make sure that the package is included in the project.
- Edit your
.csproj
orDirectory.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.