T# Template Language
T# is the compile-time template language at the heart of Metalama. It looks like C# and uses C# syntax, but it has different semantics — some code runs at compile time to generate other code that runs at runtime.
What is T#?
T# is not a separate language. It's a subset of C# with special compile-time semantics. When you write an aspect template, the Metalama compiler analyzes each expression and statement to determine whether it should:
- Execute at compile time (to generate code), or
- Be emitted as run-time code (to execute when the application runs)
The Key Insight
In a T# template, some lines are instructions to the compiler ("generate this code"), while other lines are the generated code itself. The same C# syntax serves both purposes.
The meta API
The meta pseudo-keyword is your gateway to compile-time operations. It's a static class with properties and methods that only exist during compilation.
meta.Proceed()
Calls the original (or next-in-chain) method implementation:
public override dynamic? OverrideMethod()
{
Console.WriteLine("Before");
var result = meta.Proceed(); // → replaced with original method body
Console.WriteLine("After");
return result;
}
meta.ProceedAsync()
The async version of meta.Proceed():
public override async Task<dynamic?> OverrideAsyncMethod()
{
Console.WriteLine("Before");
var result = await meta.ProceedAsync(); // → awaits original async method
Console.WriteLine("After");
return result;
}
meta.Target
Provides compile-time access to the target declaration's metadata:
| Property | Type | Description |
|---|---|---|
meta.Target.Method | IMethod | The target method |
meta.Target.Method.Name | string | Method name (compile-time constant) |
meta.Target.Method.ReturnType | IType | Return type |
meta.Target.Parameters | IParameterList | Method parameters |
meta.Target.Type | INamedType | The containing type |
meta.Target.FieldOrProperty | IFieldOrProperty | Target field/property (in property aspects) |
meta.Target.Constructor | IConstructor | Target constructor |
meta.This and meta.Base
Access instance members dynamically:
public override dynamic? OverrideMethod()
{
// meta.This resolves to 'this' but allows dynamic member access
var name = meta.This.Name; // Access any property of the target instance
// meta.Base calls the base implementation (for virtual overrides)
return meta.Base.MyMethod();
}
meta.Tags
Pass data from BuildAspect() to templates:
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
builder.Override(nameof(OverrideMethod),
tags: new { EventName = "CustomEvent" });
}
[Template]
public override dynamic? OverrideMethod()
{
var eventName = (string)meta.Tags["EventName"]!; // "CustomEvent"
Console.WriteLine($"Event: {eventName}");
return meta.Proceed();
}
meta.CompileTime() and meta.RunTime()
Explicitly control code scope:
public override dynamic? OverrideMethod()
{
// Force a value to be compile-time
var paramCount = meta.CompileTime(meta.Target.Parameters.Count);
// Force a compile-time expression to emit as run-time code
var runtimeValue = meta.RunTime(someCompileTimeExpression);
return meta.Proceed();
}
Compile-Time Expressions
The Metalama compiler determines the scope of each expression based on its type and usage.
Compile-Time Types
Any expression involving these types is compile-time:
IMethod,IType,IParameter,IField,IProperty(code model interfaces)IExpression,IStatement(template building blocks)- Values returned by
meta.*properties - Variables declared with types from
Metalama.Framework.Code
Automatic Scope Detection
public override dynamic? OverrideMethod()
{
// ┌─ COMPILE-TIME: meta.Target.Method is a code model object
var method = meta.Target.Method;
// ┌─ COMPILE-TIME: .Name on a code model object returns compile-time string
var name = method.Name;
// ┌─ RUN-TIME: Console.WriteLine is a run-time call
// (but 'name' is interpolated as a compile-time constant)
Console.WriteLine($"Method: {name}");
// ┌─ COMPILE-TIME: Iterating over compile-time collection
foreach (var param in meta.Target.Parameters)
{
// ┌─ RUN-TIME: Generated for each parameter
Console.WriteLine($" {param.Name} = {param.Value}");
}
// └─ The foreach itself disappears; only the unrolled lines remain
return meta.Proceed();
}
Compile-Time Control Flow
Compile-Time if
When the condition is a compile-time expression, the if is evaluated at compile time. Only the matching branch is emitted:
public override dynamic? OverrideMethod()
{
// Compile-time if: only ONE branch is generated
if (meta.Target.Method.IsAsync)
{
Console.WriteLine("This is an async method");
}
else
{
Console.WriteLine("This is a sync method");
}
return meta.Proceed();
}
For a sync method, the generated code is simply:
Console.WriteLine("This is a sync method");
The if statement and the async branch are completely absent from the output.
Compile-Time foreach
Iterating over compile-time collections (like meta.Target.Parameters) produces one copy of the loop body per element:
// Template:
foreach (var p in meta.Target.Parameters)
{
Console.WriteLine($"{p.Name} = {p.Value}");
}
// Generated for DoWork(int x, string y):
Console.WriteLine($"x = {x}");
Console.WriteLine($"y = {y}");
Compile-Time switch
Works like compile-time if — only the matching case is emitted:
switch (meta.Target.Parameters.Count)
{
case 0:
Console.WriteLine("No parameters");
break;
case 1:
Console.WriteLine($"One parameter: {meta.Target.Parameters[0].Name}");
break;
default:
Console.WriteLine($"Multiple parameters: {meta.Target.Parameters.Count}");
break;
}
The dynamic? Return Type
Aspect template methods return dynamic? because they must work with any return type:
public override dynamic? OverrideMethod()
{
// Works for: void, int, string, Task<Recipe>, ValueTask<bool>, etc.
return meta.Proceed();
}
How It Works
- For
voidmethods:meta.Proceed()returnsnull, and thereturn nullis optimized away - For value types: The dynamic is resolved to the actual type at compile time
- For reference types: Same as above
- For
Task<T>: Combined withOverrideAsyncMethod()pattern
You never need to cast the return value — Metalama handles type resolution at compile time.
Template Methods
Methods marked with [Template] can be used as templates in BuildAspect():
public class MyAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Use the template to override all public methods
foreach (var method in builder.Target.Methods.Where(m => m.Accessibility == Accessibility.Public))
{
builder.With(method).Override(nameof(LogTemplate));
}
}
[Template]
public dynamic? LogTemplate()
{
Console.WriteLine($"Called: {meta.Target.Method.Name}");
return meta.Proceed();
}
}
Template vs. Override
| Feature | OverrideMethod() | [Template] method |
|---|---|---|
| Usage | Simple aspects (one override) | Complex aspects (BuildAspect) |
| Applied to | Automatically to target | Manually via builder.Override() |
| Naming | Must be named OverrideMethod | Any name |
| Multiple | One per aspect | Multiple templates per aspect |
Property Templates
For OverrideFieldOrPropertyAspect, the template is a property:
public class TrimAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get => meta.Proceed();
set
{
// Trim string values before setting
if (meta.Target.FieldOrProperty.Type.Is(typeof(string)))
{
meta.Target.FieldOrProperty.Value = value?.Trim();
}
else
{
meta.Proceed();
}
}
}
}
Field-to-Property Promotion
When applied to a field, Metalama automatically promotes the field to a property:
// Source code:
[Trim]
public string Name; // Field
// Generated code:
private string _name; // Backing field (auto-generated)
public string Name // Property (promoted from field)
{
get => _name;
set => _name = value?.Trim();
}
Limitation: Fields used with
reforoutcannot be promoted to properties. Metalama will report an error.
Compile-Time Helper Methods
You can extract compile-time logic into helper methods using [CompileTime]:
public class MyAspect : OverrideMethodAspect
{
[CompileTime]
private static bool ShouldLogParameter(IParameter parameter)
{
// This runs at compile time to decide which parameters to log
return !parameter.Attributes.Any(a => a.Type.Name == "SensitiveAttribute");
}
public override dynamic? OverrideMethod()
{
foreach (var param in meta.Target.Parameters)
{
if (ShouldLogParameter(param)) // Compile-time call
{
Console.WriteLine($" {param.Name} = {param.Value}");
}
}
return meta.Proceed();
}
}
Common Patterns
Try/Finally (Guaranteed Cleanup)
public override dynamic? OverrideMethod()
{
var resource = AcquireResource();
try
{
return meta.Proceed();
}
finally
{
ReleaseResource(resource);
}
}
Conditional Execution
public override dynamic? OverrideMethod()
{
if (!IsAuthorized())
{
throw new UnauthorizedAccessException();
}
return meta.Proceed();
}
Value Transformation
public override dynamic? OverrideMethod()
{
var result = meta.Proceed();
// Transform the result before returning
return TransformResult(result);
}
Swallow and Replace
public override dynamic? OverrideMethod()
{
try
{
return meta.Proceed();
}
catch (SpecificException)
{
return default; // Swallow exception, return default
}
}
Common Pitfalls
| Pitfall | Explanation | Solution |
|---|---|---|
Using nameof() for introduced members | nameof() resolves at the aspect's compile time, not the target's | Use string literals |
| Mixing compile-time and run-time in complex expressions | The compiler may not correctly infer the scope | Use meta.CompileTime() or meta.RunTime() to be explicit |
Using Debugger.Break() in templates | Debugger.Break() would be emitted as run-time code | Use meta.DebugBreak() for compile-time breakpoints |
Assuming foreach over parameters has run-time overhead | Compile-time foreach is unrolled — no loop exists at runtime | This is actually a feature, not a pitfall |
| Storing compile-time values in instance fields | Aspect instances don't exist at runtime | Use meta.Tags to pass data from BuildAspect() to templates |
Summary
| Feature | Description |
|---|---|
| T# | C# syntax with compile-time semantics |
meta.Proceed() | Invoke the original method |
meta.Target | Access target declaration metadata |
meta.This/meta.Base | Dynamic instance access |
meta.Tags | Pass data from BuildAspect to templates |
Compile-time if | Only matching branch is generated |
Compile-time foreach | Loop is unrolled at compile time |
dynamic? return | Universal return type for any method signature |
[Template] | Mark methods as reusable templates |
[CompileTime] | Mark helper methods as compile-time only |
Next: Aspect Base Classes — Detailed guide to each base class.