NService... umm, where's the bus?
This post is part of a series describing the improvements in NServiceBus 6.0.
After many years of service, we bid our trusty IBus
farewell. It’s served us well almost since the very beginning, providing access to many operations like sending messages, subscribing to events, and even manipulating headers.
Over the years, the IBus
interface has become like a kitchen knife block, with knives for paring, chopping, cutting steak, and maybe even a pair of herb scissors. Although it’s convenient for storing many knives and makes them easily accessible, you don’t need all of them when you’re having a steak dinner. So it is with IBus
. Methods like Reply
and ForwardCurrentMessageTo
are always available on the interface but only make sense in the context of handling an incoming message. If you use them in other scenarios, such as during endpoint startup, they may throw an exception.
With version 6 of NServiceBus, we’ve removed IBus
entirely and replaced it with focused, context-specific interfaces that provide clear guidance on what can and can’t be done based on the methods they expose. This ensures that, in any given circumstance, you’ll have the exact tools needed for the job and nothing else cluttering up your workspace.
🔗Handlers
When implementing IHandleMessages<T>
, you’ll notice an additional parameter on the Handle
method. This parameter is of type IMessageHandlerContext
, which exposes methods such as Publish
and Send
. More importantly, the IMessageHandlerContext
type also excludes methods like Subscribe
and Unsubscribe
— actions that you really shouldn’t be performing in a message handler anyway. Migrating your handlers should only require replacing IBus
with the newly provided IMessageHandlerContext
. For example:
public class v5_MyMessageHandler : IHandleMessages<MyMessage>
{
private IBus bus;
public MyMessageHandler(IBus bus)
{
this.bus = bus;
}
public void Handle(MyMessage message)
{
var messageId = bus.CurrentMessageContext.Id;
bus.Publish<MyEvent>(e => e.Value = messageId);
}
}
becomes
public class v6_MyMessageHandler : IHandleMessages<MyMessage>
{
public async Task Handle(MyMessage message, IMessageHandlerContext context)
{
await context.Publish<MyEvent>(e => e.Value = context.MessageId);
}
}
As you can see, in addition to the removal of IBus
in version 6 of NServiceBus, the API for handling messages is now async
as well. More information on this change can be found in our documentation on asynchronous handlers.
🔗Moving off IBus
As you can imagine, removing IBus
introduces many changes. For example, you no longer have to inject IBus
into your classes using dependency injection. Instead, you’re provided with a context-specific object in the method you’re working with. Let’s take a look at how much simpler our endpoint startup and shutdown code can be without IBus
:
public class v5_Startup : IWantToRunWhenBusStartsAndStops
{
private readonly IBus bus;
public Startup(IBus bus)
{
this.bus = bus;
}
public void Start()
{
bus.Publish<MyEvent>();
}
public void Stop()
{
}
}
becomes:
public class v6_Startup : IWantToRunWhenEndpointStartsAndStops
{
public async Task Start(IMessageSession session)
{
await session.Publish<MyEvent>();
}
public async Task Stop(IMessageSession session)
{
}
}
The IMessageSession
parameter presents you with just the operations that are valid at this specific extensiblity point.
🔗Endpoint configuration
The NServiceBus configuration API remains very similar, but it underwent some renaming. Instead of an IBus
, you receive an IEndpointInstance
, which offers all available bus operations outside the message processing pipeline. For example, here is how you would initialize an endpoint prior to sending a message or publishing an event:
BusConfiguration v5_busConfiguration = new BusConfiguration();
// other endpoint configuration code goes here
IStartableBus v5_startableBus = Bus.Create(v5_busConfiguration);
IBus v5_bus = v5_startableBus.Start();
//use v5_bus to .Send and/or .Publish as required
becomes
EndpointConfiguration v6_endpointConfiguration = new EndpointConfiguration();
// other endpoint configuration code goes here
IEndpointInstance v6_endpoint = await Endpoint.Start(v6_endpointConfiguration);
//use v6_endpoint to .Send and/or .Publish as required
Note that with the removal of IBus
, Bus.Create
has been replaced with the Endpoint.Start
method, which allows you to start your endpoint with less ceremony.
🔗Impact on Dependency Injection
In previous versions of NServiceBus, the IBus
would be registered in the IoC container automatically. With IBus
no longer in use, this registration doesn’t need to happen. Additionally, the new context-specific interfaces will not be registered in the IoC container, either. Instead, as we saw in the samples above, they will be provided to the NServiceBus extensibility points as parameters.
That said, if you do need access to an IEndpointInstance
from an IoC container—for example, if you need to send a message from an ASP.NET MVC Controller or a WPF ViewModel—it can be registered as shown here using the Ninject container:
IEndpointInstance endpoint = await Endpoint.Start(config);
kernel.Bind<IEndpointInstance>().ToConstant(endpoint);
Once the object instance is registered with the container, IEndpointInstance
will be available via dependency injection wherever you may need it:
public class DefaultController :
Controller
{
IEndpointInstance endpoint;
public DefaultController(IEndpointInstance endpoint)
{
this.endpoint = endpoint;
}
public ActionResult Index()
{
return View();
}
[AllowAnonymous]
public async Task<ActionResult> Send()
{
await endpoint.Send("Samples.Mvc.Endpoint", new MyMessage())
.ConfigureAwait(false);
return RedirectToAction("Index", "Default");
}
}
🔗Testing within a context
Context-specific interfaces also have a nice effect on the unit testing experience with NServiceBus, enabling the Arrange Act Assert pattern many developers know and love. It changes this…
Test.Handler<MyHandler>()
.ExpectSend<OutgoingMessage>(m => true)
.OnMessage(new IncomingMessage());
…to this:
var handler = new MyHandler();
var testableMessageHandlerContext = new TestableMessageHandlerContext();
await handler.Handle(new IncomingMessage(), testableMessageHandlerContext);
testableMessageHandlerContext.SentMessages.Should().HaveCount(1);
Although this may look like more effort to achieve the same result, this approach provides a lot more testing flexibility. The example above uses the FluentAssertions package to verify results instead of relying on the predefined assertions in the NServiceBus.Testing
package. Also, if you don’t like the provided testing classes, you’re now able to create alternatives using your favorite mocking library.
🔗Summary
We’re really excited about the evolution from the IBus
interface to more contextual ones. Besides making your code more testable, it simplifies your configuration and makes using the API a much richer experience.
So come and see how much easier it is to develop with the new context-specific interfaces in NServiceBus version 6.
Actual knives not included ;-)
🔗Footnotes
About the Author: Tim Bussmann is a developer at Particular Software who loves building intention-revealing APIs. When not at his computer, he enjoys hiking in the mountains in his native Switzerland.