NullReferenceException when resolving ITransactionalSession

As usual, isolating the problem allowed me to find it and resolve it.

The resolution failure in Program.Main was a red herring. I’m 100% sure what is happening, but the endpoint is not quite ready when that resolution happens. Introducing a short Task.Delay allowed it to resolve as expected. In any case, that symptom can be ignored.

The original cause of the issue is that we are using the transactional session from a BackgroundService. Once I realized that, the problem became more obvious - the hosted service is a singleton and the transactional session and the db context that support it are scoped.

The fix was for me to remove both of those dependencies from the background service’s constructor and instead use service location.

There may already be a sample or other guidance for using transactional session from a long-running service, but if not, it would be helpful to have one.

The working code from my repro app looks like this:

using NServiceBus.TransactionalSession;

namespace TransactionalSessionRepro;

public class MyHostedService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public MyHostedService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
        int id = 1;

        while (true)
        {
            await timer.WaitForNextTickAsync(stoppingToken);

            await using var scope = _serviceProvider.CreateAsyncScope();
            var transactionalSession = scope.ServiceProvider.GetRequiredService<ITransactionalSession>();
            
            // it is important to open the transactional session BEFORE resolving the db context
            // if the session is not opened before the db context is resolved, NSB will not 
            // hook itself into the persistence pipeline
            await transactionalSession.Open(new SqlPersistenceOpenSessionOptions(), stoppingToken);

            var context = scope.ServiceProvider.GetRequiredService<SomeDbContext>();

            for (int i = 0; i < 10; i++)
            {
                var data = new MyData
                {
                    Id = id++,
                    Value = i.ToString()
                };
                await context.AddAsync(data, stoppingToken);
            }

            await transactionalSession.Commit(stoppingToken);
        }
    }
}