NancyFX + .NET Core + NHibernate

553 Views Asked by At

I have created a small API based on NancyFx on .NET Core 2. It uses AutoFac as the IOC container and NHibernate 5.3 to access the database.

I have run into a problem with threading and the CurrentSessionContext. Basically when I enter the AfterRequest pipeline, I am usually on another thread, and then the CurrentSessionContext doesn't know about the binding I did at the beginning of the request.

I have tried to use the WebSessionContext instead, but since I am using the stack I am, there is no HttpContext.Current. To get access to the HttpContext you have to inject the Microsoft.AspNetCore.Http.IHttpContextAccessor where you need it.

How can I tell NHibernate to bind to my own context somehow, so I my session isn't lost between BeforeRequest and AfterRequest?

To make it easy to wrap my data accecss in a transaction I have added the following to my Nancy Bootstrapper:

    protected override void ApplicationStartup(ILifetimeScope container, IPipelines pipelines)
    {
        base.ApplicationStartup(container, pipelines);
        ConfigureNHibernateSessionPerRequest(container, pipelines);
    }

    private void ConfigureNHibernateSessionPerRequest(ILifetimeScope container, IPipelines pipelines)
    {
        pipelines.BeforeRequest += ctx => CreateSession(container);
        pipelines.AfterRequest += ctx => CommitSession(container);
        pipelines.OnError += (ctx, ex) => RollbackSession(container);
    }

    private Response CreateSession(ILifetimeScope container)
    {
        var provider = container.Resolve<INHibernaterSessionFactoryProvider>();
        var sessionFactory = provider.Factory;

        var requestSession = sessionFactory.OpenSession();
        CurrentSessionContext.Bind(requestSession);
        requestSession.BeginTransaction();

        return null;
    }

    private AfterPipeline CommitSession(ILifetimeScope container)
    {
        var provider = container.Resolve<INHibernaterSessionFactoryProvider>();
        var sessionFactory = provider.Factory;

        if (CurrentSessionContext.HasBind(sessionFactory))
        {
            var requestSession = sessionFactory.GetCurrentSession();
            requestSession.Transaction.Commit();
            CurrentSessionContext.Unbind(sessionFactory);
            requestSession.Dispose();
        }
        return null;
    }

    private Response RollbackSession(ILifetimeScope container)
    {
        var provider = container.Resolve<INHibernaterSessionFactoryProvider>();
        var sessionFactory = provider.Factory;

        if (CurrentSessionContext.HasBind(sessionFactory))
        {
            var requestSession = sessionFactory.GetCurrentSession();
            requestSession.Transaction.Rollback();
            CurrentSessionContext.Unbind(sessionFactory);
            requestSession.Dispose();
        }
        return null;
    }

My hibernate.cfg.xml looks like this:

    <?xml version="1.0" encoding="utf-8" ?>
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
            <session-factory>
                    <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
                    <property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>
                    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
                    <property name="connection.connection_string">...</property>
                    <property name="show_sql">true</property>
                    <property name ="current_session_context_class">thread_static</property>
            </session-factory>
    </hibernate-configuration>

And I wire up the Sessionfactory like this:

    var configuration = new Configuration();
        configuration.Configure();
        configuration.AddAssembly(Assembly.GetExecutingAssembly());
        _factory = configuration.BuildSessionFactory();
2

There are 2 best solutions below

1
On

You can try using AspNetCore middleware since that will give you access to HttpContext.

0
On

I found a solution that works, but isn't really as nice as I would have liked it to be.

In my Nancy Bootstrapper I have added a public static property that I populate from the ApplicationStartup hook:

public class Bootstrapper : AutofacNancyBootstrapper
{
    public static IHttpContextAccessor HttpContextAccessor { get; private set; }

    protected override void ApplicationStartup(ILifetimeScope container, IPipelines pipelines)
    {
        HttpContextAccessor = container.Resolve<IHttpContextAccessor>();
    }
}

Then I have created a new custom CurrentSessionContext that I just called CoreSessionContext. It extends the abstract MapBasedSessionContext just like the WebSessionContext does, and then I inject the HttpContextAccessor in the constructor by accessing the static property on the Bootstrapper.

public class CoreSessionContext : MapBasedSessionContext
{
    private IHttpContextAccessor _httpContextAccessor;
    private const string SessionFactoryMapKey = "NHibernate.Context.WebSessionContext.SessionFactoryMapKey";

    public CoreSessionContext(ISessionFactoryImplementor factory) : base(factory)
    {
        _httpContextAccessor = Bootstrapper.HttpContextAccessor;
    }

    protected override IDictionary GetMap()
    {
        return _httpContextAccessor.HttpContext.Items[SessionFactoryMapKey] as IDictionary;
    }

    protected override void SetMap(IDictionary value)
    {
        _httpContextAccessor.HttpContext.Items[SessionFactoryMapKey] = value;
    }
}

The last thing I did was to remove the current_session_context_class element from the hibernate.cfg.xml file and then wire up the SessionFactory with my custom session context like this in line three:

        var configuration = new Configuration();
        configuration.Configure();
        configuration.CurrentSessionContext<CoreSessionContext>();
        configuration.AddAssembly(Assembly.GetExecutingAssembly());
        _factory = configuration.BuildSessionFactory();

Then I am able to use the HttpContext provided by AspNetCore.

Not as pretty as I would like it to be, but it works.