Search code examples
c#expression-treeslinq-expressions

Access method group within expression tree


I am trying to write an expression tree that can subscribe to an event given by EventInfo with a method given by MethodInfo. The expression tree should compile into an Action<object, object> where the parameters are the event source object and the subscribing object. The EventInfo and MethodInfos are guaranteed to be compatible.

Here is what I have so far:

// Given the following
object Source = /**/;           // the object that will fire an event
EventInfo SourceEvent = /**/;   // the event that will be fired
object Target = /**/;           // the object that will subscribe to the event
MethodInfo TargetMethod = /**/; // the method that will react to the event

// setting up objects involved
var sourceParam = Expression.Parameter(typeof(object), "source");
var targetParam = Expression.Parameter(typeof(object), "target");
var sourceParamCast = Expression.Convert(sourceParam, SourceEvent.DeclaringType);
var targetParamCast = Expression.Convert(targetParam, TargetMethod.DeclaringType);

// Get subscribing method group. This is where things fail
var targetMethodRef = Expression.MakeMemberAccess(targetParamCast, TargetMethod);
// Subscribe to the event
var addMethodCall = Expression.Call(sourceParamCast, SourceEvent.AddMethod, targetMethodRef);

var lambda = Expression.Lambda<Action<object, object>>(addMethodCall, sourceParam, targetParam);
var subscriptionAction = lambda.Compile();

// And then later, subscribe to the event
subscriptionAction(Source, Target);

At the call to MakeMemberAccess I get the following exception:

ArgumentException: Member 'void theMethodName()' not field or property

The goal here is for targetMethodRef to essentially represent what would appear on the right side of += when subscribing to an event with a method.

TLDR: How do I create an expression for passing a method group on an object as a parameter to a function call within an expression tree?


Solution

  • It should be this. The complexity here is that you have to create a delegate inside the lambda method with the CreateDelegate. Sadly it doesn't seem possible to create an open delegate (a delegate without the target) to be compiled inside the lambda method and then "close" it inside the lambda method when the lambda method is executed. Or at least I don't know how to do it. CreateDelegate sadly is a little slow.

    static Action<object, object> MakeFunc(EventInfo sourceEvent, MethodInfo targetMethod)
    {
        // setting up objects involved
        var sourceParam = Expression.Parameter(typeof(object), "source");
        var targetParam = Expression.Parameter(typeof(object), "target");
        var sourceParamCast = Expression.Convert(sourceParam, sourceEvent.DeclaringType);
        var targetParamCast = Expression.Convert(targetParam, targetMethod.DeclaringType);
        var createDelegate = typeof(Delegate).GetMethod(nameof(Delegate.CreateDelegate), BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(Type), typeof(object), typeof(MethodInfo) }, null);
    
        // Create a delegate of type sourceEvent.EventHandlerType
        var createDelegateCall = Expression.Call(createDelegate, Expression.Constant(sourceEvent.EventHandlerType), targetParam, Expression.Constant(targetMethod));
    
        // Cast the Delegate to its real type
        var delegateCast = Expression.Convert(createDelegateCall, sourceEvent.EventHandlerType);
    
        // Subscribe to the event
        var addMethodCall = Expression.Call(sourceParamCast, sourceEvent.AddMethod, delegateCast);
    
        var lambda = Expression.Lambda<Action<object, object>>(addMethodCall, sourceParam, targetParam);
        var subscriptionAction = lambda.Compile();
    
        return subscriptionAction;
    }
    

    Mmmh... can be done by calling the delegate constructor. Built by trial (haven't found much documentation about this):

    static Action<object, object> MakeFunc(EventInfo sourceEvent, MethodInfo targetMethod)
    {
        // setting up objects involved
        var sourceParam = Expression.Parameter(typeof(object), "source");
        var targetParam = Expression.Parameter(typeof(object), "target");
        var sourceParamCast = Expression.Convert(sourceParam, sourceEvent.DeclaringType);
        var targetParamCast = Expression.Convert(targetParam, targetMethod.DeclaringType);
    
        ConstructorInfo delegateContructror = sourceEvent.EventHandlerType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(object), typeof(IntPtr) }, null);
        IntPtr fp = targetMethod.MethodHandle.GetFunctionPointer();
    
        // create the delegate
        var newDelegate = Expression.New(delegateContructror, targetParam, Expression.Constant(fp));
    
        // Subscribe to the event
        var addMethodCall = Expression.Call(sourceParamCast, sourceEvent.AddMethod, newDelegate);
    
        var lambda = Expression.Lambda<Action<object, object>>(addMethodCall, sourceParam, targetParam);
        var subscriptionAction = lambda.Compile();
    
        return subscriptionAction;
    }
    

    Delegates have a constructor with two parameters, the target object and a IntPtr that is the native function pointer to the method. It is normally used by the CIL with the ldftn/ldvirtftn, but the .MethodHandle.GetFunctionPointer() is the same "thing". So we call this constructor inside the lambda expression we build.