AMG
AMG

Reputation: 146

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().ParameterType, 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.

Upvotes: 3

Views: 286

Answers (2)

Evk
Evk

Reputation: 101603

Casting from Action<CommandA> to Action<ICommand> is not type-safe, because former accepts only CommandA, while latter accepts any ICommand, such as CommandB. For that reason

Delegate.CreateDelegate(typeof(HandlerDelegate), ...)

Also fails, since signature of HandleA (and HandleB) is incompatible with HandlerDelegate.

One way to solve it is construct delegate with correct type and invoke it dynamically with DynamicInvoke, for example:

var parameters = handlerInfo.GetParameters();
//                                           constructing correct delegate here
var dynamicHandler = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(parameters.Single().ParameterType), handlerGroup, handlerInfo);
HandlerDelegate handler = (p) =>
{
    // invoking
    dynamicHandler.DynamicInvoke(p);
};
cache.Add(parameters.Single().ParameterType, handler);

But much better way is to create expression tree and compile it to delegate:

var parameters = handlerInfo.GetParameters();
// expression of type ICommand
var expressionArg = Expression.Parameter(typeof(ICommand), "x");
// this is handlerInfo.HandleA((CommandA) x)
var callExp = Expression.Call(Expression.Constant(handlerGroup), handlerInfo, Expression.Convert(expressionArg, parameters.Single().ParameterType));
// this is delegate x => handlerInfo.HandleA((CommandA) x)
var handler = Expression.Lambda<HandlerDelegate>(callExp, new[] { expressionArg }).Compile();                    
cache.Add(parameters.Single().ParameterType, handler);

Upvotes: 2

Jon Skeet
Jon Skeet

Reputation: 1503230

I would encapsulate your cache separately; you can the add specific delegates via method groups. For example:

public class HandlerCache
{
    private readonly Dictionary<Type, HandlerDelegate> handlers = new
        Dictionary<Type, HandlerDelegate>();

    public void AddHandler<T>(Action<T> action) where T : ICommand =>
        handlers.Add(typeof(T), command => action((T) command);

    public void HandleCommand(ICommand command)
    {
        var commandType = command.GetType();
        if (!handlers.TryGetValue(commandType, out var handler))
        {
            throw new ArgumentException($"Unable to handle commands of type {commandType}");
        }
        handler(command);
    }
}

You could then use it like this:

var cache = new HandlerCache();
// This bit could be done by reflection if you want
var handlerGroup = new HandlerGroup();
cache.AddHandler(handlerGroup.HandleA);
cache.AddHandler(handlerGroup.HandleB);
HandlerDelegate handler = cache.HandleCommand;
// Use handler however you want

Obviously, there will always be room for error - there's always going to be the possibility that you'll be passed a command you can't handle. Note that this doesn't currently handle inheritance, in that if there's a class derived from CommandA, that won't be matched in HandleCommand; you could potentially go via the base type etc if you wanted to find the most specific available handler.

If you add the handlers to the cache via reflection, this approach means you can determine the types without adding any attributes - you'd just need to determine the appropriate parameter type and add the method to the cache that way. As always, the reflection code would be a bit annoying to write, but at least you could add new handlers with minimal effort.

Upvotes: 0

Related Questions