diff --git a/doc/reference/modules/architecture.xml b/doc/reference/modules/architecture.xml index 7600ddcbb53..15e6f2b6637 100644 --- a/doc/reference/modules/architecture.xml +++ b/doc/reference/modules/architecture.xml @@ -239,12 +239,21 @@ + + + NHibernate.Context.AsyncLocalSessionContext - current sessions are tracked + by current asynchronous flow. You are responsible to bind and unbind an + ISession instance with static methods of class + CurrentSessionContext. Binding operations from inner flows will not be + propagated to outer or siblings flows. + + NHibernate.Context.CallSessionContext - current sessions are tracked - by CallContext. You are responsible to bind and unbind an - ISession instance with static methods of class CurrentSessionContext - . + by CallContext. You are responsible to bind and unbind an + ISession instance with static methods of class + CurrentSessionContext. @@ -257,26 +266,26 @@ - NHibernate.Context.WebSessionContext - - stores the current session in HttpContext. - You are responsible to bind and unbind an ISession - instance with static methods of class CurrentSessionContext. + NHibernate.Context.WebSessionContext - + stores the current session in HttpContext. + You are responsible to bind and unbind an ISession + instance with static methods of class CurrentSessionContext. NHibernate.Context.WcfOperationSessionContext - current sessions are tracked by WCF OperationContext. You need to register the WcfStateExtension - extension in WCF. You are responsible to bind and unbind an ISession - instance with static methods of class CurrentSessionContext. + extension in WCF. You are responsible to bind and unbind an ISession + instance with static methods of class CurrentSessionContext. NHibernate.Context.ManagedWebSessionContext - current - sessions are tracked by HttpContext. Removed in NHibernate 4.0 - - NHibernate.Context.WebSessionContext should be used instead. - You are responsible to bind and unbind an ISession instance with static methods + sessions are tracked by HttpContext. Removed in NHibernate 4.0 + - NHibernate.Context.WebSessionContext should be used instead. + You are responsible to bind and unbind an ISession instance with static methods on this class, it never opens, flushes, or closes an ISession itself. @@ -287,7 +296,8 @@ defines which NHibernate.Context.ICurrentSessionContext implementation should be used. Typically, the value of this parameter would just name the implementation class to use (including the assembly name); for the out-of-the-box implementations, however, - there are corresponding short names: "call", "thread_static", "web" and "wcf_operation", + there are corresponding short names: async_local, call, + thread_static, web and wcf_operation, respectively. diff --git a/src/NHibernate.Test/ConnectionTest/AsyncLocalSessionContextFixture.cs b/src/NHibernate.Test/ConnectionTest/AsyncLocalSessionContextFixture.cs new file mode 100644 index 00000000000..18e4fcec7da --- /dev/null +++ b/src/NHibernate.Test/ConnectionTest/AsyncLocalSessionContextFixture.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using NHibernate.Cfg; +using NHibernate.Context; +using NUnit.Framework; + +namespace NHibernate.Test.ConnectionTest +{ + [TestFixture] + public class AsyncLocalSessionContextFixture : ConnectionManagementTestCase + { + protected override ISession GetSessionUnderTest() + { + var session = OpenSession(); + session.BeginTransaction(); + return session; + } + + protected override void Configure(Configuration configuration) + { + base.Configure(cfg); + cfg.SetProperty(Environment.CurrentSessionContextClass, "async_local"); + } + + [Test] + public async Task AsyncLocalIsolation() + { + using (var session = OpenSession()) + { + CurrentSessionContext.Bind(session); + AssertCurrentSession(session, "Unexpected session after outer bind."); + + await SubBind(session); + AssertCurrentSession(session, "Unexpected session after end of SubBind call."); + } + } + + private async Task SubBind(ISession firstSession) + { + AssertCurrentSession(firstSession, "Unexpected session at start of SubBind."); + + using (var session = OpenSession()) + { + CurrentSessionContext.Bind(session); + AssertCurrentSession(session, "Unexpected session after inner bind."); + + await Dummy(); + AssertCurrentSession(session, "Unexpected session after dummy await."); + } + } + + private Task Dummy() + { + return Task.FromResult(0); + } + + private void AssertCurrentSession(ISession session, string message) + { + Assert.That( + Sfi.GetCurrentSession(), + Is.EqualTo(session), + "{0} {1} instead of {2}.", message, + Sfi.GetCurrentSession().GetSessionImplementation().SessionId, + session.GetSessionImplementation().SessionId); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 391185dca8a..7f4cfefeec6 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -208,6 +208,7 @@ + diff --git a/src/NHibernate/Context/AsyncLocalSessionContext.cs b/src/NHibernate/Context/AsyncLocalSessionContext.cs new file mode 100644 index 00000000000..f1ced4059c2 --- /dev/null +++ b/src/NHibernate/Context/AsyncLocalSessionContext.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using NHibernate.Engine; + +namespace NHibernate.Context +{ + // Session contextes are serializable while not actually serializing any data. Others contextes just retrieve it back + // from their context, if it does still live when/where they are de-serialized. For having that with AsyncLocal, + // we would need to store it as static, and then we need to use a MapBasedSessionContext. + // But this would cause bindings operations done in inner flows to be potentially propagated to outer flows, depending + // on which flow has initialized the map. This is undesirable. + // So current implementation just loses its context in case of serialization, since AsyncLocal is not serializable. + // Another option would be to change MapBasedSessionContext for recreating a new dictionary from the + // previous one at each change, essentially using those dictionaries as immutable objects. + /// + /// Provides a current session + /// for current asynchronous flow. + /// + [Serializable] + public class AsyncLocalSessionContext : CurrentSessionContext + { + private readonly AsyncLocal _session = new AsyncLocal(); + + // Constructor signature required for dynamic invocation code. + public AsyncLocalSessionContext(ISessionFactoryImplementor factory) { } + + protected override ISession Session + { + get => _session.Value; + set => _session.Value = value; + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index 9b1459ae687..c8ce4165ee6 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -1224,6 +1224,8 @@ private ICurrentSessionContext BuildCurrentSessionContext() { case null: return null; + case "async_local": + return new AsyncLocalSessionContext(this); case "call": return new CallSessionContext(this); case "thread_static": diff --git a/src/NHibernate/NHibernate.csproj b/src/NHibernate/NHibernate.csproj index 80ae2fee47c..6841701fd9e 100644 --- a/src/NHibernate/NHibernate.csproj +++ b/src/NHibernate/NHibernate.csproj @@ -144,6 +144,7 @@ +