This is possible, and only a little bit complicated. What you'll want to do is search your components for methods that have your Handles
attribute, and call them via reflection.
Let's assume we have the following interfaces:
public interface IComponent
{
}
public interface IMessage
{
};
Let's also create the Handles
attribute that will let us tag methods as handling a specific message type:
[AttributeUsage(AttributeTargets.Method)]
public class HandlesAttribute : Attribute
{
public Type MessageType { get; private set; }
public HandlesAttribute(Type messageType)
{
MessageType = messageType;
}
};
Now we'll create a message broker which will be responsible for finding all message handling methods in a given list of components. We'll use reflection to do this. First we'll find all methods that have the Handles
attribute, and then we'll check if they have the required single IMessage
parameter:
public class MessageBroker
{
// Encapsulates a target object and a method to call on that object.
// This is essentially our own version of a delegate that doesn't require
// us to explicitly name the type of the arguments the method takes.
private class Handler
{
public IComponent Component;
public MethodInfo Method;
};
private Dictionary<Type, List<Handler>> m_messageHandlers = new Dictionary<Type, List<Handler>>();
public MessageBroker(List<IComponent> components)
{
foreach (var component in components)
{
var componentType = component.GetType();
// Get all private and public methods.
var methods = componentType.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 one argument.
var parameters = method.GetParameters();
if (parameters.Length != 1)
{
Console.WriteLine(string.Format("Method {0} has too many arguments", method.Name));
continue;
}
// That one argument must be derived from IMessage.
if (!typeof(IMessage).IsAssignableFrom(parameters[0].ParameterType))
{
Console.WriteLine(string.Format("Method {0} does not have an IMessage as an argument", method.Name));
continue;
}
// Success, so register!
RegisterHandler(handlesAttributes[0].MessageType, component, method);
}
}
}
// Register methodInfo on component as a handler for messageType messages.
private void RegisterHandler(Type messageType, IComponent component, MethodInfo methodInfo)
{
List<Handler> handlers = null;
if (!m_messageHandlers.TryGetValue(messageType, out handlers))
{
// If there are no handlers attached to this message type, create a new list.
handlers = new List<Handler>();
m_messageHandlers[messageType] = handlers;
}
var handler = new Handler() { Component = component, Method = methodInfo };
handlers.Add(handler);
}
}
The constructor above logs a warning message and ignores any methods that don't match the signature that we require (i.e. one parameter which derives from IMessage).
Now let's add a method to handle a message. This will call any registered handlers using Invoke
:
// Passes the given message to all registered handlers that are capable of handling this message.
public void HandleMessage(IMessage message)
{
List<Handler> handlers = null;
var messageType = message.GetType();
if (m_messageHandlers.TryGetValue(messageType, out handlers))
{
foreach (var handler in handlers)
{
var target = handler.Component;
var methodInfo = handler.Method;
// Invoke the method directly and pass in the method object.
// Note that this assumes that the target method takes only one parameter of type IMessage.
methodInfo.Invoke(target, new object[] { message });
}
}
else
{
Console.WriteLine(string.Format("No handler found for message of type {0}", messageType.FullName));
}
}
};
And now to test it we'll use these example messages and component. I've added some misconfigured methods for testing as well (i.e. wrong parameters):
public class ExampleMessageA : IMessage
{
};
public class ExampleMessageB : IMessage
{
};
public class ExampleComponent : IComponent
{
[Handles(typeof(ExampleMessageA))]
public void HandleMessageA(ExampleMessageA message)
{
Console.WriteLine(string.Format("Handling message of type ExampleMessageA: {0}", message.GetType().FullName));
}
[Handles(typeof(ExampleMessageB))]
public void HandleMessageB(ExampleMessageB message)
{
Console.WriteLine(string.Format("Handling message of type ExampleMessageB: {0}", message.GetType().FullName));
}
[Handles(typeof(ExampleMessageA))]
public void HandleMessageA_WrongType(object foo)
{
Console.WriteLine(string.Format("HandleMessageA_WrongType: {0}", foo.GetType().FullName));
}
[Handles(typeof(ExampleMessageA))]
public void HandleMessageA_MultipleArgs(object foo, object bar)
{
Console.WriteLine(string.Format("HandleMessageA_WrongType: {0}", foo.GetType().FullName));
}
}
And finally to bring it all together:
var components = new List<IComponent>() { new ExampleComponent() };
var messageBroker = new MessageBroker(components);
// A message has been received and deserialised into the correct type.
// For prototyping here we will just instantiate it.
var messageA = new ExampleMessageA();
messageBroker.HandleMessage(messageA);
var messageB = new ExampleMessageB();
messageBroker.HandleMessage(messageB);
You should get the following output:
Method HandleMessageA_WrongType does not have an IMessage as an argument
Method HandleMessageA_MultipleArgs has too many arguments
Handling message of type ExampleMessageA: Program+ExampleMessageA
Handling message of type ExampleMessageB: Program+ExampleMessageB
Full fiddle that you can play with is here.
To improve method call performance you could rewrite the MethodInfo.Invoke
using the techniques mentioned here.