diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 1f763ac1df9..883085fc45a 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -1,4 +1,4 @@ -projects: +projects: - filePath: NHibernate/NHibernate.csproj targetFramework: netcoreapp2.0 concurrentRun: true @@ -7,7 +7,7 @@ - pattern: ^.*(Hql\.g).*$ analyzation: methodConversion: -#TODO 6.0: Remove ignore rule for IQueryBatchItem.ProcessResults + #TODO 6.0: Remove ignore rule for IQueryBatchItem.ProcessResults - conversion: Ignore name: ProcessResults containingTypeName: IQueryBatchItem @@ -62,7 +62,7 @@ - conversion: Ignore name: CloseSessionFromSystemTransaction containingTypeName: ISessionImplementor -# TODO 6.0: Remove ignore rule for IStatelessSession.Close + # TODO 6.0: Remove ignore rule for IStatelessSession.Close - conversion: Ignore name: Close containingTypeName: IStatelessSession @@ -114,7 +114,7 @@ - conversion: Ignore name: GetEnumerator containingTypeName: IFutureEnumerable -# TODO 6.0: Consider if ComputeFlattenedParameters should remain ignored or not + # TODO 6.0: Consider if ComputeFlattenedParameters should remain ignored or not - conversion: Ignore name: ComputeFlattenedParameters containingTypeName: SqlQueryImpl @@ -124,12 +124,17 @@ - conversion: ToAsync name: ExecuteNonQuery containingTypeName: IBatcher + - conversion: ToAsync + name: BeginTransaction + containingTypeName: DriverBase - conversion: ToAsync rule: EventListener - conversion: ToAsync rule: Cache - conversion: ToAsync rule: TransactionCompletion + alwaysAwait: + - name: BeginTransaction typeConversion: - conversion: Ignore name: EnumerableImpl @@ -139,6 +144,10 @@ - conversion: Ignore name: DependentContext containingTypeName: AdoNetWithSystemTransactionFactory + asyncExtensionMethods: + projectFiles: + - fileName: AsyncExtensions.cs + projectName: NHibernate ignoreSearchForAsyncCounterparts: - name: GetFieldValue - name: IsDBNull @@ -212,6 +221,8 @@ projectFiles: - fileName: LinqExtensionMethods.cs projectName: NHibernate + - fileName: AsyncExtensions.cs + projectName: NHibernate typeConversion: - conversion: Ignore name: ObjectAssert @@ -307,6 +318,9 @@ methodRules: - filters: - hasAttributeName: ObsoleteAttribute name: Obsolete +- filters: + - containingTypeName: AsyncExtensions + name: BeginTransactionAsync typeRules: - filters: - containingAssemblyName: NHibernate diff --git a/src/NHibernate.Test/Async/TransactionTest/TransactionNotificationFixture.cs b/src/NHibernate.Test/Async/TransactionTest/TransactionNotificationFixture.cs index bf9a1f445a1..49f64598152 100644 --- a/src/NHibernate.Test/Async/TransactionTest/TransactionNotificationFixture.cs +++ b/src/NHibernate.Test/Async/TransactionTest/TransactionNotificationFixture.cs @@ -138,6 +138,16 @@ public async Task ShouldNotifyAfterTransactionWithOwnConnectionAsync(bool usePre public partial class CustomTransaction : ITransaction { + public Task BeginAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task BeginAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + public Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); diff --git a/src/NHibernate.Test/TypesTest/AbstractDateTimeTypeFixture.cs b/src/NHibernate.Test/TypesTest/AbstractDateTimeTypeFixture.cs index 67b36a0d2cc..042b3a96378 100644 --- a/src/NHibernate.Test/TypesTest/AbstractDateTimeTypeFixture.cs +++ b/src/NHibernate.Test/TypesTest/AbstractDateTimeTypeFixture.cs @@ -548,7 +548,7 @@ protected DateTimeKind GetTypeKind() } } - public class ClientDriverWithParamsStats : IDriver + public partial class ClientDriverWithParamsStats : IDriver { private readonly Dictionary _usedSqlTypes = new Dictionary(); private readonly Dictionary _usedDbTypes = new Dictionary(); diff --git a/src/NHibernate/AdoNet/AsyncExtensions.cs b/src/NHibernate/AdoNet/AsyncExtensions.cs new file mode 100644 index 00000000000..55e0b8d7c1a --- /dev/null +++ b/src/NHibernate/AdoNet/AsyncExtensions.cs @@ -0,0 +1,43 @@ +#if NETFX || NETSTANDARD2_0 || NETCOREAPP2_0 +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace NHibernate +{ + internal static class AsyncExtensions + { + public static Task BeginTransactionAsync(this DbConnection connection, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + try + { + return Task.FromResult(connection.BeginTransaction()); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + public static Task BeginTransactionAsync(this DbConnection connection, IsolationLevel isolationLevel, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + try + { + return Task.FromResult(connection.BeginTransaction(isolationLevel)); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + } +} +#endif diff --git a/src/NHibernate/AdoNet/ResultSetWrapper.cs b/src/NHibernate/AdoNet/ResultSetWrapper.cs index c801723e499..b6014a01b89 100644 --- a/src/NHibernate/AdoNet/ResultSetWrapper.cs +++ b/src/NHibernate/AdoNet/ResultSetWrapper.cs @@ -13,7 +13,7 @@ namespace NHibernate.AdoNet /// and Postgres). /// /// - public class ResultSetWrapper : DbDataReader + public partial class ResultSetWrapper : DbDataReader { private DbDataReader rs; private readonly ColumnNameCache columnNameCache; diff --git a/src/NHibernate/Async/AdoNet/ConnectionManager.cs b/src/NHibernate/Async/AdoNet/ConnectionManager.cs index 4032066ee0a..e209d7aee73 100644 --- a/src/NHibernate/Async/AdoNet/ConnectionManager.cs +++ b/src/NHibernate/Async/AdoNet/ConnectionManager.cs @@ -106,6 +106,22 @@ async Task InternalGetConnectionAsync() } } + public async Task BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + EnsureTransactionIsCreated(); + await (_transaction.BeginAsync(isolationLevel, cancellationToken)).ConfigureAwait(false); + return _transaction; + } + + public async Task BeginTransactionAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + EnsureTransactionIsCreated(); + await (_transaction.BeginAsync(cancellationToken)).ConfigureAwait(false); + return _transaction; + } + public async Task CreateCommandAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Driver/DriverBase.cs b/src/NHibernate/Async/Driver/DriverBase.cs new file mode 100644 index 00000000000..a1f6d1c68c2 --- /dev/null +++ b/src/NHibernate/Async/Driver/DriverBase.cs @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using NHibernate.Engine; +using NHibernate.SqlCommand; +using NHibernate.SqlTypes; +using NHibernate.Util; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Driver +{ + using System.Threading.Tasks; + using System.Threading; + public abstract partial class DriverBase : IDriver, ISqlParameterFormatter + { + + /// + /// Begin an ADO . + /// + /// The isolation level requested for the transaction. + /// The connection on which to start the transaction. + /// A cancellation token that can be used to cancel the work + /// The started . + public virtual async Task BeginTransactionAsync(IsolationLevel isolationLevel, DbConnection connection, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (isolationLevel == IsolationLevel.Unspecified) + { + return await (connection.BeginTransactionAsync(cancellationToken)).ConfigureAwait(false); + } + return await (connection.BeginTransactionAsync(isolationLevel, cancellationToken)).ConfigureAwait(false); + } + + #if NETFX + #else + + #endif + } +} diff --git a/src/NHibernate/Async/Driver/DriverExtensions.cs b/src/NHibernate/Async/Driver/DriverExtensions.cs new file mode 100644 index 00000000000..ca1150bf957 --- /dev/null +++ b/src/NHibernate/Async/Driver/DriverExtensions.cs @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Data; +using System.Data.Common; +using NHibernate.AdoNet; +using NHibernate.SqlTypes; +using NHibernate.Util; + +namespace NHibernate.Driver +{ + using System.Threading.Tasks; + using System.Threading; + public static partial class DriverExtensions + { + + // 6.0 TODO: merge into IDriver + /// + /// Begin an ADO . + /// + /// The driver. + /// The isolation level requested for the transaction. + /// The connection on which to start the transaction. + /// A cancellation token that can be used to cancel the work + /// The started . + public static async Task BeginTransactionAsync(this IDriver driver, IsolationLevel isolationLevel, DbConnection connection, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (driver is DriverBase driverBase) + { + return await (driverBase.BeginTransactionAsync(isolationLevel, connection, cancellationToken)).ConfigureAwait(false); + } + + // So long for custom drivers not deriving from DriverBase, they will have to wait for 6.0 if they + // need the feature. + if (isolationLevel == IsolationLevel.Unspecified) + { + return await (connection.BeginTransactionAsync(cancellationToken)).ConfigureAwait(false); + } + return await (connection.BeginTransactionAsync(isolationLevel, cancellationToken)).ConfigureAwait(false); + } + } +} diff --git a/src/NHibernate/Async/ITransaction.cs b/src/NHibernate/Async/ITransaction.cs index ea6ea4b3ab9..bf84f852bfa 100644 --- a/src/NHibernate/Async/ITransaction.cs +++ b/src/NHibernate/Async/ITransaction.cs @@ -11,14 +11,26 @@ using System; using System.Data; using System.Data.Common; +using System.Threading.Tasks; using NHibernate.Transaction; namespace NHibernate { - using System.Threading.Tasks; using System.Threading; public partial interface ITransaction : IDisposable { + /// + /// Begin the transaction with the default isolation level. + /// + /// A cancellation token that can be used to cancel the work + Task BeginAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Begin the transaction with the specified isolation level. + /// + /// Isolation level of the transaction + /// A cancellation token that can be used to cancel the work + Task BeginAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default(CancellationToken)); /// /// Flush the associated ISession and end the unit of work. diff --git a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs index ed3948ad38e..770634cb5c9 100644 --- a/src/NHibernate/Async/Impl/AbstractSessionImpl.cs +++ b/src/NHibernate/Async/Impl/AbstractSessionImpl.cs @@ -212,6 +212,49 @@ protected async Task AfterOperationAsync(bool success, CancellationToken cancell } } + /// + /// Begin a NHibernate transaction + /// + /// A cancellation token that can be used to cancel the work + /// A NHibernate transaction + public async Task BeginTransactionAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + 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 await (ConnectionManager.BeginTransactionAsync(cancellationToken)).ConfigureAwait(false); + } + } + + /// + /// Begin a NHibernate transaction with the specified isolation level + /// + /// The isolation level + /// A cancellation token that can be used to cancel the work + /// A NHibernate transaction + public async Task BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + 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 await (ConnectionManager.BeginTransactionAsync(isolationLevel, cancellationToken)).ConfigureAwait(false); + } + } + public abstract Task CreateFilterAsync(object collection, IQueryExpression queryExpression, CancellationToken cancellationToken); public abstract Task EnumerableAsync(IQueryExpression queryExpression, QueryParameters queryParameters, CancellationToken cancellationToken); diff --git a/src/NHibernate/Async/Transaction/AdoNetTransactionFactory.cs b/src/NHibernate/Async/Transaction/AdoNetTransactionFactory.cs index 2fd5c3fa458..41da2ed1eb5 100644 --- a/src/NHibernate/Async/Transaction/AdoNetTransactionFactory.cs +++ b/src/NHibernate/Async/Transaction/AdoNetTransactionFactory.cs @@ -51,7 +51,7 @@ async Task InternalExecuteWorkInIsolationAsync() if (transacted) { - trans = connection.BeginTransaction(); + trans = await (connection.BeginTransactionAsync(cancellationToken)).ConfigureAwait(false); } await (work.DoWorkAsync(connection, trans, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Transaction/AdoTransaction.cs b/src/NHibernate/Async/Transaction/AdoTransaction.cs index a8b249130fa..97009f4ae72 100644 --- a/src/NHibernate/Async/Transaction/AdoTransaction.cs +++ b/src/NHibernate/Async/Transaction/AdoTransaction.cs @@ -23,6 +23,71 @@ namespace NHibernate.Transaction public partial class AdoTransaction : ITransaction { + public Task BeginAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return BeginAsync(IsolationLevel.Unspecified, cancellationToken); + } + + /// + /// Begins the on the + /// used by the . + /// + /// + /// Thrown if there is any problems encountered while trying to create + /// the . + /// + public async Task BeginAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + using (session.BeginProcess()) + { + if (begun) + { + return; + } + + if (commitFailed) + { + throw new TransactionException("Cannot restart transaction after failed commit"); + } + + if (isolationLevel == IsolationLevel.Unspecified) + { + isolationLevel = session.Factory.Settings.IsolationLevel; + } + + log.Debug("Begin ({0})", isolationLevel); + + try + { + trans = await (session.Factory.ConnectionProvider.Driver.BeginTransactionAsync(isolationLevel, session.Connection, cancellationToken)).ConfigureAwait(false); + } + catch (OperationCanceledException) { throw; } + catch (HibernateException) + { + // Don't wrap HibernateExceptions + throw; + } + catch (Exception e) + { + log.Error(e, "Begin transaction failed"); + throw new TransactionException("Begin failed with SQL exception", e); + } + + begun = true; + committed = false; + rolledBack = false; + + session.AfterTransactionBegin(this); + foreach (var dependentSession in session.ConnectionManager.DependentSessions) + dependentSession.AfterTransactionBegin(this); + } + } + private async Task AfterTransactionCompletionAsync(bool successful, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Driver/DriverBase.cs b/src/NHibernate/Driver/DriverBase.cs index be5c81366e1..9e18bdf0255 100644 --- a/src/NHibernate/Driver/DriverBase.cs +++ b/src/NHibernate/Driver/DriverBase.cs @@ -14,7 +14,7 @@ namespace NHibernate.Driver /// /// Base class for the implementation of IDriver /// - public abstract class DriverBase : IDriver, ISqlParameterFormatter + public abstract partial class DriverBase : IDriver, ISqlParameterFormatter { private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(DriverBase)); diff --git a/src/NHibernate/Driver/DriverExtensions.cs b/src/NHibernate/Driver/DriverExtensions.cs index d370269c35e..3bf194d61ba 100644 --- a/src/NHibernate/Driver/DriverExtensions.cs +++ b/src/NHibernate/Driver/DriverExtensions.cs @@ -6,7 +6,7 @@ namespace NHibernate.Driver { - public static class DriverExtensions + public static partial class DriverExtensions { internal static void AdjustParameterForValue(this IDriver driver, DbParameter parameter, SqlType sqlType, object value) { diff --git a/src/NHibernate/ITransaction.cs b/src/NHibernate/ITransaction.cs index 9ed0a1dff78..c9f87032395 100644 --- a/src/NHibernate/ITransaction.cs +++ b/src/NHibernate/ITransaction.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.Common; +using System.Threading.Tasks; using NHibernate.Transaction; namespace NHibernate @@ -110,5 +111,31 @@ public static void RegisterSynchronization( $"{transaction.GetType()} does not support {nameof(ITransactionCompletionSynchronization)}"); registerMethod.Invoke(transaction, new object[] { synchronization }); } + + public static Task BeginAsync(this ITransaction transaction, IsolationLevel isolationLevel) + { + try + { + transaction.Begin(isolationLevel); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + public static Task BeginAsync(this ITransaction transaction) + { + try + { + transaction.Begin(); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } } }