danielwertheim

danielwertheim


notes from a passionate developer

Developer that lives by the mantra "code is meant to be shared".

Share


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Routemeister reaches v1

Daniel WertheimDaniel Wertheim

Routemeister is not a big library. It's quite tiny actually and after having felt at it a bit in real usage, I decided to make a few changes to it and I now consider to API "stable" so it has reached v1.0.0.

What is it?

For new readers... Routemeister is a small NuGet built with one single purpose. Effectively performing in-process async message routing. It can be used if you e.g. are dispatching messages cross process using RabbitMQ or ActiveMQ and then want to dispatch the message to typed handlers within the consuming process.

It currently supports fire-and-forget routing, so no request-response pattern. So you could e.g. use it to represent command-handlers or event-handlers in a CQRS scenario. Where commands are sent to one handler and events are published to many handlers.

Actually, you can get request-response supported really easy. See: Routemeister how to support request response

What has changed?

Extracted from the release notes:

So how do I use it now?

First, install it. It's distributed via NuGet.

install-package routemeister  

Now you need to define a custom message handler marker interface. The requirements are:

//You own this interface. Create it and name it as you want.
public interface IHandle<in T>  
{
    //Can be named whatever.
    //Must return a Task
    //Must take one class argument only
    Task HandleAsync(T message);
}

Use this interface to create some concrete handlers (classes implementing your interface). Each class can naturally implement the interface multiple times to handle different types of messages. Each message type being processed can (if you want) be processed by multiple message handlers (different classes).

public class MyHandler :  
    IHandle<MyConcreteMessage>,
    IHandle<ISomeInterfaceMessage>
{
    public Task HandleAsync(MyConcreteMessage message)
    {
        //Do something with the message
    }

    public Task HandleAsync(ISomeInterfaceMessage message)
    {
        //Do something with the message
    }
}

public class SomeOtherHandler :  
    IHandle<MyConcreteMessage>,
    IHandle<ISomeInterfaceMessage>
{
    public Task HandleAsync(MyConcreteMessage message)
    {
        //Do something with the message
    }

    public Task HandleAsync(ISomeInterfaceMessage message)
    {
        //Do something with the message
    }
}

Create routes

In order to invoke the message handlers defined above, you will use an implementation of IAsyncMessageRouter. These needs MessageRoutes which contains many MessageRoute instances.

The created MessageRoutes should be kept around. Don't recreate them all the time.

You create message routes using a MessageRouteFactory. The factory needs to know in what assemblies to look for message handlers. It also needs to know which interface is used as the marker interface.

var factory = new MessageRouteFactory();  
var routes = factory.Create(  
    typeof(SomeType).Assembly,
    typeof(IHandle<>));

You can of course add from many different assemblies or marker interfaces:

var factory = new MessageRouteFactory();  
var routes = factory.Create(  
    new [] { assembly1, assembly2 },
    typeof(IHandle<>));

routes.Add(factory.Create(  
    new [] { assembly1, assembly2 },
    typeof(IHandle<>)));

routes.Add(factory.Create(  
    new [] { assembly1, assembly2, assembly3 },
    typeof(IAnotherHandle<>)));

Use the routes

The routes can now be used as you want to route messages manually, or you can start using an existing router:

Neither of these routers are complex. They route sequentially and fail immediately if an exception is thrown. The simplest one is SequentialAsyncMessageRouter. Go and have a look. You can easily create your own.

var router = new SequentialAsyncMessageRouter(  
    messageHandlerCreator, //See more below
    routes);

await router.RouteAsync(new MyConcreteMessage  
{
    //Some data
}).ConfigureAwait(false);

Strategy for resolving the message handlers

To the existing routers, you pass a MessageHandlerCreator delegate, which is responsible for creating an instance of the message handler class.

public delegate object MessageHandlerCreator(  
    Type messageHandlerType,
    MessageEnvelope envelope);

This is where you decide on how the handlers should be created and where you would hook in your IoC-container. You could pass Activator.CreateInstance(handlerType) but you probably want to use your IoC container or something, so that additional dependencies are resolved via the container.

//Using Activator
var router = new SequentialAsyncMessageRouter(  
    (handlerType, envelope) => Activator.CreateInstance(handlerType),
    routes);

//Using IoC
var router = new SequentialAsyncMessageRouter(  
    (handlerType, envelope) => yourContainer.Resolve(handlerType),
    routes);

The MessageEnvelope is something you could make use of to carry state e.g. using the MiddlewareEnabledAsyncMessageRouter. This lets you register hooks into the message pipeline via router.Use. Hence you could use that to e.g. acomplish per-request scope with your IoC. See below for sample using Autofac.

Sample using Autofac Lifetime scopes to get per request resolving

var router = new MiddlewareEnabledAsyncMessageRouter(  
    (handlerType, envelope) => envelope.GetScope(handlerType),
    routes);

router.Use(next => async envelope =>  
{
    using(var scope = parentscope.BeginLifetimeScope())
    {
        envelope.SetScope(scope);
        return next(envelope);
    }
});

In this case SetScope and GetScope are just custom extenion methods that accesses envelope.SetState("scope", scope) and envelope.GetState("scope") as Autofac.ILifetimeScope.

Now, when ever a message is routed, it will be passed through the pipeline that you hooked into above.

await router.RouteAsync(new MyConcreteMessage  
{
    //Some data
}).ConfigureAwait(false);

Manual routing

You could of course use the produced MessageRoutes manually.

var message = new MyConcreteMessage  
{
    //Some data
};
var messageType = message.GetType();  
var route = routes.GetRoute(messageType);  
foreach (var action in route.Actions)  
    await action.Invoke(
        GetHandler(action.HandlerType),
        message).ConfigureAwait(false);

Hope it helps you solve a problem.

Cheers,

//Daniel

Developer that lives by the mantra "code is meant to be shared".

Comments