samedi 7 avril 2018

Creating delegates from a MethodInfo that has a more derived parameter than the delegate signature

I can't figure out how to create delegates when the target method parameter is a type derived from the parameter type specified in the delegate's signature. Is this even possible? My project involves creating command handlers for many different commands. The commands are arranged in groups each with their own properties and dependencies (of which I omitted for brevity here.) I'm trying to extract those handler methods (marked with a 'Handler' attribute) from their group, creating delegates from the resulting MethodInfo, and storing them in a Dictionary, keyed to the command type. The rough idea is below:

public delegate void HandlerDelegate(ICommand command);

public class Handler: Attribute
{
}

public interface ICommand
{
}

public class CommandA: ICommand
{
    public string CmdASpecific = "Command A";
}

public class CommandB: ICommand
{
    public string CmdBSpecific = "Command B";
}

public class HandlerGroup
{
    [Handler]
    public void HandleA(CommandA command)
    {
        Console.WriteLine($"{command.CmdASpecific} Handled");
    }

    [Handler]
    public void HandleB(CommandB command)
    {
        Console.WriteLine($"{command.CmdBSpecific} Handled");
    }
}

I scan though a HandlerGroup instance with reflection, extracting the methods with the 'Handler' attribute, create a delegate from them and add them to a Dictionary (keyed by the type of parameter the handler expects) to call later:

public void Main(){
        var handlerGroup = new HandlerGroup();

        Dictionary<Type, HandlerDelegate> cache = new Dictionary<Type, HandlerDelegate>();

        foreach (var handlerInfo in handlerGroup.GetType().GetMethods())
        {
            if ((Handler)handlerInfo.GetCustomAttribute(typeof(Handler), false) is Handler handlerAttribute)
            {
                var parameters = handlerInfo.GetParameters();
                HandlerDelegate handler = (HandlerDelegate)Delegate.CreateDelegate(typeof(HandlerDelegate), handlerGroup, handlerInfo.Name);
                cache.Add(parameters.Single().GetType(), handler);
            }
        }

        var cmdA = new CommandA();
        var cmdB = new CommandB();

        cache[cmdA.GetType()](cmdA); //Should write 'Command A Handled'
        cache[cmdB.GetType()](cmdB); //Should write 'Command B Handled'
}

The CreateDelegate method fails with a System.ArgumentException: 'Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.' exception.

I can get around this by adding a property to the Handler attribute:

public class Handler: Attribute
{
    public Type CommandType;
    public Handler(Type commandType)
    {
        CommandType = commandType;
    }
}

And using this HandlerGroup instead:

public class HandlerGroup
{
    [Handler(typeof(CommandA))]
    public void HandleA(ICommand command)
    {
        var tmp = (CommandA)command;
        Console.WriteLine($"{tmp.CmdASpecific} Handled");
    }

    [Handler(typeof(CommandB))]
    public void HandleB(ICommand command)
    {
        var tmp = (CommandB)command;
        Console.WriteLine($"{tmp.CmdBSpecific} Handled");
    }
}

And then adding it to the cache with the Handler attribute's CommandType property instead of the handler methods' parameter type:

            if (handlerInfo.GetCustomAttribute(typeof(Handler), false) is Handler handlerAttribute)
            {
                HandlerDelegate handler = (HandlerDelegate)Delegate.CreateDelegate(typeof(HandlerDelegate), handlerGroup, handlerInfo.Name);
                cache.Add(handlerAttribute.CommandType, handler);
            }

Are there really any other options? While this works, I really don't want to have to rely on assuming the handlers are implemented in a certain way.





Aucun commentaire:

Enregistrer un commentaire