NHibernate Listener access to message context in NServiceBus 7.2

We are upgrading from version 5 to the latest version and as you know IBus is not available from the container any more and we have been passing it through the Handle method in IHandleMessages<>. However, currently (using the old version) we have been accessing message content (headers such as correlation id or user details) to persist data using NHibernate EventListeners such as IPreUpdateEventListener. However, in the new version we don’t have access to IBus and cannot add IMessageHandlerContext to the container either. Is there any way to overcome this issue? I need to be able to read message headers from these event listeners and publish other events.

Are NHibernate EventListeners resolved from the container or created “magically” by NHibernate?

No they are set when configuring NHibernate:

new Fluently().ConfigureDatabase(connectionStringName, SetAuditGlobalProperties, logSqlToConsole)
                .ConfigureCache<SysCacheProvider>()
                .ConfigureMappingsAndConventions(domainRegistration, clientConfiguration)
                .ExposeConfiguration(c =>
                {
                    var eventFiringListener = new EventFiringListener(container, messageRegistration.RegisteredEvents);
                    c.SetListeners(ListenerType.PreInsert, new IPreInsertEventListener[] { new SetEntityBasePropertiesListener(userNameProvider, container) });
                    c.SetListeners(ListenerType.PreUpdate, new IPreUpdateEventListener[] { new SetEntityBasePropertiesListener(userNameProvider, container), eventFiringListener });
                    c.SetListeners(ListenerType.PreDelete, new IPreDeleteEventListener[] { eventFiringListener });
                    c.SetListeners(ListenerType.PostInsert, new IPostInsertEventListener[] { eventFiringListener });
                    exposeConfiguration?.Invoke(c);
                }).BuildConfiguration();

Do you have any solution for accessing the IMessageHandlerContext there to publish events and read message headers?

Yeah. What you can do it create a behavior for the invoke handler context that captures the headers you need and stores them in a static field using AsyncLocal. AsyncLocal will ensure you can access the instance of the headers that match the current message being processed.

I strongly suggest not making the whole IMessageHandlerContext available as async local because you can easily create a lot of code that depends on it while you should strive to write your new code in such a way that does require the async local and depends on passing the message handling context as an argument. So keep the async local data to bare minimum required by the NHibernate listeners.

I have tried that but it is occasionally null. Do I need to add that behaviour at the top of my pipeline?

The behavior can be anywhere in the pipeline as all you need is the headers. If you need message body it would have to be in the logical receive part. Here is an example of such a behavior that is used to pass a header to NHibernate’s connection provider via AsyncLocal. It is part of the multi-tenant sample.

I have already done that but it is still null in event listeners

      `  configuration.Pipeline.Register(typeof(ContextBehaviour), "Injects context handler into context accessor");`

public class ContextBehaviour : Behavior<IInvokeHandlerContext>
    {
        public override async Task Invoke(IInvokeHandlerContext context, Func<Task> next)
        {
            ContextAccessor.Set(context);
            
            await next();

        }
        public class Registration : RegisterStep
        {
            public Registration()
                : base(stepId: "ContextBehaviour", behavior: typeof(ContextBehaviour), description: "Sets IMessageHandlerContext for ContextAccessor")
            {
            }
        }



    }

 public static class ContextAccessor 
    {
        private static AsyncLocal<IMessageHandlerContext> messageHandlerContext = new AsyncLocal<IMessageHandlerContext>();
        
        public static IMessageHandlerContext Get() => messageHandlerContext.Value;

        public static void Set(IMessageHandlerContext context) => messageHandlerContext.Value = context;
    }




private async Task FireEventAsync(IIncomingPhysicalMessageContext context,    IDictionary<string, ConstructorInfo> events, Type entityType, string eventSuffix, int entityId)
        {
            var eventName = entityType.Name + eventSuffix;
            ConstructorInfo constructorInfo;

            if (!events.TryGetValue(eventName, out constructorInfo))
            {
                return;
            }

            var @event = constructorInfo.Invoke(new object[] { entityId });
            await ContextAccessor.Get().Publish(@event); // This returns an exception because .Get() returns null

        }

I am really out of ideas why this doesn’t work! I wonder if it is related to NHibernate event listeners running on a different thread?

Do they? Last time I looked at NHibernate all the events surrounding the session lifecycle were executed synchronously on the same thread as invoked the ISession APIs. But that might have changes. Indeed, if the events are executed on a separate thread, there is probably no way to pass the context there.

Yes. I mean separate thread from the pipeline in NServiceBus. The IMessageHandlerContext I set via the pipeline is null in event listeners. I am trying other ways but the breaking changes in NSB upgrades has caused us so much trouble.

@rezajp

Hmmm there is one thing I don’t understand. If the NHibernate event handlers are invoked on a separate thread then this should not work in NServiceBus 5 as well.

It was irrelevant in version 5 because IBus was available in the container.

If I’m not mistaken v5 used a thread storage to bind the ambivalence of IBus and resolved that from the container. So that means the could there was always executed outside the IBus that represents the message handling part and therefore can be compared to using message session.

@rezajp

So how did you use to pass the Bus to the NHibernate components in the V5 version?

In v5 we had an instance of IBus which was resolved from the container. @danielmarbach may be right. I am not 100% sure about it.
Currently I have solved the issue by intercepting session.save and setting certain values manually (those which were set in the listener) and raising the events using messagehandlercontext.
I have a new problem however, that is now I am getting errors as I have an IIdConvention in nhibernate config to generate the IDs for my entities using some custom sql scripts but all this runs using a different session/transaction so it doesn’t work i.e. errors that the transaction is complete or deadlocked