I’m currently working through endpoint hosting scenarios in Azure, and right now the biggest clunkiness is shimming in NServiceBus into an ASP.NET Core application.
Where is my services.AddNServiceBus? It’s pretty straightforward with IHostedService, but it’s a lot of boilerplate code for the community extension. I’d expect at the least something like:
There’s some other problems too - I don’t have access to an outbox in web applications, so I have to make my own version of it, like domain events, then re-send those messages out later.
Historically NServiceBus always started from the assumption that it owns the container. This is causing issues, and headaches, with the way ASP.Net Core bootstraps applications.
We have a spike that splits the endpoint “startup” sequence into 2 main steps, where only the second one requires a built and configured container. The spike intends to demonstrate that we can add this in a non-breaking way, so that it can be released in a minor.
Once we have that ready an battle-tested we can start working on adding the IServiceCollection/IApplicationBuilder cosmetic things, that exactly as you suggest will probably be services.AddNServiceBus(...) and app.UseNServiceBus(...).
using System.Runtime.ExceptionServices;
using NServiceBus;
class SessionAndConfigurationHolder
{
public SessionAndConfigurationHolder(EndpointConfiguration endpointConfiguration)
{
EndpointConfiguration = endpointConfiguration;
}
public EndpointConfiguration EndpointConfiguration { get; }
public IMessageSession MessageSession { get; internal set; }
public ExceptionDispatchInfo StartupException { get; internal set; }
}
using System;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NServiceBus;
class NServiceBusService : IHostedService
{
public NServiceBusService(SessionAndConfigurationHolder holder)
{
this.holder = holder;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
endpoint = await Endpoint.Start(holder.EndpointConfiguration).ConfigureAwait(false);
}
catch (Exception e)
{
holder.StartupException = ExceptionDispatchInfo.Capture(e);
return;
}
holder.MessageSession = endpoint;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (endpoint != null)
{
await endpoint.Stop().ConfigureAwait(false);
}
holder.MessageSession = null;
holder.StartupException = null;
}
readonly SessionAndConfigurationHolder holder;
IEndpointInstance endpoint;
}
using System;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using NServiceBus;
public static class AddNServiceBusServiceCollectionExtensions
{
public static IServiceCollection AddNServiceBus(this IServiceCollection services, string endpointName, Action<EndpointConfiguration> configuration)
{
var endpointConfiguration = new EndpointConfiguration(endpointName);
configuration(endpointConfiguration);
return services.AddNServiceBus(endpointConfiguration);
}
static IServiceCollection AddNServiceBus(this IServiceCollection services, EndpointConfiguration configuration)
{
var management = new SessionAndConfigurationHolder(configuration);
services.AddSingleton(provider =>
{
if (management.MessageSession != null)
{
return management.MessageSession;
}
SpinWait.SpinUntil(() => management.MessageSession != null || management.StartupException != null);
management.StartupException?.Throw();
return management.MessageSession;
});
services.AddSingleton(configuration);
services.AddSingleton(management);
services.AddHostedService<NServiceBusService>();
return services;
}
}
It uses SpinWait to compensate for the difference between how webhost starts hosted services vs how the generic host starts hosted services. The web host starts hosted services concurrent to the web api pipeline which can mean you serve web request before the endpoint is initialized
But like Mauro mentioned we’ll smooth out the DI edges first and then provide something official.