Accessing saga data (via sql persistence)

We are working on bumping our revision for NServiceBus.Persistence.Sql from 4.5.1 to 6.5.1 with NServiceBus version fixed to 7.2.4.

In some of our integration tests, we peek the saga data using the code pasted below. Unfortunately, this code (we don’t know the origin, the developer who built it has left a while ago) now throws exception as follows when attempting to get a session:

System.NullReferenceException: Object reference not set to an instance of an object.
   at CurrentSessionHolder.SetCurrentSession(StorageSession session) in /_/src/SqlPersistence/SynchronizedStorage/CurrentSessionHolder.cs:line 11
   at SynchronizedStorage.OpenSession(ContextBag contextBag) in /_/src/SqlPersistence/SynchronizedStorage/SynchronizedStorage.cs:line 24

When debugged, we can see it as follows:

Oddly enough, in our application code (not the test code), we can still inject ISynchronizedStorage and pipelineContext.Value is not null (as an experiment).

Our integration test framework spins up the endpoint host and uses the same IoC, it’s really hard to tell what’s wrong.

However, the Sql.Persistence library apparently has changed drastically. So, we are wondering perhaps there is a different way to achieve the same (peek saga data). Or even better, we are missing a now-mandatory initialization step in our implementation. Here’s the code:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NServiceBus;
using NServiceBus.Extensibility;
using NServiceBus.Persistence;
using NServiceBus.Sagas;
using NServiceBus.Transport;

namespace ND3Framework.ServiceBus.NServiceBus7.Sagas
{
    public class GetSagaData : IGetSagaData
    {
        private readonly ISagaPersister _sagaPersister;
        private readonly ISynchronizedStorage _synchronizedStorage;

        public GetSagaData(ISagaPersister sagaPersister,
            ISynchronizedStorage synchronizedStorage)
        {
            _sagaPersister = sagaPersister;
            _synchronizedStorage = synchronizedStorage;
        }

        public async Task<T> Get<T>(Guid id)
        where T : class, IContainSagaData
        {
            using (var session = await GetSession().ConfigureAwait(false))
            {
                var sagaData = await _sagaPersister.Get<T>(id, session, new ContextBag()).ConfigureAwait(false);
                return sagaData;
            }
        }

        public async Task<T> Get<T>(string property, object value) where T : class, IContainSagaData
        {
            using (var session = await GetSession().ConfigureAwait(false))
            {
                var sagaData = await _sagaPersister.Get<T>(property, value, session, new ContextBag()).ConfigureAwait(false);
                return sagaData;
            }
        }

        private async Task<CompletableSynchronizedStorageSession> GetSession()
        {
            var context = new ContextBag();
            context.Set(new IncomingMessage(Guid.NewGuid().ToString(), new Dictionary<string, string>(), new byte[] { }));
            return await _synchronizedStorage.OpenSession(context).ConfigureAwait(false);
        }
    }
}

Any help / clues / insights will be appreciated.

Hi

The issuse you experience is caused by the fact that between 4 and 6 we have added a feature that allows injecting SQL persistence synchronized session context via the DI container. In order to do this we added code to the SynchronizedStorage.cs that uses the class called CurrentSessionHolder to keep track of the current session in an AsyncLocal. That AsyncLocal is initialized by a behavior in the pipeline.

Unfortunately these classes are all internal so you can’t easily initialize the AsyncLocal to make it work.

We have recenly released NServiceBus 7.8.x and SQL persistence 6.6.x. These no longer depend on the AsyncLocal mechanism. Please given them a try if it resolves the problem.

Alternatively, because this is the test code, you can write some reflection code to mimic what the behavior I mentioned does. It would have to be invoked at the beginning of your GetSession() method.

1 Like

Thanks for the reply!

We are now trying to upgrade to 7.8 on the sides. I’d really appreciate if you can clarify what you mean by “reflection code to mimic the behaviour” a bit though. If we can do this in the meantime, that’d be really helpful.

Hi

In order to make the test work, you need to wrap the GetSession() method with this piece:

using (currentSessionHolder.CreateScope())
{
    return GetSession();
}

in order to initialize the AsyncLocal. Unfortunately the CurrentSessionHolder class is internal so you need to create an instance of CurrentSessionHolder via reflection (e.g. using Activator.CreateInstance) and then call the method, also via reflection.

1 Like

Thanks @SzymonPobiega for the pointer. It’s a bit hacky but the following is the working code:

private async Task<CompletableSynchronizedStorageSession> GetSession()
{
    FixSynchronizedStorage();
    var context = new ContextBag();
    context.Set(new IncomingMessage(Guid.NewGuid().ToString(), new Dictionary<string, string>(), new byte[] { }));
    return await _synchronizedStorage.OpenSession(context).ConfigureAwait(false);
}

private void FixSynchronizedStorage()
{
    var currentSessionHolderType = typeof(ISqlStorageSession).Assembly.GetTypes().Single(t => t.Name == "CurrentSessionHolder");
    var currentSessionHolder = Activator.CreateInstance(currentSessionHolderType, true);

    // https://stackoverflow.com/a/3110320/68887
    var createScopeMethod = currentSessionHolderType.GetMethod("CreateScope");
    createScopeMethod.Invoke(currentSessionHolder, null);

    // https://stackoverflow.com/a/6280540/68887
    var prop = _synchronizedStorage.GetType().GetField("currentSessionHolder", BindingFlags.NonPublic | BindingFlags.Instance);
    object boxed = _synchronizedStorage;
    prop.SetValue(boxed, currentSessionHolder);
    currentSessionHolder = (ISynchronizedStorage)boxed;
}

Using the scope with using did not really work, so, I had to explicitly assign the session holder myself. Sharing for others that might come across this issue.

1 Like