Open sandboxFocusImprove this doc

Generating code based on the code model

When you have a Metalama.Framework.Code representation of a declaration, you'll often want to access it from your generated run-time code. For instance, you'll often need to generate code that calls an IMethod or accesses an IProperty.

What are invokers?

Invokers are APIs that let you generate run-time code from compile-time declarations. When you write template code (which executes at compile-time), you use invokers to create compile-time expressions (IExpression) that represent method calls, property accesses, and other operations. These expressions can then be used anywhere in your template and will be expanded into actual C# code when the template is applied.

The invoker functionality is implemented in the Metalama.Framework.Code.Invokers namespace. The IMethod, IFieldOrProperty, IIndexer, and IEvent interfaces derive from IMethodInvoker, IFieldOrPropertyInvoker, IIndexerInvoker, and IEventInvoker respectively.

For scenarios where members are known at design time (when you write the aspect), you can also use the dynamic typing approach described in Dynamic typing in templates. Invokers provide more flexibility and control at compile time.

Warning

The invoker API isn't type-safe. Invokers will happily generate code that doesn't compile because of mismatched types. For example, you can call method.Invoke("wrong", "types") even if the method expects integers. The invoker API doesn't validate argument types or return types. Always verify that the code you generate matches the actual member signatures. The resulting invalid code will only be caught when the transformed code is compiled, confusing the aspect user.

Calling a method

To generate an expression that represents the invocation of an IMethod, use the method.Invoke method.

Example: dynamic method invocation

In the following example, the [CallAfter(methodName)] aspect overrides the target method and calls a specified method after successful execution. The aspect author doesn't know which method will be called — this is determined by the aspect user when they apply the attribute. The aspect queries the code model to find the method by name, then uses the Invoke method to generate a call to it.

1using Metalama.Framework.Aspects;
2using System.Linq;
3
4namespace Doc.CallAfter;
5
6public class CallAfterAttribute : OverrideMethodAspect
7{
8    private readonly string _methodName;
9
10    public CallAfterAttribute( string methodName )
11    {
12        this._methodName = methodName;
13    }
14
15    public override dynamic? OverrideMethod()
16    {
17        // Execute the method.
18        var result = meta.Proceed();
19        
20        // Call the after method.
21        var method = meta.Target.Type.AllMethods
22            .OfName( this._methodName )
23            .Single( p => p.Parameters.Count == 0 );
24
25        method.Invoke();
26
27        return result;
28    }
29}
Source Code
1using System;
2
3namespace Doc.CallAfter;
4
5public class TestClass
6{
7    private int _state;
8
9    [CallAfter( nameof(CheckState) )]
10    public void Open()
11    {
12        this._state++;
13    }
14


15    [CallAfter( nameof(CheckState) )]
16    public void Close()
17    {
18        this._state--;
19    }
20


21    private void CheckState()
22    {
23        if ( this._state < 0 )
24        {
25            throw new InvalidOperationException();
26        }
27    }
28}
Transformed Code
1using System;
2
3namespace Doc.CallAfter;
4
5public class TestClass
6{
7    private int _state;
8
9    [CallAfter(nameof(CheckState))]
10    public void Open()
11    {
12        this._state++;
13        object result = null;
14        CheckState();
15    }
16
17    [CallAfter(nameof(CheckState))]
18    public void Close()
19    {
20        this._state--;
21        object result = null;
22        CheckState();
23    }
24
25    private void CheckState()
26    {
27        if (this._state < 0)
28        {
29            throw new InvalidOperationException();
30        }
31    }
32}
Note

A production-ready aspect should validate that the method exists in BuildAspect and report a diagnostic error if it doesn't, rather than allowing the template to fail at compile time with an exception.

Specifying the target object and nullability behavior

Before we go on with explaining the invoker API for other kinds of members, we must discuss a few options:

  • Target object (receiver). By default, when used with a non-static member, invokers generate calls for the current (this) instance. To specify a different instance, use the member.WithObject method.
  • Nullability behavior. By default, invokers use the . operator to access the member. If the target object is nullable, you might want to use ?. instead. You can choose this behavior with the member.WithOptions method.

Example

IParameter p = meta.Target.Parameters[0];
var method = meta.Target.Type.Methods.OfName("Print").Single();

method.WithOptions( InvokerOptions.NullConditionalIfNullable ).WithObject( p ).Invoke( "Hello, world." );

Suppose that this template snippet is applied to a method with a nullable parameter:

[SayHelloWorld]
void MyMethod( Printer? printer )

The template would generate the following code:

printer?.Print( "Hello, world." );

Without WithObject, this would have been written instead of printer. Without WithOptions, the simple dot . would have been generated instead of ?..

Accessing a field or property

Fields and properties inherit the IExpression interface. As with any expression, you can use the IExpression.Value property to read or assign the field or property in a template. For fields, you can also use ref when accessing the Value property.

For instance:

// Compile-time code querying the code model.
var targetProperty = meta.Target.Type.Properties["Target"];
var sourceProperty = meta.Target.Type.Properties["Source"];
var field = meta.Target.Type.Fields["TheField"];

// Referencing the properties in run-time code.
targetProperty.Value = sourceProperty.Value?.Trim();
SomeMethod( ref field.Value );

This generates the following code:

Target = Source?.Trim();
SomeMethod( ref TheField );

Accessing an event

Use event.Add, event.Remove, or event.Raise to generate code that adds handlers to, removes handlers from, or raises an event.

Working with indexers

You can access indexer items using the this[ params object[] ] or this[ params IExpression[] ] indexer of the IIndexerInvoker interface, which returns an IExpression. This lets you access elements in a natural way.

For instance:

var indexer = meta.Type.Indexers.Single();
indexer[0,0].Value += indexer[0,1].Value;

The template above generates the following code:

this[0,0] = this[0,1]

Working with tuples

Creating a tuple instance

Use CreateCreateInstanceExpression to create a tuple instantiation expression.

For instance, in a template, you can use the following code:

var tupleType = TypeFactory.CreateTupleType( (typeof(decimal), "Quantity"), (typeof(string), "ProductCode" ) );
var tupleInstance = tupleType.CreateCreateInstanceExpression(42, "HAT").Value;

This generates the following code:

var tupleInstance = (Quantity: 42, ProductCode: "HAT");

You can also pass an array of IExpression to CreateCreateInstanceExpression if the tuple items are known as compile-time expressions instead of C# expressions.

Accessing tuple elements

Tuple elements are represented as fields in the tuple type. Use the following syntax to access their value:

// Get the first element of a tuple
var firstElement = tupleType.TupleElements[0].WithObject( tupleInstance ).Value;