MSMQ Best Practices
Performance recommendations
- Avoid unnecessary database lookups before deciding if a message can be dropped.
- Prioritization should be given in code for dropping the message as quickly and efficiently as possible.
- If database lookups are required in deciding if a message can be dropped, can these be cached (with a refresh timeout)?
- If you are dropping messages, is there a better way to only receive the correct messages?
- If you are subscribed to an event processor publisher, does that publisher provide any kind of subscription filtering? Filtering can be achieved via database event subscription tables, (a list of subscribers with their input queues, and a list of events each input queue is interested in).
Separate event types
Publishers should translate event types into separate System.Types, with hierarchies to support grouping.
For example, with some kind of delivery event data, these could be group interfaces such as ProofOfDeliveryGroup, ProofOfCollectionGroup, etc.
Inherited from ProofOfDeliveryGroup would be the specific interface types such as ManualProofOfDelivery, DeliveredByCourierToSite, etc.
Inherited from ProofOfDeliveryGroup would be the specific interface types such as ManualProofOfDelivery, DeliveredByCourierToSite, etc.
This
allows subscribers to use NServiceBus idioms for subscribing to only
the messages they need and removes the need for specialised publication
filtering as seen in the above step 5 publisher.
NServiceBus
recommends interfaces for events because you can effectively do
multiple inheritance, which isn’t possible for classes and allows for
handling “groups” of events, as well as gentle evolution of events.
Separate the handlers
Favour multiple event handlers over a small number of more complex handlers.Each handler should do one thing and be named after that one thing. The order of operation of the event handlers can be specified (see below) and this will start to read like a pseudo specification of what happens when an event of that type arrives.
As
with any unit of code, message handlers should aim to follow 'Single
Responsibility Principle' and only have one reason to change, and so one
should favour multiple small handlers over fewer large ones. However,
only if there is no implicit coupling (e.g. through bespoke handler
ordering), in which case look for other ways to accomplish this.
General recommendations
- There should only one place to subscribe to any given event, though publishers can be scaled out if necessary, as long as each instance shares the same subscription storage database.
- To avoid coupling, either:
- Publish a new message when a message is handled (successfully or otherwise), so another handler (potentially in a different endpoint) can handle it in a new transaction.
- Use NServiceBus Sagas.
- Using separate messages and/or sagas allows implementation of the “no business reason to change” philosophy, where all failures are technical failures, and attempts can be made to overcome then with automatic retries etc, using separate, chained transactions. This is especially helpful when dealing with resources such as email or the file system that do not participate in the ambient distributed transaction while a message is being handled.
- It is possible to perform validation/mutation of messages before any handlers are invoked (Message Mutators). Again, prefer this over handler ordering.
- Mutators are not automatically registered using dependency injection.
- Mutators are registered using:
endpointConfiguration.RegisterMessageMutator(new MyIncomingMessageMutator());
endpointConfiguration.RegisterMessageMutator(new MyOutgoingTransportMessageMutator());
Handler ordering
NSB documentationMultiple classes may implement
IHandleMessages
for the same message. In this scenario, all handlers will execute in
the same transaction scope. These handlers can be invoked in any order
but the order of execution can be specified in code.The way NServiceBus works is:
- Find the list of possible handlers for a message.
- If an order has been specified for any of those handlers, move them to the start of the list.
- Execute the handlers.
Specifying one handler to run first
public class SpecifyMessageHandlerOrder : ISpecifyMessageHandlerOrdering { public void SpecifyOrder(Order order) { order.SpecifyFirst<handlerb>(); } }
Specifying multiple handlers to run in order
public class SpecifyMessageHandlerOrder : ISpecifyMessageHandlerOrdering { public void SpecifyOrder(Order order) { order.Specify( typeof(HandlerB), typeof(HandlerA), typeof(HandlerC)); } }
Example
public class OrderReceivedEventHandlerOrdering : ISpecifyMessageHandlerOrdering { public void SpecifyOrder(Order order) { order.Specify( typeof(ValidateOrderEventHandler), typeof(CheckForDuplicateOrderEventHandler), typeof(PlaceOrderEventHandler), typeof(SendOrderEmailConfirmationEventHandler)); } }
With the configuration API
This is typically done within the EndpointConfig class.configuration.LoadMessageHandlers( First<HandlerB> .Then<HandlerA>() .AndThen<HandlerC>());
Preferred method
Using the interface ISpecifyMessageHandlerOrdering is the preferred method, as these can be placed within the area of concern. This makes it easier to maintain as you don't have to go searching for the ordering of the handlers.Dropping messages
If you are not going to process all messages, but decide to drop/filter some out, have a separate handler for these and make this the first handler:public class SpecifyMessageHandlerOrder : ISpecifyMessageHandlerOrdering { public void SpecifyOrder(Order order) { order.Specify( typeof(MessageFiltering), // FilterMessage, IgnoreInapplicableEvents, IgnoreIfNotLatestEvent, etc typeof(HandleSomeMessage); } }You may wish to have several message filters, all with their own criteria: order.Specify(First
The takeaway is to be as efficient as possible in dropping messages if it's of no interest.
Event processor filtering
However, it’s generally preferable that publishers simply publish all events that are subscribed to. This leaves the subscribers in charge of what messages they receive and which of those to ignore and how. Filtering of published messages increases coupling between publisher and subscriber and should only be used as a last resort when subscribers have not been implemented/deployed in a scalable way.However, if your events come from CDC (Change data capture) and you want to take advantage of event filtering, you need something similar to the below:
CREATE TABLE EventProcessor ( Id INT NOT NULL IDENTITY(1,1), [Name] VARCHAR(200) NOT NULL, [Description] VARCHAR(512) NULL, [EndpointAddress] VARCHAR(512) NULL, -- Name of msmq queue [Enabled] BIT NOT NULL, CONSTRAINT [PK_EventProcessor] PRIMARY KEY CLUSTERED (Id) ); CREATE TABLE EventProcessorEventFilter ( Id INT NOT NULL IDENTITY(1,1), EventProcessorId INT NOT NULL, WantedEventId INT NOT NULL, CONSTRAINT [PK_EventProcessorEventFilter] PRIMARY KEY CLUSTERED (Id) ); GO CREATE UNIQUE INDEX [IX_EventProcessorEventFilter] ON EventProcessorEventFilter (EventProcessorId, WantedEventId);
ALTER TABLE EventProcessorEventFilter ADD CONSTRAINT [FK_EventProcessorEventFilter__EventProcessor] FOREIGN KEY (EventProcessorId) REFERENCES EventProcessor (Id);
ALTER TABLE EventProcessorEventFilter ADD CONSTRAINT [FK_EventProcessorEventFilter__SomeEventEnumTable] FOREIGN KEY (WantedEventId) REFERENCES SomeEventEnumTable (Id); GO