Search code examples
nservicebus

How to inject dependency into NServiceBus pipeline behavior?


I've been following the NServiceBus samples, specifically for how to use an entity framework (core) DbContext integrated with Sql Persistence so that I can save dbcontext state changes along with the outbox messages. This is the sample: https://docs.particular.net/samples/entity-framework-core/

I've modified the unit of work code a little to support creation of an aspnet core DI scoped DbContext. The relevant code follows:

    public class UnitOfWork<TDbContext>
        where TDbContext : DbContext
    {
        private Func<SynchronizedStorageSession, IServiceProvider, TDbContext> _contextFactory;
        private TDbContext _context;
        private IServiceProvider _serviceProvider;

        public UnitOfWork(Func<SynchronizedStorageSession, IServiceProvider, TDbContext> contextFactory, IServiceProvider serviceProvider)
        {
            _contextFactory = contextFactory;
            _serviceProvider = serviceProvider;
        }

        public TDbContext GetDataContext(SynchronizedStorageSession storageSession)
        {
            if (_context == null)
            {
                _context = _contextFactory(storageSession, _serviceProvider);
            }
            return _context;
        }
    }

    public class UnitOfWorkSetupBehavior<TDbContext> : Behavior<IIncomingLogicalMessageContext>
        where TDbContext : DbContext
    {
        private readonly Func<SynchronizedStorageSession, IServiceProvider, TDbContext> _contextFactory;
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public UnitOfWorkSetupBehavior(Func<SynchronizedStorageSession, IServiceProvider, TDbContext> contextFactory, IServiceScopeFactory serviceScopeFactory)
        {
            _contextFactory = contextFactory;
            _serviceScopeFactory = serviceScopeFactory;
        }

        public override async Task Invoke(IIncomingLogicalMessageContext context, Func<Task> next)
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                var uow = new UnitOfWork<TDbContext>(_contextFactory, scope.ServiceProvider);
                context.Extensions.Set(uow);

                await next().ConfigureAwait(false);

                context.Extensions.Remove<UnitOfWork<TDbContext>>();
            }
        }
    }

    public static class EndpointConfigurationExtensions
    {
        public static void RegisterUnitOfWork<TDbContext>(this EndpointConfiguration endpointConfiguration, IServiceScopeFactory serviceScopeFactory)
            where TDbContext : DbContext
        {
            var pipeline = endpointConfiguration.Pipeline;
            pipeline.Register(new UnitOfWorkSetupBehavior<TDbContext>((storageSession, serviceProvider) =>
            {
                var dbConnection = storageSession.SqlPersistenceSession().Connection;
                var dbContextFactory = serviceProvider.GetService<IDbContextConnectionFactory<TDbContext>>();
                var dbContext = dbContextFactory.GetDbContext(dbConnection);

                //Use the same underlying ADO.NET transaction
                dbContext.Database.UseTransaction(storageSession.SqlPersistenceSession().Transaction);

                //Call SaveChanges before completing storage session
                storageSession.SqlPersistenceSession().OnSaveChanges(x => dbContext.SaveChangesAsync());

                return dbContext;
            }, serviceScopeFactory), "Sets up unit of work for the message");
        }
    }

    public static class UnitOfWorkContextExtensions
    {
        public static TDbContext DataContext<TDbContext>(this IMessageHandlerContext context)
            where TDbContext : DbContext
        {
            var uow = context.Extensions.Get<UnitOfWork<TDbContext>>();
            return uow.GetDataContext(context.SynchronizedStorageSession);
        }
    }

For this to work the behavior needs an injected IServiceScopeFactory.

Now all examples I've been able to find of behavior registration only show the type manually instantiated and passed in to the endpointconfiguration's pipeline.

Is there a way to either gain access to an IServiceScopeFactory via the behavior's Invoke method (maybe by the context via some extension perhaps?), or is it possible to register the behavior itself such that I can construct it with services created by the DI container?

FYI I took a look at this Q&A which gave me the idea of injecting the IServiceScopeFactory. Unfortunately, the answer doesn't show how to actually get an instance of the interface.


Solution

  • You would use context.builder.Build<T>(); within the Invoke method to resolve any objects like IServiceScopeFactory.

    Make sure that the IServiceScopeFactory is registered in the DI container. For example, during your endpoint initialization:

    endpointConfiguration.RegisterComponents(registration: x =>
        {
            x.ConfigureComponent<IServiceScopeFactory>(yourServiceScopeFactory);
        });
    

    You can also do this by creating a Feature