Search code examples
c#expression-treeslinq-expressions

Dynamically created event handler that writes ito a `ref` parameter


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();
    }
}

Solution

  • Your problem is simply that:

    1. The type of lambda is determined at runtime, based on the type of body.
    2. body is a BlockExpression, and the return value of a block is given by the final expression in the block.
    3. The final expression in the block is a BinaryExpression (the Expression.Assign(p, unbox)), and that has a type which is the type of p.
    4. Therefore your lambda returns something other than void (i.e. the type of the final p being assigned to).
    5. Therefore 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:

    1. Use the overload of Expression.Lambda which takes the type of lambda to create:
    var lambda = Expression.Lambda(handlerType, body, parameters);
    

    Or:

    1. Use the overload of Expression.Block which lets you specify the block's return type:
    var body = Expression.Block(typeof(void), new[] { varPValues }, statements);