Convention-based message handlers
This post is part of the NServiceBus 10 Improvements series.
- Preparing NServiceBus for trimming and AOT
- Redesigning the NServiceBus pipeline yet again: The nerd sniping of Daniel Marbach
- Multiple endpoint hosting
- Convention-based message handlers (this post)
- Next post coming August 4

In the previous post, we talked about how NServiceBus version 10.2 can now host multiple endpoints within the same host process.
Since we’re already talking about removing constraints and limitations from the past, why don’t we talk about another long-standing NServiceBus requirement…
What if you didn’t need IHandleMessages<T>?
🔗How to define a handler?
Since the beginning of time, message handlers have been classes that implement IHandleMessages<T>. It doesn’t just start with I because it’s an interface, it’s like the class literally says “I handle messages of type T”. 1
But some people find IHandleMessages<T> a bit constraining. An interface is a contract, and a contract is not flexible.
If you wanted to inject services from the dependency injection container into your handler, well, too bad. That method signature wouldn’t fit the interface contract. You want to add a CancellationToken to the end of the Handle method to support cooperative cancellation? Can’t do that.
The CancellationToken use case is so strong that we flirted with the idea of convention-based handlers when we were working on cooperative cancellation in NServiceBus 8. We needed to make the cancellation token available to message handlers, and the prevailing pattern is to add CancellationToken as the last parameter of a method. There was even an existing Roslyn analyzer CA2016 with a code fix that forwards the token to methods that accept it. Still, we absolutely did not want to break IHandleMessages and force everyone to change the signature of all of their message handlers. 2
In the end, adopting convention-based handlers proved to be too much scope for the cooperative cancellation effort. 3 Instead, we added CancellationToken as a property of IMessageHandlerContext and created our own Roslyn analyzer and code fix to help you identify and forward cancellation tokens from the message context to methods you call, the same as CA2016 does natively.
But for NServiceBus 10, our work on explicit handler registration to support multiple endpoints had us “in the neighborhood,” so to speak, and we decided the time for convention-based handlers was now.
🔗By convention
This is how a message handler is defined using IHandleMessages<T>:
public class MyHandler : IHandleMessages<MyMessage>
{
public async Task Handle(MyMessage message, IMessageHandlerContext context)
{
// …
}
}
In NServiceBus 10, there is a Roslyn analyzer diagnostic NSB0022 4 which includes a code fix that offers to mark the handler with a new [Handler] attribute that supports the source-generated handler registration APIs:
[Handler]
public class MyHandler : IHandleMessages<MyMessage>
{
public async Task Handle(MyMessage message, IMessageHandlerContext context)
{
// …
}
}
The attribute is required for source generation because running source generators off of implemented interfaces is a Really Bad Idea®, in terms of performance, that you should Never Ever Do. But we thought, why require a class to identify itself as a handler twice, once through the interface, and once by attribute?
So now, you can remove the interface, and you have a convention-based handler:
[Handler]
public class MyHandler
{
public async Task Handle(MyMessage message, IMessageHandlerContext context)
{
// …
}
}
It’s important to note that convention-based handlers will not be discovered by assembly scanning and must be registered using the APIs that the attribute enables.
But now that the interface constraints are gone, we can be more flexible and include additional types. All of this is now possible:
[Handler]
public class MyHandler
{
public async Task Handle(MyMessage message,
IMessageHandlerContext context,
IDatabaseService database,
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
CancellationToken cancellationToken)
{
…
}
}
Basically, CancellationToken plus anything present in the dependency injection container is now fair game in a convention-based handle method.
🔗What about scaffolding?
One of the nice things interfaces still have going for them is that they allow auto-scaffolding, but we’ve got you covered here, too.
When you add IHandleMessages<SomeMessage> to a class declaration, your editor will offer to implement the missing members for you, 5 which is great if you hate useless typing like me. But how does this work if you dump IHandleMessages and want to decorate with [Handler] instead?
We added a Roslyn analyzer and code fix pair that makes this just as easy as the interface-led design. First, you create your empty class and mark it with the [Handler] attribute, as I’ve done here in Rider:
When you look at the available fixes, you can see there are actually two fixes available to choose from:

One will implement IHandleMessages, while the other will add a convention-based handle method. Picking the convention-based option gets you a scaffolded method 6 that includes a message, the message handler context, and a cancellation token.

Of course, you’re free to add additional dependencies as you like. There’s no stuffy interface to hold you back!
🔗Why not sagas?
For now, there are no convention-based Handle methods in sagas. You still need to use IHandleMessages<T> and IAmStartedByMessages<T> to identify message types handled by the saga and implement those interfaces the same way you always have.
Sagas are just more inherently complex. You have to identify the saga data type. The distinction between the two interfaces, which controls which messages can create a new saga, is important. You need to be able to access saga data and operations, such as requesting timeouts, from the saga base type. The saga internals need to be able to identify which message properties identify the correct saga data instance. And lastly, all the persistence libraries need to understand the entire saga metadata so they can perform tasks like SQL Persistence script generation.
For now, that’s a bridge too far. But we’re interested in your feedback. If you love convention-based handlers and want us to extend them, we may revisit sagas at a later date.
🔗Summary
Sometimes you can’t keep a good idea down.
Although convention-based handlers didn’t make it into NServiceBus version 8, they’re now available starting in NServiceBus version 10.2.
Whether you want to have a CancellationToken directly in your Handle method, or inject dozens of different services from DI, 7 the IHandleMessages<T> interface no longer stands in your way.
Check out the new documentation on convention-based handlers, as well as the documentation on registering handlers and sagas for more info on how everything works.
More from the NServiceBus 10 Improvements series:
- Preparing NServiceBus for trimming and AOT
- Redesigning the NServiceBus pipeline yet again: The nerd sniping of Daniel Marbach
- Multiple endpoint hosting
- Convention-based message handlers (this post)
- Next post coming August 4
By the way, some people really love this, but there are others that really hate it. It doesn't exactly rise to the level of tabs vs. spaces, but feelings on both sides are very strong.
We already had to do that once in NServiceBus 6 to support async/await. We were forced to change from synchronous
void Handle(TMessage)toTask Handle(TMessage, IMessageHandlerContext), and it was so painful we never want to force that kind of change on our users again if we can help it.That was already a gargantuan project that required changes to nearly every single async method in every one of our repos.
A separate diagnostic
NSB0025does the same for sagas.I just realized how old I am getting when I recalled that Visual Studio didn't always do that. You needed to get ReSharper for even that simple refactoring.
It even adds the required namespaces!
Oh my gosh, please don't, refactor that mess!