Examining how NServiceBus configures and uses RabbitMQ / AMQP

NServiceBus acts as a .Net (C#) distributed messaging abstraction layer for message-driven systems.  It runs on top of a variety of transports including MSMQ, Azure Storage Queues, ActiveMQ on RabbitMQ.  This blog describes how NServiceBus configures and uses RabbitMQ using the Video Store Sample program available in the NServiceBus github repository .

It looks like the images attached to this article have been removed from the google image store

Background


Message-driven systems are asynchronous distributed systems that have two main message concepts, at least in our project. They have commands, targeted messages, where the sender knows the intended destination. The producer sends the messages to a target. They also have publish/subscribe or event-based messages where the sender target is unknown. The sender posts the event which is then received by parties that have registered interest, and subscribed.
  • Producers: Programs that send messages or post events 
  • Consumers: Programs that receive messages or capture events 
  • Queues: A place where messages are collected and distributed. Messages are put in queues where they are read by consumers.

AMQP Background

AMQP is an open standard-based messaging system. Some of the fundamental building blocks leveraged by NServiceBus are

  • Channels: These are the path that producers communicate over when sending messages 
  • Queues: Consumers attach to these to read messages. Queues exist to serve consumers 
  • Exchanges: Aggregation and distribution points used to route incoming messages from the channels to the queues. There can be multiple upstream producers and multiple consumers for the data coming out of an exchange. There are a variety of exchange types discussed in the RabbitMQ documentation. Exchanges can have upstream and downstream exchanges meaning a message can pass through multiple exchanges before getting to a queue.
All AMQP messages flow through some type of exchange on their way to a queue. The standard mandates a default exchange that binds to every queue by routing key. Messages targeted at a specific queue route through the default exchange with the routing key set to the target queue name.

Useful rabbit tutorials can be found here http://www.rabbitmq.com/getstarted.html

NServiceBus Background

NServiceBus uses a type-driven model similar to MVC that is familiar to .Net developers.  NServiceBus messages are explicitly defined concrete classes or interfaces  Each message or event type is a different C# type.   It supports message routing and topic subscriptions based on explicit C# types, namespaces, or assemblies.  Content-based routing is not supported.  Classes and interfaces can be declared as NServiceBus messages and events with marker interfaces or through a config file.  The latter is nice when you don’t want any NServiceBus dependencies in your message model.

  • Commands: Point-to-point messages sent by a client with an expected target. The target is configured in the executable’s properties config file. Commands may or may not expect a reply or an event. The caller must know the target and routing is configured on the sender’s end. 
  • Messages: Usually these are the types of asynchronous replies to commands. The system provides auto-correlation support so that the originator receives the reply. These are different from Events where all interested parties can see messages sent as a result of some action. The process replying doesn’t have to know the target. NServiceBus takes care of the routing. 
  • Events: These are pub/sub-messages that are broadcast to interested parties. The message producer has no idea who might be interested in the message. Routing is driven off of the endpoints that are interested in the message.
Message producers know where they want to send commands. Commands, direct messages, are routed to explicit endpoints or queues.  Message target queues are defined in an XML file configuration usually web.config or app.config.  Here is an example from the NServiceVideoStore example that binds a namespace to an outbound queue name:

     <add Assembly="VideoStore.Messages" Namespace="VideoStore.Messages.Events" Endpoint="VideoStore.Sales"/>

The pub/sub model is different.  Message publishers do not know who is interested in a message.  It is the consumers that declare their interest in messages of various types.  NServiceBus discovers program interest in specific event types when it scans assemblies for messages with certain interfaces and signatures. This example declares a handler class interested in the OrderAccepted event type:

   class OrderAcceptedHandler : IHandleMessages<OrderAccepted>

The actual handler code itself other .Net MVC conventions. This handler processes OrderAccepted events:

       public void Handle(OrderAccepted message)

The VideoStore Example
NServiceBus has a Video Store example that has five (5) executables including a web interface, saga processor and hosted NServiceBus endpoints:
  • ContentManagement: Simulates a download service
  • CustomerRelations:
  • ECommerce: A simulated web store
  • Operations:
  • Sales: A sales management saga 
There are three (3) targeted messages, commands:  
  • ProvisionDownloadRequest: Sent from Content Management to Operations
  • SubmitOrder: Sent from ECommerce to Sales
  • CancelOrder: sent from ECommerce to sales. 
There are five (5) published events that are processed in a publish/subscribe type way.
  • ClientBecamePreferred: Published by Customer Relations. Subscribed to by CustomerRelations.
  • DownloadIsReady: Published by ContentManagement. Subscribed to by ECommerce
  • OrderAccepted: Published by the Sales saga. Subscribed to ContentManagement and CustomerRelations
  • OrderCancelled: Published by the Sales saga. Subscribed to by ECommerce
  • OrderPlaced: Published by the Sales saga. Subscribed to by ECommerce
Here is the command/message/event configuration from the VideoStore example that tells NServiceBus how to categorize the various message types.

   Configure.Instance
       .DefiningCommandsAs(t => t.Namespace !=
null
            && t.Namespace.StartsWith("VideoStore")  
            && t.Namespace.EndsWith("Commands"))
       .DefiningEventsAs(t => t.Namespace !=
null
            && t.Namespace.StartsWith("VideoStore")
            && t.Namespace.EndsWith("Events"))
       .DefiningMessagesAs(t => t.Namespace !=
null
            && t.Namespace.StartsWith("VideoStore")
            && t.Namespace.EndsWith("RequestResponse"))
       .DefiningEncryptedPropertiesAs(p => p.Name.StartsWith(
"Encrypted"));

NServiceBus on RabbitMQ

NServiceBus endpoints all have a single inbound queue.  All messages sent to that endpoint go over that single queue where they are then distributed to the individual handlers.  NServiceBus configures the message and exchange routing so that all Commands and Events destined for an endpoint end up in that endpoint’s single inbound queue.  RabbitMQ creates that inbound queue on endpoint startup when NServiceBus binds that endpoint to a queue name in RabbitMQ. Note: AMQP brokers create queues when a receiver binds to a queue name, not when a message is sent to a queue name.

NServiceBus makes use of the default message exchange for all Command routing. It explicitly RabbitMQ exchanges for Event pub/sub traffic.
 
Commands
NServiceBus uses the AMQP default exchange that is automatically configured in every RabbitMQ VHost.  The VideoStore example has two Command producers with three commands between them. There are two endpoints that receive these commands.

  • ContentManagement sends the ProvisionDownloadRequest command to the Operations endpoint through the Operations queue 
  • ECommerce sends the SubmitOrder and CancelOrder commands to the Sales endpoint through the Sales queue.


Commands are sent with Bus.Send. NServiceBus knows to set the AMQP routing key to the endpoint name before pushing onto the channel and into the default exchange.  The default exchange naturally uses the routing to find a queue with the same name.


Events

NServiceBus uses the AMQP exchange-to-exchange and exchange-to-queue routing to build a delivery map for published events. This lets NServiceBus support any many-producer each with many event types with many interested parties.

The NServiceBus publish command matches the C# Type of the event to an exchange name and sends the event to that exchange. The exchange points at all the endpoint exchanges that front the individual endpoint queues. This diagram shows how the Video Store’s three (3) event producers post the five (5) different event types to three (3) different consumers.



It’s pretty straightforward once you see the diagram.

  1. NServiceBus scans for all the Event types based on either the marker interfaces or the Unobtrusive configuration file. It builds a Fanout exchange for each event type. 
  2. NServiceBus scans every endpoint to see if they support any events.  It builds a a Queue Proxy exchange for each identified endpoint with the same name as the queue.   
    1. You can see in the picture above that two of the endpoint queues do not have matching exchanges. This means they don’t subscribe to any events. 
  3. NServiceBus uses the same scan to build a binding between the Event exchanges and the appropriate Queue proxy exchanges. 
  4. The endpoints listen on the queues for messages.

Events are posted using Bus.Publish() . The publishing process looks like this:

  1. NServiceBus sends them to the exchange with the same name as the event type. 
  2. The exchange then passes the message to all bound queue (proxy) exchanges. 
  3. The queue proxy exchanges forward all the gathered messages onto their bound queues.

Wrapup


NServiceBus has a type-based messaging and routing model that it implements on top of RabbitMQ transport by leveraging the AMQP exchange and queue architecture.  It only uses the AMQP fanout exchange.  AMQP header and content-based routing is not used because it is not needed to support the NServiceBus routing and distribution design.

Comments

  1. Joe
    Outstanding article. The best I have seen yet.
    I'm curious on your diagram.

    You have producers to exchanges to queues.
    And consumers reading from queues. I'm under the impression in AMPQ that you never directly write to a queue and you never directly read from a queue.

    So would the diagram not have a "channel" from which the Consumer C1 opens to read from the Queue?

    And would the producer not open an channel to the exchange?

    It seems trivial but to me it's what separates AMPQ from MSMQ. The entire concept of "you don't directly write to queues and you don't read from queues". It's why MSMQ is considered a dump transport. And why AMPQ transports can infer Enterprise Patterns like Publish/Subscribe etc..

    Love this post!

    Eric

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. HI Joe
    This is a great post.
    However the link to the sample source code doe snot work. Do you have it somewhere else as well where i can take a look?

    ReplyDelete

Post a Comment

Popular posts from this blog

Understanding your WSL2 RAM and swap - Changing the default 50%-25%

Installing the RNDIS driver on Windows 11 to use USB Raspberry Pi as network attached

DNS for Azure Point to Site (P2S) VPN - getting the internal IPs