Autofac + NServiceBus + IStartable – making them play nicely

As part of a recent project I was installing NServiceBus as a replacement for Nimbus in an existing legacy system. The system makes use of Autofac for DI and adds various IStartable services to the container (background services etc).

The goal – to continue using Autofac as the DI container for both the application and the NServiceBus message handlers, as well as allow the IEndpointInstance (bus) to be injected into / consumed by IStartable services in the container. Removing the existing usages of IStartable wasn’t really a viable option due to the size of the codebase / time constraints.

Here’s how I tackled it:

public static class BusRegisterExtensions
{
    public static void RegisterBusEndpoint(this ContainerBuilder builder, string endpointName)
    {
        builder.Register(context =>;
            {
                var busConnectionStringSetting = context.Resolve();
                var endpointConfig = CreateEndpointConfiguration(endpointName, busConnectionStringSetting);
 
                return new StartableEndpointInstanceProxy(endpointConfig, context.Resolve<ILifetimeScope>());
            })
            .As<IStartable>()
            .As<IEndpointInstance>()
            .SingleInstance();
    }
 
    public static EndpointConfiguration CreateEndpointConfiguration(string endpointName, BusConnectionStringSetting busConnectionString)
    {
        // ...
    }
}
 
internal class StartableEndpointInstanceProxy : IEndpointInstance, IStartable
{
    private readonly EndpointConfiguration _endpointConfig;
    private readonly ILifetimeScope _scope;
    private IEndpointInstance _instance;
 
    public StartableEndpointInstanceProxy(EndpointConfiguration endpointConfig, ILifetimeScope scope)
    {
        _endpointConfig = endpointConfig;
        _scope = scope;
    }
 
    public void Start()
    {
        _endpointConfig.UseContainer(customizations =>
        {
            customizations.ExistingLifetimeScope(_scope);
        });
 
        _instance = Endpoint.Start(_endpointConfig).ConfigureAwait(false).GetAwaiter().GetResult();
    }
 
    // Just proxy all methods to the endpoint
    #region Implementation of IMessageSession
 
    public async Task Send(object message, SendOptions options)
    {
        await _instance.Send(message, options);
    }
 
    public async Task Send(Action messageConstructor, SendOptions options)
    {
        await _instance.Send(messageConstructor, options);
    }
 
    // .. snip (omitted for brevity)
 
    #endregion
}

The idea behind the StartableEndpointInstanceProxy is to have the bus automatically started when the container is built (implements IStartable) whilst also passing the lifetime scope to the bus (so the bus message handlers resolve components from the Autofac container).

It creates the NServiceBus endpoint and then delegates the implementation of IEndpointInstance to it (implemenntation omitted for brevity).

This allows other IStartable services to depend on the bus (IEndpointInstance) – Autofac specifies that IStartable dependencies will be started prior to any IStartable dependants.

We can register a type such as StartupSender to the container and everything “Just Works” (TM)

public class StartupSender : IStartable
{
    private readonly IEndpointInstance _bus;
 
    public StartupSender(IEndpointInstance bus)
    {
        _bus = bus;
    }
 
    public void Start()
    {
        Task.Run(() =>; Send());
    }
 
    private async Task Send()
    {
        var evt = new StartupEvent()
        {
            Sent = DateTimeOffset.Now,
            Instance = Environment.MachineName,
        };
 
        await _bus.Publish(evt);
    }
}

StartupSender declares a dependency on IEndpointInstance (i.e. the bus). Because our registered implementation of IEndpointInstance is an IStartable and StartupSender depends on StartableEndpointInstanceProxy, the code in StartableEndpointInstanceProxy::Start (which creates the actual endpoint and configures it to use the lifetime scope from the Autofac container) will run before the StartupSender::Start code – publishing to the bus will therefore succeed.

Leave a Reply