3

Phew, what a title...

I am working on a server for a learning project. I've spent quite some time trying to figure out how to frame this question properly. In the beginning, I don't think I even knew exactly what I was trying to achieve.

What I am working with is a server that has N components (N >= 0).

  • Each component is added at runtime using DI.
  • Each component is a "black box". When it receives a message, it is considered a "unit of work" and has everything it needs to go from start to finish.
  • Each component is responsible for providing info for subscribing to a message, and providing a handler for that message. I plan to achieve this using an attribute on the handler function.

An example of a "handler":

[Handles(ExampleMessage)]
private void handleExampleMessage(ExampleMessage message)
{
    DoStuff();
}

This is the clearest way I can think of framing my question:

How can I achieve a typed "message broker" system like how ASP.NET MVC provides typed models to a controller action from a serialized input.

So what I'd like to achieve is:

Serialized message -> Strongly typed message -> message service -> call handler function with *strongly typed* message as an argument

I thought of a few things:

First thing I tried was simply de-serializing the message to a dynamic, but not having inellisense and compile-time checking is too high a cost for the simplicity of dynamic for me.

Then I tried creating static de-serialization methods at runtime using reflection, and using the return value from those to call the "handlers", but it got so ugly and spaghetti, I just had to drop it (although I am of course still open to this option if someone has an elegant, performance conscious way)

Finally I tried using a Message type that all messages inherit from, but I end up getting stuck when I try to use a Dictionary<Action<Message>, Message> to call the appropriate handlers

Matthew Goulart
  • 2,873
  • 4
  • 28
  • 63
  • If your message is json, for example, it contains no meta-info. So you'd need your components to provide a method to handle their own deserialisation, or at least return a boolean to say if the object is of their supplied type or not. – monty May 31 '18 at 03:36
  • @monty Good point, I should have mentioned that I am using MessagePack for .net. The messages are classes, so when they are de-serialized, (`Serializer.Deserialize(byte[] data)`) the metadata is inferred by the Type. – Matthew Goulart May 31 '18 at 03:38
  • so you already have the ability to deserialise the incoming data into the correct type? – monty May 31 '18 at 03:47
  • 1
    @monty I do, it requires creating run-time deserializer methods using reflection, as I mentioned, but it's do-able. I decided to add an identifier to the serialized messages and keep a `Dictionary` to keep track of their types. – Matthew Goulart May 31 '18 at 03:50

1 Answers1

4

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.

Sam
  • 3,320
  • 1
  • 17
  • 22
  • I like the approach, but in my case, the messages are handled by individual methods I.E. a component can have any number of handlers that handle any number of *different* messages. I'll try to see if I can adapt your solution. – Matthew Goulart May 31 '18 at 04:17
  • Sure thing, I created a new [fiddle](https://dotnetfiddle.net/J98Ser) to include an example of that. I also refactored the message handler dictionary into a `MessageBroker` class. – Sam May 31 '18 at 04:35
  • @MatthewGoulart then each component would have multiple classes that implement an IHandler (instead of IComponent) interface – monty May 31 '18 at 04:35
  • @Sam see now that solution is very elegant, the *only* thing I'd like to add is for the handlers to received *typed* messages, as in *not* just the interface, but the underlying type. Obviously, I could just cast the message in the handler, but I'd rather not if possible. Can you think of a way to integrate that into this method? If not, I think I can live with this solution as-is, it's quite elegant. – Matthew Goulart May 31 '18 at 05:05
  • I wrote up another [example](https://dotnetfiddle.net/LsbrLM) that is more in line with your original question (i.e. using the attributes to define the methods, and multiple methods handling messages in a component). Let me have a think about how passing in the actual concrete type could work. – Sam May 31 '18 at 05:12
  • @Sam I had something similar, although nowhere near as elegant as what you propose, but I too got stuck on getting the concrete type into the method without explicitly casting. It's why I mentioned ASP.NET's way of going from a serialized model to a strongly typed one, although I'd be willing to bet it's at the expense of efficiency... – Matthew Goulart May 31 '18 at 05:20
  • @MatthewGoulart Alright check [this](https://dotnetfiddle.net/Ehngb1) out! It's a bit more complicated now. It lets you put the actual type in the method signature, and it will display a warning message if the method has more than one parameter, or the parameter is not an `IMessage`. The key is to call the method via the `MethodInfo` object. You could even go one step further and get rid of the `Handles` attribute entirely and just scan for methods that take an `IMessage`-derived type. – Sam May 31 '18 at 05:33
  • This seems to do *exactly* what I was hoping for. I like how the `reflection` is done *once*, it won't affect performance while the app is running. I'm just curious about the overhead of calling `MethodInfo.Invoke()`, is there any downside to this method? – Matthew Goulart May 31 '18 at 05:53
  • I haven't profiled it at all, but my understanding from a quick search is that `MethodInfo.Invoke` is slower than calling a method directly or even via a `Delegate` instance. I'm not totally sure how to get around that in this case though. You *might* be able to use [`DynamicMethod`](https://msdn.microsoft.com/en-us/library/system.reflection.emit.dynamicmethod(v=vs.110).aspx) to generate the method call at runtime. But if this solves your problems right now, I would just roll with it and profile it later if there are perf issues. It shouldn't be too hard to retrofit a solution. – Sam May 31 '18 at 06:06
  • I'll rewrite my answer using the last fiddle then since it more accurately answers your question! – Sam May 31 '18 at 06:07
  • 1
    Related: https://blogs.msmvps.com/jonskeet/2008/08/09/making-reflection-fly-and-exploring-delegates/ – Matthew Goulart May 31 '18 at 06:12
  • One year later, I'm reading over this and I realised that we don't even need to specify the `MessageType` in the `HandlesAttribute`; we can infer it from the method argument. I can't believe I didn't think of that earlier. It seems so obvious! Here's an updated fiddle: https://dotnetfiddle.net/dymxtH – Sam Feb 28 '19 at 12:38