NHibernate,以及奇怪的“Session is Closed!”错误

18
注意:现在我已经打出来了,我必须为这个超长的问题道歉,不过,我认为这里呈现的所有代码和信息在某种程度上都是相关的。

好吧,在我的ASP.NET Webforms应用程序中,我遇到了奇怪的“Session Is Closed”错误,而且是在随机的时间点。然而,今天它终于在同一个地方一遍又一遍地发生。我几乎可以确定在我的代码中没有任何东西正在处理或关闭会话,因为使用会话的代码片段都很好地包含在所有其他代码之外,正如你下面所看到的。

我还使用Ninject作为我的IOC,这可能/可能不重要。

好的,首先是我的SessionFactoryProviderSessionProvider类:


SessionFactoryProvider

public class SessionFactoryProvider : IDisposable
{
    ISessionFactory sessionFactory;

    public ISessionFactory GetSessionFactory()
    {
        if (sessionFactory == null)
            sessionFactory =
                Fluently.Configure()
                        .Database(
                            MsSqlConfiguration.MsSql2005.ConnectionString(p =>
                                p.FromConnectionStringWithKey("QoiSqlConnection")))
                        .Mappings(m =>
                            m.FluentMappings.AddFromAssemblyOf<JobMapping>())
                        .BuildSessionFactory();

        return sessionFactory;
    }

    public void Dispose()
    {
        if (sessionFactory != null)
            sessionFactory.Dispose();
    }
}

SessionProvider

public class SessionProvider : IDisposable
{
    ISessionFactory sessionFactory;
    ISession session;

    public SessionProvider(SessionFactoryProvider sessionFactoryProvider)
    {
        this.sessionFactory = sessionFactoryProvider.GetSessionFactory();
    }

    public ISession GetCurrentSession()
    {
        if (session == null)
            session = sessionFactory.OpenSession();

        return session;
    }

    public void Dispose()
    {
        if (session != null)
        {
            session.Dispose();                
        }
    }
}

这两个类被Ninject连接起来,如下所示: NHibernateModule
public class NHibernateModule : StandardModule
{        
    public override void Load()
    {
        Bind<SessionFactoryProvider>().ToSelf().Using<SingletonBehavior>();
        Bind<SessionProvider>().ToSelf().Using<OnePerRequestBehavior>();
    }
}

据我所知,这些功能都能正常工作。

现在是我的 BaseDao<T> 类:


BaseDao

public class BaseDao<T> : IDao<T> where T : EntityBase
{
    private SessionProvider sessionManager;
    protected ISession session { get { return sessionManager.GetCurrentSession(); } }

    public BaseDao(SessionProvider sessionManager)
    {
        this.sessionManager = sessionManager;
    }        

    public T GetBy(int id)
    {
        return session.Get<T>(id);
    }

    public void Save(T item)        
    {
        using (var transaction = session.BeginTransaction())
        {
            session.SaveOrUpdate(item);

            transaction.Commit();
        }
    }

    public void Delete(T item)
    {
        using (var transaction = session.BeginTransaction())
        {
            session.Delete(item);

            transaction.Commit();
        }
    }

    public IList<T> GetAll()
    {
        return session.CreateCriteria<T>().List<T>();
    }

    public IQueryable<T> Query()
    {
        return session.Linq<T>();
    }        
}

这个东西可以像下面这样在Ninject中绑定:


DaoModule

public class DaoModule : StandardModule
{
    public override void Load()
    {
        Bind(typeof(IDao<>)).To(typeof(BaseDao<>))
                            .Using<OnePerRequestBehavior>();
    }
}

现在导致这个问题的网络请求是当我保存一个对象时,直到今天我做了一些模型更改才出现,但是我的模型更改并没有以任何方式改变数据访问代码。尽管它更改了一些NHibernate映射(如果有人感兴趣,我也可以发布这些内容)。

据我所知,在调用BaseDao<SomeClass>.Get后调用BaseDao<SomeOtherClass>.Get,然后调用BaseDao<TypeImTryingToSave>.Save

就是在Save()中的第三次调用。

using (var transaction = session.BeginTransaction())

出现“Session is Closed!”错误或异常的故障:

Session is closed!
Object name: 'ISession'.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.ObjectDisposedException: Session is closed!
Object name: 'ISession'.

实际上,通过调试器可以看到第三次从SessionProvider请求会话时,它确实已关闭且未连接。

我已验证我的SessionFactoryProviderSessionProviderDispose在请求结束后而不是在Dao进行Save调用之前被调用。

现在我有点困惑。 有几件事情浮现在脑海中。

  • 我是否有任何明显错误?
  • NHibernate是否会在我没有要求关闭会话的情况下关闭会话?
  • 有什么变通方法或想法吗?

提前致谢。


1
你能否在一个相对独立的单元测试中重现这个问题? - Michael Maddox
1
请始终发布完整的堆栈跟踪。 - Mauricio Scheffer
每个DAO操作都进行事务处理?这种微观管理可能不是一个好主意。如果您需要在单个事务中保存或删除两个DAO,该怎么办?阅读此链接以获取更多解释和想法:http://msdn.microsoft.com/en-us/magazine/ee819139.aspx - JustAMartin
@Martin Aye,我已经转向每个请求一个事务的模型了。虽然我发表这个问题已经有一段时间了 :) - Sekhat
虽然问题很长,但是非常易读和清晰。继续保持! - Oliver
3个回答

19

ASP.NET是多线程的,因此访问ISession必须是线程安全的。假设您正在使用每个请求一个session的方式,最简单的方法是使用NHibernate内置的上下文会话处理。

首先配置NHibernate使用web session context类:

sessionFactory = Fluently.Configure()
    .Database(
        MsSqlConfiguration.MsSql2005.ConnectionString(p =>
            p.FromConnectionStringWithKey("QoiSqlConnection")))
    .Mappings(m => m.FluentMappings.AddFromAssemblyOf<JobMapping>())
    .ExposeConfiguration(x => x.SetProperty("current_session_context_class", "web")
    .BuildSessionFactory();

然后使用ISessionFactory.GetCurrentSession()获取现有的会话,如果没有,则将新会话绑定到工厂。以下是我要复制粘贴的代码,用于打开和关闭一个会话。

    public ISession GetContextSession()
    {
        var factory = GetFactory(); // GetFactory returns an ISessionFactory in my helper class
        ISession session;
        if (CurrentSessionContext.HasBind(factory))
        {
            session = factory.GetCurrentSession();
        }
        else
        {
            session = factory.OpenSession();
            CurrentSessionContext.Bind(session);
        }
        return session;
    }

    public void EndContextSession()
    {
        var factory = GetFactory();
        var session = CurrentSessionContext.Unbind(factory);
        if (session != null && session.IsOpen)
        {
            try
            {
                if (session.Transaction != null && session.Transaction.IsActive)
                {
                    session.Transaction.Rollback();
                    throw new Exception("Rolling back uncommited NHibernate transaction.");
                }
                session.Flush();
            }
            catch (Exception ex)
            {
                log.Error("SessionKey.EndContextSession", ex);
                throw;
            }
            finally
            {
                session.Close();
                session.Dispose();
            }
        }
    }        

好的,问题解决了,谢谢 :) 你能详细解释一下是什么原因导致了这个问题吗?我猜你提到 ASP.NET 是多线程的(我知道)可能与此有关?通常情况下,你不能在不同的线程之间使用相同的会话吗? - Sekhat
1
当我开始使用session-per-request时,我遇到了完全相同的问题。使用NHibernate Profiler,我可以看到会话为链接资源(如CSS和JavaScript文件)以及网页请求而打开。偶尔,其中一个请求将触发EndRequest并导致会话关闭,而同一请求中的另一个线程仍在引用它(竞争条件)。 - Jamie Ide
1
什么调用了EndContextSession()函数? - Ronnie Overby
1
我在Global.asax中的Application_EndRequest方法中调用它。你也可以使用HttpHandler。 - Jamie Ide
1
截至2016年2月12日,关于上下文会话的文档链接为http://nhibernate.info/doc/nhibernate-reference/architecture.html。 - Manfred

9
我在运行Web API项目的集成测试时偶尔遇到Session Closed/ObjectDisposedExceptions异常。这里提供的任何提示都没有解决问题,所以我构建了NHibernate的调试版本以获取更多信息。我发现当控制器的Get函数返回响应对象的IEnumerable时,它正在执行一次linq查询。结果发现我们的仓库正在执行linq查询并返回域对象的IEnumerable,但从未调用ToList()方法强制评估查询。服务反过来返回了IEnumerable,控制器使用响应对象包装可枚举对象并返回。在返回期间,linq查询最终被执行,但是在某个时刻NHibernate会话被关闭,因此抛出异常。解决方案是确保我们总是在using会话块内在仓库中调用.ToList()方法,然后再返回给服务。

3
我建议你在 SessionImpl.Close / SessionImpl.Dispose设置断点 并查看堆栈跟踪,以确定是谁在调用它。你也可以为自己构建一个调试版本的 NH 并执行相同的操作。

谢谢您的提示,不幸的是显示了相同的事情。SessionProvider.Dispose 调用 SessionImpl.Dispose,然后在抛出异常之后调用 SessionImpl.Close - Sekhat
这意味着,在“执行时间”内没有明确关闭它的内容,这意味着有些可疑的事情正在发生... - Sekhat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接