Project "Caravela" 0.3 / / Caravela Documentation / Conceptual Documentation / Creating Aspects / Writing code templates

Writing code templates

The specificity of a tool like Caravela, compared to simple code generation APIs, is that Caravela is able to modify existing code, not only generate new code. Instead of giving you access to the syntax tree, which is extremely complex and error-prone (and you can still do it anyway with Caravela.Framework.Sdk if you feel brave), Caravela lets you express code transformations in plain C# using a template language named Caravela Template Language.

You can compare Caravela Template Language to Razor. Razor allows you to create dynamic web pages by mixing server-side C# code and client-side HTML. With Caravela Template Language, you have compile-time and run-time code and, basically, the compile-time code generates the run-time code. The difference with Razor is that in Caravela both the compile-time and run-time code are the same language: C#. Caravela interprets every expression or statement in a template as having either run-time scope or compile-time scope. Compile-time expressions are generally initiated by calls to the meta API.

Initial example

Before moving forward, let's illustrate this concept with an example. The next aspect writes text to the console before and after the execution of a method, but special care is taken for out parameters and void methods. This is achieved by a conditional compile-time logic which generates simple run-time code. Compile-time code is highlighted differently, so you can see which part of the code executes at compile time and which executes at run time.

Note

To benefit from syntax highlighting in Visual Studio, install the PostSharp "Caravela" Tools for Visual Studio.

                using System;
using System.Text;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Code.Syntax;

namespace Caravela.Documentation.SampleCode.AspectFramework.LogParameters
{
    public class LogAttribute : OverrideMethodAspect
    {

        public override dynamic OverrideMethod()
        {
            // Build a formatting string.
            var methodName = BuildInterpolatedString();

            // Write entry message.
            var entryMessage = methodName.Clone();
            entryMessage.AddText(" started.");
            Console.WriteLine(entryMessage.ToInterpolatedString());

            try
            {
                // Invoke the method.
                dynamic result = meta.Proceed();

                // Display the success message.
                var successMessage = methodName.Clone();
                if (meta.Target.Method.ReturnType.Is(typeof(void)))
                {
                    successMessage.AddText(" succeeded.");
                }
                else
                {
                    successMessage.AddText(" returned ");
                    successMessage.AddExpression(result);
                    successMessage.AddText(".");
                }

                Console.WriteLine(successMessage.ToInterpolatedString() );

                return result;
            }
            catch (Exception e)
            {
                // Display the failure message.
                var failureMessage = methodName.Clone();
                failureMessage.AddText(" failed: ");
                failureMessage.AddExpression(e.Message);
                Console.WriteLine(failureMessage.ToInterpolatedString());
                throw;
            }
        }

        private static InterpolatedStringBuilder BuildInterpolatedString()
        {
            var stringBuilder = InterpolatedStringBuilder.Create();
            stringBuilder.AddText(meta.Target.Type.ToDisplayString(CodeDisplayFormat.MinimallyQualified));
            stringBuilder.AddText(".");
            stringBuilder.AddText(meta.Target.Method.Name);
            stringBuilder.AddText("(");
            var i = meta.CompileTime(0);

            foreach (var p in meta.Target.Parameters)
            {
                var comma = i > 0 ? ", " : "";

                if (p.IsOut())
                {
                    stringBuilder.AddText($"{comma}{p.Name} = <out> ");
                }
                else
                {
                    stringBuilder.AddText($"{comma}{p.Name} = {{");
                    stringBuilder.AddExpression(p.Value);
                    stringBuilder.AddText("}");
                }

                i++;
            }
            stringBuilder.AddText(")");

            return stringBuilder;
        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.LogParameters
{
    internal class TargetCode
    {
        [Log]
        private void VoidMethod(int a, out int b)
        {
            b = a;
        }

        [Log]
        private int IntMethod(int a)
        {
            return a;
        }

    }
}
              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.LogParameters
{
    internal class TargetCode
    {
        [Log]
        private void VoidMethod(int a, out int b)
        {
            Console.WriteLine($"TargetCode.VoidMethod(a = {{a}}, b = <out> ) started.");
            try
            {
                b = a;
                object result = null;
                Console.WriteLine($"TargetCode.VoidMethod(a = {{a}}, b = <out> ) succeeded.");
                return;
            }
            catch (Exception e)
            {
                Console.WriteLine($"TargetCode.VoidMethod(a = {{a}}, b = <out> ) failed: {e.Message}");
                throw;
            }
        }

        [Log]
        private int IntMethod(int a)
        {
            Console.WriteLine($"TargetCode.IntMethod(a = {{a}}) started.");
            try
            {
                int result;
                result = a;
                goto __aspect_return_1;
            __aspect_return_1:
                Console.WriteLine($"TargetCode.IntMethod(a = {{a}}) returned {result}.");
                return result;
            }
            catch (Exception e)
            {
                Console.WriteLine($"TargetCode.IntMethod(a = {{a}}) failed: {e.Message}");
                throw;
            }
        }

    }
}
              

Writing compile-time code

Compile-time expressions are expressions that either contain a call to a compile-time method, or contain a reference to a compile-time local variable or a compile-time aspect member. Compile-time expressions are executed at compile time, when the aspect is applied to a target.

Compile-time statements are statements, such as if, foreach or meta.DebugBreak();, that are executed at compile time.

meta API

The entry point of the compile-time API is the meta static class. The name of this class is intentionally lower case to convey the sentiment that it is something unusual and gives access to some kind of magic. The meta class is the entry point to the meta model and the members of this class can be invoked only in the context of a template.

The meta exposes the following members:

  • The meta.Proceed method invokes the target method or accessor being intercepted, which can be either the next aspect applied on the same target or the target source implementation itself.
  • The meta.Target property gives access to the declaration to which the template is applied.
  • The meta.Target.Parameters property gives access to the current method or accessor parameters.
  • The meta.Diagnostics property allows your aspect to report or suppress diagnostics. See Reporting and suppressing diagnostics for details.
  • The meta.This property represents the this instance. Together with meta.Base, meta.ThisStatic, and meta.BaseStatic properties, it allows your template to access members of the target class using dynamic code (see below).
  • The meta.Tags property gives access to an arbitrary dictionary that has been passed to the advice factory method.
  • The meta.CompileTime method coerces a neutral expression into a compile-time expression.
  • meta.RunTime method converts the result of a compile-time expression into a run-time value (see below).

Compile-time local variables

Local variables are run-time by default. To declare a compile-time local variable, you must initialize it to a compile-time value. If you need to initialize the compile-time variable to a literal value such as 0 or "text", use the meta.CompileTime method to convert the literal into a compile-time value.

Examples:

  • In var i = 0, i is a run-time variable.
  • In var i = meta.CompileTime(0), i is a compile-time variable.
  • In var parameters = meta.Target.Parameters, parameters is compile-time variable.
Note

It is not allowed to assign a compile-time variable from a block whose execution depends on a run-time condition, including:

  • a run-time if, else, for, foreach, while;
  • a catch or finally.

Aspect members

Aspect members are compile-time and can be accessed from templates. For instance, an aspect custom attribute can define a property that can be set when when the custom attribute is applied to a target declaration and then read from the aspect compile-time code.

There are a few exceptions to this rule:

  • aspect members whose signature contain a run-time-only type cannot be accessed from a template.
  • aspect members annotated with the [Template] attribute (or overriding members that are, such as OverrideMethod) cannot be invoked from a template.
  • aspect members annotated with the [Introduce] or [InterfaceMember] attribute are considered run-time (see Introducing Members and Implementing Interfaces).

Example

The following example shows a simple Retry aspect. The maximum number of attempts can be configured by setting a property of the custom attribute. This property is compile-time.

                using System;
using System.Threading;
using Caravela.Framework.Aspects;

namespace Caravela.Documentation.SampleCode.AspectFramework.Retry
{
    internal class RetryAttribute : OverrideMethodAspect
    {
        public int MaxAttempts { get; set; } = 5;

        public override dynamic OverrideMethod()
        {
            for (var i = 0; ; i++)
            {
                try
                {
                    return meta.Proceed();
                }
                catch (Exception e) when (i < this.MaxAttempts)
                {
                    Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
                    Thread.Sleep(100);
                }
            }
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.Retry
{
    internal class TargetCode
    {
        [Retry]
        private void RetryDefault()
        {
            throw new Exception();
        }

        [Retry(MaxAttempts = 10)]
        private void RetryTenTimes()
        {
            throw new Exception();
        }
    }
}

              
                using System;
using System.Threading;

namespace Caravela.Documentation.SampleCode.AspectFramework.Retry
{
    internal class TargetCode
    {
        [Retry]
        private void RetryDefault()
        {
            for (var i = 0; ; i++)
            {
                try
                {
                    throw new Exception();
                    return;
                }
                catch (Exception e) when (i < 5)
                {
                    Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
                    Thread.Sleep(100);
                }
            }
        }

        [Retry(MaxAttempts = 10)]
        private void RetryTenTimes()
        {
            for (var i = 0; ; i++)
            {
                try
                {
                    throw new Exception();
                    return;
                }
                catch (Exception e) when (i < 10)
                {
                    Console.WriteLine($"{e.Message}. Retrying in 100 ms.");
                    Thread.Sleep(100);
                }
            }
        }
    }
}
              

Compile-time if

If the condition of an if statement is a compile-time expression, the if statement will be interpreted at compile-time.

Note

It is not allowed to have a compile-time if inside a block whose execution depends on a run-time condition, including a run-time if, else, for, foreach, while, switch, catch or finally.

Example

In the following example, the aspect prints a different string for static methods than for instance ones.

                using System;
using Caravela.Framework.Aspects;

namespace Caravela.Documentation.SampleCode.AspectFramework.CompileTimeIf
{
    internal class CompileTimeIfAttribute : OverrideMethodAspect
    {
        public override dynamic OverrideMethod()
        {
            if (meta.Target.Method.IsStatic)
            {
                Console.WriteLine($"Invoking {meta.Target.Method.ToDisplayString()}");
            }
            else
            {
                Console.WriteLine($"Invoking {meta.Target.Method.ToDisplayString()} on instance {meta.This.ToString()}.");
            }

            return meta.Proceed();
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.CompileTimeIf
{
    internal class TargetCode
    {
        [CompileTimeIf]
        public void InstanceMethod()
        {
            Console.WriteLine("InstanceMethod");
        }

        [CompileTimeIf]
        public static void StaticMethod()
        {
            Console.WriteLine("StaticMethod");
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.CompileTimeIf
{
    internal class TargetCode
    {
        [CompileTimeIf]
        public void InstanceMethod()
        {
            Console.WriteLine($"Invoking TargetCode.InstanceMethod() on instance {base.ToString()}.");
            Console.WriteLine("InstanceMethod");
            return;
        }

        [CompileTimeIf]
        public static void StaticMethod()
        {
            Console.WriteLine($"Invoking TargetCode.StaticMethod()");
            Console.WriteLine("StaticMethod");
            return;
        }
    }
}
              

Compile-time foreach

If the expression of a foreach statement is a compile-time expression, the foreach statement will be interpreted at compile-time.

Note

It is not allowed to have a compile-time foreach inside a block whose execution depends on a run-time condition, including a run-time if, else, for, foreach, while, switch, catch or finally.

Example

The following aspect uses a foreach loop to print the value of each parameter of the method to which it is applied.

                using System;
using System.Linq;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;

namespace Caravela.Documentation.SampleCode.AspectFramework.CompileTimeForEach
{
    internal class CompileTimeForEachAttribute : OverrideMethodAspect
    {
        public override dynamic OverrideMethod()
        {
            foreach (var p in meta.Target.Parameters.Where(p => p.RefKind != RefKind.Out))
            {
                Console.WriteLine($"{p.Name} = {p.Value}");
            }

            return meta.Proceed();
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.CompileTimeForEach
{
    internal class TargetCode
    {
        [CompileTimeForEach]
        private void Method(int a, string b)
        {
            Console.WriteLine("Hello, world.");
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.CompileTimeForEach
{
    internal class TargetCode
    {
        [CompileTimeForEach]
        private void Method(int a, string b)
        {
            Console.WriteLine($"a = {a}");
            Console.WriteLine($"b = {b}");
            Console.WriteLine("Hello, world.");
            return;
        }
    }
}
              

No compile-time for, while and goto

It is not possible to create compile-time for or while loops. goto statements are forbidden in templates. If you need a compile-time for, you can use the following construct:

            foreach (int i in meta.CompileTime( Enumerable.Range( 0, n ) ))

          

If the approach above is not possible, you can try to move your logic to a compile-time aspect function (not a template method), have this function return an enumerable, and use the return value in a foreach loop in the template method.

typeof, nameof expressions

typeof and nameof expressions in compile-time code are always pre-compiled into compile-time expression, which makes it possible for compile-time code to reference run-time types.

Custom compile-time types and methods

If you need to move some compile-time logic from the template to a method, you can create a method in the aspect. It will automatically be considered as compile-time.

If you want to share compile-time code between aspects, you can create a compile-time class by marking it with the [CompileTimeOnly] custom attribute.

Writing dynamic run-time code

Dynamic typing

Templates use the dynamic type to represent types that are unknown by the developer of the template. For instance, an aspect may not know in advance the return type of the methods to which it is applied. The return type is represented by the dynamic type.

            dynamic? OverrideMethod() 
{ 
    return default;
}

          

All dynamic compile-time code is transformed into strongly-typed run-time code. When the template is expanded, dynamic variables are transformed into var variables. Therefore, all dynamic variables must be initialized.

In a template, it is not possible to generate code that uses dynamic typing at run time.

Converting compile-time values to run-time values

You can use meta.RunTime( expression ) to convert the result of a compile-time expression into a run-time value. The compile-time expression will be evaluated at compile time, and its result will be converted into syntax that represents that value. Conversions are possible for the following compile-time types:

It is not possible to build custom converters at the moment. However, you can generate an expression as a string, parse it, and use it in a run-time expression. See Parsing C# code for details.

Example

                using System;
using System.Linq;
using Caravela.Framework.Aspects;

namespace Caravela.Documentation.SampleCode.AspectFramework.ConvertToRunTime
{
    internal class ConvertToRunTimeAspect : OverrideMethodAspect
    {
        public override dynamic OverrideMethod()
        {
            var parameterNamesCompileTime = meta.Target.Parameters.Select(p => p.Name).ToList();
            var parameterNames = meta.RunTime(parameterNamesCompileTime);
            var buildTime = meta.RunTime(new Guid("13c139ea-42f5-4726-894d-550406357978"));
            var parameterType = meta.RunTime( meta.Target.Parameters[0].ParameterType.ToType() );

            return null;
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.ConvertToRunTime
{
    internal class TargetCode
    {
        [ConvertToRunTimeAspect]
        private void Method(string a, int c, DateTime e) { }
    }
}

              
                using System;
using System.Collections.Generic;

namespace Caravela.Documentation.SampleCode.AspectFramework.ConvertToRunTime
{
    internal class TargetCode
    {
        [ConvertToRunTimeAspect]
        private void Method(string a, int c, DateTime e)
        {
            var parameterNames = new List<string> { "a", "c", "e" };
            var buildTime = new Guid(331430378, 17141, 18214, 137, 77, 85, 4, 6, 53, 121, 120);
            var parameterType = typeof(string);
            return;
        }
    }
}
              

Dynamic code

The meta API exposes some properties of dynamic type and some methods returning dynamic values. These members are compile-time, but their value represents a declaration that you can dynamically read at run time. In the case of writable properties, it is also possible to set the value.

Dynamic values are a bit magic because their compile-time value translates into syntax that is injected in the transformed code.

For instance, meta.Target.Parameters["p"].Value refers to p parameter of the target method and compiles simply into the syntax p. It is possible to read this parameter and, if this is an out or ref parameter, it is also possible to write it.

            // Translates into: Console.WriteLine( "p = " + p );
Console.WriteLine( "p = " + meta.Target.Parameters["p"].Value );


// Translates into: this.MyProperty = 5;
meta.Property.Value = 5;

          

You can also write dynamic code on the left of a dynamic expression:

            // Translates into: this.OnPropertyChanged( "X" );
meta.This.OnPropertyChanged( "X" );

          

You can combine dynamic code and compile-time expressions:

            // Translated into: this.OnPropertyChanged( "MyProperty" );
meta.This.OnPropertyChanged( meta.Property.Name );

          

Generating calls to the code model

When you have a Caravela.Framework.Code representation of a declaration, you may want to access it from your generated run-time code. You can do this by using the Invokers property exposed by the IMethod, IFieldOrProperty or IEvent interfaces.

For details, see the documentation of the Caravela.Framework.Code.Invokers namespace.

Generating run-time arrays

The first way to generate run-time array is to declare a variable of array type and to use a statement to set each element, for instance:

            var args = new object[2];
args[0] = "a";
args[1] = DateTime.Now;
MyRunTimeMethod( args );

          

If you want to generate an array as a single-line expression, you can use the ArrayBuilder class.

For instance:

            var arrayBuilder = ArrayBuilder.Create();
arrayBuilder.Add( "a" );
arrayBuilder.Add( DateTime.Now );
MyRunTimeMethod( arrayBuilder.ToArray() );

          

This will generate the following code:

            MyRunTimeMethod( new object[] { "a", DateTime.Now });

          

Generating interpolated strings

Instead of generating a string as an array separately and using string.Format, you can generate an interpolated string using the InterpolatedStringBuilder class.

The following example shows how an InterpolatedStringBuilder can be used to automatically implement the ToString method.

                using System;
using System.Linq;
using System.Text;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Code.Syntax;

namespace Caravela.Documentation.SampleCode.AspectFramework.ToString
{
    class ToStringAttribute : Attribute, IAspect<INamedType>
    {
        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
        public string IntroducedToString()
        {
            var stringBuilder = InterpolatedStringBuilder.Create();
            stringBuilder.AddText("{ ");
            stringBuilder.AddText(meta.Target.Type.Name );
            stringBuilder.AddText(" ");

            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsStatic ).ToList();

            var i = meta.CompileTime( 0 );

            foreach ( var field in fields)
            {
                if ( i > 0 )
                {
                    stringBuilder.AddText(", ");
                }

                stringBuilder.AddText(field.Name);
                stringBuilder.AddText("=");
                stringBuilder.AddExpression(field.Invokers.Final.GetValue(meta.This) );

                i++;
            }

            stringBuilder.AddText(" }");


            return stringBuilder.ToInterpolatedString();

        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.ToString
{
    [ToString]
    class TargetCode
    {
        int x;
        public string Y { get; set; }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.ToString
{
    [ToString]
    class TargetCode
    {
        int x;
        public string Y { get; set; }


        public override string ToString()
        {
            return $"{ TargetCode x={x}, Y={Y} }";
        }
    }
}
              

Parsing C# code

Sometimes it is easier to generate the run-time code as a simple text instead of using a complex meta API. If you want to use C# code represented as a string in your code, you can do it using the meta.ParseExpression method. This method returns an IExpression, which is a compile-time object that you can use anywhere in compile-time code. The IExpression interface exposes the run-time expression in the Value property.

For instance, consider the following template code:

            var expression = meta.ParseExpression("(a + b)/c");
MyRunTimeMethod( expression.Value );

          

This will generate the following run-time code:

            MyRunTimeMethod((a + b)/c)

          
Note

The string expression is inserted as is without any validation or transformation. Always specify the full namespace of any declaration used in a text expression.

Capturing run-time expressions into compile-time objects

If you want to manipulate a run-time expression as a compile-time object, you can do it using the meta.DefineExpression method. This allows you to have expressions that depend on compile-time conditions and control flows. The DefineExpression method returns an IExpression, the same interface returned by ParseExpression. The IExpression is a compile-time object that you can use anywhere in compile-time code. It exposes the run-time expression in the Value property.

The following example is taken from the clone aspect. It declares a local variable named clone, but the expression assigned to the variable depends on whether the Clone method is an override.

            IExpression baseCall;

if (meta.Target.Method.IsOverride)
{
    meta.DefineExpression(meta.Base.Clone(), out baseCall);
}
else
{
    meta.DefineExpression(meta.Base.MemberwiseClone(), out baseCall);
}

// Define a local variable of the same type as the target type.
var clone = meta.Cast(meta.Target.Type, baseCall);

          

This template generates either var clone = (TargetType) base.Clone(); or var clone = (TargetType) this.MemberwiseClone();.

Debugging templates

See Debugging Code With Aspects.