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 MethodInfo
s into Delegate
s (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 Delegate
s as the actual Delegate
s 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