Search code examples
c#c#-4.0lambdaexpression-treesfunc

Making a lambda expression to call a method from a generic class


I have these simple interface:

public interface IQuery<TResult> { }

public interface IQueryHandler<in TQuery, out TResult> 
    where TQuery : IQuery<TResult> {
    TResult Handle(TQuery query);
}

And there are some implementation of them. I'm trying to create an expression-tree to call Handle method on a specified handler. I mean:

var query = new MyQuery(); // which MyQuery implements IQuery<int>
object handler = _someServiceProvider
    .Get<IQueryHandler<MyQuery, int>>();

Also, there is a MyQueryHandler:

public class MyQueryHandler : IQueryHandler<MyQuery, int> {
    public int Handle(MyQuery query) { return 20; }
}

Now, I'm trying to create a Func<object, MyQuery, int> to call like this:

var func = GetMethod<MyQuery, int>(handler);
var result = func(handler, query);

And here is my GetMethod implementation:

    private Func<object, TQuery, TResult> GetMethod<TQuery, TResult>(object obj)
        where TQuery : IQuery<TResult> {

        var methodInfo = obj.GetType().GetMethod(nameof(IQueryHandler<TQuery, TResult>.Handle));

        var insExp = Expression.Parameter(typeof(object), "ins");

        var inputExp = Expression.Parameter(typeof(TQuery), "query");

        var instanceExp = Expression.Variable(obj.GetType(), "instance");

        var assExp = Expression.Assign(instanceExp, Expression.Convert(insExp, obj.GetType()));

        var castExp = Expression.Convert(inputExp, methodInfo.GetParameters()[0].ParameterType);

        var callExp = Expression.Call(instanceExp, methodInfo, castExp);

        var blockExp = Expression.Block(new Expression[] {
            insExp,
            inputExp,
            instanceExp,
            assExp,
            castExp,
            callExp
        });

        var func =
            Expression.Lambda<Func<object, TQuery, TResult>>(
                blockExp,
                insExp,
                inputExp).Compile();
        return func;
    }

But, when I try to compile Lambda, I get this error:

An exception of type 'System.InvalidOperationException' occurred in System.Core.dll but was not handled in user code

Additional information: variable 'instance' of type 'Namespace.MyQueryHandler' referenced from scope '', but it is not defined

Where am I wrong? What I missed? Do you have any idea? Thanks in advance.


Solution

  • As far as I can tell, you're trying to write this function:

    TResult f(object ins, TQuery query)
    {
        var instance = (MyQueryHandler)ins;
        return instance.Handle(query);
    }
    

    To do this using expression trees, you have to declare the variable in your Expression.Block, but then only specify the two statements above, not all subexpressions:

    var blockExp = Expression.Block(new[] { instanceExp }, new Expression[] {
        assExp,
        callExp
    });
    

    But a simpler option would be to write the following function instead:

    TResult f(object ins, TQuery query)
    {
        return ((MyQueryHandler)ins).Handle(query);
    }
    

    That would look like this:

    var callExp = Expression.Call(
        Expression.Convert(insExp, obj.GetType()), methodInfo, castExp);
    
    var func =
        Expression.Lambda<Func<object, TQuery, TResult>>(
            callExp,
            insExp,
            inputExp).Compile();