In Metalama, expressions are compile-time objects that implement the IExpression interface.
Expressions represent C# syntax—not their result. For instance, 1+1 and 2 are two different expressions, although they evaluate to the same value at run time.
This article covers different ways to create IExpression objects.
Two-way convertibility between IExpression and dynamic
As noted in Dynamic typing in templates, all dynamic objects in a template actually implement the IExpression interface, so it's safe to cast a dynamic into an IExpression in a template. An expression can be converted back to a dynamic either by using a cast or the Value property.
Therefore, IExpression objects are compile-time objects that represent run-time syntax:
- When typed as the compile-time
IExpression, an expression can be used in compile-time APIs. - When typed as
dynamic, expressions can be used in run-time APIs.
Capturing a C# expression into an IExpression
The simplest way to create an expression in a T# template is to write plain C# code and then capture its syntax. The ExpressionFactory.Capture method captures the C# syntax tree of an expression into an IExpression object without evaluating it.
// Defines a run-time local variable.
var now = DateTime.Now;
// Captures the reference to the local variable "now".
var expression1 = ExpressionFactory.Capture( now );
// Captures the expression "DateTime.Now".
var expression2 = ExpressionFactory.Capture( DateTime.Now );
Capturing a dynamic expression
Warning
When the compile-time type of the expression to capture is dynamic, it must be explicitly cast to IExpression to work around limitations of the C# language.
Example:
// A compile-time object representing a method.
IMethod method;
// Invokes the method and stores the result in a run-time local variable.
// At compile time, `result` is dynamic.
var result = method.Invoke();
// Captures the reference to the local variable `result`.
// The `(IExpression)` cast is necessary.
var expression = ExpressionFactory.Capture( (IExpression) result );
Generating expressions using a StringBuilder-like API
When you need to construct complex expressions programmatically or dynamically, the ExpressionBuilder class provides a text-based approach. It offers convenient methods like AppendLiteral, AppendTypeName, and AppendExpression. The AppendVerbatim method must be used for anything else, such as keywords or punctuation.
When you're done building the expression, call the ToExpression method. It returns 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 isn't 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.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3using System;
4
5namespace Doc.ExpressionBuilder_;
6
7public class NotInAttribute : ContractAspect
8{
9 private readonly int[] _forbiddenValues;
10
11 public NotInAttribute( params int[] forbiddenValues )
12 {
13 this._forbiddenValues = forbiddenValues;
14 }
15
16 public override void Validate( dynamic? value )
17 {
18 // Build the expression.
19 var expressionBuilder = new ExpressionBuilder();
20 expressionBuilder.AppendExpression( value );
21 expressionBuilder.AppendVerbatim( " is ( " );
22
23 var requiresOr = meta.CompileTime( false );
24
25 foreach ( var forbiddenValue in this._forbiddenValues )
26 {
27 if ( requiresOr )
28 {
29 expressionBuilder.AppendVerbatim( " or " );
30 }
31 else
32 {
33 requiresOr = true;
34 }
35
36 expressionBuilder.AppendLiteral( forbiddenValue );
37 }
38
39 expressionBuilder.AppendVerbatim( ")" );
40 var condition = expressionBuilder.ToExpression();
41
42 // Use the expression in run-time code.
43 if ( condition.Value )
44 {
45 throw new ArgumentOutOfRangeException();
46 }
47 }
48}
1namespace Doc.ExpressionBuilder_;
2
3public class Customer
4{
5 [NotIn( 0, 1, 100 )]
6 public int Id { get; set; }
7}
1using System;
2
3namespace Doc.ExpressionBuilder_;
4
5public class Customer
6{
7 private int _id;
8
9 [NotIn(0, 1, 100)]
10 public int Id
11 {
12 get
13 {
14 return _id;
15 }
16
17 set
18 {
19 if (value is (0 or 1 or 100))
20 {
21 throw new ArgumentOutOfRangeException();
22 }
23
24 _id = value;
25 }
26 }
27}
Example: using ExpressionBuilder in BuildAspect
Since ExpressionBuilder works outside of templates, it's essential for generating expressions in contexts like BuildAspect. The following example introduces two fields: a static counter and an instance ID initialized using Interlocked.Increment. The initializer expression must be built programmatically because BuildAspect isn't a template.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System.Threading;
5
6namespace Doc.ExpressionBuilderToExpression;
7
8public class AutoIncrementIdAspect : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 // Introduce the static counter field.
13 var counterField = builder.IntroduceField(
14 "_nextId",
15 typeof(int),
16 buildField: f => f.IsStatic = true );
17
18 // Build the initializer expression: Interlocked.Increment(ref _nextId)
19 var initializerBuilder = new ExpressionBuilder();
20 initializerBuilder.AppendTypeName( typeof(Interlocked) );
21 initializerBuilder.AppendVerbatim( ".Increment(ref " );
22 initializerBuilder.AppendVerbatim( counterField.Declaration.Name );
23 initializerBuilder.AppendVerbatim( ")" );
24
25 // Introduce the instance ID field with the computed initializer.
26 builder.IntroduceField(
27 "_id",
28 typeof(int),
29 buildField: f =>
30 {
31 f.Writeability = Writeability.ConstructorOnly;
32 f.InitializerExpression = initializerBuilder.ToExpression();
33 } );
34 }
35}
36
1namespace Doc.ExpressionBuilderToExpression;
2
3[AutoIncrementIdAspect]
4internal class Foo
5{
6 public void DoSomething() { }
7}
8
1using System.Threading;
2
3namespace Doc.ExpressionBuilderToExpression;
4
5[AutoIncrementIdAspect]
6internal class Foo
7{
8 public void DoSomething() { }
9
10 private readonly int _id = Interlocked.Increment(ref _nextId);
11 private static int _nextId;
12}
13
Parsing string-based C# expressions
If you already have a string representing an expression or statement, you can convert it into an IExpression using ExpressionFactory.Parse.
Example: parsing expressions
The _logger field is accessed through a parsed expression in the following example.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3
4namespace Doc.ParseExpression;
5
6internal class LogAttribute : OverrideMethodAspect
7{
8 public override dynamic? OverrideMethod()
9 {
10 var logger = ExpressionFactory.Parse( "this._logger" );
11
12 logger.Value?.WriteLine( $"Executing {meta.Target.Method}." );
13
14 return meta.Proceed();
15 }
16}
1using System;
2using System.IO;
3
4namespace Doc.ParseExpression;
5
6internal class Program
7{
8 private TextWriter _logger = Console.Out;
9
10 [Log]
11 private void Foo() { }
12
13 private static void Main()
14 {
15 new Program().Foo();
16 }
17}
1using System;
2using System.IO;
3
4namespace Doc.ParseExpression;
5
6internal class Program
7{
8 private TextWriter _logger = Console.Out;
9
10 [Log]
11 private void Foo()
12 {
13 _logger?.WriteLine("Executing Program.Foo().");
14 }
15
16 private static void Main()
17 {
18 new Program().Foo();
19 }
20}
Executing Program.Foo().
Generating run-time arrays
You can generate run-time arrays using two approaches:
Using statements (multi-line approach)
The straightforward way is to declare an array variable and set each element using statements:
var args = new object[2];
args[0] = "a";
args[1] = DateTime.Now;
MyRunTimeMethod(args);
The limitation of this approach is that it requires multiple statements and can't be used where a single expression is needed.
Using ArrayBuilder (expression approach)
To generate an array as a single expression, use the ArrayBuilder class. This is particularly useful when passing arrays as method arguments or in other contexts where expressions are required.
For instance:
var arrayBuilder = new ArrayBuilder();
arrayBuilder.Add("a");
arrayBuilder.Add(DateTime.Now);
MyRunTimeMethod(arrayBuilder.ToValue());
This generates 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.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code.SyntaxBuilders;
3using System.Linq;
4
5namespace Doc.ToString;
6
7internal class ToStringAttribute : TypeAspect
8{
9 [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
10 public string IntroducedToString()
11 {
12 var stringBuilder = new InterpolatedStringBuilder();
13 stringBuilder.AddText( "{ " );
14 stringBuilder.AddText( meta.Target.Type.Name );
15 stringBuilder.AddText( " " );
16
17 var fields = meta.Target.Type.FieldsAndProperties
18 .Where( f => f is { IsImplicitlyDeclared: false, IsStatic: false } )
19 .ToList();
20
21 var i = meta.CompileTime( 0 );
22
23 foreach ( var field in fields )
24 {
25 if ( i > 0 )
26 {
27 stringBuilder.AddText( ", " );
28 }
29
30 stringBuilder.AddText( field.Name );
31 stringBuilder.AddText( "=" );
32 stringBuilder.AddExpression( field.Value );
33
34 i++;
35 }
36
37 stringBuilder.AddText( " }" );
38
39 return stringBuilder.ToValue();
40 }
41}
1namespace Doc.ToString;
2
3[ToString]
4internal class Foo
5{
6 private int _x;
7
8 public string? Y { get; set; }
9}
1namespace Doc.ToString;
2
3[ToString]
4internal class Foo
5{
6 private int _x;
7
8 public string? Y { get; set; }
9
10 public override string ToString()
11 {
12 return $"{{ Foo _x={_x}, Y={Y} }}";
13 }
14}
Converting compile-time values to run-time values
Use meta.RunTime(expression) to convert the result of a compile-time expression into a run-time expression. The compile-time expression is evaluated at compile time, and its value is 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 the "Converting custom objects" section below 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.
1using Metalama.Framework.Aspects;
2using System;
3using System.Linq;
4
5namespace Doc.ConvertToRunTime;
6
7internal class ConvertToRunTimeAspect : OverrideMethodAspect
8{
9 public override dynamic? OverrideMethod()
10 {
11 var parameterNamesCompileTime = meta.Target.Parameters.Select( p => p.Name ).ToList();
12 var parameterNames = meta.RunTime( parameterNamesCompileTime );
13
14 var buildTime = meta.RunTime(
15 meta.CompileTime( new Guid( "13c139ea-42f5-4726-894d-550406357978" ) ) );
16
17 var parameterType = meta.RunTime( meta.Target.Parameters[0].Type.ToType() );
18
19 return null;
20 }
21}
1using System;
2
3namespace Doc.ConvertToRunTime;
4
5internal class Foo
6{
7 [ConvertToRunTimeAspect]
8 private void Bar( string a, int c, DateTime e )
9 {
10 Console.WriteLine( $"Method({a}, {c}, {e})" );
11 }
12}
1using System;
2using System.Collections.Generic;
3
4namespace Doc.ConvertToRunTime;
5
6internal class Foo
7{
8 [ConvertToRunTimeAspect]
9 private void Bar(string a, int c, DateTime e)
10 {
11 var parameterNames = new List<string>
12 {
13 "a",
14 "c",
15 "e"
16 };
17 var buildTime = new Guid(331430378, 17141, 18214, 137, 77, 85, 4, 6, 53, 121, 120);
18 var parameterType = typeof(string);
19 }
20}
Converting custom objects
You can have classes that exist both at compile-time 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's structurally equivalent to the current value. Note that your implementation of IExpressionBuilder isn't a template, so you'll need to use the ExpressionBuilder class to generate your code.
Example: custom converter
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System.Collections.Generic;
5using System.Linq;
6
7namespace Doc.CustomSyntaxSerializer;
8
9public class MemberCountAspect : TypeAspect
10{
11 // Introduces a method that returns a dictionary of method names with the number of overloads
12 // of this method.
13 [Introduce]
14 public Dictionary<string, MethodOverloadCount> GetMethodOverloadCount()
15 {
16 var dictionary = meta.Target.Type.Methods
17 .GroupBy( m => m.Name )
18 .Select( g => new MethodOverloadCount( g.Key, g.Count() ) )
19 .ToDictionary( m => m.Name, m => m );
20
21 return dictionary;
22 }
23}
24
25// This class is both compile-time and run-time.
26// It implements IExpressionBuilder to convert its compile-time value to an expression that results
27// in the equivalent run-time value.
28public class MethodOverloadCount : IExpressionBuilder
29{
30 public MethodOverloadCount( string name, int count )
31 {
32 this.Name = name;
33 this.Count = count;
34 }
35
36 public string Name { get; }
37
38 public int Count { get; }
39
40 public IExpression ToExpression()
41 {
42 var builder = new ExpressionBuilder();
43 builder.AppendVerbatim( "new " );
44 builder.AppendTypeName( typeof(MethodOverloadCount) );
45 builder.AppendVerbatim( "(" );
46 builder.AppendLiteral( this.Name );
47 builder.AppendVerbatim( ", " );
48 builder.AppendLiteral( this.Count );
49 builder.AppendVerbatim( ")" );
50
51 return builder.ToExpression();
52 }
53}
1namespace Doc.CustomSyntaxSerializer;
2
3[MemberCountAspect]
4public class TargetClass
5{
6 public void Method1() { }
7
8 public void Method1( int a ) { }
9
10 public void Method2() { }
11}
1using System.Collections.Generic;
2
3namespace Doc.CustomSyntaxSerializer;
4
5[MemberCountAspect]
6public class TargetClass
7{
8 public void Method1() { }
9
10 public void Method1(int a) { }
11
12 public void Method2() { }
13
14 public Dictionary<string, MethodOverloadCount> GetMethodOverloadCount()
15 {
16 return new Dictionary<string, MethodOverloadCount>
17 {
18 {
19 "Method1",
20 new MethodOverloadCount("Method1", 2)
21 },
22 {
23 "Method2",
24 new MethodOverloadCount("Method2", 1)
25 }
26 };
27 }
28}