Default Topologies - NServiceBus with RabbitMq Part 1

NServiceBus has excellent features and while not free can lower the total cost of ownership if you have a large messaging based platform. In this first part of this series on NServiceBus and the RabbitMqTransport, we'll look at the default RabbitMq topologies generated by NServiceBus. All source code is on Github.

Before you begin it is assumed that you have:

  • installed NServiceBus locally

  • followed the excellent NServiceBus tutorial that creates a retail based demo using the MsmqTransport.

In this post we'll port that retail demo over to RabbitMq and see what exchanges, queues and bindings get generated.

The demo consists of four endpoints:

  • The Client UI sends a PlaceOrder command directly to the Sales endpoint.

  • The Sales endpoint publishes the event OrderPlaced to whomever wants to receive it.

  • The Billing endpoint subscribes to the OrderPlaced event and publishes the OrderBilled event to whomever wants it.

  • The Shipping endpoint subscribes to both the OrderPlaced and OrderBilled. Currently this post does not implement a saga for this though that is what is needed.

Direct or Conventional Routing Topology

We have our first decision to make. There are two routing topology conventions that NServiceBus supports by default:

  • Conventional

When sending a command, the message goes to a fanout exchange with the same name as the receiving endpoint.

When sending an event, the message is sent to an exchange with the name of the event type. Bound to that exchange are exchanges for each of the base types of that event. For example NServiceBus.IEvent, NServiceBus.IMessage and System.Object.

  • Direct

When sending a command, the message goes to the default (direct) exchange, with the routing key as the name of the receiving endpoint. The default exchange is bound to all queues, and routes messages to the queue which has the same name as the message routing key.

When sending an event, the message goes to the amq.topic exchange, with the routing key as the message type. For example: Messages-Events-OrderPlaced

We'll do both.

The Retail Demo with the Direct Routing Topology

Step 1 - Create the Rabbit.ClientUI project

If you have followed the tutorial mentioned at the start of the post then you should have a solution with the five projects: ClientUI, Sales, Billing, Shipping and Messages.

Create a new solution folder "MsmqTransport" and put the ClientUI, Sales, Billing and Shipping projects in it. But leave the Messages project, we'll be using that.

Create a new solution folder "RabbitMqTransport". Add a new console application project "Rabbit.ClientUI". Add NServiceBus and NServiceBus.RabbitMq nuget packages to the project. Add a reference to the Messages project. Copy the code in Program.cs from the ClientUI project into your Program.cs.

You just need to change the code:


Console.Title = "ClientUI";

var endpointConfiguration = new EndpointConfiguration("ClientUI");

var transport = endpointConfiguration.UseTransport<MsmqTransport>();
var routing = transport.Routing();
routing.RouteToEndpoint(typeof(PlaceOrder), "Sales");

To the following:


Console.Title = "Rabbit.ClientUI";

var endpointConfiguration = new EndpointConfiguration("Rabbit.ClientUI");

var transport = endpointConfiguration.UseTransport<RabbitMQTransport>();
transport.ConnectionString("host=localhost");
transport.UsePublisherConfirms(true);
transport.UseDirectRoutingTopology();

var routing = transport.Routing();
routing.RouteToEndpoint(typeof(PlaceOrder), "Rabbit.Sales");

When you run the Rabbit ClientUI it will not generate any RabbitMq artefacts. All messages will be sent to the default (direct) exchange with the routing key "Rabbit.Sales". Currently we have no Rabbit.Sales endpoint and no Rabbit.Sales queue for the message to be routed to. If we sent a message now RabbitMq would return the message saying no queue could be found.

Step 2 - Create the Rabbit.Sales project

Add a new console application project "Rabbit.Sales". Add NServiceBus and NServiceBus.RabbitMq nuget packages to the project. Add a reference to the Messages project. Copy the code in Program.cs from the Sales project into your Program.cs in Rabbit.Sales.

Change the following code:


Console.Title = "Sales";

var endpointConfiguration = new EndpointConfiguration("Sales");

var transport = endpointConfiguration.UseTransport<MsmqTransport>();

To the following:


Console.Title = "Rabbit Sales";

var endpointConfiguration = new EndpointConfiguration("Rabbit.Sales");

var transport = endpointConfiguration.UseTransport<RabbitMQTransport>();
transport.ConnectionString("host=localhost");
transport.UsePublisherConfirms(true);
transport.UseDirectRoutingTopology();

The Rabbit.Sales endpoint doesn't send any commands so we don't need to specify any RouteToEndpoint here, that is for commands only.

Now let's copy over the PlaceOrderHandler file to our Rabbit.Sales project. The only thing you need to do is make sure you change the namespace. No other changes are required.

Notice that we haven't specified that we receive the PlaceOrder command or where the OrderPlaced event should be sent to. In the PlaceOrderHandler, we just publish the OrderPlaced event.


public class PlaceOrderHandler : IHandleMessages<PlaceOrder>
{
    static ILog logger = LogManager.GetLogger<PlaceOrderHandler>();

    public Task Handle(PlaceOrder message, IMessageHandlerContext context)
    {
        logger.Info($"Received PlaceOrder, OrderId = {message.OrderId}");

        // This is normally where some business logic would occur
            
        var orderPlaced = new OrderPlaced
        {
            OrderId = message.OrderId
        };
        return context.Publish(orderPlaced);
    }
}

By default, when this endpoint starts it will create a queue with the name "Rabbit.Sales". The Rabbit.ClientUI sends the PlaceOrder command to the default exchange with the routing key "Rabbit.Sales", and so the message gets delivered to this endpoint's queue "Rabbit.Sales".

And where does the OrderPlaced event get sent? It goes to the amq.topic exchange with the routing key: Messages-Events-OrderPlaced. This is because the full name of the OrderPlaced event (including namespace) is Messages.Events.OrderPlaced.

Now let's create the Rabbit.Billing endpoint.

Step 3 - Create the Rabbit.Billing project

Add a new console application project "Rabbit.Billing". Add NServiceBus and NServiceBus.RabbitMq nuget packages to the project. Add a reference to the Messages project. Copy the code in Program.cs from the Billing project into your Program.cs in Rabbit.Billing.

Change the following code:


Console.Title = "Billing";

var endpointConfiguration = new EndpointConfiguration("Billing");

var transport = endpointConfiguration.UseTransport<MsmqTransport>();
var routing = transport.Routing();
routing.RegisterPublisher(typeof(OrderPlaced), "Sales");

To the following:


Console.Title = "Rabbit Billing";

var endpointConfiguration = new EndpointConfiguration("Rabbit.Billing");
            
var transport = endpointConfiguration.UseTransport<RabbitMQTransport>();
transport.ConnectionString("host=localhost");
transport.UsePublisherConfirms(true);
transport.UseDirectRoutingTopology();

Notice again that we need not register anything in order to be a subscriber. NServiceBus will detect that via the use of the event handler. It will set up a binding to the amq.topic exchange with the binding key: Messages-Events-OrderPlaced.#

Now copy the OrderPlacedHandler file over to the Rabbit.Billing project and update the namespace. In this event handler we declare the event type that we consume and also send the OrderBilled event.


public class OrderPlacedHandler : IHandleMessages<OrderPlaced>
{
    static ILog logger = LogManager.GetLogger<OrderPlacedHandler>();

    public Task Handle(OrderPlaced message, IMessageHandlerContext context)
    {
        logger.Info($"Received OrderPlaced, OrderId = {message.OrderId} - charging credit card");


        var orderBilled = new OrderBilled
        {
            OrderId = message.OrderId
        };
        return context.Publish(orderBilled);
    }
    

 

Let's review all the artefacts and endpoints generated so far.

 

Step 4 - Create the Rabbit.Shipping project

Add a new console application project "Rabbit.Shipping". Add NServiceBus and NServiceBus.RabbitMq nuget packages to the project. Add a reference to the Messages project. Copy the code in Program.cs from the Shipping project into your Program.cs in Rabbit.Shipping.

Change the following code:


Console.Title = "Shipping";

var endpointConfiguration = new EndpointConfiguration("Shipping");

var transport = endpointConfiguration.UseTransport<MsmqTransport>();
var routing = transport.Routing();
routing.RegisterPublisher(typeof(OrderPlaced), "Sales");
routing.RegisterPublisher(typeof(OrderBilled), "Billing");

To the following:


Console.Title = "Rabbit Shipping";

var endpointConfiguration = new EndpointConfiguration("Rabbit.Shipping");

var transport = endpointConfiguration.UseTransport<RabbitMQTransport>();
transport.ConnectionString("host=localhost");
transport.UsePublisherConfirms(true);
transport.UseDirectRoutingTopology();

Now copy over the OrderPlacedHandler and OrderBilledHandler files and update their namespace. NServiceBus will create the Rabbit.Shipping queue and know to create the bindings to the amq.topic exchange with the correct binding keys.

Let's see the final view now that everything is set up.

By using a topic exchange for the events, it makes for a really simple routing topology. However, it might not be ideal for large messaging systems or messaging systems that generate large volumes of events.

Let's look at the NServiceBus conventional routing topology.

The Retail Demo with the Conventional Routing Topology

It's really easy to update the endpoints to use the conventional routing topology. Just remove the transport.UseDirectRoutingTopology(); line from each endpoint. 

If you are running RabbitMq locally you might wish to wipe all queues and bindings created, so you can get a clear picture of what exchanges, queues and bindings the conventional topology consists of. Just run the following commands on your local RabbitMq service:

  • rabbitmqctl stop_app

  • rabbitmqctl reset

  • rabbitmqctl start_app

Now start up your endpoints. Let's see what the picture is now.

That's quite a bit different. Let's go over the differences again.

Commands get sent to a fanout exchange with the name of the receiving endpoint. That endpoint binds its queue to that fanout exchange.

Events get sent to an exchange with the name of the type. That exchange then binds to exchanges with the base types of that message type.

Each endpoint that subscribes to that event create their own exchange and queue with the name of the endpoint, then bind their exchange to the event type exchange.

There are some benefits to this approach:

  • Base type exchanges allow you to create polymorphic routing based on message inheritance. All with fanout exchanges which have better performance characteristics than topic exchanges.

  • Fanout exchanges scale better than topic exchanges

  • Exchange to exchange binding provides for a more flexible routing topology that can be built on.

In the next part we'll look at how adding a CancelOrder command and OrderCancelled event affect these topologies.