Warm tip: This article is reproduced from stackoverflow.com, please click
c# linq-expressions

Partial evaluation of expressions

发布于 2020-03-31 22:55:09

I want to capture a method call as an expression, but evaluate its arguments in a type-safe and IDE-friendly way beforehand.

Take the following example class:

class Test
{
    public string SomeMethod(int a, string b) { ... }
}

I now want to pass a call to SomeMethod to a function receiving an Expression:

HandleMethodCall<Test>(test => test.SomeMethod("string".Length, 5.ToString()));

Right now HandleMethodCall has the signature

static void HandleMethodCall<T>(Expression<Func<T, object>> expr)

Unfortunately, in this case the expression passed to HandleMethodCall does not only contain test.SomeMethod(), but expression trees for the arguments as well.

A workaround would be to split the method call and its arguments into different arguments for HandleMethodCall, but this has the cost of losing compile-time type-safety and IDE support for showing argument names.

Is there any way to make the compiler evaluate parts of an expression before putting them into an Expression object?

Or, alternatively, is it possible to manually invoke evaluation of the expression trees for the method parameters?

Questioner
Jan Wichelmann
Viewed
18
Jan Wichelmann 2020-01-31 19:19

It looks like it is not possible to specify this at compile time, but one can compile and evaluate the expression parts at runtime using Expression.Lambda<>(...).Compile()() (see documentation).

This results in the following implementation (no error checking for sake of simplicity):

public static void HandleMethodCall<T>(Expression<Func<T, object>> expr)
{
    // Extract method call
    var methodCall = (MethodCallExpression)expr.Body;

    // Run through expected arguments and evaluate the supplied ones
    var expectedArguments = methodCall.Method.GetParameters();
    for(int i = 0; i < expectedArguments.Length; ++i)
    {
        Console.Write(expectedArguments[i].Name + ": ");

        // Since we do not know the argument type in advance, we have to use Func<object>
        var argumentEvaluationExpression = Expression.Lambda<Func<object>>(Expression.Convert(methodCall.Arguments[i], typeof(object)));
        object argumentValue = argumentEvaluationExpression.Compile()();

        Console.WriteLine(argumentValue);
    }
}

Calling this function with

HandleMethodCall<Test>(test => test.SomeMethod("string".Length, "1" + " 2 " + (1 + 2).ToString()));

outputs the expected result

a: 6
b: 1 2 3

However, I am not sure whether this will really work under all conditions. In addition, I expect this to be quite slow due to the Compile() step.

Thanks to @RyanWilson for the helpful comments, which allowed me to figure out this solution!