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?
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;
}
Delegate
s 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.