NServiceBus makes it easy to support dynamic dispatch and polymorphic message routing scenarios. This technique makes systems more flexible and maintainable by encouraging messages and handlers to be decoupled from each other. It's useful for evolving systems by allowing the addition of new features without affecting existing functionality. It's best described using an example.
Using NServiceBus for dynamic dispatch
Consider the following handler stubs for a UserCreated
message which inherits from the IEvent
stub interface:
public class SaveUser : IHandleMessages<UserCreated>
{
public Task Handle(UserCreated message, IMessageHandlerContext context) {
// Do stuff when the user is created
return Task.CompletedTask;
}
}
public class Audit : IHandleMessages<IMessage>
{
public Task Handle(IMessage message, IMessageHandlerContext context) {
// Audit the message
return Task.CompletedTask;
}
}
When a message of type UserCreated
is published, both handlers will be invoked independently which in itself is useful. However, this idea becomes more powerful as the system evolves and more functionality is added.
Imagine there is a separate process that must happen if a user is created as part of an advertising campaign. For example, say the subscriber must register statistics about the campaign. In NServiceBus, this can be done by defining a new event and handler:
public class UserCreatedFromCampaign : UserCreated
{
public Guid CampaignId { get; set; }
}
public class RecordStatistics : IHandleMessages<UserCreatedFromCampaign>
{
public Task Handle(UserCreatedFromCampaign message, IMessageHandlerContext context) {
// Record statistics
return Task.CompletedTask;
}
}
With the introduction of this new code, when a UserCreatedFromCampaign
message is published, the RecordStatistics
handler is invoked as expected. But since UserCreatedFromCampaign
inherits from UserCreated
, the SaveUser
handler is also invoked, along with the Audit
handler (since UserCreated
implements the IMessage
stub interface). That is, new functionality has been added without having to change the existing handlers. This is the essence of dynamic dispatch: the system decides at runtime which handlers to invoke based on the message type that is being processed.
In this example, the new event, UserCreatedFromCampaign
, has a clear relationship with UserCreated
. However, it's possible that other events could be raised in relation to campaigns, not just users being created. This is where polymorphic routing can help.
Multiple inheritance to support polymorphic routing
In the updated scenario, there are two concerns: a user is created and a campaign event occurred. The handlers for both events look as follows:
public class SaveUser : IHandleMessages<UserCreated>
{
public Task Handle(UserCreated message, IMessageHandlerContext context) {
// Do stuff when the user is created
return Task.CompletedTask;
}
}
public class RecordCampaignActivity : IHandleMessages<CampaignActivityOccurred>
{
public Task Handle(CampaignActivityOccurred message, IMessageHandlerContext context) {
// Do stuff when an event related to a campaign happened
return Task.CompletedTask;
}
}
Now assume the system should perform some action specifically when a user is created from a campaign. For example, perhaps an email is sent to the user thanking them for engaging. This can be accomplished with an event that inherits from both UserCreated
and CampaignActivityOccurred
as follows:
public interface UserCreated : IEvent
{
Guid UserId { get; set; }
}
public interface CampaignActivityOccurred : IEvent
{
Guid CampaignId { get; set; }
}
public interface UserCreatedFromCampaign : UserCreated, CampaignActivityOccurred
{
}
When a user is created from a campaign, the publisher can publish the latter event. It will get handled by the SaveUser
and RecordCampaignActivity
handlers defined earlier, and perhaps another handler that explicitly handles UserCreatedFromCampaign
events. That is, the message is routed to multiple handlers based on the interfaces it implements; this is polymorphic routing. Furthermore, other campaign events can be created that inherit from CampaignActivityOccurred
without interfering with any functionality around creating users as part of a campaign.
Combining this approach with the technique of using interfaces as messages will make your system more resilient, extensible, and maintainable over time as handlers can be defined independent of each other much more easily.