NHibernate Listener access to message context in NServiceBus 7.2

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?

Reza,

if the async local doesn’t work and NHibernate really does a thread switch then also your previous code around V5 would have executed on the bus flavor that is not part of the incoming message transaction and therefore messages would go out even if the message was rolled back. Have you verified that?

Like Szymon mentioned with the information we have it is very difficult to help you. Maybe you can package up the code you have into a zip or upload it to some drive and then open a ticket with support at particular dot net pointing to that code so that we can have a closer look.

FYI I did for example check the load event listener and they are not executed on a different thread. So resolving the message handler context in such a listener should be possible by using the async local trick mentioned at the beginning of this conversation.

Regards
Daniel

I am not sure of the message roll back but I know normally it worked in v5 without a problem as we used IBus in the listener to publish events. In the new versions we do not have the instance of IBus in the container so we need to use AsyncLocal as suggested to get IMessageHandlerContext instance but for some reason it is null sometimes. I will try it again.

AsyncLocal value is null when the Listener event is raise due to cascading inserts or updates in NHibernate. For example if we have an entity User and the user has Logins property, every time user gets updated, there maybe new Login items persisted via NHibernate. In this case, it seems like they are on different threads and there is no way to access IMessageHandlerContext.

Also, I tried using UniformSession but that doesn’t help either as the events/messages published/sent using UniformSession don’t have the correct headers.

@rezajp At this point I think we’ve reached the limit of what we’re able to help you with here. Please open a support case pointing to this thread so that we can schedule a call with you. We’ll be able to take a look directly at your code that way and hopefully get you unstuck.