NHibernate Listener access to message context in NServiceBus 7.2

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

Can you share the code of your custom convention? Alternatively if you can’t share your code in public you can raise a support ticket.

@SzymonPobiega this is the code. It is an NHibernate IIdConvention

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common;
using Dialects;
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.AcceptanceCriteria;
using FluentNHibernate.Conventions.Inspections;
using FluentNHibernate.Conventions.Instances;
using NHibernate.Cfg;
using NHibernate.Dialect;
using NHibernate.Mapping;

public class CustomIdentityHiLoGeneratorConvention : IIdConvention, IIdConventionAcceptance
{
    public const string NextHiValueColumnName = "NextHiValue";
    public const string NHibernateHiLoIdentityTableName = "NHibernateHiLoIdentity";
    public const string TableColumnName = "Entity";

    private static readonly int IdBatchSize = ConfigurationHelpers.GetAppSettingAsInt("NHibernate.HiLoIdBatchSizeOverride") ?? 50;

    public void Accept(IAcceptanceCriteria<IIdentityInspector> criteria)
    {
        criteria.Expect(x => x.Type == typeof(int) || x.Type == typeof(uint) || x.Type == typeof(long) || x.Type == typeof(ulong)).Expect(x => x.Generator.EntityType == null);
    }

    public void Apply(IIdentityInstance instance)
    {
        instance.GeneratedBy.HiLo(
            NHibernateHiLoIdentityTableName,
            NextHiValueColumnName,
            IdBatchSize.ToString(),
            builder => builder.AddParam("where", $"{TableColumnName} = '{instance.EntityType.Name}'"));
    }

    public static void CreateHighLowScript(Configuration config, int idBatchSize)
    {
        var initialHiLoBatchNumber = 1000000 / idBatchSize;

        var schemaGroups = config.ClassMappings.Where(PersistentClassIsHilo).GroupBy(x => x.Table.Schema).ToArray();
        var script = new StringBuilder();

        foreach (var schemaGroup in schemaGroups)
        {
            CreateHiLoScriptForSchema(script, schemaGroup, initialHiLoBatchNumber);
        }

        config.AddAuxiliaryDatabaseObject(new SimpleAuxiliaryDatabaseObject(
            script.ToString(),
            null,
            new HashSet<string> { typeof(MsSql2000Dialect).FullName, typeof(MsSql2005Dialect).FullName, typeof(MsSql2008WithFunctionsDialect).FullName }));
    }

    private static bool PersistentClassIsHilo(PersistentClass persistentClass)
    {
        var identifier = persistentClass.Identifier as SimpleValue;

        if (identifier == null)
        {
            return false;
        }

        return identifier.IdentifierGeneratorStrategy == "hilo";
    }

    private static void CreateHiLoScriptForSchema(StringBuilder script, IGrouping<string, PersistentClass> schemaGroup, int initialHiLoBatchNumber)
    {
        var fullTableName = schemaGroup.Key == null ? NHibernateHiLoIdentityTableName : $"{schemaGroup.Key}.{NHibernateHiLoIdentityTableName}";
        script.AppendFormat("DELETE FROM {0};", fullTableName);
        script.AppendLine();
        script.AppendFormat("ALTER TABLE {0} ADD {1} VARCHAR(128) NOT NULL;", fullTableName, TableColumnName);
        script.AppendLine();
        script.AppendFormat("CREATE NONCLUSTERED INDEX IX_{1}_{2} ON {0} (Entity ASC);", fullTableName, NHibernateHiLoIdentityTableName, TableColumnName);
        script.AppendLine();
        script.AppendLine("GO");
        script.AppendLine();

        foreach (var tableName in schemaGroup.Select(m => m.Table.Name).Distinct())
        {
            script.AppendFormat($"INSERT INTO {fullTableName} ({TableColumnName}, {NextHiValueColumnName}) VALUES ('{tableName}',{initialHiLoBatchNumber});");
            script.AppendLine();
        }
    }
}

So the script appears to be executed at deploy time, not at runtime so how does it affect NServiceBus? Do you mean that NHibernate’s HiLo ID generation strategy does not work when used inside NServiceBus?

Apologies I missed this reply. This runs in every insert to establish the new record primary key value based on the current values in the table. It does work but occasionally fails due to deadlock or saying transaction is closed. I think it is due to the new changes in session creation in NServiceBus Nhibernate plugin. I am not sure.

Hi

Can you include the stack trace from the logs when the problem happens? I am not sure how exactly NHibernate executes the query to fetch the Hi value for the HiLo algorithm. The issue might be either on NHibernate or NServiceBus side. Hard to tell at this point where.

Also, what transaction level do you use in NServiceBus? Default TransactionScope?

Szymon

I am not sure. It executes that at every insert to evaluate the new ID.

Regarding TransactionScope, I believe it is the default. I had to remove unitOfWork.WrapHandlersInATransactionScope because it was erroring. Something about Outbox but I don’t remember exactly.

We are still blocked by the fact that we cannot access the IMessageHandlerContext in the EventListeners for NHibernate. I have been digging around this for days and I tried various workarounds but it seems impossible to fix it without being able to publish events inside the listener (same correlation id etc). I tried intercepting the saves/updates and publish the events myself but the problem is with cascading inserts/updates now.
Do you have any recommendations?