Ian Mercer
Ian Mercer

Reputation: 39277

Explicit conversion of parameters in Expression Trees created from MethodInfo

I have the method below which converts a (non-static) MethodInfo into a compiled Expression (Func) which I can then call.

This works great: I can call it with a method expecting both reference objects and value types.

BUT unlike the original method where I could call a method that had a parameter expecting a double and pass it an int this compiled expression doesn't support that and throws an InvalidCastException.

How do I modify this to support the same type of implicit casting that happens during a normal method call?

Bonus question: should the instanceExp use the DeclaringType or the ReflectedType from the MethodInfo?

public Func<object, object[], object> Create(MethodInfo methodInfo)
{
    var methodParams = methodInfo.GetParameters();
    var arrayParameter = Expression.Parameter(typeof(object[]), "array");

    var arguments =
        methodParams.Select((p, i) => Expression.Convert(
            Expression.ArrayAccess(arrayParameter, Expression.Constant(i)), p.ParameterType))
            .Cast<Expression>()
            .ToList();

    var instanceParameter = Expression.Parameter(typeof(object), "controller");

    var instanceExp = Expression.Convert(instanceParameter, methodInfo.DeclaringType);
    var callExpression = Expression.Call(instanceExp, methodInfo, arguments);

    var bodyExpression = Expression.Convert(callExpression, typeof(object));

    return Expression.Lambda<Func<object, object[], object>>(
        bodyExpression, instanceParameter, arrayParameter)
        .Compile();
}

--- EDIT

The working solution is:

var changeTypeMethod = typeof(Convert).GetMethod("ChangeType", new Type[] { typeof(object), typeof(TypeCode) });
var arguments =
     methodParams.Select((p, i) =>
         !typeof(IConvertible).IsAssignableFrom(p.ParameterType)
             // If NOT IConvertible, don't try to convert it
             ? (Expression)Expression.Convert(
                 Expression.ArrayAccess(arrayParameter, Expression.Constant(i)), p.ParameterType)
            :
             // Otherwise add an explicit conversion to the correct type to handle int <--> double etc.
            (Expression)Expression.Convert(
                Expression.Call(changeTypeMethod,
                    Expression.ArrayAccess(arrayParameter, Expression.Constant(i)),
                    Expression.Constant(Type.GetTypeCode(p.ParameterType))),
                p.ParameterType)
        )
        .ToList();

Upvotes: 4

Views: 970

Answers (1)

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

The problem is the same as in this piece of C# code:

object a = 123;
double b = (double)a; // InvalidCastException

The reason is that a is an object, so in order to make it a double the cast must unwrap it, and then transform an int to double. The language allows the cast to do only one thing - it will either unwrap or transform, but not both. You need to tell the compiler how to do this cast explicitly by telling it that there is an int wrapped inside the object:

double b = (double)((int)a); // Works

If you could do the same thing in your LINQ expression, your compiled expression will work as well. However, you may not know the actual type of the parameter at the time you generate your expression, so you may want to go for a different strategy - wire in a call to Convert.ChangeType method, which can unwrap and cast at the same time.

Upvotes: 5

Related Questions