Why does Nhibernate share the session across multi

2019-02-23 16:59发布

问题:

We have an MVC project that constructs the NHibernate dependecies via StructureMap like this

var sessionFactory = ConnectionRegistry.CreateSessionFactory<NHibernate.Context.WebSessionContext>();
For<ISessionFactory>().Singleton().Use(sessionFactory);
For<INHibernateSessionManager>().Singleton().Use<NHibernateWebSessionManager>();

The ConnectionRegistry.CreateSessionFactory looks like this

public static ISessionFactory CreateSessionFactory<T>() where T : ICurrentSessionContext
        {
            if (_sessionFactory == null)
            {
                lock (_SyncLock)
                {
                    if (_sessionFactory == null)
                    {
                        var cfg = Fluently.Configure()
                            .Database(MsSqlConfiguration.MsSql2005.ConnectionString(DataFactory.ConnectionString))
                            .CurrentSessionContext<T>()
                            .Mappings(m => m.FluentMappings.AddFromAssemblyOf<IInstanceFactory>())
                            .ExposeConfiguration(c => c.SetProperty("generate_statistics", "true"))
                            .ExposeConfiguration(c => c.SetProperty("sql_exception_converter", typeof(SqlServerExceptionConverter).AssemblyQualifiedName));

                        try
                        {
                            _sessionFactory = cfg.BuildSessionFactory();
                        }
                        catch (Exception ex)
                        {
                            Debug.Write("Error loading Fluent Mappings: " + ex);
                            throw;
                        }
                    }
                }
            }

            return _sessionFactory;
        }

NHibernateWebSessionManager looks like this

public ISession Session
        {
            get
            {               
                return OpenSession();
            }
        }

public ISession OpenSession()
        {
            if(CurrentSessionContext.HasBind(SessionFactory))
            _currentSession = SessionFactory.GetCurrentSession();
            else
            {
                _currentSession = SessionFactory.OpenSession();
                CurrentSessionContext.Bind(_currentSession);
            }
            return _currentSession;
        }

        public void CloseSession()
        {
            if (_currentSession == null) return;
            if (!CurrentSessionContext.HasBind(SessionFactory)) return;
            _currentSession = CurrentSessionContext.Unbind(SessionFactory);
            _currentSession.Dispose();
            _currentSession = null;
        }

In Application_EndRequest, we do this

ObjectFactory.GetInstance<INHibernateSessionManager>().CloseSession();
ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();

Our controllers are persistence agnostic and actions call out to query model providers or command processors which have the sessionManager injected and manage their own transactions.

For example:

public ActionResult EditDetails(SiteDetailsEditViewModel model)
{
    _commandProcessor.Process(new SiteEditCommand { //mappings }

    //redirect
}

In the CommandProcessor:

public void Process(SiteEditCommand command)
        {
            using (var tran = _session.BeginTransaction())
            {
                var site = _session.Get<DeliveryPoint>(command.Id);
                site.SiteName = command.Name;
                //more mappings
                tran.Commit();
            }
        }

We also have an ActionFilter attribute that logs access to each controller action.

public void OnActionExecuted(ActionExecutedContext filterContext)
{
    SessionLogger.LogUserActionSummary(session, _userActionType);
}

The SessionLogger also manages its own transactions from an injected SessionManager

public void LogUserActionSummary(int sessionId, string userActionTypeDescription)
        {

            using (var tx = _session.BeginTransaction())
            {
                //get activity summary
                _session.Save(userActivitySummary);
                tx.Commit();
            }
        }

All of this works fine until I have two browsers accessing the app. In this scenario intermittent errors are thrown because the (NHibernate) Session is closed. NHProfiler shows SQL statements created from both CommandProcessor methods and SessionLogger methods from both browser sessions within the same transaction.

How can this occur given the WebSessionContext scope? I've also tried setting the scope of the sessionManager to HybridHttpOrThreadLocalScoped via structureMap.

回答1:

The problem is combination of a singleton:

For<INHibernateSessionManager>().Singleton().Use<NHibernateWebSessionManager>();

Having reference to an object from a different scope (webrequest context)

_currentSession = SessionFactory.GetCurrentSession();

This canot work properly in multithread environment (as mentioned in cases of two concurrent browsers accessing it). First request could already force to set the field _currentSession, which is then (for a while) used even for the second one. The first Application_EndRequest will close it ... and lasting one will recreate it...

When relying on NHibernate scopes, follow it fully:

return SessionFactory.GetCurrentSession(); // inside is the scope handled

SessionManager.Open()

public ISession OpenSession()
{
  if(CurrentSessionContext.HasBind(SessionFactory))
  {
     return SessionFactory.GetCurrentSession();
  }
  // else  
  var session = SessionFactory.OpenSession();
  NHibernate.Context.CurrentSessionContext.Bind(session);
  return session;
}

Then even singleton returning correct instances should work. But for a SessionManager I would use HybridHttpOrThreadLocalScoped anyway.

For<INHibernateSessionManager>()
  .HybridHttpOrThreadLocalScoped()
  .Use<NHibernateWebSessionManager>();