T# is the template language used by Metalama to generate code. Templates are methods that combine compile-time logic with run-time code generation: they execute at compile-time to produce the C# code that will run in your application.
The syntax of T# is fully compatible with C#, meaning any valid T# code is valid C# code. However, T# has different semantics: the T# compiler executes within the compiler or IDE to generate C# source code, while the C# compiler transforms source code into IL (binary) files.
Understanding code scopes
T# templates mix 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 generate run-time expressions for the transformed code.
Metalama analyzes T# code and separates compile-time portions from run-time portions using inference rules. Compile-time expressions and statements typically begin with the meta pseudo-keyword. meta is a static class, but it serves as a marker that begins 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 doesn't 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 through the CompileTimeAttribute custom attribute. It searches for this attribute not only on the member but also on the declaring type and on 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.
Run-time-or-compile-time code
Run-time-or-compile-time code can execute either at run time or at compile time.
Run-time-or-compile-time code is annotated with the RunTimeOrCompileTimeAttribute custom attribute.
Aspect classes are run-time-or-compile-time because they must be accessible in both contexts. Aspects are represented as custom attributes that can be accessed at run time using System.Reflection, but they're also instantiated at compile time by Metalama. Therefore, the constructors and public properties of aspects must be usable in both run-time and compile-time contexts.
However, some methods of aspect classes are purely compile-time. They can't execute at run time because they access APIs that exist only at compile time. These methods must be annotated with CompileTimeAttribute.
Example
The following aspect adds logging before and after the execution of a method.
In the code below, compile-time code is highlighted 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
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}
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}
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}
Note
To benefit from syntax highlighting in Visual Studio, install the Visual Studio Tools for Metalama. See Configuring your IDE for IDE-specific information.
The expression meta.Target.Method (with an implicit trailing .ToString()) is a compile-time expression. At compile time, it's replaced by the name and signature of the method to which the aspect is applied.
The call to meta.Proceed() indicates that the original method body should be injected at that point.
Comparison with Razor
If you're familiar with ASP.NET, T# can be compared to Razor. Razor lets you create dynamic web pages by mixing two languages: C# for server-side code and HTML for client-side code. Similarly, T# mixes two kinds of code: compile-time code that generates run-time code.
The key difference is that in T# both the compile-time and run-time code use the same language: C#. Metalama analyzes templates and determines whether each expression or statement has run-time scope, compile-time scope, or both. Compile-time expressions typically begin with 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:
- The compile-time compilation contains only compile-time code. It's compiled against .NET Standard 2.0. It's then loaded within the compiler or IDE process and executed at compile or design time.
- 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.