diff --git a/.editorconfig b/.editorconfig index 15901d7cb25..983372b5b1f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,6 +27,9 @@ dotnet_diagnostic.NUnit2031.severity = suggestion dotnet_diagnostic.NUnit2049.severity = suggestion # The SameAs constraint always fails on value types as the actual and the expected value cannot be the same reference dotnet_diagnostic.NUnit2040.severity = suggestion +dotnet_diagnostic.CA1849.severity = error +dotnet_diagnostic.CA2007.severity = error +dotnet_code_quality.CA2007.output_kind = DynamicallyLinkedLibrary [*.xsd] indent_style = tab diff --git a/.github/workflows/NetCoreTests.yml b/.github/workflows/NetCoreTests.yml index 093c1e99534..da628006784 100644 --- a/.github/workflows/NetCoreTests.yml +++ b/.github/workflows/NetCoreTests.yml @@ -16,7 +16,7 @@ jobs: DB_INIT: | docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=P@ssw0rd" -e "MSSQL_PID=Express" -p 1433:1433 -d --name sqlexpress mcr.microsoft.com/mssql/server:2019-latest; - DB: SqlServer2008-MicrosoftDataSqlClientDriver - CONNECTION_STRING: "Server=localhost;initial catalog=nhibernate;User Id=sa;Password=P@ssw0rd;packet size=4096;" + CONNECTION_STRING: "Server=localhost;initial catalog=nhibernate;User Id=sa;Password=P@ssw0rd;packet size=4096;TrustServerCertificate=true;" OS: ubuntu-latest DB_INIT: | docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=P@ssw0rd" -e "MSSQL_PID=Express" -p 1433:1433 -d --name sqlexpress mcr.microsoft.com/mssql/server:2019-latest; diff --git a/src/NHibernate.Test/Ado/BatcherFixture.cs b/src/NHibernate.Test/Ado/BatcherFixture.cs index df7a68d452c..ff3bfd7aace 100644 --- a/src/NHibernate.Test/Ado/BatcherFixture.cs +++ b/src/NHibernate.Test/Ado/BatcherFixture.cs @@ -4,9 +4,18 @@ namespace NHibernate.Test.Ado { - [TestFixture] +#if NET6_0_OR_GREATER + [TestFixture(true)] +#endif + [TestFixture(false)] public class BatcherFixture: TestCase { + private readonly bool _useDbBatch; + + public BatcherFixture(bool useDbBatch) + { + _useDbBatch = useDbBatch; + } protected override string MappingsAssembly { get { return "NHibernate.Test"; } @@ -22,10 +31,22 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Environment.FormatSql, "true"); configuration.SetProperty(Environment.GenerateStatistics, "true"); configuration.SetProperty(Environment.BatchSize, "10"); + #if NET6_0_OR_GREATER + if (_useDbBatch) + { + configuration.SetProperty(Environment.BatchStrategy, typeof(DbBatchBatcherFactory).AssemblyQualifiedName); + } + #endif } protected override bool AppliesTo(Engine.ISessionFactoryImplementor factory) { +#if NET6_0_OR_GREATER + if (_useDbBatch) + { + return factory.Settings.BatcherFactory is DbBatchBatcherFactory && factory.Settings.ConnectionProvider.Driver is Driver.DriverBase driverBase && driverBase.CanCreateBatch; + } +#endif return !(factory.Settings.BatcherFactory is NonBatchingBatcherFactory); } @@ -129,20 +150,25 @@ public void SqlClientOneRoundTripForUpdateAndInsert() Cleanup(); } - [Test, NetFxOnly] + [Test] [Description("SqlClient: The batcher log output should be formatted")] public void BatchedoutputShouldBeFormatted() { #if NETFX if (Sfi.Settings.BatcherFactory is SqlClientBatchingBatcherFactory == false) Assert.Ignore("This test is for SqlClientBatchingBatcher only"); +#elif NET6_0_OR_GREATER + if (Sfi.Settings.BatcherFactory is DbBatchBatcherFactory == false) + Assert.Ignore("This test is for DbBatchBatcherFactory only"); +#else + Assert.Ignore("This test is for NETFX and NET6_0_OR_GREATER only"); #endif using (var sqlLog = new SqlLogSpy()) { FillDb(); var log = sqlLog.GetWholeLog(); - Assert.IsTrue(log.Contains("INSERT \n INTO")); + Assert.That(log, Does.Contain("INSERT \n INTO").IgnoreCase); } Cleanup(); @@ -213,7 +239,7 @@ public void AbstractBatcherLog() foreach (var loggingEvent in sl.Appender.GetEvents()) { string message = loggingEvent.RenderedMessage; - if(message.ToLowerInvariant().Contains("insert")) + if(message.Contains("insert")) { Assert.That(message, Does.Contain("batch").IgnoreCase); } diff --git a/src/NHibernate.Test/Async/Ado/BatcherFixture.cs b/src/NHibernate.Test/Async/Ado/BatcherFixture.cs index 17dfbcbeaa9..96146f114a3 100644 --- a/src/NHibernate.Test/Async/Ado/BatcherFixture.cs +++ b/src/NHibernate.Test/Async/Ado/BatcherFixture.cs @@ -16,9 +16,18 @@ namespace NHibernate.Test.Ado { using System.Threading.Tasks; using System.Threading; - [TestFixture] +#if NET6_0_OR_GREATER + [TestFixture(true)] +#endif + [TestFixture(false)] public class BatcherFixtureAsync: TestCase { + private readonly bool _useDbBatch; + + public BatcherFixtureAsync(bool useDbBatch) + { + _useDbBatch = useDbBatch; + } protected override string MappingsAssembly { get { return "NHibernate.Test"; } @@ -34,10 +43,22 @@ protected override void Configure(Configuration configuration) configuration.SetProperty(Environment.FormatSql, "true"); configuration.SetProperty(Environment.GenerateStatistics, "true"); configuration.SetProperty(Environment.BatchSize, "10"); + #if NET6_0_OR_GREATER + if (_useDbBatch) + { + configuration.SetProperty(Environment.BatchStrategy, typeof(DbBatchBatcherFactory).AssemblyQualifiedName); + } + #endif } protected override bool AppliesTo(Engine.ISessionFactoryImplementor factory) { +#if NET6_0_OR_GREATER + if (_useDbBatch) + { + return factory.Settings.BatcherFactory is DbBatchBatcherFactory && factory.Settings.ConnectionProvider.Driver is Driver.DriverBase driverBase && driverBase.CanCreateBatch; + } +#endif return !(factory.Settings.BatcherFactory is NonBatchingBatcherFactory); } @@ -101,20 +122,25 @@ public async Task OneRoundTripUpdateAsync() await (CleanupAsync()); } - [Test, NetFxOnly] + [Test] [Description("SqlClient: The batcher log output should be formatted")] public async Task BatchedoutputShouldBeFormattedAsync() { #if NETFX if (Sfi.Settings.BatcherFactory is SqlClientBatchingBatcherFactory == false) Assert.Ignore("This test is for SqlClientBatchingBatcher only"); +#elif NET6_0_OR_GREATER + if (Sfi.Settings.BatcherFactory is DbBatchBatcherFactory == false) + Assert.Ignore("This test is for DbBatchBatcherFactory only"); +#else + Assert.Ignore("This test is for NETFX and NET6_0_OR_GREATER only"); #endif using (var sqlLog = new SqlLogSpy()) { await (FillDbAsync()); var log = sqlLog.GetWholeLog(); - Assert.IsTrue(log.Contains("INSERT \n INTO")); + Assert.That(log, Does.Contain("INSERT \n INTO").IgnoreCase); } await (CleanupAsync()); @@ -185,7 +211,7 @@ public async Task AbstractBatcherLogAsync() foreach (var loggingEvent in sl.Appender.GetEvents()) { string message = loggingEvent.RenderedMessage; - if(message.ToLowerInvariant().Contains("insert")) + if(message.Contains("insert")) { Assert.That(message, Does.Contain("batch").IgnoreCase); } diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index f5fa0f408b7..32cb0011d9b 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -64,7 +64,7 @@ - + diff --git a/src/NHibernate.sln b/src/NHibernate.sln index 4eec9b87633..0b2a7f25d0b 100644 --- a/src/NHibernate.sln +++ b/src/NHibernate.sln @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\appveyor.yml = ..\appveyor.yml ..\ReleaseProcedure.txt = ..\ReleaseProcedure.txt ..\global.json = ..\global.json + ..\.editorconfig = ..\.editorconfig EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHibernate", "NHibernate\NHibernate.csproj", "{5909BFE7-93CF-4E5F-BE22-6293368AF01D}" diff --git a/src/NHibernate/Action/BulkOperationCleanupAction.cs b/src/NHibernate/Action/BulkOperationCleanupAction.cs index 5308d2834d7..2558e497610 100644 --- a/src/NHibernate/Action/BulkOperationCleanupAction.cs +++ b/src/NHibernate/Action/BulkOperationCleanupAction.cs @@ -166,8 +166,8 @@ public virtual void Init() [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] public virtual async Task InitAsync(CancellationToken cancellationToken) { - await EvictEntityRegionsAsync(cancellationToken); - await EvictCollectionRegionsAsync(cancellationToken); + await EvictEntityRegionsAsync(cancellationToken).ConfigureAwait(false); + await EvictCollectionRegionsAsync(cancellationToken).ConfigureAwait(false); } } } diff --git a/src/NHibernate/AdoNet/ConnectionManager.cs b/src/NHibernate/AdoNet/ConnectionManager.cs index 882452b4d98..8cd5d4c0533 100644 --- a/src/NHibernate/AdoNet/ConnectionManager.cs +++ b/src/NHibernate/AdoNet/ConnectionManager.cs @@ -514,6 +514,30 @@ public void EnlistInTransaction(DbCommand command) } } +#if NET6_0_OR_GREATER + /// + /// Enlist a batch in the current transaction, if any. + /// + /// The batch to enlist. + public void EnlistInTransaction(DbBatch batch) + { + if (batch == null) + throw new ArgumentNullException(nameof(batch)); + + if (_transaction != null) + { + _transaction.Enlist(batch); + return; + } + + if (batch.Transaction != null) + { + _log.Warn("set a nonnull DbBatch.Transaction to null because the Session had no Transaction"); + batch.Transaction = null; + } + } +#endif + /// /// Enlist the connection into provided transaction if the connection should be enlisted. /// Do nothing in case an explicit transaction is ongoing. diff --git a/src/NHibernate/AdoNet/DbBatchBatcher.cs b/src/NHibernate/AdoNet/DbBatchBatcher.cs new file mode 100644 index 00000000000..4914a107764 --- /dev/null +++ b/src/NHibernate/AdoNet/DbBatchBatcher.cs @@ -0,0 +1,314 @@ +#if NET6_0_OR_GREATER +using System; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NHibernate.AdoNet.Util; +using NHibernate.Driver; +using NHibernate.Exceptions; + +namespace NHibernate.AdoNet +{ + public class DbBatchBatcher : AbstractBatcher + { + private int _batchSize; + private int _totalExpectedRowsAffected; + private DbBatch _currentBatch; + private StringBuilder _currentBatchCommandsLog; + private readonly int _defaultTimeout; + private readonly ConnectionManager _connectionManager; + + public DbBatchBatcher(ConnectionManager connectionManager, IInterceptor interceptor) + : base(connectionManager, interceptor) + { + _batchSize = Factory.Settings.AdoBatchSize; + _defaultTimeout = Driver.GetCommandTimeout(); + + _currentBatch = CreateConfiguredBatch(); + //we always create this, because we need to deal with a scenario in which + //the user change the logging configuration at runtime. Trying to put this + //behind an if(log.IsDebugEnabled) will cause a null reference exception + //at that point. + _currentBatchCommandsLog = new StringBuilder().AppendLine("Batch commands:"); + _connectionManager = connectionManager; + } + + public override int BatchSize + { + get => _batchSize; + set => _batchSize = value; + } + + protected override int CountOfStatementsInCurrentBatch => _currentBatch.BatchCommands.Count; + + private void LogBatchCommand(DbCommand batchUpdate) + { + string lineWithParameters = null; + var sqlStatementLogger = Factory.Settings.SqlStatementLogger; + if (sqlStatementLogger.IsDebugEnabled || Log.IsDebugEnabled()) + { + lineWithParameters = sqlStatementLogger.GetCommandLineWithParameters(batchUpdate); + var formatStyle = sqlStatementLogger.DetermineActualStyle(FormatStyle.Basic); + lineWithParameters = formatStyle.Formatter.Format(lineWithParameters); + _currentBatchCommandsLog.Append("command ") + .Append(_currentBatch.BatchCommands.Count) + .Append(':') + .AppendLine(lineWithParameters); + } + + if (Log.IsDebugEnabled()) + { + Log.Debug("Adding to batch:{0}", lineWithParameters); + } + } + + private void AddCommandToBatch(DbCommand batchUpdate) + { + var dbBatchCommand = Driver.CreateDbBatchCommandFromDbCommand(_currentBatch, batchUpdate); + + _currentBatch.BatchCommands.Add(dbBatchCommand); + } + + public override void AddToBatch(IExpectation expectation) + { + _totalExpectedRowsAffected += expectation.ExpectedRowCount; + var batchUpdate = CurrentCommand; + Driver.AdjustCommand(batchUpdate); + LogBatchCommand(batchUpdate); + AddCommandToBatch(batchUpdate); + + if (CountOfStatementsInCurrentBatch >= _batchSize) + { + ExecuteBatchWithTiming(batchUpdate); + } + } + + public override Task AddToBatchAsync(IExpectation expectation, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + _totalExpectedRowsAffected += expectation.ExpectedRowCount; + var batchUpdate = CurrentCommand; + Driver.AdjustCommand(batchUpdate); + LogBatchCommand(batchUpdate); + AddCommandToBatch(batchUpdate); + + if (CountOfStatementsInCurrentBatch >= _batchSize) + { + return ExecuteBatchWithTimingAsync(batchUpdate, cancellationToken); + } + + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + protected override void DoExecuteBatch(DbCommand ps) + { + try + { + Log.Debug("Executing batch"); + CheckReaders(); + Prepare(_currentBatch); + if (Factory.Settings.SqlStatementLogger.IsDebugEnabled) + { + Factory.Settings.SqlStatementLogger.LogBatchCommand(_currentBatchCommandsLog.ToString()); + } + int rowsAffected; + try + { + rowsAffected = _currentBatch.ExecuteNonQuery(); + } + catch (DbException e) + { + throw ADOExceptionHelper.Convert(Factory.SQLExceptionConverter, e, "could not execute batch command."); + } + + Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, rowsAffected, ps); + } + finally + { + ClearCurrentBatch(); + } + } + + protected override async Task DoExecuteBatchAsync(DbCommand ps, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + Log.Debug("Executing batch"); + await CheckReadersAsync(cancellationToken).ConfigureAwait(false); + await PrepareAsync(_currentBatch, cancellationToken).ConfigureAwait(false); + if (Factory.Settings.SqlStatementLogger.IsDebugEnabled) + { + Factory.Settings.SqlStatementLogger.LogBatchCommand(_currentBatchCommandsLog.ToString()); + } + int rowsAffected; + try + { + rowsAffected = await _currentBatch.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + catch (DbException e) + { + throw ADOExceptionHelper.Convert(Factory.SQLExceptionConverter, e, "could not execute batch command."); + } + + Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, rowsAffected, ps); + } + finally + { + ClearCurrentBatch(); + } + } + + private DbBatch CreateConfiguredBatch() + { + var result = Driver.CreateBatch(); + if (_defaultTimeout > 0) + { + try + { + result.Timeout = _defaultTimeout; + } + catch (Exception e) + { + if (Log.IsWarnEnabled()) + { + Log.Warn(e, e.ToString()); + } + } + } + + return result; + } + + private void ClearCurrentBatch() + { + _currentBatch.Dispose(); + _totalExpectedRowsAffected = 0; + _currentBatch = CreateConfiguredBatch(); + + if (Factory.Settings.SqlStatementLogger.IsDebugEnabled) + { + _currentBatchCommandsLog = new StringBuilder().AppendLine("Batch commands:"); + } + } + + public override void CloseCommands() + { + base.CloseCommands(); + + // Prevent exceptions when closing the batch from hiding any original exception + // (We do not know here if this batch closing occurs after a failure or not.) + try + { + ClearCurrentBatch(); + } + catch (Exception e) + { + Log.Warn(e, "Exception clearing batch"); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + // Prevent exceptions when closing the batch from hiding any original exception + // (We do not know here if this batch closing occurs after a failure or not.) + try + { + _currentBatch.Dispose(); + } + catch (Exception e) + { + Log.Warn(e, "Exception closing batcher"); + } + } + + /// + /// Prepares the for execution in the database. + /// + /// + /// This takes care of hooking the up to an + /// and if one exists. It will call Prepare if the Driver + /// supports preparing batches. + /// + protected void Prepare(DbBatch batch) + { + try + { + var sessionConnection = _connectionManager.GetConnection(); + + if (batch.Connection != null) + { + // make sure the commands connection is the same as the Sessions connection + // these can be different when the session is disconnected and then reconnected + if (batch.Connection != sessionConnection) + { + batch.Connection = sessionConnection; + } + } + else + { + batch.Connection = sessionConnection; + } + + _connectionManager.EnlistInTransaction(batch); + Driver.PrepareBatch(batch); + } + catch (InvalidOperationException ioe) + { + throw new ADOException("While preparing " + string.Join(Environment.NewLine, batch.BatchCommands.Select(x => x.CommandText)) + " an error occurred", ioe); + } + } + + /// + /// Prepares the for execution in the database. + /// + /// + /// This takes care of hooking the up to an + /// and if one exists. It will call Prepare if the Driver + /// supports preparing batches. + /// + protected async Task PrepareAsync(DbBatch batch, CancellationToken cancellationToken) + { + try + { + var sessionConnection = await _connectionManager.GetConnectionAsync(cancellationToken).ConfigureAwait(false); + + if (batch.Connection != null) + { + // make sure the commands connection is the same as the Sessions connection + // these can be different when the session is disconnected and then reconnected + if (batch.Connection != sessionConnection) + { + batch.Connection = sessionConnection; + } + } + else + { + batch.Connection = sessionConnection; + } + + _connectionManager.EnlistInTransaction(batch); + Driver.PrepareBatch(batch); + } + catch (InvalidOperationException ioe) + { + throw new ADOException("While preparing " + string.Join(Environment.NewLine, batch.BatchCommands.Select(x => x.CommandText)) + " an error occurred", ioe); + } + } + } +} +#endif diff --git a/src/NHibernate/AdoNet/DbBatchBatcherFactory.cs b/src/NHibernate/AdoNet/DbBatchBatcherFactory.cs new file mode 100644 index 00000000000..c6928c106f8 --- /dev/null +++ b/src/NHibernate/AdoNet/DbBatchBatcherFactory.cs @@ -0,0 +1,14 @@ +#if NET6_0_OR_GREATER +using NHibernate.Engine; + +namespace NHibernate.AdoNet +{ + public class DbBatchBatcherFactory : IBatcherFactory + { + public IBatcher CreateBatcher(ConnectionManager connectionManager, IInterceptor interceptor) + { + return new DbBatchBatcher(connectionManager, interceptor); + } + } +} +#endif diff --git a/src/NHibernate/Async/ITransaction.cs b/src/NHibernate/Async/ITransaction.cs index ea6ea4b3ab9..c76af0124de 100644 --- a/src/NHibernate/Async/ITransaction.cs +++ b/src/NHibernate/Async/ITransaction.cs @@ -35,5 +35,8 @@ public partial interface ITransaction : IDisposable /// /// A cancellation token that can be used to cancel the work Task RollbackAsync(CancellationToken cancellationToken = default(CancellationToken)); + +#if NET6_0_OR_GREATER +#endif } } diff --git a/src/NHibernate/Driver/DbProviderFactoryDriveConnectionCommandProvider.cs b/src/NHibernate/Driver/DbProviderFactoryDriveConnectionCommandProvider.cs index 3c3a36db553..d1773548451 100644 --- a/src/NHibernate/Driver/DbProviderFactoryDriveConnectionCommandProvider.cs +++ b/src/NHibernate/Driver/DbProviderFactoryDriveConnectionCommandProvider.cs @@ -25,5 +25,10 @@ public DbCommand CreateCommand() { return dbProviderFactory.CreateCommand(); } +#if NET6_0_OR_GREATER + public DbBatch CreateBatch() => dbProviderFactory.CreateBatch(); + + public bool CanCreateBatch => dbProviderFactory.CanCreateBatch && dbProviderFactory.CreateCommand().CreateParameter() is ICloneable; +#endif } -} \ No newline at end of file +} diff --git a/src/NHibernate/Driver/DriverBase.cs b/src/NHibernate/Driver/DriverBase.cs index be5c81366e1..74feae035b0 100644 --- a/src/NHibernate/Driver/DriverBase.cs +++ b/src/NHibernate/Driver/DriverBase.cs @@ -329,6 +329,70 @@ public virtual void AdjustCommand(DbCommand command) { } +#if NET6_0_OR_GREATER + public void PrepareBatch(DbBatch batch) + { + AdjustBatch(batch); + OnBeforePrepare(batch); + + if (SupportsPreparingCommands && prepareSql) + { + batch.Prepare(); + } + } + + /// + /// Override to make any adjustments to the DbBatch object. (e.g., Oracle custom OUT parameter) + /// Parameters have been bound by this point, so their order can be adjusted too. + /// This is analogous to the RegisterResultSetOutParameter() function in Hibernate. + /// + protected virtual void OnBeforePrepare(DbBatch command) + { + } + + /// + /// Override to make any adjustments to each DbBatch object before it added to the batcher. + /// + /// The batch. + /// + /// This method is similar to the but, instead be called just before execute the command (that can be a batch) + /// is executed before add each single command to the batcher and before . + /// If you have to adjust parameters values/type (when the command is full filled) this is a good place where do it. + /// + public virtual void AdjustBatch(DbBatch batch) + { + + } + + public virtual DbBatch CreateBatch() + { + throw new NotImplementedException(); + } + + public virtual bool CanCreateBatch => false; + + /// + /// Override to use a custom mechanism to create a from a . + /// The default implementation relies on the parameters implementing (and properly supporting) + /// + /// + /// + /// + public virtual DbBatchCommand CreateDbBatchCommandFromDbCommand(DbBatch dbBatch, DbCommand dbCommand) + { + var dbBatchCommand = dbBatch.CreateBatchCommand(); + dbBatchCommand.CommandText = dbCommand.CommandText; + dbBatchCommand.CommandType = dbCommand.CommandType; + + foreach (var param in dbCommand.Parameters) + { + dbBatchCommand.Parameters.Add(((ICloneable) param).Clone()); + } + return dbBatchCommand; + } + +#endif + public DbParameter GenerateOutputParameter(DbCommand command) { var param = GenerateParameter(command, "ReturnValue", SqlTypeFactory.Int32); diff --git a/src/NHibernate/Driver/IDriveConnectionCommandProvider.cs b/src/NHibernate/Driver/IDriveConnectionCommandProvider.cs index db6de57696d..12df92b7ddb 100644 --- a/src/NHibernate/Driver/IDriveConnectionCommandProvider.cs +++ b/src/NHibernate/Driver/IDriveConnectionCommandProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Data.Common; namespace NHibernate.Driver @@ -6,5 +7,9 @@ public interface IDriveConnectionCommandProvider { DbConnection CreateConnection(); DbCommand CreateCommand(); +#if NET6_0_OR_GREATER + DbBatch CreateBatch() => throw new NotImplementedException(); + bool CanCreateBatch => false; +#endif } -} \ No newline at end of file +} diff --git a/src/NHibernate/Driver/IDriver.cs b/src/NHibernate/Driver/IDriver.cs index 61e40b64213..ec77500dabf 100644 --- a/src/NHibernate/Driver/IDriver.cs +++ b/src/NHibernate/Driver/IDriver.cs @@ -163,5 +163,48 @@ public interface IDriver /// The minimal date supplied as a supported by this driver. /// DateTime MinDate { get; } + +#if NET6_0_OR_GREATER + /// + /// Create a + /// + /// + /// + DbBatch CreateBatch() => throw new NotImplementedException(); + + /// + /// Can this driver create es? + /// + bool CanCreateBatch => false; + + /// + /// Make any adjustments to each object before it is added to the batcher. + /// + /// The batch. + /// + /// This method should be executed before adding each single batch to the batcher. + /// If you have to adjust parameters values/type (when the command is fullfilled) this is a good place to do it. + /// + void AdjustBatch(DbBatch dbBatch) => throw new NotImplementedException(); + + /// + /// Prepare the by calling . + /// May be a no-op if the driver does not support preparing commands, or for any other reason. + /// + /// The batch. + void PrepareBatch(DbBatch dbBatch) => throw new NotImplementedException(); + + /// + /// Creates (clones) a from a , + /// copying its , + /// and all its parameters. + /// The returned will not be added to + /// + /// + /// + /// + /// + DbBatchCommand CreateDbBatchCommandFromDbCommand(DbBatch dbBatch, DbCommand dbCommand) => throw new NotImplementedException(); +#endif } } diff --git a/src/NHibernate/Driver/ReflectionBasedDriver.cs b/src/NHibernate/Driver/ReflectionBasedDriver.cs index b8108102826..f9664726e3f 100644 --- a/src/NHibernate/Driver/ReflectionBasedDriver.cs +++ b/src/NHibernate/Driver/ReflectionBasedDriver.cs @@ -7,16 +7,16 @@ namespace NHibernate.Driver public abstract class ReflectionBasedDriver : DriverBase { protected const string ReflectionTypedProviderExceptionMessageTemplate = "The DbCommand and DbConnection implementation in the assembly {0} could not be found. " - + "Ensure that the assembly {0} is located in the application directory or in the Global " - + "Assembly Cache. If the assembly is in the GAC, use element in the " - + "application configuration file to specify the full name of the assembly."; + + "Ensure that the assembly {0} is located in the application directory or in the Global " + + "Assembly Cache. If the assembly is in the GAC, use element in the " + + "application configuration file to specify the full name of the assembly."; private readonly IDriveConnectionCommandProvider connectionCommandProvider; /// /// If the driver use a third party driver (not a .Net Framework DbProvider), its assembly version. /// - protected Version DriverVersion { get; } + protected Version DriverVersion { get; } /// /// Initializes a new instance of with @@ -50,7 +50,7 @@ protected ReflectionBasedDriver(string providerInvariantName, string driverAssem if (string.IsNullOrEmpty(providerInvariantName)) { #endif - throw new HibernateException(string.Format(ReflectionTypedProviderExceptionMessageTemplate, driverAssemblyName)); + throw new HibernateException(string.Format(ReflectionTypedProviderExceptionMessageTemplate, driverAssemblyName)); #if NETFX || NETSTANDARD2_1_OR_GREATER } var factory = DbProviderFactories.GetFactory(providerInvariantName); @@ -73,5 +73,11 @@ public override DbCommand CreateCommand() { return connectionCommandProvider.CreateCommand(); } + +#if NET6_0_OR_GREATER + public override DbBatch CreateBatch() => connectionCommandProvider.CreateBatch(); + + public override bool CanCreateBatch => connectionCommandProvider.CanCreateBatch; +#endif } } diff --git a/src/NHibernate/Driver/ReflectionDriveConnectionCommandProvider.cs b/src/NHibernate/Driver/ReflectionDriveConnectionCommandProvider.cs index 4d007ba96fb..7ff2705c0c2 100644 --- a/src/NHibernate/Driver/ReflectionDriveConnectionCommandProvider.cs +++ b/src/NHibernate/Driver/ReflectionDriveConnectionCommandProvider.cs @@ -21,6 +21,14 @@ public ReflectionDriveConnectionCommandProvider(System.Type connectionType, Syst } this.connectionType = connectionType; this.commandType = commandType; +#if NET6_0_OR_GREATER + _canCreateBatch = new Lazy(() => { + using (var connection = CreateConnection()) + { + return connection.CanCreateBatch && connection.CreateCommand().CreateParameter() is ICloneable; + } + }); +#endif } #region IDriveConnectionCommandProvider Members @@ -36,5 +44,21 @@ public DbCommand CreateCommand() } #endregion + +#if NET6_0_OR_GREATER + private Lazy _canCreateBatch; + + public DbBatch CreateBatch() + { + using (var connection = CreateConnection()) + { + var batch = connection.CreateBatch(); + batch.Connection = null; + return batch; + } + } + + public bool CanCreateBatch => _canCreateBatch.Value; +#endif } -} \ No newline at end of file +} diff --git a/src/NHibernate/ITransaction.cs b/src/NHibernate/ITransaction.cs index 9ed0a1dff78..e9b0d6bec48 100644 --- a/src/NHibernate/ITransaction.cs +++ b/src/NHibernate/ITransaction.cs @@ -63,7 +63,7 @@ public partial interface ITransaction : IDisposable bool WasCommitted { get; } /// - /// Enlist the in the current Transaction. + /// Enlist a in the current Transaction. /// /// The to enlist. /// @@ -81,6 +81,17 @@ public partial interface ITransaction : IDisposable "RegisterSynchronization(ITransactionCompletionSynchronization)': the TransactionExtensions extension " + "method will call it.")] void RegisterSynchronization(ISynchronization synchronization); + +#if NET6_0_OR_GREATER + /// + /// Enlist a in the current Transaction. + /// + /// The to enlist. + /// + /// It is okay for this to be a no op implementation. + /// + void Enlist(DbBatch batch) => throw new NotImplementedException(); +#endif } // 6.0 TODO: merge into ITransaction diff --git a/src/NHibernate/Transaction/AdoTransaction.cs b/src/NHibernate/Transaction/AdoTransaction.cs index 8adc842d936..5ad87538bbb 100644 --- a/src/NHibernate/Transaction/AdoTransaction.cs +++ b/src/NHibernate/Transaction/AdoTransaction.cs @@ -86,6 +86,58 @@ public void Enlist(DbCommand command) } } +#if NET6_0_OR_GREATER + /// + /// Enlist a in the current . + /// + /// The to enlist in this Transaction. + /// + /// + /// This takes care of making sure the 's Transaction property + /// contains the correct or if there is no + /// Transaction for the ISession - ie BeginTransaction() not called. + /// + /// + /// This method may be called even when the transaction is disposed. + /// + /// + public void Enlist(DbBatch batch) + { + if (trans == null) + { + if (log.IsWarnEnabled()) + { + if (batch.Transaction != null) + { + log.Warn("set a nonnull DbBatch.Transaction to null because the Session had no Transaction"); + } + } + + batch.Transaction = null; + return; + } + else + { + if (log.IsWarnEnabled()) + { + // got into here because the command was being initialized and had a null Transaction - probably + // don't need to be confused by that - just a normal part of initialization... + if (batch.Transaction != null && batch.Transaction != trans) + { + log.Warn("The DbBatch had a different Transaction than the Session. This can occur when " + + "Disconnecting and Reconnecting Sessions because the PreparedCommand Cache is Session specific."); + } + } + log.Debug("Enlist DbBatch"); + + // If you try to assign a disposed transaction to a command with MSSQL, it will leave the command's + // transaction as null and not throw an error. With SQLite, for example, it will throw an exception + // here instead. Because of this, we set the trans field to null in when Dispose is called. + batch.Transaction = trans; + } + } +#endif + // Since 5.2 [Obsolete("Use RegisterSynchronization(ITransactionCompletionSynchronization) instead")] public void RegisterSynchronization(ISynchronization sync)