Skip to main content

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:

PropertyTypeDescription
meta.Target.MethodIMethodThe target method
meta.Target.Method.NamestringMethod name (compile-time constant)
meta.Target.Method.ReturnTypeITypeReturn type
meta.Target.ParametersIParameterListMethod parameters
meta.Target.TypeINamedTypeThe containing type
meta.Target.FieldOrPropertyIFieldOrPropertyTarget field/property (in property aspects)
meta.Target.ConstructorIConstructorTarget 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 void methods: meta.Proceed() returns null, and the return null is 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 with OverrideAsyncMethod() 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

FeatureOverrideMethod()[Template] method
UsageSimple aspects (one override)Complex aspects (BuildAspect)
Applied toAutomatically to targetManually via builder.Override()
NamingMust be named OverrideMethodAny name
MultipleOne per aspectMultiple 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 ref or out cannot 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

PitfallExplanationSolution
Using nameof() for introduced membersnameof() resolves at the aspect's compile time, not the target'sUse string literals
Mixing compile-time and run-time in complex expressionsThe compiler may not correctly infer the scopeUse meta.CompileTime() or meta.RunTime() to be explicit
Using Debugger.Break() in templatesDebugger.Break() would be emitted as run-time codeUse meta.DebugBreak() for compile-time breakpoints
Assuming foreach over parameters has run-time overheadCompile-time foreach is unrolled — no loop exists at runtimeThis is actually a feature, not a pitfall
Storing compile-time values in instance fieldsAspect instances don't exist at runtimeUse meta.Tags to pass data from BuildAspect() to templates

Summary

FeatureDescription
T#C# syntax with compile-time semantics
meta.Proceed()Invoke the original method
meta.TargetAccess target declaration metadata
meta.This/meta.BaseDynamic instance access
meta.TagsPass data from BuildAspect to templates
Compile-time ifOnly matching branch is generated
Compile-time foreachLoop is unrolled at compile time
dynamic? returnUniversal 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.