diff --git a/src/NHibernate.Test/Async/SessionBuilder/Fixture.cs b/src/NHibernate.Test/Async/SessionBuilder/Fixture.cs index ac089680980..c481dde92b9 100644 --- a/src/NHibernate.Test/Async/SessionBuilder/Fixture.cs +++ b/src/NHibernate.Test/Async/SessionBuilder/Fixture.cs @@ -54,14 +54,28 @@ private void CanSetAutoJoinTransaction(T sb) where T : ISessionBuilder false, true); } + private void CanSetAutoJoinTransactionOnStateless(T sb) where T : IStatelessSessionBuilder + { + var options = DebugSessionFactory.GetCreationOptions(sb); + CanSetOnStateless( + sb, sb.AutoJoinTransaction, () => options.ShouldAutoJoinTransaction, + sb is ISharedStatelessSessionBuilder ssb ? ssb.AutoJoinTransaction : default(Func), + // initial value + true, + // values + false, true); + } + [Test] public async Task CanSetConnectionAsync() { var sb = Sfi.WithOptions(); await (CanSetConnectionAsync(sb)); + await (CanSetConnectionOnStatelessAsync(Sfi.WithStatelessOptions())); using (var s = sb.OpenSession()) { await (CanSetConnectionAsync(s.SessionWithOptions())); + await (CanSetConnectionOnStatelessAsync(s.StatelessSessionWithOptions())); } } @@ -109,12 +123,10 @@ public async Task CanSetConnectionAsync() } } - [Test] - public async Task CanSetConnectionOnStatelessAsync() + private async Task CanSetConnectionOnStatelessAsync(T sb, CancellationToken cancellationToken = default(CancellationToken)) where T : IStatelessSessionBuilder { - var sb = Sfi.WithStatelessOptions(); var sbType = sb.GetType().Name; - var conn = await (Sfi.ConnectionProvider.GetConnectionAsync(CancellationToken.None)); + var conn = await (Sfi.ConnectionProvider.GetConnectionAsync(cancellationToken)); try { var options = DebugSessionFactory.GetCreationOptions(sb); @@ -123,9 +135,31 @@ public async Task CanSetConnectionOnStatelessAsync() Assert.AreEqual(conn, options.UserSuppliedConnection, $"{sbType}: After call with a connection"); Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with a connection"); - fsb = sb.Connection(null); - Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with null"); - Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with null"); + if (sb is ISharedStatelessSessionBuilder ssb) + { + var sharedOptions = (ISharedSessionCreationOptions)options; + Assert.IsFalse(sharedOptions.IsTransactionCoordinatorShared, $"{sbType}: Transaction coordinator shared before sharing"); + Assert.IsNull(sharedOptions.ConnectionManager, $"{sbType}: Connection manager shared before sharing"); + + var fssb = ssb.Connection(); + // Sharing connection shares the connection manager, not the connection. + Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with previous session connection"); + Assert.IsTrue(sharedOptions.IsTransactionCoordinatorShared, $"{sbType}: Transaction coordinator not shared after sharing"); + Assert.IsNotNull(sharedOptions.ConnectionManager, $"{sbType}: Connection manager not shared after sharing"); + Assert.AreEqual(sb, fssb, $"{sbType}: Unexpected fluent return on shared"); + + fsb = sb.Connection(null); + Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with null"); + Assert.IsFalse(sharedOptions.IsTransactionCoordinatorShared, $"{sbType}: Transaction coordinator shared after un-sharing"); + Assert.IsNull(sharedOptions.ConnectionManager, $"{sbType}: Connection manager shared after un-sharing"); + Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after un-sharing"); + } + else + { + fsb = sb.Connection(null); + Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with null"); + Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with null"); + } } finally { @@ -218,5 +252,25 @@ private void CanSet(T sb, Func setter, Func getter, Func( + T sb, Func setter, Func getter, Func shared, V initialValue, + params V[] values) where T : IStatelessSessionBuilder + { + var sbType = sb.GetType().Name; + Assert.AreEqual(initialValue, getter(), $"{sbType}: Initial value"); + if (shared != null) + { + var fssb = shared(); + Assert.AreEqual(values.Last(), getter(), $"{sbType}: After call with shared setting"); + Assert.AreEqual(sb, fssb, $"{sbType}: Unexpected fluent return on shared"); + } + foreach (var value in values) + { + var fsb = setter(value); + Assert.AreEqual(value, getter(), $"{sbType}: After call with {value}"); + Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with {value}"); + } + } } } diff --git a/src/NHibernate.Test/Async/TransactionTest/TransactionFixture.cs b/src/NHibernate.Test/Async/TransactionTest/TransactionFixture.cs index 2e94735f7b7..ab4a0091a75 100644 --- a/src/NHibernate.Test/Async/TransactionTest/TransactionFixture.cs +++ b/src/NHibernate.Test/Async/TransactionTest/TransactionFixture.cs @@ -170,6 +170,35 @@ public async Task FlushFromTransactionAppliesToSharingSessionAsync() } } + [Test] + public async Task FlushFromTransactionAppliesToSharingStatelessSessionAsync() + { + using (var s = OpenSession()) + { + var builder = s.StatelessSessionWithOptions().Connection(); + + using (var s1 = builder.OpenStatelessSession()) + using (var s2 = builder.OpenStatelessSession()) + using (var t = s.BeginTransaction()) + { + var p1 = new Person(); + var p2 = new Person(); + var p3 = new Person(); + await (s1.InsertAsync(p1)); + await (s2.InsertAsync(p2)); + await (s.SaveAsync(p3)); + await (t.CommitAsync()); + } + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.That(await (s.Query().CountAsync()), Is.EqualTo(3)); + await (t.CommitAsync()); + } + } + // Taken and adjusted from NH1632 When_commiting_items_in_DTC_transaction_will_add_items_to_2nd_level_cache [Test] public async Task WhenCommittingItemsWillAddThemTo2ndLevelCacheAsync() @@ -210,4 +239,4 @@ public async Task WhenCommittingItemsWillAddThemTo2ndLevelCacheAsync() } } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/DebugSessionFactory.cs b/src/NHibernate.Test/DebugSessionFactory.cs index 041003c9c74..99331bef6d9 100644 --- a/src/NHibernate.Test/DebugSessionFactory.cs +++ b/src/NHibernate.Test/DebugSessionFactory.cs @@ -386,7 +386,8 @@ public static ISessionCreationOptions GetCreationOptions(ISessionBuilder s public static ISessionCreationOptions GetCreationOptions(IStatelessSessionBuilder sessionBuilder) { - return ((StatelessSessionBuilder)sessionBuilder).CreationOptions; + return (sessionBuilder as StatelessSessionBuilder)?.CreationOptions ?? + (ISessionCreationOptions)sessionBuilder; } internal class SessionBuilder : ISessionBuilder diff --git a/src/NHibernate.Test/SessionBuilder/Fixture.cs b/src/NHibernate.Test/SessionBuilder/Fixture.cs index dfd17982e68..e3afc27b87d 100644 --- a/src/NHibernate.Test/SessionBuilder/Fixture.cs +++ b/src/NHibernate.Test/SessionBuilder/Fixture.cs @@ -47,9 +47,11 @@ public void CanSetAutoJoinTransaction() { var sb = Sfi.WithOptions(); CanSetAutoJoinTransaction(sb); + CanSetAutoJoinTransactionOnStateless(Sfi.WithStatelessOptions()); using (var s = sb.OpenSession()) { CanSetAutoJoinTransaction(s.SessionWithOptions()); + CanSetAutoJoinTransactionOnStateless(s.StatelessSessionWithOptions()); } } @@ -64,21 +66,16 @@ private void CanSetAutoJoinTransaction(T sb) where T : ISessionBuilder false, true); } - [Test] - public void CanSetAutoJoinTransactionOnStateless() + private void CanSetAutoJoinTransactionOnStateless(T sb) where T : IStatelessSessionBuilder { - var sb = Sfi.WithStatelessOptions(); - - var sbType = sb.GetType().Name; var options = DebugSessionFactory.GetCreationOptions(sb); - Assert.That(options.ShouldAutoJoinTransaction, Is.True, $"{sbType}: Initial value"); - var fsb = sb.AutoJoinTransaction(false); - Assert.That(options.ShouldAutoJoinTransaction, Is.False, $"{sbType}: After call with false"); - Assert.That(fsb, Is.SameAs(sb), $"{sbType}: Unexpected fluent return after call with false"); - - fsb = sb.AutoJoinTransaction(true); - Assert.That(options.ShouldAutoJoinTransaction, Is.True, $"{sbType}: After call with true"); - Assert.That(fsb, Is.SameAs(sb), $"{sbType}: Unexpected fluent return after call with true"); + CanSetOnStateless( + sb, sb.AutoJoinTransaction, () => options.ShouldAutoJoinTransaction, + sb is ISharedStatelessSessionBuilder ssb ? ssb.AutoJoinTransaction : default(Func), + // initial value + true, + // values + false, true); } [Test] @@ -86,9 +83,11 @@ public void CanSetConnection() { var sb = Sfi.WithOptions(); CanSetConnection(sb); + CanSetConnectionOnStateless(Sfi.WithStatelessOptions()); using (var s = sb.OpenSession()) { CanSetConnection(s.SessionWithOptions()); + CanSetConnectionOnStateless(s.StatelessSessionWithOptions()); } } @@ -136,10 +135,8 @@ private void CanSetConnection(T sb) where T : ISessionBuilder } } - [Test] - public void CanSetConnectionOnStateless() + private void CanSetConnectionOnStateless(T sb) where T : IStatelessSessionBuilder { - var sb = Sfi.WithStatelessOptions(); var sbType = sb.GetType().Name; var conn = Sfi.ConnectionProvider.GetConnection(); try @@ -150,9 +147,31 @@ public void CanSetConnectionOnStateless() Assert.AreEqual(conn, options.UserSuppliedConnection, $"{sbType}: After call with a connection"); Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with a connection"); - fsb = sb.Connection(null); - Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with null"); - Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with null"); + if (sb is ISharedStatelessSessionBuilder ssb) + { + var sharedOptions = (ISharedSessionCreationOptions)options; + Assert.IsFalse(sharedOptions.IsTransactionCoordinatorShared, $"{sbType}: Transaction coordinator shared before sharing"); + Assert.IsNull(sharedOptions.ConnectionManager, $"{sbType}: Connection manager shared before sharing"); + + var fssb = ssb.Connection(); + // Sharing connection shares the connection manager, not the connection. + Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with previous session connection"); + Assert.IsTrue(sharedOptions.IsTransactionCoordinatorShared, $"{sbType}: Transaction coordinator not shared after sharing"); + Assert.IsNotNull(sharedOptions.ConnectionManager, $"{sbType}: Connection manager not shared after sharing"); + Assert.AreEqual(sb, fssb, $"{sbType}: Unexpected fluent return on shared"); + + fsb = sb.Connection(null); + Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with null"); + Assert.IsFalse(sharedOptions.IsTransactionCoordinatorShared, $"{sbType}: Transaction coordinator shared after un-sharing"); + Assert.IsNull(sharedOptions.ConnectionManager, $"{sbType}: Connection manager shared after un-sharing"); + Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after un-sharing"); + } + else + { + fsb = sb.Connection(null); + Assert.IsNull(options.UserSuppliedConnection, $"{sbType}: After call with null"); + Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with null"); + } } finally { @@ -259,5 +278,25 @@ private void CanSet(T sb, Func setter, Func getter, Func( + T sb, Func setter, Func getter, Func shared, V initialValue, + params V[] values) where T : IStatelessSessionBuilder + { + var sbType = sb.GetType().Name; + Assert.AreEqual(initialValue, getter(), $"{sbType}: Initial value"); + if (shared != null) + { + var fssb = shared(); + Assert.AreEqual(values.Last(), getter(), $"{sbType}: After call with shared setting"); + Assert.AreEqual(sb, fssb, $"{sbType}: Unexpected fluent return on shared"); + } + foreach (var value in values) + { + var fsb = setter(value); + Assert.AreEqual(value, getter(), $"{sbType}: After call with {value}"); + Assert.AreEqual(sb, fsb, $"{sbType}: Unexpected fluent return after call with {value}"); + } + } } } diff --git a/src/NHibernate.Test/TransactionTest/TransactionFixture.cs b/src/NHibernate.Test/TransactionTest/TransactionFixture.cs index 7e49744ed50..b06d8d76326 100644 --- a/src/NHibernate.Test/TransactionTest/TransactionFixture.cs +++ b/src/NHibernate.Test/TransactionTest/TransactionFixture.cs @@ -173,6 +173,35 @@ public void FlushFromTransactionAppliesToSharingSession() } } + [Test] + public void FlushFromTransactionAppliesToSharingStatelessSession() + { + using (var s = OpenSession()) + { + var builder = s.StatelessSessionWithOptions().Connection(); + + using (var s1 = builder.OpenStatelessSession()) + using (var s2 = builder.OpenStatelessSession()) + using (var t = s.BeginTransaction()) + { + var p1 = new Person(); + var p2 = new Person(); + var p3 = new Person(); + s1.Insert(p1); + s2.Insert(p2); + s.Save(p3); + t.Commit(); + } + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + Assert.That(s.Query().Count(), Is.EqualTo(3)); + t.Commit(); + } + } + // Taken and adjusted from NH1632 When_commiting_items_in_DTC_transaction_will_add_items_to_2nd_level_cache [Test] public void WhenCommittingItemsWillAddThemTo2ndLevelCache() @@ -213,4 +242,4 @@ public void WhenCommittingItemsWillAddThemTo2ndLevelCache() } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Async/ISession.cs b/src/NHibernate/Async/ISession.cs index 80b1232f940..45cc1b9f9e3 100644 --- a/src/NHibernate/Async/ISession.cs +++ b/src/NHibernate/Async/ISession.cs @@ -16,6 +16,7 @@ using NHibernate.Engine; using NHibernate.Event; using NHibernate.Event.Default; +using NHibernate.Impl; using NHibernate.Stat; using NHibernate.Type; @@ -23,6 +24,7 @@ namespace NHibernate { using System.Threading.Tasks; using System.Threading; + public partial interface ISession : IDisposable { diff --git a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs index 72a5433f7f0..95a6fd28a4d 100644 --- a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs @@ -11,6 +11,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Data; using System.Data.Common; using System.Linq; using NHibernate.AdoNet; diff --git a/src/NHibernate/Async/Impl/SessionImpl.cs b/src/NHibernate/Async/Impl/SessionImpl.cs index d3d7a100bf4..5ab05babfb5 100644 --- a/src/NHibernate/Async/Impl/SessionImpl.cs +++ b/src/NHibernate/Async/Impl/SessionImpl.cs @@ -11,7 +11,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data; using System.Data.Common; using System.Linq.Expressions; using System.Runtime.Serialization; diff --git a/src/NHibernate/Async/Impl/StatelessSessionImpl.cs b/src/NHibernate/Async/Impl/StatelessSessionImpl.cs index 1872e963450..ce0fe896e0a 100644 --- a/src/NHibernate/Async/Impl/StatelessSessionImpl.cs +++ b/src/NHibernate/Async/Impl/StatelessSessionImpl.cs @@ -11,10 +11,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data; -using System.Data.Common; using System.Linq.Expressions; -using NHibernate.AdoNet; using NHibernate.Cache; using NHibernate.Collection; using NHibernate.Criterion; diff --git a/src/NHibernate/Engine/ISessionImplementor.cs b/src/NHibernate/Engine/ISessionImplementor.cs index f32306fde2d..772d7691249 100644 --- a/src/NHibernate/Engine/ISessionImplementor.cs +++ b/src/NHibernate/Engine/ISessionImplementor.cs @@ -274,8 +274,16 @@ public partial interface ISessionImplementor bool IsOpen { get; } /// - /// Is the ISession currently connected? + /// Is the session connected? /// + /// + /// if the session is connected. + /// + /// + /// A session is considered connected if there is a (regardless + /// of its state) or if the field connect is true. Meaning that it will connect + /// at the next operation that requires a connection. + /// bool IsConnected { get; } FlushMode FlushMode { get; set; } diff --git a/src/NHibernate/ISession.cs b/src/NHibernate/ISession.cs index 0ac9aafc3ed..71d09cb207c 100644 --- a/src/NHibernate/ISession.cs +++ b/src/NHibernate/ISession.cs @@ -6,11 +6,28 @@ using NHibernate.Engine; using NHibernate.Event; using NHibernate.Event.Default; +using NHibernate.Impl; using NHibernate.Stat; using NHibernate.Type; namespace NHibernate { + // 6.0 TODO: Convert to interface methods + public static class SessionExtensions + { + /// + /// Obtain a builder with the ability to grab certain information from + /// this session. The built IStatelessSession will require its own disposal. + /// + /// The session from which to build a stateless session. + /// The session builder. + public static ISharedStatelessSessionBuilder StatelessSessionWithOptions(this ISession session) + { + var impl = session as SessionImpl ?? throw new NotSupportedException("Only SessionImpl sessions are supported."); + return impl.StatelessSessionWithOptions(); + } + } + /// /// The main runtime interface between a .NET application and NHibernate. This is the central /// API class abstracting the notion of a persistence service. @@ -174,8 +191,16 @@ public partial interface ISession : IDisposable bool IsOpen { get; } /// - /// Is the ISession currently connected? + /// Is the session connected? /// + /// + /// if the session is connected. + /// + /// + /// A session is considered connected if there is a (regardless + /// of its state) or if the field connect is true. Meaning that it will connect + /// at the next operation that requires a connection. + /// bool IsConnected { get; } /// diff --git a/src/NHibernate/ISharedStatelessSessionBuilder.cs b/src/NHibernate/ISharedStatelessSessionBuilder.cs new file mode 100644 index 00000000000..349de35d8b4 --- /dev/null +++ b/src/NHibernate/ISharedStatelessSessionBuilder.cs @@ -0,0 +1,57 @@ +using System.Data.Common; +using NHibernate.Connection; + +namespace NHibernate +{ + // NH different implementation: will not try to support covariant return type for specializations + // until it is needed. + /// + /// Specialized with access to stuff from another session. + /// + // 6.0 TODO: implement covariance the way used for ISharedSessionBuilder + public interface ISharedStatelessSessionBuilder : IStatelessSessionBuilder + { + #region 6.0 TODO: implement covariance the way used for ISharedSessionBuilder + + /// + /// Adds a specific connection to the session options. + /// + /// The connection to use. + /// , for method chaining. + /// + /// Note that the second-level cache will be disabled if you + /// supply a ADO.NET connection. NHibernate will not be able to track + /// any statements you might have executed in the same transaction. + /// Consider implementing your own . + /// + new ISharedStatelessSessionBuilder Connection(DbConnection connection); + + /// + /// Should the session be automatically enlisted in ambient system transaction? + /// Enabled by default. Disabling it does not prevent connections having auto-enlistment + /// enabled to get enlisted in current ambient transaction when opened. + /// + /// Should the session be automatically explicitly + /// enlisted in ambient transaction. + /// , for method chaining. + new ISharedStatelessSessionBuilder AutoJoinTransaction(bool autoJoinTransaction); + + #endregion + + /// + /// Signifies that the connection from the original session should be used to create the new session. + /// The original session remains responsible for it and its closing will cause sharing sessions to be no + /// more usable. + /// Causes specified ConnectionReleaseMode and AutoJoinTransaction to be ignored and + /// replaced by those of the original session. + /// + /// , for method chaining. + ISharedStatelessSessionBuilder Connection(); + + /// + /// Signifies that the AutoJoinTransaction flag from the original session should be used to create the new session. + /// + /// , for method chaining. + ISharedStatelessSessionBuilder AutoJoinTransaction(); + } +} diff --git a/src/NHibernate/IStatelessSession.cs b/src/NHibernate/IStatelessSession.cs index c5291a3e13b..495c8703b81 100644 --- a/src/NHibernate/IStatelessSession.cs +++ b/src/NHibernate/IStatelessSession.cs @@ -45,8 +45,16 @@ public partial interface IStatelessSession : IDisposable bool IsOpen { get; } /// - /// Is the IStatelessSession currently connected? + /// Is the session connected? /// + /// + /// if the session is connected. + /// + /// + /// A session is considered connected if there is a (regardless + /// of its state) or if the field connect is true. Meaning that it will connect + /// at the next operation that requires a connection. + /// bool IsConnected { get; } /// diff --git a/src/NHibernate/Impl/AbstractSessionImpl.cs b/src/NHibernate/Impl/AbstractSessionImpl.cs index 7d946e7a978..64382f60c32 100644 --- a/src/NHibernate/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Impl/AbstractSessionImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Data; using System.Data.Common; using System.Linq; using NHibernate.AdoNet; @@ -31,6 +32,11 @@ public abstract partial class AbstractSessionImpl : ISessionImplementor private bool closed; + /// Get the current NHibernate transaction. + public ITransaction Transaction => ConnectionManager.Transaction; + + protected bool IsTransactionCoordinatorShared { get; } + public ITransactionContext TransactionContext { get; set; @@ -38,7 +44,7 @@ public ITransactionContext TransactionContext private bool isAlreadyDisposed; - private static readonly INHibernateLogger logger = NHibernateLogger.For(typeof(AbstractSessionImpl)); + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(AbstractSessionImpl)); public Guid SessionId { get; } @@ -46,11 +52,34 @@ internal AbstractSessionImpl() { } protected internal AbstractSessionImpl(ISessionFactoryImplementor factory, ISessionCreationOptions options) { - _factory = factory; - Timestamp = factory.Settings.CacheProvider.NextTimestamp(); - _flushMode = options.InitialSessionFlushMode; - Interceptor = options.SessionInterceptor ?? EmptyInterceptor.Instance; SessionId = factory.Settings.TrackSessionId ? Guid.NewGuid() : Guid.Empty; + using (BeginContext()) + { + _factory = factory; + Timestamp = factory.Settings.CacheProvider.NextTimestamp(); + _flushMode = options.InitialSessionFlushMode; + Interceptor = options.SessionInterceptor ?? EmptyInterceptor.Instance; + + if (options is ISharedSessionCreationOptions sharedOptions && sharedOptions.IsTransactionCoordinatorShared) + { + // NH specific implementation: need to port Hibernate transaction management. + IsTransactionCoordinatorShared = true; + if (options.UserSuppliedConnection != null) + throw new SessionException("Cannot simultaneously share transaction context and specify connection"); + var connectionManager = sharedOptions.ConnectionManager; + connectionManager.AddDependentSession(this); + ConnectionManager = connectionManager; + } + else + { + ConnectionManager = new ConnectionManager( + this, + options.UserSuppliedConnection, + options.SessionConnectionReleaseMode, + Interceptor, + options.ShouldAutoJoinTransaction); + } + } } #region ISessionImplementor Members @@ -82,7 +111,16 @@ public ISessionFactoryImplementor Factory protected set => _factory = value; } - public abstract IBatcher Batcher { get; } + // 6.0 TODO: remove virtual. + /// + public virtual IBatcher Batcher + { + get + { + CheckAndUpdateSessionStatus(); + return ConnectionManager.Batcher; + } + } public abstract void CloseSessionFromSystemTransaction(); public virtual IList List(IQueryExpression queryExpression, QueryParameters parameters) @@ -218,19 +256,24 @@ public virtual IQuery GetNamedSQLQuery(string name) } } + // 6.0 TODO: remove virtual from below properties. + /// + public virtual ConnectionManager ConnectionManager { get; protected set; } + /// + public virtual bool IsConnected => ConnectionManager.IsConnected; + /// + public virtual DbConnection Connection => ConnectionManager.GetConnection(); + public abstract IQueryTranslator[] GetQueries(IQueryExpression query, bool scalar); public abstract EventListeners Listeners { get; } - public abstract ConnectionManager ConnectionManager { get; } public abstract bool IsEventSource { get; } public abstract object GetEntityUsingInterceptor(EntityKey key); public abstract IPersistenceContext PersistenceContext { get; } public abstract CacheMode CacheMode { get; set; } public abstract bool IsOpen { get; } - public abstract bool IsConnected { get; } public abstract string FetchProfile { get; set; } public abstract string BestGuessEntityName(object entity); public abstract string GuessEntityName(object entity); - public abstract DbConnection Connection { get; } public abstract int ExecuteNativeUpdate(NativeSQLQuerySpecification specification, QueryParameters queryParameters); public abstract FutureCriteriaBatch FutureCriteriaBatch { get; protected internal set; } public abstract FutureQueryBatch FutureQueryBatch { get; protected internal set; } @@ -370,7 +413,9 @@ protected bool IsAlreadyDisposed public abstract void Flush(); - public abstract bool TransactionInProgress { get; } + // 6.0 TODO: remove virtual. + /// + public virtual bool TransactionInProgress => ConnectionManager.IsInActiveTransaction; #endregion @@ -379,6 +424,14 @@ protected internal void SetClosed() closed = true; } + protected DbConnection CloseConnectionManager() + { + if (!IsTransactionCoordinatorShared) + return ConnectionManager.Close(); + ConnectionManager.RemoveDependentSession(this); + return null; + } + private void InitQuery(IQuery query, NamedQueryDefinition nqd) { query.SetCacheable(nqd.IsCacheable); @@ -471,6 +524,45 @@ protected void AfterOperation(bool success) } } + /// + /// Begin a NHibernate transaction + /// + /// A NHibernate transaction + public ITransaction BeginTransaction() + { + using (BeginProcess()) + { + if (IsTransactionCoordinatorShared) + { + // Todo : should seriously consider not allowing a txn to begin from a child session + // can always route the request to the root session... + Log.Warn("Transaction started on non-root session"); + } + + return ConnectionManager.BeginTransaction(); + } + } + + /// + /// Begin a NHibernate transaction with the specified isolation level + /// + /// The isolation level + /// A NHibernate transaction + public ITransaction BeginTransaction(IsolationLevel isolationLevel) + { + using (BeginProcess()) + { + if (IsTransactionCoordinatorShared) + { + // Todo : should seriously consider not allowing a txn to begin from a child session + // can always route the request to the root session... + Log.Warn("Transaction started on non-root session"); + } + + return ConnectionManager.BeginTransaction(isolationLevel); + } + } + protected void EnlistInAmbientTransactionIfNeeded() { _factory.TransactionFactory.EnlistInSystemTransactionIfNeeded(this); diff --git a/src/NHibernate/Impl/ISharedSessionCreationOptions.cs b/src/NHibernate/Impl/ISharedSessionCreationOptions.cs index a0fd77451f0..25c0a57d6f5 100644 --- a/src/NHibernate/Impl/ISharedSessionCreationOptions.cs +++ b/src/NHibernate/Impl/ISharedSessionCreationOptions.cs @@ -7,6 +7,7 @@ namespace NHibernate.Impl /// some part of the "transaction context" of another Session. /// /// + /// public interface ISharedSessionCreationOptions : ISessionCreationOptions { // NH note: naming "adjusted" for converting Java methods to properties while avoiding conflicts with @@ -15,4 +16,4 @@ public interface ISharedSessionCreationOptions : ISessionCreationOptions // NH different implementation: need to port Hibernate transaction management. ConnectionManager ConnectionManager { get; } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index 78514bffb33..ff9e630ca99 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -1390,10 +1390,19 @@ public virtual T FlushMode(FlushMode flushMode) } } - // NH different implementation: will not try to support covariant return type for specializations - // until it is needed. - internal class StatelessSessionBuilderImpl : IStatelessSessionBuilder, ISessionCreationOptions + // NH specific: implementing return type covariance with interface is a mess in .Net. + internal class StatelessSessionBuilderImpl : StatelessSessionBuilderImpl + { + public StatelessSessionBuilderImpl(SessionFactoryImpl sessionFactory) : base(sessionFactory) + { + SetSelf(this); + } + } + + internal class StatelessSessionBuilderImpl : IStatelessSessionBuilder, ISessionCreationOptions where T : IStatelessSessionBuilder { + // NH specific: implementing return type covariance with interface is a mess in .Net. + private T _this; private readonly SessionFactoryImpl _sessionFactory; public StatelessSessionBuilderImpl(SessionFactoryImpl sessionFactory) @@ -1401,18 +1410,23 @@ public StatelessSessionBuilderImpl(SessionFactoryImpl sessionFactory) _sessionFactory = sessionFactory; } + protected void SetSelf(T self) + { + _this = self; + } + public virtual IStatelessSession OpenStatelessSession() => new StatelessSessionImpl(_sessionFactory, this); - public IStatelessSessionBuilder Connection(DbConnection connection) + public virtual IStatelessSessionBuilder Connection(DbConnection connection) { UserSuppliedConnection = connection; - return this; + return _this; } public IStatelessSessionBuilder AutoJoinTransaction(bool autoJoinTransaction) { ShouldAutoJoinTransaction = autoJoinTransaction; - return this; + return _this; } public FlushMode InitialSessionFlushMode => FlushMode.Always; diff --git a/src/NHibernate/Impl/SessionImpl.cs b/src/NHibernate/Impl/SessionImpl.cs index ae48d621780..cb86f7c0fd2 100644 --- a/src/NHibernate/Impl/SessionImpl.cs +++ b/src/NHibernate/Impl/SessionImpl.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data; using System.Data.Common; using System.Linq.Expressions; using System.Runtime.Serialization; @@ -56,8 +55,6 @@ public sealed partial class SessionImpl : AbstractSessionImpl, IEventSource, ISe [NonSerialized] private int _suspendAutoFlushCount; - private readonly ConnectionManager connectionManager; - [NonSerialized] private readonly IDictionary enabledFilters = new Dictionary(); @@ -71,8 +68,6 @@ public sealed partial class SessionImpl : AbstractSessionImpl, IEventSource, ISe private readonly bool autoCloseSessionEnabled; [NonSerialized] private readonly ConnectionReleaseMode connectionReleaseMode; - [NonSerialized] - private readonly bool _transactionCoordinatorShared; #region System.Runtime.Serialization.ISerializable Members @@ -104,7 +99,7 @@ private SessionImpl(SerializationInfo info, StreamingContext context) enabledFilters = (IDictionary)info.GetValue("enabledFilters", typeof(Dictionary)); enabledFilterNames = (List)info.GetValue("enabledFilterNames", typeof(List)); - connectionManager = (ConnectionManager)info.GetValue("connectionManager", typeof(ConnectionManager)); + ConnectionManager = (ConnectionManager)info.GetValue("connectionManager", typeof(ConnectionManager)); } /// @@ -122,11 +117,11 @@ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext contex { log.Debug("writting session to serializer"); - if (!connectionManager.IsReadyForSerialization) + if (!ConnectionManager.IsReadyForSerialization) { throw new InvalidOperationException("Cannot serialize a Session while connected"); } - if (_transactionCoordinatorShared) + if (IsTransactionCoordinatorShared) { throw new InvalidOperationException("Cannot serialize a Session sharing its transaction coordinator"); } @@ -143,7 +138,7 @@ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext contex info.AddValue("enabledFilters", enabledFilters, typeof(IDictionary)); info.AddValue("enabledFilterNames", enabledFilterNames, typeof(List)); - info.AddValue("connectionManager", connectionManager, typeof(ConnectionManager)); + info.AddValue("connectionManager", ConnectionManager, typeof(ConnectionManager)); } #endregion @@ -194,21 +189,6 @@ internal SessionImpl(SessionFactoryImpl factory, ISessionCreationOptions options listeners = factory.EventListeners; connectionReleaseMode = options.SessionConnectionReleaseMode; - if (options is ISharedSessionCreationOptions sharedOptions && sharedOptions.IsTransactionCoordinatorShared) - { - // NH specific implementation: need to port Hibernate transaction management. - _transactionCoordinatorShared = true; - if (options.UserSuppliedConnection != null) - throw new SessionException("Cannot simultaneously share transaction context and specify connection"); - connectionManager = sharedOptions.ConnectionManager; - connectionManager.AddDependentSession(this); - } - else - { - connectionManager = new ConnectionManager( - this, options.UserSuppliedConnection, connectionReleaseMode, Interceptor, options.ShouldAutoJoinTransaction); - } - if (factory.Statistics.IsStatisticsEnabled) { factory.StatisticsImplementor.OpenSession(); @@ -249,16 +229,6 @@ protected internal set } } - /// - public override IBatcher Batcher - { - get - { - CheckAndUpdateSessionStatus(); - return connectionManager.Batcher; - } - } - public ConnectionReleaseMode ConnectionReleaseMode { get { return connectionReleaseMode; } @@ -298,10 +268,7 @@ public DbConnection Close() try { - if (!_transactionCoordinatorShared) - return connectionManager.Close(); - connectionManager.RemoveDependentSession(this); - return null; + return CloseConnectionManager(); } finally { @@ -1319,41 +1286,6 @@ public void Refresh(object obj, LockMode lockMode) } } - public ITransaction BeginTransaction(IsolationLevel isolationLevel) - { - using (BeginProcess()) - { - if (_transactionCoordinatorShared) - { - // Todo : should seriously consider not allowing a txn to begin from a child session - // can always route the request to the root session... - log.Warn("Transaction started on non-root session"); - } - - return connectionManager.BeginTransaction(isolationLevel); - } - } - - public ITransaction BeginTransaction() - { - using (BeginProcess()) - { - if (_transactionCoordinatorShared) - { - // Todo : should seriously consider not allowing a txn to begin from a child session - // can always route the request to the root session... - log.Warn("Transaction started on non-root session"); - } - - return connectionManager.BeginTransaction(); - } - } - - public ITransaction Transaction - { - get { return connectionManager.Transaction; } - } - /// /// /// @@ -1394,11 +1326,6 @@ public override void Flush() } } - public override bool TransactionInProgress - { - get { return ConnectionManager.IsInActiveTransaction; } - } - public bool IsDirty() { using (BeginProcess()) @@ -1505,34 +1432,13 @@ public override void InitializeCollection(IPersistentCollection collection, bool } } - public override DbConnection Connection - { - get { return connectionManager.GetConnection(); } - } - - /// - /// Gets if the ISession is connected. - /// - /// - /// if the ISession is connected. - /// - /// - /// An ISession is considered connected if there is an (regardless - /// of its state) or if it the field connect is true. Meaning that it will connect - /// at the next operation that requires a connection. - /// - public override bool IsConnected - { - get { return connectionManager.IsConnected; } - } - /// public DbConnection Disconnect() { using (BeginProcess()) { log.Debug("disconnecting session"); - return connectionManager.Disconnect(); + return ConnectionManager.Disconnect(); } } @@ -1541,7 +1447,7 @@ public void Reconnect() using (BeginProcess()) { log.Debug("reconnecting session"); - connectionManager.Reconnect(); + ConnectionManager.Reconnect(); } } @@ -1550,7 +1456,7 @@ public void Reconnect(DbConnection conn) using (BeginProcess()) { log.Debug("reconnecting session"); - connectionManager.Reconnect(conn); + ConnectionManager.Reconnect(conn); } } @@ -1899,6 +1805,11 @@ public ISharedSessionBuilder SessionWithOptions() return new SharedSessionBuilderImpl(this); } + public ISharedStatelessSessionBuilder StatelessSessionWithOptions() + { + return new SharedStatelessSessionBuilderImpl(this); + } + public void Clear() { using (BeginProcess()) @@ -2033,11 +1944,6 @@ private string[] ParseFilterParameterName(string filterParameterName) return new[] { filterName, parameterName }; } - public override ConnectionManager ConnectionManager - { - get { return connectionManager; } - } - public IMultiQuery CreateMultiQuery() { using (BeginProcess()) @@ -2519,5 +2425,69 @@ public override ISharedSessionBuilder Connection(DbConnection connection) // NH different implementation: need to port Hibernate transaction management. public ConnectionManager ConnectionManager => _shareTransactionContext ? _session.ConnectionManager : null; } + + // NH specific: allow to build a stateless session from a normal session. + private class SharedStatelessSessionBuilderImpl : SessionFactoryImpl.StatelessSessionBuilderImpl, + ISharedStatelessSessionBuilder, ISharedSessionCreationOptions + { + private readonly SessionImpl _session; + private bool _shareTransactionContext; + + public SharedStatelessSessionBuilderImpl(SessionImpl session) + : base((SessionFactoryImpl)session.Factory) + { + _session = session; + SetSelf(this); + } + + #region 6.0 TODO: implement covariance the way used for ISharedSessionBuilder + + ISharedStatelessSessionBuilder ISharedStatelessSessionBuilder.AutoJoinTransaction(bool autoJoinTransaction) + { + AutoJoinTransaction(autoJoinTransaction); + return this; + } + + ISharedStatelessSessionBuilder ISharedStatelessSessionBuilder.Connection(DbConnection connection) + { + Connection(connection); + return this; + } + + #endregion + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SharedSessionBuilder + + public virtual ISharedStatelessSessionBuilder Connection() + { + // Ensure any previously user supplied connection is removed. + base.Connection(null); + // We share the connection manager + _shareTransactionContext = true; + return this; + } + + public virtual ISharedStatelessSessionBuilder AutoJoinTransaction() + { + AutoJoinTransaction(_session.ConnectionManager.ShouldAutoJoinTransaction); + return this; + } + + // NH different implementation, avoid an error case. + public override IStatelessSessionBuilder Connection(DbConnection connection) + { + _shareTransactionContext = false; + return base.Connection(connection); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SharedSessionCreationOptions + + public virtual bool IsTransactionCoordinatorShared => _shareTransactionContext; + + // NH different implementation: need to port Hibernate transaction management. + public ConnectionManager ConnectionManager => _shareTransactionContext ? _session.ConnectionManager : null; + } } } diff --git a/src/NHibernate/Impl/StatelessSessionImpl.cs b/src/NHibernate/Impl/StatelessSessionImpl.cs index 7127f8f786f..1775af7f0c4 100644 --- a/src/NHibernate/Impl/StatelessSessionImpl.cs +++ b/src/NHibernate/Impl/StatelessSessionImpl.cs @@ -1,10 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data; -using System.Data.Common; using System.Linq.Expressions; -using NHibernate.AdoNet; using NHibernate.Cache; using NHibernate.Collection; using NHibernate.Criterion; @@ -28,9 +25,6 @@ public partial class StatelessSessionImpl : AbstractSessionImpl, IStatelessSessi { private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(StatelessSessionImpl)); - [NonSerialized] - private readonly ConnectionManager connectionManager; - [NonSerialized] private readonly StatefulPersistenceContext temporaryPersistenceContext; @@ -40,8 +34,6 @@ internal StatelessSessionImpl(SessionFactoryImpl factory, ISessionCreationOption using (BeginContext()) { temporaryPersistenceContext = new StatefulPersistenceContext(this); - connectionManager = new ConnectionManager(this, options.UserSuppliedConnection, ConnectionReleaseMode.AfterTransaction, - EmptyInterceptor.Instance, options.ShouldAutoJoinTransaction); if (log.IsDebugEnabled()) { @@ -95,15 +87,6 @@ public override long Timestamp get { throw new NotSupportedException(); } } - public override IBatcher Batcher - { - get - { - CheckAndUpdateSessionStatus(); - return connectionManager.Batcher; - } - } - public override void CloseSessionFromSystemTransaction() { Dispose(true); @@ -311,11 +294,6 @@ public override EventListeners Listeners get { throw new NotSupportedException(); } } - public override ConnectionManager ConnectionManager - { - get { return connectionManager; } - } - public override bool IsEventSource { get { return false; } @@ -345,11 +323,6 @@ public override bool IsOpen get { return !IsClosed; } } - public override bool IsConnected - { - get { return connectionManager.IsConnected; } - } - public override FlushMode FlushMode { get { return FlushMode.Commit; } @@ -377,11 +350,6 @@ public override string GuessEntityName(object entity) } } - public override DbConnection Connection - { - get { return connectionManager.GetConnection(); } - } - public IStatelessSession SetBatchSize(int batchSize) { Batcher.BatchSize = batchSize; @@ -401,19 +369,8 @@ public void ManagedFlush() } } - public override bool TransactionInProgress - { - get { return Transaction.IsActive; } - } - #region IStatelessSession Members - /// Get the current Hibernate transaction. - public ITransaction Transaction - { - get { return connectionManager.Transaction; } - } - public override CacheMode CacheMode { get { return CacheMode.Ignore; } @@ -455,7 +412,7 @@ public void ManagedClose() { throw new SessionException("Session was already closed!"); } - ConnectionManager.Close(); + CloseConnectionManager(); SetClosed(); } } @@ -780,28 +737,6 @@ public IQueryOver QueryOver(Expression> alias) where T : class } } - /// - /// Begin a NHibernate transaction - /// - /// A NHibernate transaction - public ITransaction BeginTransaction() - { - return BeginTransaction(IsolationLevel.Unspecified); - } - - /// - /// Begin a NHibernate transaction with the specified isolation level - /// - /// The isolation level - /// A NHibernate transaction - public ITransaction BeginTransaction(IsolationLevel isolationLevel) - { - using (BeginProcess()) - { - return connectionManager.BeginTransaction(isolationLevel); - } - } - #endregion #region IDisposable Members @@ -916,14 +851,14 @@ public override int ExecuteUpdate(IQueryExpression queryExpression, QueryParamet public override FutureCriteriaBatch FutureCriteriaBatch { - get { throw new System.NotSupportedException("future queries are not supported for stateless session"); } - protected internal set { throw new System.NotSupportedException("future queries are not supported for stateless session"); } + get { throw new NotSupportedException("future queries are not supported for stateless session"); } + protected internal set { throw new NotSupportedException("future queries are not supported for stateless session"); } } public override FutureQueryBatch FutureQueryBatch { - get { throw new System.NotSupportedException("future queries are not supported for stateless session"); } - protected internal set { throw new System.NotSupportedException("future queries are not supported for stateless session"); } + get { throw new NotSupportedException("future queries are not supported for stateless session"); } + protected internal set { throw new NotSupportedException("future queries are not supported for stateless session"); } } public override IEntityPersister GetEntityPersister(string entityName, object obj)