- 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.
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 documentation
Multiple 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.
The remaining handlers (i.e. ones not specified in the ordering) are executed in a non-deterministic order.
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
.Then());
etc.
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