Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 541fefb

Browse files
committedMar 14, 2024
Add support for DbBatch
1 parent 1a960b0 commit 541fefb

17 files changed

+611
-12
lines changed
 

‎src/NHibernate.Test/Ado/BatcherFixture.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44

55
namespace NHibernate.Test.Ado
66
{
7-
[TestFixture]
7+
#if NET6_0_OR_GREATER
8+
[TestFixture(true)]
9+
#endif
10+
[TestFixture(false)]
811
public class BatcherFixture: TestCase
912
{
13+
private readonly bool _useDbBatch;
14+
15+
public BatcherFixture(bool useDbBatch)
16+
{
17+
_useDbBatch = useDbBatch;
18+
}
1019
protected override string MappingsAssembly
1120
{
1221
get { return "NHibernate.Test"; }
@@ -22,10 +31,22 @@ protected override void Configure(Configuration configuration)
2231
configuration.SetProperty(Environment.FormatSql, "true");
2332
configuration.SetProperty(Environment.GenerateStatistics, "true");
2433
configuration.SetProperty(Environment.BatchSize, "10");
34+
#if NET6_0_OR_GREATER
35+
if (_useDbBatch)
36+
{
37+
configuration.SetProperty(Environment.BatchStrategy, typeof(DbBatchBatcherFactory).AssemblyQualifiedName);
38+
}
39+
#endif
2540
}
2641

2742
protected override bool AppliesTo(Engine.ISessionFactoryImplementor factory)
2843
{
44+
#if NET6_0_OR_GREATER
45+
if (_useDbBatch)
46+
{
47+
return factory.Settings.BatcherFactory is DbBatchBatcherFactory && factory.Settings.ConnectionProvider.Driver is Driver.DriverBase driverBase && driverBase.CanCreateBatch;
48+
}
49+
#endif
2950
return !(factory.Settings.BatcherFactory is NonBatchingBatcherFactory);
3051
}
3152

@@ -129,13 +150,18 @@ public void SqlClientOneRoundTripForUpdateAndInsert()
129150
Cleanup();
130151
}
131152

132-
[Test, NetFxOnly]
153+
[Test]
133154
[Description("SqlClient: The batcher log output should be formatted")]
134155
public void BatchedoutputShouldBeFormatted()
135156
{
136157
#if NETFX
137158
if (Sfi.Settings.BatcherFactory is SqlClientBatchingBatcherFactory == false)
138159
Assert.Ignore("This test is for SqlClientBatchingBatcher only");
160+
#elif NET6_0_OR_GREATER
161+
if (Sfi.Settings.BatcherFactory is DbBatchBatcherFactory == false)
162+
Assert.Ignore("This test is for DbBatchBatcherFactory only");
163+
#else
164+
Assert.Ignore("This test is for NETFX and NET6_0_OR_GREATER only");
139165
#endif
140166

141167
using (var sqlLog = new SqlLogSpy())

‎src/NHibernate.Test/Async/Ado/BatcherFixture.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@ namespace NHibernate.Test.Ado
1616
{
1717
using System.Threading.Tasks;
1818
using System.Threading;
19-
[TestFixture]
19+
#if NET6_0_OR_GREATER
20+
[TestFixture(true)]
21+
#endif
22+
[TestFixture(false)]
2023
public class BatcherFixtureAsync: TestCase
2124
{
25+
private readonly bool _useDbBatch;
26+
27+
public BatcherFixtureAsync(bool useDbBatch)
28+
{
29+
_useDbBatch = useDbBatch;
30+
}
2231
protected override string MappingsAssembly
2332
{
2433
get { return "NHibernate.Test"; }
@@ -34,10 +43,22 @@ protected override void Configure(Configuration configuration)
3443
configuration.SetProperty(Environment.FormatSql, "true");
3544
configuration.SetProperty(Environment.GenerateStatistics, "true");
3645
configuration.SetProperty(Environment.BatchSize, "10");
46+
#if NET6_0_OR_GREATER
47+
if (_useDbBatch)
48+
{
49+
configuration.SetProperty(Environment.BatchStrategy, typeof(DbBatchBatcherFactory).AssemblyQualifiedName);
50+
}
51+
#endif
3752
}
3853

3954
protected override bool AppliesTo(Engine.ISessionFactoryImplementor factory)
4055
{
56+
#if NET6_0_OR_GREATER
57+
if (_useDbBatch)
58+
{
59+
return factory.Settings.BatcherFactory is DbBatchBatcherFactory;
60+
}
61+
#endif
4162
return !(factory.Settings.BatcherFactory is NonBatchingBatcherFactory);
4263
}
4364

‎src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<ItemGroup>
6363
<PackageReference Include="log4net" Version="2.0.15" />
6464
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.7.0" />
65-
<PackageReference Include="Microsoft.Data.SqlClient" Version="3.1.3" />
65+
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
6666
<PackageReference Include="NHibernate.Caches.CoreDistributedCache.Memory" Version="5.9.0" />
6767
<PackageReference Include="NHibernate.Caches.Util.JsonSerializer" Version="5.9.0" />
6868
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.117" />

‎src/NHibernate/AdoNet/ConnectionManager.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,30 @@ public void EnlistInTransaction(DbCommand command)
514514
}
515515
}
516516

517+
#if NET6_0_OR_GREATER
518+
/// <summary>
519+
/// Enlist a batch in the current transaction, if any.
520+
/// </summary>
521+
/// <param name="batch">The batch to enlist.</param>
522+
public void EnlistInTransaction(DbBatch batch)
523+
{
524+
if (batch == null)
525+
throw new ArgumentNullException(nameof(batch));
526+
527+
if (_transaction is ITransactionWithBatchSupport transactionWithBatch)
528+
{
529+
transactionWithBatch.Enlist(batch);
530+
return;
531+
}
532+
533+
if (batch.Transaction != null)
534+
{
535+
_log.Warn("set a nonnull DbCommand.Transaction to null because the Session had no Transaction");
536+
batch.Transaction = null;
537+
}
538+
}
539+
#endif
540+
517541
/// <summary>
518542
/// Enlist the connection into provided transaction if the connection should be enlisted.
519543
/// Do nothing in case an explicit transaction is ongoing.
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
#if NET6_0_OR_GREATER
2+
using System;
3+
using System.Data.Common;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using NHibernate.AdoNet.Util;
9+
using NHibernate.Driver;
10+
using NHibernate.Engine;
11+
using NHibernate.Exceptions;
12+
13+
namespace NHibernate.AdoNet
14+
{
15+
public class DbBatchBatcher : AbstractBatcher
16+
{
17+
private int _batchSize;
18+
private int _totalExpectedRowsAffected;
19+
private DbBatch _currentBatch;
20+
private StringBuilder _currentBatchCommandsLog;
21+
private readonly int _defaultTimeout;
22+
private readonly ConnectionManager _connectionManager;
23+
24+
public DbBatchBatcher(ConnectionManager connectionManager, IInterceptor interceptor)
25+
: base(connectionManager, interceptor)
26+
{
27+
_batchSize = Factory.Settings.AdoBatchSize;
28+
_defaultTimeout = Driver.GetCommandTimeout();
29+
30+
_currentBatch = CreateConfiguredBatch();
31+
//we always create this, because we need to deal with a scenario in which
32+
//the user change the logging configuration at runtime. Trying to put this
33+
//behind an if(log.IsDebugEnabled) will cause a null reference exception
34+
//at that point.
35+
_currentBatchCommandsLog = new StringBuilder().AppendLine("Batch commands:");
36+
_connectionManager = connectionManager;
37+
}
38+
39+
public override int BatchSize
40+
{
41+
get { return _batchSize; }
42+
set { _batchSize = value; }
43+
}
44+
45+
protected override int CountOfStatementsInCurrentBatch
46+
{
47+
get { return _currentBatch.BatchCommands.Count; }
48+
}
49+
50+
public override void AddToBatch(IExpectation expectation)
51+
{
52+
_totalExpectedRowsAffected += expectation.ExpectedRowCount;
53+
var batchUpdate = CurrentCommand;
54+
Driver.AdjustCommand(batchUpdate);
55+
string lineWithParameters = null;
56+
var sqlStatementLogger = Factory.Settings.SqlStatementLogger;
57+
if (sqlStatementLogger.IsDebugEnabled || Log.IsDebugEnabled())
58+
{
59+
lineWithParameters = sqlStatementLogger.GetCommandLineWithParameters(batchUpdate);
60+
var formatStyle = sqlStatementLogger.DetermineActualStyle(FormatStyle.Basic);
61+
lineWithParameters = formatStyle.Formatter.Format(lineWithParameters);
62+
_currentBatchCommandsLog.Append("command ")
63+
.Append(_currentBatch.BatchCommands.Count)
64+
.Append(":")
65+
.AppendLine(lineWithParameters);
66+
}
67+
if (Log.IsDebugEnabled())
68+
{
69+
Log.Debug("Adding to batch:{0}", lineWithParameters);
70+
}
71+
72+
AddCommandToBatch(batchUpdate);
73+
74+
if (CountOfStatementsInCurrentBatch >= _batchSize)
75+
{
76+
ExecuteBatchWithTiming(batchUpdate);
77+
}
78+
}
79+
80+
private void AddCommandToBatch(DbCommand batchUpdate)
81+
{
82+
var dbBatchCommand = _currentBatch.CreateBatchCommand();
83+
dbBatchCommand.CommandText = batchUpdate.CommandText;
84+
dbBatchCommand.CommandType = batchUpdate.CommandType;
85+
86+
foreach (var param in batchUpdate.Parameters)
87+
{
88+
dbBatchCommand.Parameters.Add(((ICloneable) param).Clone());
89+
}
90+
_currentBatch.BatchCommands.Add(dbBatchCommand);
91+
}
92+
93+
public override Task AddToBatchAsync(IExpectation expectation, CancellationToken cancellationToken)
94+
{
95+
if (cancellationToken.IsCancellationRequested)
96+
{
97+
return Task.FromCanceled<object>(cancellationToken);
98+
}
99+
try
100+
{
101+
_totalExpectedRowsAffected += expectation.ExpectedRowCount;
102+
var batchUpdate = CurrentCommand;
103+
Driver.AdjustCommand(batchUpdate);
104+
string lineWithParameters = null;
105+
var sqlStatementLogger = Factory.Settings.SqlStatementLogger;
106+
if (sqlStatementLogger.IsDebugEnabled || Log.IsDebugEnabled())
107+
{
108+
lineWithParameters = sqlStatementLogger.GetCommandLineWithParameters(batchUpdate);
109+
var formatStyle = sqlStatementLogger.DetermineActualStyle(FormatStyle.Basic);
110+
lineWithParameters = formatStyle.Formatter.Format(lineWithParameters);
111+
_currentBatchCommandsLog.Append("command ")
112+
.Append(_currentBatch.BatchCommands.Count)
113+
.Append(":")
114+
.AppendLine(lineWithParameters);
115+
}
116+
if (Log.IsDebugEnabled())
117+
{
118+
Log.Debug("Adding to batch:{0}", lineWithParameters);
119+
}
120+
121+
AddCommandToBatch(batchUpdate);
122+
123+
if (CountOfStatementsInCurrentBatch >= _batchSize)
124+
{
125+
return ExecuteBatchWithTimingAsync(batchUpdate, cancellationToken);
126+
}
127+
return Task.CompletedTask;
128+
}
129+
catch (Exception ex)
130+
{
131+
return Task.FromException<object>(ex);
132+
}
133+
}
134+
135+
protected override void DoExecuteBatch(DbCommand ps)
136+
{
137+
try
138+
{
139+
Log.Debug("Executing batch");
140+
CheckReaders();
141+
Prepare(_currentBatch);
142+
if (Factory.Settings.SqlStatementLogger.IsDebugEnabled)
143+
{
144+
Factory.Settings.SqlStatementLogger.LogBatchCommand(_currentBatchCommandsLog.ToString());
145+
}
146+
int rowsAffected;
147+
try
148+
{
149+
rowsAffected = _currentBatch.ExecuteNonQuery();
150+
}
151+
catch (DbException e)
152+
{
153+
throw ADOExceptionHelper.Convert(Factory.SQLExceptionConverter, e, "could not execute batch command.");
154+
}
155+
156+
Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, rowsAffected, ps);
157+
}
158+
finally
159+
{
160+
ClearCurrentBatch();
161+
}
162+
}
163+
164+
protected override async Task DoExecuteBatchAsync(DbCommand ps, CancellationToken cancellationToken)
165+
{
166+
cancellationToken.ThrowIfCancellationRequested();
167+
try
168+
{
169+
Log.Debug("Executing batch");
170+
await (CheckReadersAsync(cancellationToken)).ConfigureAwait(false);
171+
//await (PrepareAsync(_currentBatch, cancellationToken)).ConfigureAwait(false);
172+
Prepare(_currentBatch);
173+
if (Factory.Settings.SqlStatementLogger.IsDebugEnabled)
174+
{
175+
Factory.Settings.SqlStatementLogger.LogBatchCommand(_currentBatchCommandsLog.ToString());
176+
}
177+
int rowsAffected;
178+
try
179+
{
180+
rowsAffected = _currentBatch.ExecuteNonQuery();
181+
}
182+
catch (DbException e)
183+
{
184+
throw ADOExceptionHelper.Convert(Factory.SQLExceptionConverter, e, "could not execute batch command.");
185+
}
186+
187+
Expectations.VerifyOutcomeBatched(_totalExpectedRowsAffected, rowsAffected, ps);
188+
}
189+
finally
190+
{
191+
ClearCurrentBatch();
192+
}
193+
}
194+
195+
private DbBatch CreateConfiguredBatch()
196+
{
197+
var result = ((IDriverWithBatchSupport) Driver).CreateBatch();
198+
if (_defaultTimeout > 0)
199+
{
200+
try
201+
{
202+
result.Timeout = _defaultTimeout;
203+
}
204+
catch (Exception e)
205+
{
206+
if (Log.IsWarnEnabled())
207+
{
208+
Log.Warn(e, e.ToString());
209+
}
210+
}
211+
}
212+
213+
return result;
214+
}
215+
216+
private void ClearCurrentBatch()
217+
{
218+
_currentBatch.Dispose();
219+
_totalExpectedRowsAffected = 0;
220+
_currentBatch = CreateConfiguredBatch();
221+
222+
if (Factory.Settings.SqlStatementLogger.IsDebugEnabled)
223+
{
224+
_currentBatchCommandsLog = new StringBuilder().AppendLine("Batch commands:");
225+
}
226+
}
227+
228+
public override void CloseCommands()
229+
{
230+
base.CloseCommands();
231+
232+
// Prevent exceptions when closing the batch from hiding any original exception
233+
// (We do not know here if this batch closing occurs after a failure or not.)
234+
try
235+
{
236+
ClearCurrentBatch();
237+
}
238+
catch (Exception e)
239+
{
240+
Log.Warn(e, "Exception clearing batch");
241+
}
242+
}
243+
244+
protected override void Dispose(bool isDisposing)
245+
{
246+
base.Dispose(isDisposing);
247+
// Prevent exceptions when closing the batch from hiding any original exception
248+
// (We do not know here if this batch closing occurs after a failure or not.)
249+
try
250+
{
251+
_currentBatch.Dispose();
252+
}
253+
catch (Exception e)
254+
{
255+
Log.Warn(e, "Exception closing batcher");
256+
}
257+
}
258+
259+
/// <summary>
260+
/// Prepares the <see cref="DbBatch"/> for execution in the database.
261+
/// </summary>
262+
/// <remarks>
263+
/// This takes care of hooking the <see cref="DbBatch"/> up to an <see cref="DbConnection"/>
264+
/// and <see cref="DbTransaction"/> if one exists. It will call <c>Prepare</c> if the Driver
265+
/// supports preparing batches.
266+
/// </remarks>
267+
protected void Prepare(DbBatch batch)
268+
{
269+
try
270+
{
271+
var sessionConnection = _connectionManager.GetConnection();
272+
273+
if (batch.Connection != null)
274+
{
275+
// make sure the commands connection is the same as the Sessions connection
276+
// these can be different when the session is disconnected and then reconnected
277+
if (batch.Connection != sessionConnection)
278+
{
279+
batch.Connection = sessionConnection;
280+
}
281+
}
282+
else
283+
{
284+
batch.Connection = sessionConnection;
285+
}
286+
287+
_connectionManager.EnlistInTransaction(batch);
288+
(Driver as IDriverWithBatchSupport).PrepareBatch(batch);
289+
}
290+
catch (InvalidOperationException ioe)
291+
{
292+
throw new ADOException("While preparing " + string.Join(Environment.NewLine, batch.BatchCommands.Select(x => x.CommandText)) + " an error occurred", ioe);
293+
}
294+
}
295+
}
296+
}
297+
#endif
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#if NET6_0_OR_GREATER
2+
using NHibernate.Engine;
3+
4+
namespace NHibernate.AdoNet
5+
{
6+
public class DbBatchBatcherFactory : IBatcherFactory
7+
{
8+
public IBatcher CreateBatcher(ConnectionManager connectionManager, IInterceptor interceptor)
9+
{
10+
return new DbBatchBatcher(connectionManager, interceptor);
11+
}
12+
}
13+
}
14+
#endif

‎src/NHibernate/Async/AdoNet/AbstractBatcher.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using NHibernate.SqlTypes;
2323
using NHibernate.Util;
2424
using NHibernate.AdoNet.Util;
25+
using System.Linq;
2526

2627
namespace NHibernate.AdoNet
2728
{

‎src/NHibernate/Async/Transaction/AdoTransaction.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace NHibernate.Transaction
2121
using System.Threading.Tasks;
2222
using System.Threading;
2323
public partial class AdoTransaction : ITransaction
24+
#if NET6_0_OR_GREATER
25+
#endif
2426
{
2527

2628
private async Task AfterTransactionCompletionAsync(bool successful, CancellationToken cancellationToken)

‎src/NHibernate/Driver/DbProviderFactoryDriveConnectionCommandProvider.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
namespace NHibernate.Driver
55
{
66
public class DbProviderFactoryDriveConnectionCommandProvider : IDriveConnectionCommandProvider
7+
#if NET6_0_OR_GREATER
8+
, IDriveConnectionCommandProviderWithBatchSupport
9+
#endif
710
{
811
private readonly DbProviderFactory dbProviderFactory;
912

@@ -25,5 +28,13 @@ public DbCommand CreateCommand()
2528
{
2629
return dbProviderFactory.CreateCommand();
2730
}
31+
#if NET6_0_OR_GREATER
32+
public DbBatch CreateBatch()
33+
{
34+
return dbProviderFactory.CreateBatch();
35+
}
36+
37+
public bool CanCreateBatch => dbProviderFactory.CanCreateBatch && dbProviderFactory.CreateCommand() is ICloneable;
38+
#endif
2839
}
29-
}
40+
}

‎src/NHibernate/Driver/DriverBase.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ namespace NHibernate.Driver
1515
/// Base class for the implementation of IDriver
1616
/// </summary>
1717
public abstract class DriverBase : IDriver, ISqlParameterFormatter
18+
#if NET6_0_OR_GREATER
19+
, IDriverWithBatchSupport
20+
#endif
1821
{
1922
private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(DriverBase));
2023

@@ -329,6 +332,51 @@ public virtual void AdjustCommand(DbCommand command)
329332
{
330333
}
331334

335+
#if NET6_0_OR_GREATER
336+
public void PrepareBatch(DbBatch batch)
337+
{
338+
AdjustBatch(batch);
339+
OnBeforePrepare(batch);
340+
341+
if (SupportsPreparingCommands && prepareSql)
342+
{
343+
batch.Prepare();
344+
}
345+
}
346+
347+
/// <summary>
348+
/// Override to make any adjustments to the DbBatch object. (e.g., Oracle custom OUT parameter)
349+
/// Parameters have been bound by this point, so their order can be adjusted too.
350+
/// This is analogous to the RegisterResultSetOutParameter() function in Hibernate.
351+
/// </summary>
352+
protected virtual void OnBeforePrepare(DbBatch command)
353+
{
354+
}
355+
356+
/// <summary>
357+
/// Override to make any adjustments to each DbBatch object before it added to the batcher.
358+
/// </summary>
359+
/// <param name="batch">The batch.</param>
360+
/// <remarks>
361+
/// This method is similar to the <see cref="OnBeforePrepare"/> but, instead be called just before execute the command (that can be a batch)
362+
/// is executed before add each single command to the batcher and before <see cref="OnBeforePrepare"/> .
363+
/// If you have to adjust parameters values/type (when the command is full filled) this is a good place where do it.
364+
/// </remarks>
365+
public virtual void AdjustBatch(DbBatch batch)
366+
{
367+
368+
}
369+
370+
public virtual DbBatch CreateBatch()
371+
{
372+
throw new NotImplementedException();
373+
}
374+
375+
public virtual bool CanCreateBatch => false;
376+
377+
378+
#endif
379+
332380
public DbParameter GenerateOutputParameter(DbCommand command)
333381
{
334382
var param = GenerateParameter(command, "ReturnValue", SqlTypeFactory.Int32);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Data.Common;
2+
3+
namespace NHibernate.Driver
4+
{
5+
#if NET6_0_OR_GREATER
6+
//TODO: Include in IDriveConnectionCommandProvider for NET6_0_OR_GREATER
7+
internal interface IDriveConnectionCommandProviderWithBatchSupport : IDriveConnectionCommandProvider
8+
{
9+
DbBatch CreateBatch();
10+
bool CanCreateBatch { get; }
11+
12+
}
13+
#endif
14+
15+
}

‎src/NHibernate/Driver/IDriver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Collections.Generic;
33
using System.Data;
44
using System.Data.Common;
5+
using System.Linq;
6+
using System.Windows.Input;
57
using NHibernate.Engine;
68
using NHibernate.SqlCommand;
79
using NHibernate.SqlTypes;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Data.Common;
2+
3+
namespace NHibernate.Driver
4+
{
5+
#if NET6_0_OR_GREATER
6+
//TODO: Include in IDriver for NET6_0_OR_GREATER
7+
internal interface IDriverWithBatchSupport : IDriver
8+
{
9+
public DbBatch CreateBatch();
10+
public bool CanCreateBatch{ get; }
11+
12+
public void AdjustBatch(DbBatch dbBatch);
13+
public void PrepareBatch(DbBatch dbBatch);
14+
}
15+
#endif
16+
}

‎src/NHibernate/Driver/ReflectionBasedDriver.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ namespace NHibernate.Driver
77
public abstract class ReflectionBasedDriver : DriverBase
88
{
99
protected const string ReflectionTypedProviderExceptionMessageTemplate = "The DbCommand and DbConnection implementation in the assembly {0} could not be found. "
10-
+ "Ensure that the assembly {0} is located in the application directory or in the Global "
11-
+ "Assembly Cache. If the assembly is in the GAC, use <qualifyAssembly/> element in the "
12-
+ "application configuration file to specify the full name of the assembly.";
10+
+ "Ensure that the assembly {0} is located in the application directory or in the Global "
11+
+ "Assembly Cache. If the assembly is in the GAC, use <qualifyAssembly/> element in the "
12+
+ "application configuration file to specify the full name of the assembly.";
1313

1414
private readonly IDriveConnectionCommandProvider connectionCommandProvider;
1515

1616
/// <summary>
1717
/// If the driver use a third party driver (not a .Net Framework DbProvider), its assembly version.
1818
/// </summary>
19-
protected Version DriverVersion { get; }
19+
protected Version DriverVersion { get; }
2020

2121
/// <summary>
2222
/// Initializes a new instance of <see cref="ReflectionBasedDriver" /> with
@@ -50,7 +50,7 @@ protected ReflectionBasedDriver(string providerInvariantName, string driverAssem
5050
if (string.IsNullOrEmpty(providerInvariantName))
5151
{
5252
#endif
53-
throw new HibernateException(string.Format(ReflectionTypedProviderExceptionMessageTemplate, driverAssemblyName));
53+
throw new HibernateException(string.Format(ReflectionTypedProviderExceptionMessageTemplate, driverAssemblyName));
5454
#if NETFX || NETSTANDARD2_1_OR_GREATER
5555
}
5656
var factory = DbProviderFactories.GetFactory(providerInvariantName);
@@ -73,5 +73,28 @@ public override DbCommand CreateCommand()
7373
{
7474
return connectionCommandProvider.CreateCommand();
7575
}
76+
77+
#if NET6_0_OR_GREATER
78+
public override DbBatch CreateBatch()
79+
{
80+
if (connectionCommandProvider is IDriveConnectionCommandProviderWithBatchSupport driveConnectionCommandProviderWithBatch)
81+
{
82+
return driveConnectionCommandProviderWithBatch.CreateBatch();
83+
}
84+
throw new NotSupportedException();
85+
}
86+
87+
public override bool CanCreateBatch
88+
{
89+
get
90+
{
91+
if (connectionCommandProvider is IDriveConnectionCommandProviderWithBatchSupport driveConnectionCommandProviderWithBatch)
92+
{
93+
return driveConnectionCommandProviderWithBatch.CanCreateBatch;
94+
}
95+
return false;
96+
}
97+
}
98+
#endif
7699
}
77100
}

‎src/NHibernate/Driver/ReflectionDriveConnectionCommandProvider.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
namespace NHibernate.Driver
66
{
77
public class ReflectionDriveConnectionCommandProvider : IDriveConnectionCommandProvider
8+
#if NET6_0_OR_GREATER
9+
, IDriveConnectionCommandProviderWithBatchSupport
10+
#endif
811
{
912
private readonly System.Type commandType;
1013
private readonly System.Type connectionType;
@@ -21,6 +24,14 @@ public ReflectionDriveConnectionCommandProvider(System.Type connectionType, Syst
2124
}
2225
this.connectionType = connectionType;
2326
this.commandType = commandType;
27+
#if NET6_0_OR_GREATER
28+
_canCreateBatch = new Lazy<bool>(() => {
29+
using (var connection = CreateConnection())
30+
{
31+
return connection.CanCreateBatch && connection.CreateCommand() is ICloneable;
32+
}
33+
});
34+
#endif
2435
}
2536

2637
#region IDriveConnectionCommandProvider Members
@@ -36,5 +47,24 @@ public DbCommand CreateCommand()
3647
}
3748

3849
#endregion
50+
51+
#if NET6_0_OR_GREATER
52+
53+
private Lazy<bool> _canCreateBatch;
54+
55+
public DbBatch CreateBatch()
56+
{
57+
using (var connection = CreateConnection())
58+
{
59+
var batch = connection.CreateBatch();
60+
batch.Connection = null;
61+
return batch;
62+
}
63+
}
64+
65+
public bool CanCreateBatch => _canCreateBatch.Value;
66+
67+
68+
#endif
3969
}
40-
}
70+
}

‎src/NHibernate/ITransaction.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public partial interface ITransaction : IDisposable
6363
bool WasCommitted { get; }
6464

6565
/// <summary>
66-
/// Enlist the <see cref="DbCommand"/> in the current Transaction.
66+
/// Enlist a <see cref="DbCommand"/> in the current Transaction.
6767
/// </summary>
6868
/// <param name="command">The <see cref="DbCommand"/> to enlist.</param>
6969
/// <remarks>
@@ -83,6 +83,20 @@ public partial interface ITransaction : IDisposable
8383
void RegisterSynchronization(ISynchronization synchronization);
8484
}
8585

86+
#if NET6_0_OR_GREATER
87+
internal interface ITransactionWithBatchSupport : ITransaction
88+
{
89+
/// <summary>
90+
/// Enlist a <see cref="DbBatch"/> in the current Transaction.
91+
/// </summary>
92+
/// <param name="batch">The <see cref="DbBatch"/> to enlist.</param>
93+
/// <remarks>
94+
/// It is okay for this to be a no op implementation.
95+
/// </remarks>
96+
void Enlist(DbBatch batch);
97+
}
98+
#endif
99+
86100
// 6.0 TODO: merge into ITransaction
87101
public static class TransactionExtensions
88102
{

‎src/NHibernate/Transaction/AdoTransaction.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ namespace NHibernate.Transaction
1313
/// the <see cref="ITransaction" /> interface.
1414
/// </summary>
1515
public partial class AdoTransaction : ITransaction
16+
#if NET6_0_OR_GREATER
17+
, ITransactionWithBatchSupport
18+
#endif
1619
{
1720
private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(AdoTransaction));
1821
private ISessionImplementor session;
@@ -86,6 +89,58 @@ public void Enlist(DbCommand command)
8689
}
8790
}
8891

92+
#if NET6_0_OR_GREATER
93+
/// <summary>
94+
/// Enlist a <see cref="DbBatch"/> in the current <see cref="ITransaction"/>.
95+
/// </summary>
96+
/// <param name="batch">The <see cref="DbBatch"/> to enlist in this Transaction.</param>
97+
/// <remarks>
98+
/// <para>
99+
/// This takes care of making sure the <see cref="DbBatch"/>'s Transaction property
100+
/// contains the correct <see cref="DbTransaction"/> or <see langword="null" /> if there is no
101+
/// Transaction for the ISession - ie <c>BeginTransaction()</c> not called.
102+
/// </para>
103+
/// <para>
104+
/// This method may be called even when the transaction is disposed.
105+
/// </para>
106+
/// </remarks>
107+
public void Enlist(DbBatch batch)
108+
{
109+
if (trans == null)
110+
{
111+
if (log.IsWarnEnabled())
112+
{
113+
if (batch.Transaction != null)
114+
{
115+
log.Warn("set a nonnull DbCommand.Transaction to null because the Session had no Transaction");
116+
}
117+
}
118+
119+
batch.Transaction = null;
120+
return;
121+
}
122+
else
123+
{
124+
if (log.IsWarnEnabled())
125+
{
126+
// got into here because the command was being initialized and had a null Transaction - probably
127+
// don't need to be confused by that - just a normal part of initialization...
128+
if (batch.Transaction != null && batch.Transaction != trans)
129+
{
130+
log.Warn("The DbCommand had a different Transaction than the Session. This can occur when " +
131+
"Disconnecting and Reconnecting Sessions because the PreparedCommand Cache is Session specific.");
132+
}
133+
}
134+
log.Debug("Enlist Command");
135+
136+
// If you try to assign a disposed transaction to a command with MSSQL, it will leave the command's
137+
// transaction as null and not throw an error. With SQLite, for example, it will throw an exception
138+
// here instead. Because of this, we set the trans field to null in when Dispose is called.
139+
batch.Transaction = trans;
140+
}
141+
}
142+
#endif
143+
89144
// Since 5.2
90145
[Obsolete("Use RegisterSynchronization(ITransactionCompletionSynchronization) instead")]
91146
public void RegisterSynchronization(ISynchronization sync)

0 commit comments

Comments
 (0)
Please sign in to comment.