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.
The problem is combination of a singleton:
Having reference to an object from a different scope (webrequest context)
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 firstApplication_EndRequest
will close it ... and lasting one will recreate it...When relying on NHibernate scopes, follow it fully:
SessionManager.Open()
Then even singleton returning correct instances should work. But for a SessionManager I would use
HybridHttpOrThreadLocalScoped
anyway.