I want to subscribe to any event existing on a given object, where I don't know the type in advance. I'm generatig event handler at run-time using System.Linq.Expressions.Expression
.
The code I have (see below) almost works... But there are unfortunately some events that don't follow the sender, EventArgs
pattern, and I have no control over it. Some of those have ref
parameters, and trying to write them back (which I unfortunately need to do) causes System.ArgumentException: 'Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.'
at lambda.Compile()
. An alternative would be to rewrite it to emit IL, but I'd really rather avoid it.
The event:
public delegate void SomeEventDelegate(ref int a, int b);
public event SomeEventDelegate SomeEvent;
Dynamically generated lambda:
(ref int a, int b) =>
{
var __values__ = new[] { a, b };
Action<string, string[], object[]>.Invoke("SomeEvent", new[] { "a", "b" }, __values__);
a = (int)__values__[0]; //this causes an exception in `lambda.Compile()`
}
Full example:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AgileObjects.ReadableExpressions; //optional, converts generated expression to more readable string
class PseudoControl
{
public delegate void SomeEventDelegate(ref int a, int b);
public event SomeEventDelegate SomeEvent;
public void RaiseEvents()
{
var a = 0;
var b = 0;
SomeEvent?.Invoke(ref a, b);
}
}
public class ControlExtender
{
public object ExtendedControl { get; set; }
public void SubscribeToAllEvents()
{
foreach (var ei in ExtendedControl.GetType().GetEvents(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public))
{
SubscribeToEvent(ei);
}
}
private void SubscribeToEvent(EventInfo ei)
{
var handlerType = ei.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (p1, p2, ...) => RaiseObjectEvent(ei.Name, new[] { pname1, pname2, ... }, new[] { pval1, pval2, ...});
var parameters = eventParams
.Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToArray();
var action = new Action<string, string[], object[]>(RaiseObjectEvent);
var constEventName = Expression.Constant(ei.Name);
var arrPNames = Expression.NewArrayInit(typeof(string), parameters.Select(p => Expression.Constant(p.Name)));
var arrPValues = Expression.NewArrayInit(typeof(object), parameters.Select(p => Expression.Convert(p, typeof(object))));
var statements = new List<Expression>();
var varPValues = Expression.Variable(typeof(object[]), "__values__");
statements.Add(Expression.Assign(varPValues, arrPValues));
statements.Add(Expression.Call(Expression.Constant(action), action.GetType().GetMethod("Invoke"), constEventName, arrPNames, varPValues));
//handle `ref` parameters being written into
for (int i = 0; i < parameters.Length; i++)
{
var p = parameters[i];
if (p.IsByRef)
{
var value = Expression.ArrayAccess(varPValues, Expression.Constant(i));
var unbox = Expression.Convert(value, p.Type);
var assign = Expression.Assign(p, unbox); //<------------- causes the error
statements.Add(assign);
}
}
var body = Expression.Block(new[] { varPValues }, statements);
var lambda = Expression.Lambda(body, parameters);
//string readable = lambda.ToReadableString(); //debug help, needs the nuget package
var del = Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
ei.AddEventHandler(ExtendedControl, del);
}
public void RaiseObjectEvent(string name, string[] pNames, object[] pValues)
{
Console.WriteLine("RaiseObjectEvent: " + name);
for (int i = 0; i < pNames.Length; i++)
{
var pname = pNames[i];
var pval = pValues[i];
Console.WriteLine($" {pname}: {pval}");
}
if (name == "SomeEvent")
{
pValues[0] = 1;
pValues[1] = 2;
}
}
}
static class Test
{
static void Main()
{
var ce = new ControlExtender();
var control = new PseudoControl();
ce.ExtendedControl = control;
ce.SubscribeToAllEvents();
//control.SomeEvent += (ref int a, int b) => { a = 1; b = 2; };
control.RaiseEvents();
}
}
Your problem is simply that:
lambda
is determined at runtime, based on the type of body
.body
is a BlockExpression
, and the return value of a block is given by the final expression in the block.BinaryExpression
(the Expression.Assign(p, unbox)
), and that has a type which is the type of p
.void
(i.e. the type of the final p
being assigned to).Delegate.CreateDelegate
throws, because you're trying to create a delegate where the type of handlerType
doesn't match the type of lambda.Compile()
, because the return type doesn't match.This doesn't happen if you only have non-ref
parameters, as in that case the final expression of your block is a MethodCallExpression
, and the method being called returns void
, which means that the block returns void
.
The solution is to either:
Expression.Lambda
which takes the type of lambda to create:var lambda = Expression.Lambda(handlerType, body, parameters);
Or:
Expression.Block
which lets you specify the block's return type:var body = Expression.Block(typeof(void), new[] { varPValues }, statements);