NullReferenceException when resolving ITransactionalSession

I am trying to use transactional session to publish events. My application fails during start up when it tries to resolve the ITransactionalSession.

No TransportInfrastructure is registered, so a NRE is thrown when TransportAddressResolver.ToTransportAddress gets called.

I am using SQL persistence (with EF8), the outbox, and the SQS transport. My project references these NSB nuget packages:

  1. NServiceBus
  2. NServiceBus.AmazonSQS
  3. NServiceBus.Extensions.Hosting
  4. NServiceBus.Persistence.Sql
  5. NServiceBus.Persistence.Sql.TransactionalSession
  6. NServiceBus.TransactionalSession

The relevant section of configuration code looks like this:

endpoint.EnableOutbox();
var persistence = endpoint.UsePersistence<SqlPersistence>();
persistence.ConnectionBuilder(() => new NpgsqlConnection(connectionString));
persistence.EnableTransactionalSession();
var dialect = persistence.SqlDialect<SqlDialect.PostgreSql>();
dialect.JsonBParameterModifier(modifier: parameter =>
    {
        var npgsqlParameter = (NpgsqlParameter)parameter;
        npgsqlParameter.NpgsqlDbType = NpgsqlDbType.Jsonb;
    });

I am resolving the ITransactionalSession instance directly in Program.cs after all bootstrapping completes:

using var x = app.Services.CreateScope();
var y = x.ServiceProvider.GetRequiredService<ITransactionalSession>(); // throws NRE as described above
var z = x.ServiceProvider.GetService<TransportInfrastructure>(); // returns null

My google-fu isn’t turning anything up that explains this, but I suspect I’m missing a step somewhere in the configuration, but I haven’t found it yet. Any pointers are very much appreciated.

Hi @cbristol,

Looking at the first code snippet it seems as if you’ve built the host and resolved ITransactionSession before finishing NServiceBus configuration. Is this a copy-past error?

Secondly, could you share a bit bigger chunk of your code or ideally a reproduction for the NRE?

Finally, could you share why you are trying to resolve TransportInfrastructure instance?

Cheers,
Tomek

Hi @tmasternak - To answer the first question, you are correct; that was a copy-paste error. I’ve edited the snippet to remove those lines.

For the second question, I do not need to resolve TransportInfrastructure, but when the line above runs to resolve ITransactionalSession, it throws due to TransportInfrastructure not being registered. The call to resolve TransportInfrastructure is only there as confirmation that it is not being correctly resolved.

It will take me a bit to distill the configuration code down, but I will try to provide an update on that later today.

Hi @cbristol,

Do you have any update?

Best,
Nisha

Not yet - I tried to create a reproduction project, but it is working as expected. I’m trying to track down the difference between the real application and the repro, but I’ve been pulled into another project for the last couple of days. I hope to have an update soon.

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);
        }
    }
}

Hi @cbristol,

thank you for sharing the results of your investigation.

I’ve captured your feedback as an improvement suggestion to our documentation Feedback: 'Using TransactionalSession with Entity Framework and ASP.NET Core' · Issue #6779 · Particular/docs.particular.net · GitHub.