Starting with NServiceBus version 10.2.0, NServiceBus supports convention-based message handlers that do not implement IHandleMessages, enabling handlers to be expressed like this:
[Handler]
public class HandlersByConvention
{
public async Task Handle(Msg1 message,
IMessageHandlerContext context,
CancellationToken cancellationToken)
{
}
public static async Task Handle(Msg2 message,
IMessageHandlerContext context,
DatabaseService database,
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
CancellationToken cancellationToken)
{
}
}
And then registered on the endpoint like this:
endpointConfiguration.Handlers.SampleProject.AddAll();
Convention-based handlers are not discovered through traditional assembly scanning, but instead are either added to an NServiceBus endpoint declaratively, or with help from Roslyn analyzers and source generators.
Handler structure
A convention-based handler can look exactly like a regular message handler, but does not implement the interface:
public class ConventionHandler
{
public async Task Handle(MyMessage message, IMessageHandlerContext context)
{
// do something with the message data
}
}
Without the rigid structure of the IHandleMessages interface, additional parameters can be added to the Handle method:
public class ConventionHandlerWithExtraParams
{
public async Task Handle(MyMessage message,
IMessageHandlerContext context,
DatabaseService database,
CancellationToken cancellationToken)
{
// do something with the message data
}
}
These requirements must be met for a class to be recognized as a convention-based message handler:
- Must contain a handler method named
Handlewhich returnsTask. - The handler method's first parameter must be a message class.
- The handler method's second parameter must be an
IMessageHandlerContext. - After the first two parameters, any additional parameters must be either a
CancellationTokenor Services registered in the host'sIServiceCollection. - It can be an instance or static method.
- A handler class may contain multiple handler methods, differing by the message type, but these methods become an inseparable unit. It is not possible to register one handler method on a class but not the other.
- If multiple handler methods use the same message type as the first parameter, they will all be executed on the same message.
Registering handlers
Because handlers not using a marker interface cannot be found by assembly scanning, they must be added to the endpoint. This can be done manually:
endpointConfiguration.AddHandler<ConventionHandler>();
However, decorating a handler class with the NServiceBus. (or NServiceBus. for sagas) enables source generation that enables all decorated handlers and/or sagas in an entire project to be added with one line of configuration.
First, decorate handlers with [Handler], or sagas with [Saga]:
[Handler]
public class DecoratedConventionHandler
{
public async Task Handle(MyMessage message, IMessageHandlerContext context)
{
}
}
This generates source code that allows all handlers and sagas from an assembly to be registered at once:
endpointConfiguration.Handlers.SampleProject.AddAll();
However, the generated source is flexible and allows registering just all handlers, just all sagas, or both from any level of the namespace hierarchy, or at the top level of the assembly:
// Add just one handler
endpointConfiguration.Handlers.SampleProject.OuterNS.InnerNS
.AddDecoratedConventionHandler();
// Add all handlers or sagas from a namespace…
endpointConfiguration.Handlers.SampleProject.OuterNS.InnerNS.AddAllHandlers();
endpointConfiguration.Handlers.SampleProject.OuterNS.InnerNS.AddAllSagas();
endpointConfiguration.Handlers.SampleProject.OuterNS.InnerNS.AddAll();
// …at any point in the namespace hierarchy
endpointConfiguration.Handlers.SampleProject.OuterNS.AddAllHandlers();
endpointConfiguration.Handlers.SampleProject.OuterNS.AddAllSagas();
endpointConfiguration.Handlers.SampleProject.OuterNS.AddAll();
// Or add all from an entire assembly
endpointConfiguration.Handlers.SampleProject.AddAllHandlers();
endpointConfiguration.Handlers.SampleProject.AddAllSagas();
endpointConfiguration.Handlers.SampleProject.AddAll();
Analyzers
While the source generation simplifies registering multiple handlers or sagas to an endpoint with one line of code, the generation relies on the [Handler] and [Saga] attributes to identify what qualifies as a handler or a saga.
The reason the source generation works on the [Handler] and [Saga] attributes is because source generators, which may run in your editor up to every time you press a key, must be very fast and efficient. Identifying generation targets using a marker attribute is the most optimized method available when using the Roslyn SDK. On the other hand, using marker interfaces to identify generation targets is explicitly called out as an anti-pattern in the source generator documentation.
Roslyn analyzers help to ensure that handlers or sagas don't accidentally escape identification, causing them to remain unregistered accidentally:
- NSB0034: Mark convention-based handlers with HandlerAttribute to enable source generation
- NSB0025: Mark sagas with SagaAttribute to enable source generation
These diagnostics default to DiagnosticSeverity. but can be upgraded to ensure handlers and sagas are not missed.
The following . settings will upgrade both diagnostics to errors so that the build will fail if the attributes are not added:
[*.cs]
# Ensure message handlers are decorated with [Handler] to enable source generation
dotnet_diagnostic.NSB0034.severity = error
# Ensure sagas are decorated with [Saga] to enable source generation
dotnet_diagnostic.NSB0034.severity = error