How to run dynamic code at runtime (part 1)
In my previous post I talked about how to read LINQ expressions using an ExpressionVisitor, however reading isn’t the only thing you can do with expressions: you can build them at runtime!
If you’ve ever used reflection you’re probably aware that you shouldn’t use them in performance-critical situations as they’re one of the slowest parts of .NET, but what if you desperately need to use it?
Enter the world of expression-building. Before I show you how anything works, I want you to compare speeds:
RawAccess: 00:00:00.0000344 b*1,0 (3,44E-06ms per iteration)
ReflectionNoCache: 00:00:00.0018118 b*52,7 (0,00018118ms per iteration)
ReflectionWithCache: 00:00:00.0009518 b*27,7 (9,518E-05ms per iteration)
ExpressionNoCache: 00:00:00.9333534 b*27132,4 (0,09333534ms per iteration)
ExpressionWithCache: 00:00:00.0000424 b*1,2 (4,24E-06ms per iteration)
Benchmark ran with .NET Core 2.2 on an AMD Ryzen 2400G @ 3.85Ghz and Windows 10.
As you can see, no methods get to the speeds of raw access, but by far the closest competitor are cached expressions.
Disclaimer: all methods benchmarked methods were ran once before starting the stopwatch, otherwise the time that the cached methods take to generate the cache on the first run make a huge impact.
Building expressions
All of the required methods to build an expression tree are contained in the Expression class.
The base expression for everything is Expression.Lambda<TDelegate>. The overload of this method that we are going to use takes in the body expression and the parameters that the lambda takes in.
The expression body can be any other expression, including a block expression.
Constant values
These are the simplest expressions, equivalent to writing e.g. 2 in C#:
Expression.Constant(2);
// 2
Constants can be of any type.
Parameters
In order to define a lambda parameter you must first create it:
ParameterExpression param = Expression.Parameter(typeof(string), "name");
You don’t need to pass in a parameter name, however it can be useful if you want to debug the generated expression later on.
Parameters also need to be passed to Expression.Lambda, and keep in mind that they must match in type and number to the lambda’s delegate type.
Operators
Binary operators allow you to do things ranging from adding two numbers to converting an object to another type.
Binary operators
All binary operators are used the same way, for example:
Expression.MakeBinary(ExpressionType.Add, Expression.Constant(1), Expression.Constant(2));
// 1 + 2
Unary operators
These can be used, for example, to convert an object to a different type:
Expression.MakeUnary(ExpressionType.Convert, Expression.Constant(2), typeof(float));
// (float)2
TIP: most binary and unary operators have shortcut functions, for example:
Expression.Add(Expression.Constant(1), Expression.Constant(2));
// 1 + 2
Expression.Convert(Expression.Constant(2), typeof(float));
// (float)2
Method calling
For this you’ll need either a MethodInfo representing the method you want to call or its name, which you can feed into Expression.Call along with the expressions for the object instance and each argument:
//Imagine a method like: void MyMethod(string word, int count);
MethodInfo m = methodof(MyClass.MyMethod);
//new MyClass().MyMethod("hello", 2 + 3)
Expression.Call(
instance: Expression.New(typeof(MyClass)),
method: m,
Expression.Constant("hello"), //string word
Expression.Add(Expression.Constant(2), Expression.Constant(3))); //int count
Here you can also see a new expression: NewExpression. It pretty much does what you think it does, it instantiates an object of the type you pass in. This overload of Expression.New selects a parameter-less constructor and throws an ArgumentException if it doesn’t find any, but you can use its other overloads to pass arguments to the constructor.
You can also use this expression builder to call static methods:
//Console.WriteLine("Hello {0}", 123)
Expression.Call(
type: typeof(Console),
methodName: "WriteLine",
typeArguments: null,
Expression.Constant("Hello {0}"),
Expression.Convert(Expression.Constant(123), typeof(object)));
In this case I’m using the overload of Expression.Call that lets you give it a type, the method’s name and its parameter types and automatically finds and calls the appropriate method.
Member assignment and reading
First of all you need the field or property that you want to read, which can be obtained through reflection, or its name.
Accessing its value is simple, just call Expression.Property() or Expression.PropertyOrField() which will return a MemberExpression that you can use anywhere you require a value:
//Imagine a property like: int MyProperty { get; set; }
PropertyInfo prop = propertyof(MyClass.MyProperty)
var instanceParam = Expression.Parameter(typeof(MyClass));
...
Expression.Add(Expression.Property(instanceParam, prop), Expression.Constant(1));
//instanceParam.MyProperty + 1
To the property’s value you must use Expression.Assign, with the left hand expression being the same MemberExpression you used for reading its value.
Expression.Assign(Expression.Property(instanceParam, prop), Expression.Constant(42));
//instanceParam.MyProperty = 42
Block expressions
Lambdas can have block bodies with multiple lines of “C# code”, represented via a list of expressions:
var numberVar = Expression.Variable(typeof(int)); //int number;
var factorVar = Expression.Variable(typeof(int)); //int factor;
var resultVar = Expression.Variable(typeof(int)); //int result;
var expressions = new Expression[]
{
Expression.Assign(numberVar, Expression.Constant(5)), //number = 5;
Expression.Assign(factorVar, Expression.Constant(2)), //factor = 2;
Expression.Assign(resultVar, Expression.Multiply(numberVar, factorVar)), //result = number * factor;
resultVar //return result;
};
var body = Expression.Block(
variables: new[] { numberVar, factorVar, resultVar },
expressions: expressions);
var func = Expression.Lambda<Func<int>>(body).Compile();
Console.WriteLine(func());
//Output: 10
Each line in the code has its C# representation as a comment, but here’s what the full block would like anyways:
Func<int> func = () => {
int number, factor, result;
number = 5;
factor = 2;
result = number * factor;
return result;
};
A couple annotations:
- The last expression in the
expressionslist is interpreted as areturnstatement that returns the value of that expression. - Just like parameters, variables don’t require a name but it can be useful to assign one to them for debugging purposes.
That is all for part 1! In part 2 I will teach you about more complex expressions like if and for statements.
Comments
Post a Comment