Enhancements in NServiceBus Hosting
The .NET Generic Host has become the de facto method of hosting applications built on .NET Core. To support the generic host, we released the NServiceBus.UseNServiceBus(…)
extension method.
While a massive improvement for easily hosting NServiceBus in a .NET Core process, a few rough edges remained. For version 1.1, we aimed to make it easier than ever.
Let’s have a look at what has changed.
🔗Logging integration
Since the release of the NServiceBus.Extensions.Logging
package, people have been asking how to use their logging framework of choice when hosting endpoints using the .NET Generic Host. This was possible before, but there was a division between the .NET Core logging and the NServiceBus logging that made things difficult.
If you wanted to use a specific logging library, you’d need to add a specific dependency for that library within the NServiceBus code. Additionally, if you wanted to set the logging level to DEBUG, you’d have to do that specifically within the NServiceBus APIs.
Now, any logging using the NServiceBus LogManager
will be automatically forwarded to Microsoft.Extensions.Logging
. So if you want to pick your own logging library, NServiceBus cooperates with whatever the generic host has set up. If you want to set the logging level, you do it the same as you would with any other .NET Core application. There’s no longer any need to add extra dependencies or specifically call NServiceBus APIs to handle things that are supposed to be the host’s job.
This also means that if you’re using the NServiceBus.Extensions.Hosting
package, you don’t need to use the NServiceBus.Extensions.Logging
package or the NServiceBus.MicrosoftLogging
packages anymore. Those are only needed if you are self-hosting an endpoint without using the Generic Host. If you aren’t, you can safely remove both packages with the latest update, and everything will just work without additional dependencies.
🔗Improved message session management for WebAPI and MVC
With the rise of the Generic Host, use of IWebHostBuilder
is discouraged for ASP.NET Core 3.1 and higher.1 We’ve seen many of our customers adopting the generic host to combine the power of NServiceBus with ASP.NET Core. A primary use case is to send messages by using the IMessageSession
interface.
A typical WebAPI controller might look like this:
[ApiController]
[Route("")]
public class SendMessageController : Controller
{
IMessageSession messageSession;
public SendMessageController(IMessageSession messageSession)
{
this.messageSession = messageSession;
}
[HttpPost]
public async Task<string> Post()
{
var message = new MyMessage();
await messageSession.Send(message);
return "Message sent to endpoint";
}
}
In order to get NServiceBus with ASP.NET Core working, you might use the following host builder configuration:
var host = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(c => c.UseStartup<Startup>())
// Not ideal ordering!
.UseNServiceBus(context =>
{
var endpointConfiguration = new EndpointConfiguration("ASPNETCore.Sender");
return endpointConfiguration;
})
Unfortunately, the host builder is order-dependent. What that means for the above example is that because we have ConfigureWebHostDefaults
declared before UseNServiceBus
, the ASPNET Core pipeline is started first and can accept incoming HTTP requests before NServiceBus gets the chance to get started. With the previous version of the package, under those circumstances, the following exception was thrown:
System.InvalidOperationException: The message session can only be used after the endpoint is started.
This occurred as a race condition, based on whether or not incoming HTTP requests attempted to use NServiceBus before it was ready.
In addition to that, it was impossible to recover from this problem at runtime—you had to restart the service. With this update, we made the exception non-permanent, which means HTTP retries done by the client would eventually be able to get a grip on a valid message session once NServiceBus is started. This means that if your app is using the incorrect configuration, it won’t completely hang the entire process one day when requests happen to arrive in an unlucky order.
We also improved the exception message to help you resolve this problem by moving the order of the builder calls like:
var host = Host.CreateDefaultBuilder()
// NServiceBus gets properly configured before accepting web requests
.UseNServiceBus(context => {
var endpointConfiguration = new EndpointConfiguration("ASPNETCore.Sender");
return endpointConfiguration;
})
.ConfigureWebHostDefaults(c => c.UseStartup<Startup>())
When possible, it’s always preferable to make sure that NServiceBus start code precedes the configuration of the web host.
🔗Improved message session management in hosted services
The generic host provides runtime extensibility via hosted services that enable any kind of meaningful work as part of starting the host. One common thing to do in the generic host is to register a hosted service which has access to the message session. The most obvious way to get access to the session is to inject the IMessageSession
instance into the constructor.
class MyHostedService : IHostedService
{
public HostedService(IMessageSession messageSession)
{
this.messageSession = messageSession;
}
}
Unfortunately, this caused a very cryptic exception that didn’t really tell you how to resolve it:
System.InvalidOperationException: The message session can only be used after the endpoint is started.
The only way to access the message session in a hosted service was to inject the service provider and not resolve the session until the StartAsync
method was called:
class MyHostedService : IHostedService
{
public HostedService(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var messageSession = serviceProvider.GetService<IMessageSession>();
await messageSession.Publish(new IAmStartedEvent());
}
}
This is not exactly intuitive, and accessing the service provider directly is not considered best-practice.2 With this release, we made accessing the message session work the way you’d expect it to do, without requiring access to the ServiceProvider. Finally, we can write the code we always wanted to in the first place:
class MyHostedService : IHostedService
{
public HostedService(IMessageSession messageSession)
{
this.messageSession = messageSession; // It works!
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await messageSession.Publish(new IAmStartedEvent());
}
}
Now if you attempt to access the message session too early (for example, in the constructor of the hosted service) or get the order of registration wrong on the host builder configuration, the exception thrown will now show you how to resolve the problem with a more meaningful message:
The message session can’t be used before NServiceBus is started. Place
UseNServiceBus()
on the host builder before registering any hosted service (i.ex.services.AddHostedService<HostedServiceAccessingTheSession>()
) or the web host configuration (i.ex.builder.ConfigureWebHostDefaults
) should hosted services or controllers require access to the session.
No more painful debugging of ordering problems and no more access to the service provider. It will just work or tell you accordingly how to resolve it.
🔗Unit of encapsulation prevents multi-hosting
By design, the generic host assumes it is the unit of encapsulation for the application or service that it hosts. All application resources applied to the same host instance share a single service collection, and therefore, a single service provider. The conclusion of these design assumptions is that a generic host can only host a single service of a specific type by default.
So, for example, you can host ASP.NET Core WebAPI together with NServiceBus, but it wouldn’t be possible to host multiple ASP.NET Core WebAPIs or NServiceBus instances as part of the generic host without doing advanced trickery like overriding controller detection for WebApi, or in case of NServiceBus, heavy customization of assembly scanning and more. What both NServiceBus and WebAPI have in common is that, by default, they assume they own all the assemblies found in the application domain for convenience and ease of use.
Still, once you have experienced the power of the .UseNServiceBus(…)
method you might be tempted to use it to host multiple NServiceBus instances on the same host like this:
var host = Host.CreateDefaultBuilder()
.UseNServiceBus(hostBuilderContext =>
{
var endpointConfiguration = new EndpointConfiguration("MyEndpoint");
// ...
return endpointConfiguration;
})
// Once == good, twice == bad
.UseNServiceBus(hostBuilderContext =>
{
var endpointConfiguration = new EndpointConfiguration("MyOtherEndpoint");
// ...
return endpointConfiguration;
})
In previous versions of the extension, this could not be prevented and would create obstructive runtime behavior like the last endpoint configuration being used overriding the previous configuration.
With this release, a proper safeguard has been put into place which warns about the improper usage.
UseNServiceBus
can only be used once on the same host instance because subsequent calls would override each other. […]
With the broad adoption of continuous deployment pipelines3 and service orchestration mechanisms4 we recommend only one NServiceBus instance per generic host. Multiple endpoints sharing the same generic host drastically increases configuration complexity and couples those endpoints together. For example if one endpoint has high CPU/memory consumption, the other hosted in the same process might starve.
These are the reasons that multiple usage of UseNServiceBus
on the same host is prevented, and we are not planning to implement support for running multiple endpoints via this extension method.
Should you still have good reasons to host multiple endpoints in the same generic host, we recommend multiple generic hosts to achieve proper isolation. Keep in mind, you still need to configure assembly scanning very carefully. Our multi-hosting sample demonstrates how to host multiple endpoints using generic host instances.
🔗Summary
With the changes in NServiceBus.Extensions.Hosting 1.1, the .NET Generic Host has become the easiest and best method of encapsulating and hosting an NServiceBus endpoint in a .NET Core process.
The improved logging integration ensures that your NServiceBus endpoint will defer to whatever logging is set up for the host without any additional dependencies. The improved session management ensures helps you ensure that NServiceBus and the web host are initialized in the correct order. Both API controllers and implementations of IHostedService
can now easily access the NServiceBus IMessageSession
to send messages. And finally, the safety check to prevent multi-hosting makes sure you don’t create an invalid configuration.
To see all this in action, check out our Using NServiceBus in an ASP.NET Core WebAPI Application sample.
Be sure to install our templates package to use NServiceBus.Extensions.Hosting in your own projects.
In the article on the ASP.NET Core Web Host, Microsoft states "This article covers the Web Host, which remains available only for backward compatibility. The Generic Host is recommended for all app types."
Microsoft's Dependency Injection recommendations state "Avoid using the service locator pattern. For example, don't invoke GetService to obtain a service instance when you can use DI instead."Also, see Mark Seemann's post Service Locator is an Anti-Pattern.
Such as GitHub Actions, Octopus Deploy, TeamCity, and dozens of others…
Kubernetes, Service Fabric, Docker Compose, etc.