Dynamic typing
When writing a template, you do not generally know in advance the exact type of the declarations to which it is applied.
For example, an aspect may not know the return type of the methods that it overrides.
There are two mechanisms to represent unknown types: dynamic
and generic types. Let's now focus on the first one. The generic approach is covered in Template parameters and type parameters.
If the return type of a method is unknown, the template can use the dynamic
return type.
dynamic? OverrideMethod()
{
return default;
}
All dynamic
compile-time code is transformed into strongly-typed run-time code. That is, we use dynamic
when the expression type is unknown to the template developer, but the type is always known when the template is applied.
Warning
In a template, it is not possible to generate code that employs dynamic
typing at run time.
APIs returning dynamic objects
The meta
API exposes some properties of the dynamic
type and some methods returning dynamic
values. These members are compile-time, but they produce a C# expression that can be used in the run-time code of the template. Because these members return a dynamic
value, they can be utilized anywhere in your template. The code will not be validated when the template is compiled but when the template is applied.
For instance, meta.This
returns a dynamic
object that represents the expression this
. Because meta.This
is dynamic
, you can write meta.This._logger
in your template, which will translate to this._logger
. This will work even if your template does not contain a member named _logger
because meta.This
returns a dynamic
; therefore, any field or method referenced on the right hand of the meta.This
expression will not be validated when the template is compiled (or in the IDE) but when the template is expanded, in the context of a specific target declaration.
Here are a few examples of APIs that return a dynamic
:
Equivalents to the
this
orbase
keywords:- meta.This, equivalent to the
this
keyword, allows calling arbitrary instance members of the target type. - meta.Base, equivalent to the
base
keyword, allows calling arbitrary instance members of the base of the target type. - meta.ThisType allows calling arbitrary static members of the target type.
- meta.BaseType allows calling arbitrary static members of the base of the target type.
- meta.This, equivalent to the
IExpression.Value allows getting or setting the value of a compile-time expression in run-time code. It is implemented, for instance, by:
meta.Target.Field.Value
,meta.Target.Property.Value
, ormeta.Target.FieldOrProperty.Value
allow getting or setting the value of the target field or property.meta.Target.Parameter.Value
allows getting or setting the value of the target parameter.meta.Target.Method.Parameters[*].Value
allows getting or setting the value of a target method's parameter.
Invokers, i.e., APIs that, given a compile-time IMethod, IField, IProperty, ... return a
dynamic
object that generates a call to this object. For instance:method.Invoke( a, b, c )
, orfield.Value
For details regarding invokers, see below, Generating calls to the call model.
Using dynamic expressions
You can write any dynamic code on the left of a dynamic expression. As with any dynamically typed code, the syntax of the code is validated, but not the existence of the invoked members.
// Translates into: this.OnPropertyChanged( "X" );
meta.This.OnPropertyChanged( "X" );
You can combine dynamic code and compile-time expressions. In the following snippet, OnPropertyChanged
is dynamically resolved but meta.Property.Name
evaluates into a string
:
// Translated into: this.OnPropertyChanged( "MyProperty" );
meta.This.OnPropertyChanged( meta.Property.Name );
Dynamic expressions can appear anywhere in an expression. In the following example, it is part of a string concatenation expression:
// Translates into: Console.WriteLine( "p = " + p );
Console.WriteLine( "p = " + meta.Target.Parameters["p"].Value );
Warning
Due to the limitations of the C# language, you cannot use extension methods on the right part of a dynamic expression. In this case, you must call the extension method in the traditional way, by specifying its type name on the left and passing the dynamic expression as an argument. An alternative approach is to cast the dynamic expression to a specified type if it is well-known.
Example: dynamic member
In the following aspect, the logging aspect uses meta.This
, which returns a dynamic
object, to access the type being enhanced. The aspect assumes that the target type defines a field named _logger
and that the type of this field has a method named WriteLine
.
Assignment of dynamic members
When the expression is writable, the dynamic
member can be used on the right hand of an assignment:
// Translates into: this.MyProperty = 5;
meta.Property.Value = 5;
Dynamic local variables
When the template is expanded, dynamic
variables are transformed into var
variables. Therefore, all dynamic
variables must be initialized.
Generating calls to the code model
When you have a Metalama.Framework.Code representation of a declaration, you may want to access it from your generated run-time code. You can do this by using one of the following methods or properties:
- IExpression.Value to generate code that represents a field, property, or parameter, because these declarations are IExpression.
- method.Invoke to generate code that invokes a method.
- indexer.GetValue or indexer.SetValue to generate code that gets or sets the value of an accessor.
- event.Add, event.Remove, or event.Raise to generate code that interacts with an event.
By default, when used with an instance member, all the methods and properties above generate calls for the current (this
) instance. To specify a different instance, use the With
method.
Example: invoking members
The following example is a variation of the previous one. The aspect no longer assumes the logger field is named _logger
. Instead, it looks for any field of type TextWriter
. Because it does not know the field's name upfront, the aspect must use the IExpression.Value property to get an expression allowing it to access the field. This property returns a dynamic
object, but we cast it to TextWriter
because we know its actual type. When the template is expanded, Metalama recognizes that the cast is redundant and simplifies it. However, the cast is useful in the T# template to get as much strongly-typed code as we can.
Converting run-time expressions into compile-time IExpression
Instead of using techniques like parsing to generate IExpression objects, it can be convenient to write the expression in T#/C# and to convert it. This allows you to have expressions that depend on compile-time conditions and control flows.
Two approaches are available depending on the situation:
When the expression is
dynamic
, you can simply use an explicit cast to IExpression. For instance:var thisParameter = meta.Target.Method.IsStatic ? meta.Target.Method.Parameters.First() : (IExpression) meta.This;
This also works when the cast is implicit, for instance:
IExpression baseCall; if (meta.Target.Method.IsOverride) { baseCall = (IExpression) meta.Base.Clone(); } else { baseCall = (IExpression) meta.Base.MemberwiseClone(); }
This template generates either
var clone = (TargetType) base.Clone();
orvar clone = (TargetType) this.MemberwiseClone();
depending on the condition.Otherwise, use the ExpressionFactory.Capture method.
You can use the WithType and WithNullability extension methods to modify the return type of the returned IExpression.
Generating run-time arrays
The first way to generate a run-time array is to declare a variable of array type and use a statement to set each element, for instance:
var args = new object[2];
args[0] = "a";
args[1] = DateTime.Now;
MyRunTimeMethod(args);
To generate an array of variable length, you can use the ArrayBuilder class.
For instance:
var arrayBuilder = new ArrayBuilder();
arrayBuilder.Add("a");
arrayBuilder.Add(DateTime.Now);
MyRunTimeMethod(arrayBuilder.ToValue());
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 implement the ToString
method automatically.
Generating expressions using a StringBuilder-like API
It is sometimes easier to generate the run-time code as simple text instead of using a complex meta API. In this situation, you can use the ExpressionBuilder class. It offers convenient methods like AppendLiteral, AppendTypeName, or AppendExpression. The AppendVerbatim method must be used for anything else, such as keywords or punctuation.
When you are done building the expression, call the ToExpression method. It will return an IExpression object. The IExpression.Value property is dynamic
and can be used in run-time code.
Note
A major benefit of ExpressionBuilder is that it can be used in a compile-time method that is not a template.
Warning
Your aspect must not assume that the target code has any required using
directives. Make sure to write fully namespace-qualified type names. Metalama will simplify the code and add the relevant using
directives when asked to produce pretty-formatted code. The best way to ensure type names are fully qualified is to use the AppendTypeName method.
Example: ExpressionBuilder
The following example uses an ExpressionBuilder to build a pattern comparing an input value to several forbidden values. Notice the use of AppendLiteral, AppendExpression, and AppendVerbatim.
Defining local variables
By default, local variables of your T# template represent a run-time local variable unless they are assigned to a build-time value. For instance, var x = 0;
defines a run-time local variable and var field = meta.Target.Field;
defines a compile-time one.
If you need to dynamically define a local variable, you can use the DefineLocalVariable method. This allows you, for instance, to define local variables in a compile-time foreach
loop.
When using the DefineLocalVariable method, you should not worry about generating unique names. Metalama will append a numerical suffix to the variable name to ensure it is unique in the target lexical scope.
Example: rollbacking field changes upon exception
The following aspect saves the value of all fields and automatic properties into a local variable before an operation is executed and rolls back these changes upon exception.
Generating statements using a StringBuilder-like API
StatementBuilder is to statements what ExpressionBuilder is to expressions. Note that it also allows you to generate blocks thanks to its BeginBlock and EndBlock methods.
Warning
Do not forget the trailing semicolon at the end of the statement.
When you are done, call the ToStatement method. You can inject the returned IStatement in run-time code by calling the InsertStatement method in the template.
Parsing C# expressions and statements
If you already have a string representing an expression or a statement, you can turn it into an IExpression or IStatement using the ExpressionFactory.Parse or StatementFactory.Parse method, respectively.
Example: parsing expressions
The _logger
field is accessed through a parsed expression in the following example.
Generating switch statements
You can use the SwitchStatementBuilder class to generate switch
statements. Note that it is limited to constant and default labels, i.e., patterns are not supported. Tuple matching is supported.
Example: SwitchStatementBuilder
The following example generates an Execute
method, which has two arguments: a message name and an opaque argument. The aspect must be used on a class with one or many ProcessFoo
methods, where Foo
is the message name. The aspect generates a switch
statement that dispatches the message to the proper method.
Converting compile-time values to run-time values
You can utilize meta.RunTime(expression)
to convert the result of a compile-time expression into a run-time expression. The compile-time expression will be evaluated at compile time, and its value will be converted into syntax representing that value. Conversions are possible for the following compile-time types:
- Literals;
- Enum values;
- One-dimensional arrays;
- Tuples;
- Reflection objects: Type, MethodInfo, ConstructorInfo, EventInfo, PropertyInfo, FieldInfo;
- Guid;
- Generic collections: List<T> and Dictionary<TKey, TValue>;
- DateTime and TimeSpan;
- Immutable collections: ImmutableArray<T> and ImmutableDictionary<TKey, TValue>;
- Custom objects implementing the IExpressionBuilder interface (see Converting custom objects from compile-time to run-time values for details).
Example: conversions
The following aspect converts the subsequent build-time values into a run-time expression: a List<string>
, a Guid
, and a System.Type
.
Converting custom objects
You can have classes that exist both at compile and run time. To allow Metalama to convert a compile-time value to a run-time value, your class must implement the IExpressionBuilder interface. The ToExpression() method must generate a C# expression that, when evaluated, returns a value that is structurally equivalent to the current value. Note that your implementation of IExpressionBuilder is not a template, so you will have to use the ExpressionBuilder class to generate your code.