MetalamaConceptual documentationCreating aspectsWriting T# templatesOverview
Open sandboxFocusImprove this doc

T# templates: Overview

T# is the template language used by Metalama. The syntax of T# is 100% compatible with C#. The distinction between T# and C# lies in the fact that the T# compiler operates within the compiler or the IDE to generate C# code, while the C# compiler generates IL (binary) files.

Scopes of code

T# templates integrate compile-time and run-time expressions and statements. Compile-time expressions and statements are evaluated at compile time in the compiler (or at design time in the IDE using the Diff Preview feature) and result in the generation of other run-time expressions.

Metalama analyzes T# and separates the compile-time portion from the run-time portion through the application of inference rules. Compile-time expressions and statements typically initiate with the meta pseudo-keyword. meta is technically a static class, but it's helpful to perceive it as a magic keyword that initiates a compile-time expression or statement.

In Metalama, every type in your source code belongs to one of the following scopes:

Run-time code

Run-time code compiles to a binary assembly and typically executes on the end user's device. In a project that does not reference the Metalama.Framework package, all code is considered run-time.

The entry point of run-time code is typically the Program.Main method.

Compile-time code

Compile-time code is executed either at compile time by the compiler or at design time by the IDE.

Metalama recognizes compile-time-only code thanks to the CompileTimeAttribute custom attribute. It searches for this attribute not only on the member but also on the declaring type, and at the base types and interfaces. Most classes and interfaces of the Metalama.Framework assembly are compile-time-only.

You can create compile-time classes by annotating them with CompileTimeAttribute.

Warning

All compile-time code must be strictly compatible with .NET Standard 2.0, even if the containing project targets a more advanced platform. Any call to an API that is not strictly .NET Standard 2.0 will be considered run-time code.

Scope-neutral code

Scope-neutral code can execute either at run time or at compile time.

Scope-neutral code is annotated with the RunTimeOrCompileTimeAttribute custom attribute.

Aspect classes are scope-neutral because aspects are a unique kind of class. Aspects are typically represented as custom attributes, which can be accessed at run time using System.Reflection, but they are also instantiated at compile time by Metalama. Therefore, it is crucial that the constructors and public properties of the aspects are both run-time and compile-time.

However, some methods of aspect classes are purely compile-time. They cannot be executed at run time because they access APIs that exist only at compile time. These methods must be annotated with CompileTimeAttribute.

Initial example

Before proceeding, let's illustrate this concept with an example. The following aspect writes some text to the console before and after the execution of a method.

In the code below, compile-time code is highlighted differently so you can see which part of the code executes at compile time and which at run time. In the different tabs on the example, you can see the aspect code (with the template), the target code (to which the aspect is applied), and the transformed code, which is the target code transformed by the aspect.

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}
Note

To benefit from syntax highlighting in Visual Studio, install the Visual Studio Tools for Metalama and PostSharp. Syntax highlighting is not supported in other IDEs.

The expression meta.Target.Method (with an implicit trailing .ToString()) is a compile-time expression. At compile time, it is replaced by the name and signature of the method to which the aspect is applied.

The call to meta.Proceed() signifies that the original method body should be injected at that point.

Comparison with Razor

T# can be compared to Razor. Razor allows you to create dynamic web pages by mixing two languages: C# for server-side code (the meta code), and HTML for client-side code. With T#, you also have two kinds of code: compile-time and run-time code. The compile-time code generates the run-time code. The difference with Razor is that in T# both the compile-time and run-time code are the same language: C#. Metalama interprets every expression or statement in a template as having either run-time scope or compile-time scope. Compile-time expressions generally initiate by calls to the meta API.

Compilation process

When Metalama compiles your project, one of the first steps is to separate the compile-time code from the run-time code. From your initial project, Metalama creates two compilations:

  1. The compile-time compilation contains only compile-time code. It is compiled against .NET Standard 2.0. It is then loaded within the compiler or IDE process and executed at compile or design time.
  2. The run-time compilation contains the run-time code. It also contains the compile-time declarations, but their implementation is replaced by throw new NotSupportedException().

During compilation, Metalama compiles the T# templates into standard C# code that generates the run-time code using the Roslyn API. This generated code, as well as any non-template compile-time code, is then zipped and embedded in the run-time assembly as a managed resource.

Warning

Intellectual property alert. The source of your compile-time code is embedded in clear text, without any obfuscation, in the run-time binary assemblies as a managed resource.