mardi 10 juillet 2018

Avoid using DynamicInvoke() from a Dictionary

I have a sort of message broker class that maps incoming message types to handler methods at runtime. It is important for me to maintain strongly typed messages in the handlers. It works by finding all classes that inherit from IMessage and creating a Delegate to invoke any method that has a Handles(TMessage) attribute on it, where TMessage matches the incoming IMessage underlying Type.

So a "handler" looks like this:

[Handles(typeof(TestMessage))]
public void HandleTestMessage(objectsender, TestMessage request)
{
    var response = new TestResponse() { TestInt = request.TestInt };

    msgService.Send(sender, response);
}

Again, I'd really like to avoid having an IMessage in the method signature, it's important to me to not have to cast the IMessage in the method body.

The broker class uses the following method to discover and register the handlers:

/// <summary>
/// Subscribes all methods with the <see cref="HandlesAttribute"/> to the given <see cref="IMessage"/> <see cref="Type"/>
/// </summary>
/// <param name="target">The object to inspect</param>
public void SubscribeAll(object target)
{
    var targetType = target.GetType();

    // Get all private and public methods.
    var methods = targetType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    foreach (var method in methods)
    {
        // If this method doesn't have the Handles attribute then ignore it.
        var handlesAttributes = (HandlesAttribute[])method.GetCustomAttributes(typeof(HandlesAttribute), false);
        if (handlesAttributes.Length != 1)
            continue;

        // The method must have only 2 arguments.
        var parameters = method.GetParameters();
        if (parameters.Length != 2)
        {
            log.LogDebug(string.Format("Method {0} has too many arguments", method.Name));
            continue;
        }

        // The second argument must be derived from IMessage.
        if (!typeof(IMessage).IsAssignableFrom(parameters[1].ParameterType))
        {
            log.LogDebug(string.Format("Method {0} does not have an IMessage as it's second argument", method.Name));
            continue;
        }

        Type genericDelegate; 

        if(method.ReturnType == typeof(void))
        {
            genericDelegate = typeof(Action<,>).MakeGenericType(parameters[0].ParameterType, handlesAttributes[0].MessageType);
        }
        else
        {
            genericDelegate = typeof(Func<,,>).MakeGenericType(parameters[0].ParameterType, handlesAttributes[0].MessageType, method.ReturnType);
        }

        var handler = method.CreateDelegate(genericDelegate, target);

        // Success, so register!
        Subscribe(
            handlesAttributes[0].MessageType,
            handler);
    }
}

and finally when a message is received, it uses this method to invoke (DynamicInvoke) the handler:

/// <summary>
/// Passes a given <see cref="IMessage"/> and optional sender to any <see cref="Handler"/>s accepting the message's underlying type 
/// </summary>
/// <param name="message">The <see cref="IMessage"/> to send</param>
/// <param name="sender">The original sender of the message</param>
private void HandleMessage(IMessage message, object sender = null)
{
    var messageType = message.GetType();
    if (messageHandlers.TryGetValue(messageType, out List<Delegate> handlers))
    {
        foreach (var handler in handlers)
        {
            handler.DynamicInvoke(new object[] { sender, message });
        }
    }
    else
    {
        log.LogError(string.Format("No handler found for message of type {0}", messageType.FullName));
        throw new NoHandlersException();
    }
}

I thought I was being smart when I converted the MethodInfos into Delegates (Action<,> or Func<,,>) but I neglected to check the implications of using DynamicInvoke over Invoke. It turns out DynamicInvoke incurs a comparatively large amount of overhead. So my question is, how can I treat the handler Delegates as the actual Delegates that they are (again, Action<,> or Func<,,>)

As you can see, in HandleMessage, I do know the underlying type of message, and sender is always an object. Basically, I need to do something like:

((Action<object, typeof(message))handler).Invoke(sender, message);

Obviously that's impossible, but it illustrates what I am trying to accomplish clearly (I think).

I tried creating a sort of in-between generic method:

public void InvokeHandler<TMessage>(Action<object, TMessage> handler, TMessage message, object sender = null)

But I just run into contravariance problems down the line when trying to invoke this method.





Aucun commentaire:

Enregistrer un commentaire