C#-to-C# Templates

Classic code generators are just text generators. They don’t offer syntax highlighting, code completion, or error checking for generated code. Metalama is a true, strongly-typed object-oriented code generator that features T#, a unique C#-to-C# template language.

T# templates are pure C# methods and expressions, 100% compatible with any C# editor. They differ from regular C# methods solely in how they are compiled.

With T#, you can seamlessly blend generated code with hand-written code.

Benefits of T# include:

  • Full Intellisense support. Enjoy syntax highlighting, code completion, member lists, and error detection.
  • 100% C# compatible. Use any C# editor of your choice.
  • High performance. Generate high-performance code without any runtime overhead.
  • No need to learn MSIL. It’s just the C# you already know.
  • Completely debuggable. Step into the code you generate. Learn more.
  • Additional syntax highlighting is available as a premium feature via Visual Studio Tools for Metalama.

Example

Consider this aspect:

public class RetryAttribute : OverrideMethodAspect
{
    public override dynamic? OverrideMethod()
    {
        for ( var i = 0;; i++ )
        {
            try
            {
                return meta.Proceed();
            }
            catch ( Exception e ) when ( i < 3 )
            {
                Console.WriteLine( $"{meta.Target.Method} failed: {e.Message}" );
                Thread.Sleep( 100 );
            }
        }
    }
}

The OverrideMethod is a T# template:

  • meta.Proceed() is replaced by the implementation of the method being overridden.
  • meta.Target.Method is the object model of the method being overridden. Here, we are calling the ToString method.

Now let’s apply this template to a method:

[Retry]
public async Task<decimal> GetExchangeRate()
{
    using var client = new HttpClient();
    var url = $"https://api.example.com/exchange?base=CZK&target=USD";

    var response = await client.GetAsync(url);
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();
    return decimal.Parse(responseString);
}

During compilation, Metalama will apply the [Retry] template to the GetExchangeRate method and generate the following code:

[Retry]
public async Task<decimal> GetExchangeRate()
{
    for ( var i = 0;; i++ )
    {
        try
        {
            using var client = new HttpClient();
            var url = $"https://api.example.com/exchange?base=CZK&target=USD";

            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            var responseString = await response.Content.ReadAsStringAsync();
            return decimal.Parse(responseString);
        }
        catch ( Exception e ) when ( i < 3 )
        {
            Console.WriteLine( $"CurrencyService.GetExchangeRate() failed: {e.Message}" );
            Thread.Sleep( 100 );
        }
    }
}

How does it work?

T# templates differentiate between compile-time and run-time code execution. Since there are no tags like {% %} or <% %> to distinguish compile-time C# from run-time C#, Metalama relies on inference rules to determine which statements and expressions are compile-time and which ones are run-time.

By default, all APIs are considered run-time. Compile-time APIs (like the meta class) must be tagged with the [CompileTime] custom attribute. When Metalama encounters a compile-time API call in your template, it uses complex inference rules to determine which parts of the template should execute at compile-time.

For example, in the snippet below, meta.Target is tagged as compile-time. By inference, meta.Target.Method.Parameters is compile-time, making the parameters variable and the foreach loop compile-time as well. In $"{p.Name} = '{p.Value}'", p.Name is compile-time, but p.Value is a compile-time expression returning a run-time expression.

public override dynamic? OverrideMethod()
{
    var parameters = meta.Target.Method.Parameters;

    foreach ( var p in parameters )
    {
        Console.WriteLine( $"{p.Name} = '{p.Value}'");
    }

    return meta.Proceed();
}

Most of the Metalama.Framework namespace is compile-time. You can extend T# with your own classes and methods by tagging them with the [CompileTime] or [RunTimeOrCompileTime] attributes.

Features

  • Compile-time local variables, foreach, if, switch.
  • Access unknown members through C# dynamic typing.
  • Template parameters.
  • Call a template from another template (including virtual calls).
  • Complete API to reflect on the code being compiled.
  • APIs to dynamically build interpolated strings, switch statements.
  • Serialization from compile-time objects to run-time expressions.