Getting Started
Architecture
NServiceBus
Transports
Persistence
Hosting
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Modernization
Samples

Convention-based handlers

Component: NServiceBus
NuGet Package: NServiceBus 10.x

Starting with NServiceBus version 10.2.0, NServiceBus supports convention-based message handlers that do not implement IHandleMessages<T>, 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<T> 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 Handle which returns Task.
  • 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 CancellationToken or Services registered in the host's IServiceCollection.
  • 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.HandlerAttribute (or NServiceBus.SagaAttribute 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.

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.Info but can be upgraded to ensure handlers and sagas are not missed.

The following .editorconfig 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