Skip to main content

Building LINQ Expressions: How to run dynamic code at runtime (part 1)

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 expressions list is interpreted as a return statement 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

Popular posts from this blog

Hangfire: setup and usage

How to setup Hangfire, a job scheduler for .NET Installing Hangfire Setting up Hangfire Setting up storage MySQL Storage In-memory storage Running jobs Fire-and-forget jobs Delayed jobs Continuation jobs Recurring jobs ASP.NET Core job integration Conclusion Hangfire is a job scheduler for .NET and .NET Core that lets you run jobs in the background. It features various different job types: Fire-and-forget jobs are executed only once , shortly after creation. Delayed jobs are very similar to fire-and-forget jobs but they wait for a specified amount of time before running. Continuation jobs, which run after their parent job has finished . Recurring jobs, perhaps the most interesting of all, run repeatedly on an interval . If you like Unix’s cron you’ll feel right at home, as this kind of job lets you specify the job interval as a cron expression . If you have never used this I recommend crontab.guru , it has a live expression editor, as well as some examp...

ConditionalWeakTable, what does it do?

C#'s ConditionalWeakTable, what does it do? C# has many lesser known features, some more useful than others, and one of them is the ConditionalWeakTable<TKey, TValue> (keep in mind that TKey and TValue must be reference types). You can think of this type as a dictionary where the keys are weakly referenced, meaning that they won’t count when the GC checks if the object has to be collected. Additionally, when the keys do eventually get collected by the GC, that entry will get removed from the dictionary. This means that you can attach arbitrary objects to any object, allowing you to do something like this: public static class Extensions { private static ConditionalWeakTable < object , dynamic > Table = new ConditionalWeakTable < object , dynamic > ( ) ; public static dynamic Data ( this object obj ) { if ( ! Table . TryGetValue ( obj , out var dyn ) ) Table . Add ( obj , dyn = new ExpandoObject ( )...

How to observe and control Windows media sessions in C#

How to control Windows media sessions in C# If you use Windows 10 and play music, you’ll most likely come across this dialog at some point: This is the media overlay, and it appears whenever you’re playing music and press a media key or you change the volume. This dialog uses the GlobalSystemMediaTransportControls family of APIs available on the Windows 10 Runtime API since version 1809. This API is usually restricted to UWP applications, however, the introduction of .NET 5 and the ability to target specific platforms in the project’s TargetFramework has made it available for use in any kind of application, like WPF or regular console programs. Fetching information from the GSMTC API The API’s entrypoint is the GlobalSystemMediaTransportControlsSessionManager . This class allows you to fetch all the current active sessions, as well as whichever one Windows thinks is the current one. In order to be able to access this class at all you must first set your project’s TargetFramewor...