Search code examples
asp.net-mvcnhibernatednx

MVC6 dnx451 Nhibernate nullreferenceexception on hasbind


I am moveing an application to MVC 6 using dnx451. When attempting to open a new session in nhibernate, I receive an NullReferenceException when checking the current context. I am using a WebSessionContext (as can be seen the stack trace); however it looks like the context is not being successfully stored in the HttpSession.

Does Nhibernate curretly work with MVC 6? I currently have it working in MVC 5. The big differnece is how I get sessions. Since MVC 6 does not use HttpModules, I have moved the opening and closing of sessions to a filter attribute (the only drawback I can see is a possible lazy loading exception if certain properties are hit in the veiw).

The filter code is as follows:

public class DbTransactionAttribute:ActionFilterAttribute
    {
        private readonly IsolationLevel isolationLevel;

        /// <summary>
        /// Creates a transaction with IsolationLevel.ReadUncommitted
        /// </summary>
        public DbTransactionAttribute() {
            isolationLevel = IsolationLevel.ReadUncommitted;
        }

        public DbTransactionAttribute(IsolationLevel isolationLevel) {
            this.isolationLevel = isolationLevel;
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext) {
            SessionManager.Instance.OpenSession();
            SessionManager.Instance.Session.BeginTransaction(isolationLevel);
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext) {
            ITransaction transaction = SessionManager.Instance.Session.Transaction;
            if (transaction.IsActive) {
                if (filterContext.Exception != null && filterContext.ExceptionHandled)
                    transaction.Rollback();
                else transaction.Commit();
            }
            transaction.Dispose();
            SessionManager.Instance.DisposeCurrentSession(); // We are finished with the session
        }
}

The Startup mehod:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection(key: "Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler(errorHandlingPath: "/Home/Error");
            }

            app.UseIISPlatformHandler();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
            Nhibernate.Context.BuildContext(env);
            Nhibernate.SessionManager.BuildSessionManager(env);
        }

The Context:

class Context {
        private static Context instance;

        private ISessionFactory sessionFactory;

        internal ISessionFactory SessionFactory { get { return sessionFactory; } }

        internal static Context Instance {
            get {
                if (instance == null) Initialize();
                return instance;
            }
        }

        internal static void BuildContext(Microsoft.AspNet.Hosting.IHostingEnvironment env) {
            if(instance == null) Initialize();
        }

        private static void Initialize() {
            instance = new Context();
            var hbrConfig = new NHibernate.Cfg.Configuration();
            var files = typeof(Context).Assembly.GetManifestResourceNames();
            hbrConfig.Configure(typeof(Context).Assembly, 
                resourceName: "Ppn.Web.Nhibernate.hibernate.cfg.xml");
            hbrConfig.AddAssembly(typeof(ProposalNumber).Assembly);

            instance.sessionFactory = hbrConfig.BuildSessionFactory();
        }
    }

The SessionManager:

public class SessionManager : ISessionManager {
        private static ISessionFactory sessionFactory;

        private static SessionManager instance;

        public static SessionManager Instance {
            get {
                return instance ?? (instance = new SessionManager(Context.Instance.SessionFactory));
            }
        }

        public static void BuildSessionManager(Microsoft.AspNet.Hosting.IHostingEnvironment env) {
            if (instance == null) instance = new SessionManager(Context.Instance.SessionFactory);
        }

        public SessionManager(ISessionFactory sessionFactory) {
            SessionManager.sessionFactory = sessionFactory;
        }

        public ISession Session {
            get {
                bool hasBind = CurrentSessionContext.HasBind(sessionFactory); //Line that fails
                ISession result;
                if (hasBind) result = sessionFactory.GetCurrentSession();
                else result = OpenSession();
                return result;
                //return CurrentSessionContext.HasBind(sessionFactory) ? sessionFactory.GetCurrentSession() : OpenSession();
            }
        }

        public ISession OpenSession() {
            ISession session = sessionFactory.OpenSession();
            CurrentSessionContext.Bind(session);
            return session;
        }

        public void DisposeCurrentSession() {
            if (CurrentSessionContext.HasBind(sessionFactory)) {
                ISession session = CurrentSessionContext.Unbind(sessionFactory);
                session.Close();
                session.Dispose();
            }
        }
    }

The Exception:

NullReferenceException: Object reference not set to an instance of an object.

    lambda_method(Closure , Object )
    NHibernate.Context.ReflectiveHttpContext.get_HttpContextCurrentItems()
    NHibernate.Context.WebSessionContext.GetMap()
    NHibernate.Context.MapBasedSessionContext.get_Session()
    NHibernate.Context.CurrentSessionContext.HasBind(ISessionFactory factory)
    Ppn.Web.Nhibernate.SessionManager.get_Session() in SessionManager.cs
                        bool hasBind = CurrentSessionContext.HasBind(sessionFactory);
    Ppn.Web.Controllers.ProposalNumberController.Index() in ProposalNumberController.cs
                    ISession dbSession = SessionManager.Instance.Session;
    --- End of stack trace from previous location where exception was thrown ---

Solution

  • I've found a solution where I create a custom ICurrentSessionContext which looks like the following:

    [Serializable]
    public class Mvc6SessionContext: MapBasedSessionContext {
        private const string SessionFactoryMapKey = "NHibernate.Context.WebSessionContext.SessionFactoryMapKey";
    
        public Mvc6SessionContext(ISessionFactoryImplementor factory) : base(factory) {}
    
        protected override IDictionary GetMap() {
            return Context.Instance.HttpContext.Items[SessionFactoryMapKey] as IDictionary;
        }
    
        protected override void SetMap(IDictionary value) {
            Context.Instance.HttpContext.Items[SessionFactoryMapKey] = value;
        }
    }
    

    We still nee to get the custom access to the context. As such I just modified the the Context item for my app as follows:

    class Context {
        private static Context instance;
    
        private ISessionFactory sessionFactory;
        private static IHttpContextAccessor contextAccessor;
    
        internal ISessionFactory SessionFactory { get { return sessionFactory; } }
    
        internal static Context Instance {
            get {
                if (instance == null) Initialize();
                return instance;
            }
        }
    
        internal HttpContext HttpContext {
            get { return contextAccessor.HttpContext; }
        }
    
        internal static void BuildContext(IApplicationBuilder app) {
            if(contextAccessor == null) contextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
            if (instance == null) Initialize();
        }
    
        private static void Initialize() {
            instance = new Context();
            var hbrConfig = new NHibernate.Cfg.Configuration();
            var files = typeof(Context).Assembly.GetManifestResourceNames();
            hbrConfig.Configure(typeof(Context).Assembly, 
                resourceName: "Ppn.Web.Nhibernate.hibernate.cfg.xml");
            hbrConfig.AddAssembly(typeof(ProposalNumber).Assembly);
            instance.sessionFactory = hbrConfig.BuildSessionFactory();
        }
    }
    

    And in the startup I pass the IApplicationBuilder

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection(key: "Logging"));
            loggerFactory.AddDebug();
    
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler(errorHandlingPath: "/Home/Error");
            }
    
            app.UseIISPlatformHandler();
    
            app.UseStaticFiles();
    
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
            Nhibernate.Context.BuildContext(app);
            Nhibernate.SessionManager.BuildSessionManager(env);
        }
    

    Last thing is to tell Nhibernate to use my implementation in the config file:

    <property name="current_session_context_class">Ppn.Web.Nhibernate.Mvc6SessionContext, Ppn.Web</property>
    

    The symantics of the whole thig should work the same as the standard "web" key used in Nhibernate. I've tested and so far so good.

    To be clear, this project is dnx451 not a .net core (which won't work for many reasons)