diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs
index 55e0de4..f632056 100644
--- a/AmalgamationTool/DynamORM.Amalgamation.cs
+++ b/AmalgamationTool/DynamORM.Amalgamation.cs
@@ -29,6 +29,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Text;
+using System.Threading;
using System;
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "This is a generated file which generates all the necessary support classes.")]
@@ -1271,14 +1272,21 @@ namespace DynamORM
/// The number of rows affected.
public int ExecuteNonQuery()
{
+ IDisposable scope = null;
try
{
+ scope = _db != null ? _db.AcquireExecutionScope() : null;
return PrepareForExecution().ExecuteNonQuery();
}
catch (Exception ex)
{
throw new DynamicQueryException(ex, this);
}
+ finally
+ {
+ if (scope != null)
+ scope.Dispose();
+ }
}
/// Executes the
/// against the ,
@@ -1289,12 +1297,16 @@ namespace DynamORM
/// An object.
public IDataReader ExecuteReader(CommandBehavior behavior)
{
+ IDisposable scope = null;
try
{
- return PrepareForExecution().ExecuteReader(behavior);
+ scope = _db != null ? _db.AcquireExecutionScope() : null;
+ return new DynamicExecutionReader(PrepareForExecution().ExecuteReader(behavior), scope);
}
catch (Exception ex)
{
+ if (scope != null)
+ scope.Dispose();
throw new DynamicQueryException(ex, this);
}
}
@@ -1304,12 +1316,16 @@ namespace DynamORM
/// An object.
public IDataReader ExecuteReader()
{
+ IDisposable scope = null;
try
{
- return PrepareForExecution().ExecuteReader();
+ scope = _db != null ? _db.AcquireExecutionScope() : null;
+ return new DynamicExecutionReader(PrepareForExecution().ExecuteReader(), scope);
}
catch (Exception ex)
{
+ if (scope != null)
+ scope.Dispose();
throw new DynamicQueryException(ex, this);
}
}
@@ -1319,14 +1335,21 @@ namespace DynamORM
/// The first column of the first row in the result set.
public object ExecuteScalar()
{
+ IDisposable scope = null;
try
{
+ scope = _db != null ? _db.AcquireExecutionScope() : null;
return PrepareForExecution().ExecuteScalar();
}
catch (Exception ex)
{
throw new DynamicQueryException(ex, this);
}
+ finally
+ {
+ if (scope != null)
+ scope.Dispose();
+ }
}
/// Gets the .
public IDataParameterCollection Parameters
@@ -1384,7 +1407,7 @@ namespace DynamORM
IsDisposed = true;
- if (_con != null)
+ if (_con != null && db.CommandsPool != null)
{
List pool = db.CommandsPool.TryGetValue(_con.Connection);
@@ -1436,7 +1459,7 @@ namespace DynamORM
/// Returns representation.
internal DynamicTransaction BeginTransaction(IsolationLevel? il, object custom, Action disposed)
{
- return new DynamicTransaction(_db, this, _singleTransaction, il, disposed, null);
+ return new DynamicTransaction(_db, this, _singleTransaction, il, disposed, custom);
}
#region IDbConnection Members
@@ -1546,6 +1569,13 @@ namespace DynamORM
/// Dynamic database is a class responsible for managing database.
public class DynamicDatabase : IExtendedDisposable
{
+ private sealed class ConnectionEntry
+ {
+ public bool External { get; set; }
+ public int LeaseCount { get; set; }
+ public DateTime CreatedUtc { get; set; }
+ public DateTime LastReleasedUtc { get; set; }
+ }
#region Internal fields and properties
private DbProviderFactory _provider;
@@ -1553,6 +1583,7 @@ namespace DynamORM
private string _connectionString;
private bool _singleConnection;
private bool _singleTransaction;
+ private bool _connectionPooling;
private string _leftDecorator = "\"";
private string _rightDecorator = "\"";
private bool _leftDecoratorIsInInvalidMembersChars = true;
@@ -1561,7 +1592,9 @@ namespace DynamORM
private int? _commandTimeout = null;
private long _poolStamp = 0;
- private DynamicConnection _tempConn = null;
+ private readonly object _executionSyncRoot = new object();
+ private int _executionOwnerThreadId = -1;
+ private int _executionLockDepth = 0;
/// Provides lock object for this database instance.
internal readonly object SyncLock = new object();
@@ -1593,6 +1626,12 @@ namespace DynamORM
/// Pool should contain dynamic commands instead of native ones.
internal Dictionary> CommandsPool { get; private set; }
+ /// Gets connection metadata tracked by the database instance.
+ private Dictionary ConnectionEntries { get; set; }
+
+ /// Gets ambient transaction-bound connections keyed by managed thread identifier.
+ private Dictionary AmbientTransactionConnections { get; set; }
+
/// Gets schema columns cache.
internal Dictionary> Schema { get; private set; }
@@ -1616,6 +1655,18 @@ namespace DynamORM
/// Gets or sets command timeout.
public int? CommandTimeout { get { return _commandTimeout; } set { _commandTimeout = value; _poolStamp = DateTime.Now.Ticks; } }
+ /// Gets or sets the preferred number of idle open connections kept in the internal pool.
+ /// Default value is 32. Applies only when is enabled.
+ public int ConnectionPoolingKeepOpenCount { get; set; }
+
+ /// Gets or sets the maximum number of connections that may remain managed by the internal pool at once.
+ /// Default value is 128. When this limit is reached, callers wait for a pooled connection to be released.
+ public int ConnectionPoolingMaximumOpenCount { get; set; }
+
+ /// Gets or sets how long an idle pooled connection may stay open before it is retired.
+ /// Default value is one hour. Applies only when is enabled.
+ public TimeSpan ConnectionPoolingConnectionLifetime { get; set; }
+
/// Gets the database provider.
public DbProviderFactory Provider { get { return _provider; } }
@@ -1804,6 +1855,14 @@ namespace DynamORM
IsDisposed = false;
InitCommon(connection.ConnectionString, options);
TransactionPool.Add(connection, new Stack());
+ CommandsPool.Add(connection, new List());
+ ConnectionEntries.Add(connection, new ConnectionEntry
+ {
+ External = true,
+ LeaseCount = 0,
+ CreatedUtc = DateTime.UtcNow,
+ LastReleasedUtc = DateTime.UtcNow,
+ });
if (!_singleConnection)
throw new InvalidOperationException("This constructor accepts only connections with DynamicDatabaseOptions.SingleConnection option.");
@@ -1815,10 +1874,16 @@ namespace DynamORM
_singleConnection = (options & DynamicDatabaseOptions.SingleConnection) == DynamicDatabaseOptions.SingleConnection;
_singleTransaction = (options & DynamicDatabaseOptions.SingleTransaction) == DynamicDatabaseOptions.SingleTransaction;
+ _connectionPooling = (options & DynamicDatabaseOptions.ConnectionPooling) == DynamicDatabaseOptions.ConnectionPooling;
DumpCommands = (options & DynamicDatabaseOptions.DumpCommands) == DynamicDatabaseOptions.DumpCommands;
+ ConnectionPoolingKeepOpenCount = 32;
+ ConnectionPoolingMaximumOpenCount = 128;
+ ConnectionPoolingConnectionLifetime = TimeSpan.FromHours(1);
TransactionPool = new Dictionary>();
CommandsPool = new Dictionary>();
+ ConnectionEntries = new Dictionary();
+ AmbientTransactionConnections = new Dictionary();
Schema = new Dictionary>();
RemainingBuilders = new List();
#if !DYNAMORM_OMMIT_OLDSYNTAX
@@ -3356,7 +3421,205 @@ namespace DynamORM
#region Connection
+ private bool UsesExecutionSerialization
+ {
+ get { return _singleConnection || _singleTransaction; }
+ }
+ private bool UsesManagedPooling
+ {
+ get { return _connectionPooling && !_singleConnection; }
+ }
+ private IDisposable EnterExecutionScope()
+ {
+ if (!UsesExecutionSerialization)
+ return null;
+
+ int currentThreadId = Thread.CurrentThread.ManagedThreadId;
+
+ lock (_executionSyncRoot)
+ {
+ while (_executionLockDepth > 0 && _executionOwnerThreadId != currentThreadId)
+ Monitor.Wait(_executionSyncRoot);
+
+ _executionOwnerThreadId = currentThreadId;
+ _executionLockDepth++;
+ }
+ return new ExecutionScope(this);
+ }
+ private void ExitExecutionScope()
+ {
+ if (!UsesExecutionSerialization)
+ return;
+
+ lock (_executionSyncRoot)
+ {
+ if (_executionLockDepth > 0)
+ _executionLockDepth--;
+
+ if (_executionLockDepth == 0)
+ {
+ _executionOwnerThreadId = -1;
+ Monitor.PulseAll(_executionSyncRoot);
+ }
+ }
+ }
+ private sealed class ExecutionScope : IDisposable
+ {
+ private DynamicDatabase _db;
+
+ public ExecutionScope(DynamicDatabase db)
+ {
+ _db = db;
+ }
+ public void Dispose()
+ {
+ DynamicDatabase db = _db;
+ if (db == null)
+ return;
+
+ _db = null;
+ db.ExitExecutionScope();
+ }
+ }
+ internal IDisposable AcquireExecutionScope()
+ {
+ return EnterExecutionScope();
+ }
+ private bool IsConnectionAmbientForAnotherThread(IDbConnection connection, int currentThreadId)
+ {
+ return AmbientTransactionConnections.Any(x => x.Key != currentThreadId && x.Value == connection);
+ }
+ private void AddManagedConnection(IDbConnection connection, bool external)
+ {
+ TransactionPool.Add(connection, new Stack());
+ CommandsPool.Add(connection, new List());
+ ConnectionEntries.Add(connection, new ConnectionEntry
+ {
+ External = external,
+ LeaseCount = 0,
+ CreatedUtc = DateTime.UtcNow,
+ LastReleasedUtc = DateTime.UtcNow,
+ });
+ }
+ private IDbConnection CreateManagedConnection()
+ {
+ IDbConnection conn = _provider.CreateConnection();
+ conn.ConnectionString = _connectionString;
+ conn.Open();
+ AddManagedConnection(conn, false);
+ return conn;
+ }
+ private void EnsureConnectionIsOpen(IDbConnection connection)
+ {
+ if (connection != null && connection.State != ConnectionState.Open)
+ connection.Open();
+ }
+ private void TrimIdleConnectionsUnderLock()
+ {
+ DateTime now = DateTime.UtcNow;
+ int idleCount = ConnectionEntries
+ .Where(x => !x.Value.External)
+ .Count(x => x.Value.LeaseCount == 0 && TransactionPool[x.Key].Count == 0 && CommandsPool[x.Key].Count == 0 && !AmbientTransactionConnections.ContainsValue(x.Key));
+
+ List toRemove = new List();
+
+ foreach (KeyValuePair item in ConnectionEntries)
+ {
+ if (item.Value.External || item.Value.LeaseCount > 0)
+ continue;
+
+ if (TransactionPool[item.Key].Count > 0 || CommandsPool[item.Key].Count > 0 || AmbientTransactionConnections.ContainsValue(item.Key))
+ continue;
+
+ bool expired = ConnectionPoolingConnectionLifetime <= TimeSpan.Zero || now - item.Value.LastReleasedUtc >= ConnectionPoolingConnectionLifetime;
+ bool abovePreferred = idleCount > ConnectionPoolingKeepOpenCount;
+
+ if (expired || abovePreferred)
+ {
+ toRemove.Add(item.Key);
+ idleCount--;
+ }
+ }
+ foreach (IDbConnection connection in toRemove)
+ {
+ ConnectionEntries.Remove(connection);
+ TransactionPool.Remove(connection);
+ CommandsPool.Remove(connection);
+
+ if (connection.State == ConnectionState.Open)
+ connection.Close();
+
+ connection.Dispose();
+ }
+ }
+ private IDbConnection GetOrCreateManagedConnectionUnderLock(out bool opened)
+ {
+ opened = false;
+ int currentThreadId = Thread.CurrentThread.ManagedThreadId;
+
+ if (_singleConnection)
+ {
+ IDbConnection single = ConnectionEntries.Count == 0 ? CreateManagedConnection() : ConnectionEntries.Keys.First();
+ if (single.State != ConnectionState.Open)
+ {
+ single.Open();
+ opened = true;
+ }
+ ConnectionEntries[single].LeaseCount++;
+ return single;
+ }
+ IDbConnection ambient;
+ if (AmbientTransactionConnections.TryGetValue(currentThreadId, out ambient))
+ {
+ EnsureConnectionIsOpen(ambient);
+ ConnectionEntries[ambient].LeaseCount++;
+ return ambient;
+ }
+ if (!UsesManagedPooling)
+ {
+ IDbConnection conn = CreateManagedConnection();
+ opened = true;
+ ConnectionEntries[conn].LeaseCount++;
+ return conn;
+ }
+ while (true)
+ {
+ TrimIdleConnectionsUnderLock();
+
+ IDbConnection pooled = ConnectionEntries
+ .Where(x => !x.Value.External && x.Value.LeaseCount == 0)
+ .Where(x => TransactionPool[x.Key].Count == 0 && CommandsPool[x.Key].Count == 0)
+ .Where(x => !IsConnectionAmbientForAnotherThread(x.Key, currentThreadId))
+ .Select(x => x.Key)
+ .FirstOrDefault();
+
+ if (pooled != null)
+ {
+ if (pooled.State != ConnectionState.Open)
+ {
+ pooled.Open();
+ opened = true;
+ }
+ ConnectionEntries[pooled].LeaseCount++;
+ return pooled;
+ }
+ if (ConnectionEntries.Count < ConnectionPoolingMaximumOpenCount)
+ {
+ IDbConnection conn = CreateManagedConnection();
+ opened = true;
+ ConnectionEntries[conn].LeaseCount++;
+ return conn;
+ }
+ Monitor.Wait(SyncLock);
+ }
+ }
/// Open managed connection.
+ ///
+ /// When is enabled, DynamORM reuses idle managed connections instead of opening and closing a new one for every operation.
+ /// When a transaction is started through , , or ,
+ /// commands executed on the same thread reuse the same underlying connection until that transaction finishes.
+ /// Other threads do not join that transaction; they use another managed connection or wait for one to become available.
+ ///
/// Opened connection.
public IDbConnection Open()
{
@@ -3366,32 +3629,8 @@ namespace DynamORM
lock (SyncLock)
{
- if (_tempConn == null)
- {
- if (TransactionPool.Count == 0 || !_singleConnection)
- {
- conn = _provider.CreateConnection();
- conn.ConnectionString = _connectionString;
- conn.Open();
- opened = true;
-
- TransactionPool.Add(conn, new Stack());
- CommandsPool.Add(conn, new List());
- }
- else
- {
- conn = TransactionPool.Keys.First();
-
- if (conn.State != ConnectionState.Open)
- {
- conn.Open();
- opened = true;
- }
- }
- ret = new DynamicConnection(this, conn, _singleTransaction);
- }
- else
- ret = _tempConn;
+ conn = GetOrCreateManagedConnectionUnderLock(out opened);
+ ret = new DynamicConnection(this, conn, _singleTransaction);
}
if (opened)
ExecuteInitCommands(ret);
@@ -3405,40 +3644,46 @@ namespace DynamORM
if (connection == null)
return;
- if (!_singleConnection && connection != null && TransactionPool.ContainsKey(connection))
+ lock (SyncLock)
{
- // Close all commands
- if (CommandsPool.ContainsKey(connection))
- {
- List tmp = CommandsPool[connection].ToList();
- tmp.ForEach(cmd => cmd.Dispose());
+ if (IsDisposed || ConnectionEntries == null || TransactionPool == null || CommandsPool == null)
+ return;
- CommandsPool[connection].Clear();
- }
- // Rollback remaining transactions
- while (TransactionPool[connection].Count > 0)
+ ConnectionEntry entry;
+ if (!ConnectionEntries.TryGetValue(connection, out entry))
+ return;
+
+ if (entry.LeaseCount > 0)
+ entry.LeaseCount--;
+
+ bool hasAmbientOwner = AmbientTransactionConnections.ContainsValue(connection);
+ bool hasTransactions = TransactionPool.ContainsKey(connection) && TransactionPool[connection].Count > 0;
+ bool hasCommands = CommandsPool.ContainsKey(connection) && CommandsPool[connection].Count > 0;
+
+ if (_singleConnection || hasAmbientOwner || hasTransactions || hasCommands || entry.LeaseCount > 0)
{
- IDbTransaction trans = TransactionPool[connection].Pop();
- trans.Rollback();
- trans.Dispose();
+ Monitor.PulseAll(SyncLock);
+ return;
}
- // Close connection
+ entry.LastReleasedUtc = DateTime.UtcNow;
+
+ if (UsesManagedPooling && !entry.External)
+ {
+ TrimIdleConnectionsUnderLock();
+ Monitor.PulseAll(SyncLock);
+ return;
+ }
+ ConnectionEntries.Remove(connection);
+ TransactionPool.Remove(connection);
+ CommandsPool.Remove(connection);
+
if (connection.State == ConnectionState.Open)
connection.Close();
- // remove from pools
- lock (SyncLock)
- {
- TransactionPool.Remove(connection);
- CommandsPool.Remove(connection);
- }
- // Set stamp
_poolStamp = DateTime.Now.Ticks;
-
- // Dispose the corpse
- connection.Dispose();
- connection = null;
+ Monitor.PulseAll(SyncLock);
}
+ connection.Dispose();
}
/// Gets or sets contains commands executed when connection is opened.
public List InitCommands { get; set; }
@@ -3457,60 +3702,59 @@ namespace DynamORM
#region Transaction
/// Begins a global database transaction.
- /// Using this method connection is set to single open
- /// connection until all transactions are finished.
+ ///
+ /// Using this method binds one managed connection to the current thread until all direct database transactions on that thread are finished.
+ /// Commands executed through the same instance on that thread reuse that transaction-bound connection.
+ /// Other threads do not participate in that transaction and use another managed connection or wait for one to become available.
+ ///
/// Returns representation.
public IDbTransaction BeginTransaction()
{
- _tempConn = Open() as DynamicConnection;
-
- return _tempConn.BeginTransaction(null, null, () =>
- {
- Stack t = TransactionPool.TryGetValue(_tempConn.Connection);
-
- if (t == null | t.Count == 0)
- {
- _tempConn.Dispose();
- _tempConn = null;
- }
- });
+ return BeginAmbientTransaction(null, null);
}
/// Begins a database transaction with the specified
/// value.
+ /// Thread ownership and connection binding follow the same rules as .
/// One of the values.
/// Returns representation.
public IDbTransaction BeginTransaction(IsolationLevel il)
{
- _tempConn = Open() as DynamicConnection;
-
- return _tempConn.BeginTransaction(il, null, () =>
- {
- Stack t = TransactionPool.TryGetValue(_tempConn.Connection);
-
- if (t == null | t.Count == 0)
- {
- _tempConn.Dispose();
- _tempConn = null;
- }
- });
+ return BeginAmbientTransaction(il, null);
}
/// Begins a database transaction with the specified
- /// value.
+ /// custom provider argument.
+ /// Thread ownership and connection binding follow the same rules as .
/// Custom parameter describing transaction options.
/// Returns representation.
public IDbTransaction BeginTransaction(object custom)
{
- _tempConn = Open() as DynamicConnection;
+ return BeginAmbientTransaction(null, custom);
+ }
+ private IDbTransaction BeginAmbientTransaction(IsolationLevel? il, object custom)
+ {
+ DynamicConnection connection = Open() as DynamicConnection;
+ int threadId = Thread.CurrentThread.ManagedThreadId;
- return _tempConn.BeginTransaction(null, custom, () =>
+ lock (SyncLock)
+ AmbientTransactionConnections[threadId] = connection.Connection;
+
+ return connection.BeginTransaction(il, custom, () =>
{
- Stack t = TransactionPool.TryGetValue(_tempConn.Connection);
+ bool releaseConnection = false;
- if (t == null | t.Count == 0)
+ lock (SyncLock)
{
- _tempConn.Dispose();
- _tempConn = null;
+ Stack t = TransactionPool.TryGetValue(connection.Connection);
+
+ if (t == null || t.Count == 0)
+ {
+ AmbientTransactionConnections.Remove(threadId);
+ releaseConnection = true;
+ Monitor.PulseAll(SyncLock);
+ }
}
+ if (releaseConnection)
+ connection.Dispose();
});
}
#endregion Transaction
@@ -3567,10 +3811,14 @@ namespace DynamORM
{
TransactionPool.Clear();
CommandsPool.Clear();
+ ConnectionEntries.Clear();
+ AmbientTransactionConnections.Clear();
RemainingBuilders.Clear();
TransactionPool = null;
CommandsPool = null;
+ ConnectionEntries = null;
+ AmbientTransactionConnections = null;
RemainingBuilders = null;
}
ClearSchema();
@@ -3578,7 +3826,6 @@ namespace DynamORM
_proc.Dispose();
_proc = null;
- _tempConn = null;
IsDisposed = true;
}
/// Gets a value indicating whether this instance is disposed.
@@ -3595,11 +3842,17 @@ namespace DynamORM
None = 0x00000000,
/// Only single persistent database connection.
+ /// Command execution is serialized inside one instance when this option is enabled.
SingleConnection = 0x00000001,
/// Only one transaction.
+ /// Command execution is serialized inside one instance when this option is enabled.
SingleTransaction = 0x00000002,
+ /// Use internal connection pooling when connections are not kept as a single shared connection.
+ /// Pooling reuses idle managed connections and is configured through , , and .
+ ConnectionPooling = 0x00000004,
+
/// Database supports top syntax (SELECT TOP x ... FROM ...).
SupportTop = 0x00000080,
@@ -6902,6 +7155,11 @@ namespace DynamORM
/// Commits the database transaction.
public void Commit()
{
+ if (_db == null)
+ {
+ _isOperational = false;
+ return;
+ }
lock (_db.SyncLock)
{
if (_isOperational)
@@ -6929,6 +7187,11 @@ namespace DynamORM
/// Rolls back a transaction from a pending state.
public void Rollback()
{
+ if (_db == null)
+ {
+ _isOperational = false;
+ return;
+ }
lock (_db.SyncLock)
{
if (_isOperational)
@@ -14612,6 +14875,70 @@ namespace DynamORM
return resultTable;
}
}
+ internal sealed class DynamicExecutionReader : IDataReader
+ {
+ private IDataReader _reader;
+ private IDisposable _scope;
+
+ public DynamicExecutionReader(IDataReader reader, IDisposable scope)
+ {
+ _reader = reader;
+ _scope = scope;
+ }
+ public object this[string name] { get { return _reader[name]; } }
+ public object this[int i] { get { return _reader[i]; } }
+ public int Depth { get { return _reader.Depth; } }
+ public bool IsClosed { get { return _reader.IsClosed; } }
+ public int RecordsAffected { get { return _reader.RecordsAffected; } }
+ public int FieldCount { get { return _reader.FieldCount; } }
+
+ public void Close()
+ {
+ if (_reader != null)
+ _reader.Close();
+
+ Dispose();
+ }
+ public void Dispose()
+ {
+ IDataReader reader = _reader;
+ IDisposable scope = _scope;
+
+ _reader = null;
+ _scope = null;
+
+ if (reader != null)
+ reader.Dispose();
+
+ if (scope != null)
+ scope.Dispose();
+ }
+ public bool GetBoolean(int i) { return _reader.GetBoolean(i); }
+ public byte GetByte(int i) { return _reader.GetByte(i); }
+ public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) { return _reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); }
+ public char GetChar(int i) { return _reader.GetChar(i); }
+ public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) { return _reader.GetChars(i, fieldoffset, buffer, bufferoffset, length); }
+ public IDataReader GetData(int i) { return _reader.GetData(i); }
+ public string GetDataTypeName(int i) { return _reader.GetDataTypeName(i); }
+ public DateTime GetDateTime(int i) { return _reader.GetDateTime(i); }
+ public decimal GetDecimal(int i) { return _reader.GetDecimal(i); }
+ public double GetDouble(int i) { return _reader.GetDouble(i); }
+ public Type GetFieldType(int i) { return _reader.GetFieldType(i); }
+ public float GetFloat(int i) { return _reader.GetFloat(i); }
+ public Guid GetGuid(int i) { return _reader.GetGuid(i); }
+ public short GetInt16(int i) { return _reader.GetInt16(i); }
+ public int GetInt32(int i) { return _reader.GetInt32(i); }
+ public long GetInt64(int i) { return _reader.GetInt64(i); }
+ public string GetName(int i) { return _reader.GetName(i); }
+ public int GetOrdinal(string name) { return _reader.GetOrdinal(name); }
+ public DataTable GetSchemaTable() { return _reader.GetSchemaTable(); }
+ public string GetString(int i) { return _reader.GetString(i); }
+ public object GetValue(int i) { return _reader.GetValue(i); }
+ public int GetValues(object[] values) { return _reader.GetValues(values); }
+ public bool IsDBNull(int i) { return _reader.IsDBNull(i); }
+ public bool NextResult() { return _reader.NextResult(); }
+ public bool Read() { return _reader.Read(); }
+ }
internal sealed class DynamicProcedureDescriptor
{
public Type ProcedureType { get; set; }
diff --git a/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs b/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs
new file mode 100644
index 0000000..88dbab0
--- /dev/null
+++ b/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs
@@ -0,0 +1,234 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System;
+using System.Data;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+
+namespace DynamORM.Tests.Helpers
+{
+ [TestFixture]
+ public class ConnectionPoolingAndLockingTests
+ {
+ private FakeProviderState _state;
+ private DynamicDatabase _db;
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (_db != null)
+ _db.Dispose();
+ }
+
+ [Test]
+ public void TestConnectionPoolingReusesIdleConnection()
+ {
+ CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
+
+ using (_db.Open()) { }
+ using (_db.Open()) { }
+
+ Assert.AreEqual(1, _state.CreatedConnections);
+ Assert.AreEqual(1, _state.OpenCalls);
+ Assert.AreEqual(0, _state.CloseCalls);
+ }
+
+ [Test]
+ public void TestConnectionPoolingWaitsForReleasedConnectionWhenAtMaximum()
+ {
+ CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
+ _db.ConnectionPoolingKeepOpenCount = 1;
+ _db.ConnectionPoolingMaximumOpenCount = 1;
+
+ var first = _db.Open();
+ var started = new ManualResetEventSlim(false);
+ var completed = new ManualResetEventSlim(false);
+
+ var task = Task.Run(() =>
+ {
+ started.Set();
+ using (_db.Open()) { }
+ completed.Set();
+ });
+
+ Assert.IsTrue(started.Wait(TimeSpan.FromSeconds(2)));
+ Assert.IsFalse(completed.Wait(TimeSpan.FromMilliseconds(200)));
+
+ first.Dispose();
+
+ Assert.IsTrue(completed.Wait(TimeSpan.FromSeconds(2)));
+ task.Wait(TimeSpan.FromSeconds(2));
+ Assert.AreEqual(1, _state.CreatedConnections);
+ }
+
+ [Test]
+ public void TestConnectionPoolingClosesExpiredIdleConnections()
+ {
+ CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
+ _db.ConnectionPoolingConnectionLifetime = TimeSpan.Zero;
+
+ using (_db.Open()) { }
+ using (_db.Open()) { }
+
+ Assert.AreEqual(2, _state.CreatedConnections);
+ Assert.GreaterOrEqual(_state.CloseCalls, 1);
+ Assert.GreaterOrEqual(_state.DisposeCalls, 1);
+ }
+
+ [Test]
+ public void TestConnectionPoolingTrimsIdleConnectionsAbovePreferredLimit()
+ {
+ CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
+ _db.ConnectionPoolingKeepOpenCount = 1;
+ _db.ConnectionPoolingMaximumOpenCount = 3;
+
+ var c1 = _db.Open();
+ var c2 = _db.Open();
+ var c3 = _db.Open();
+
+ c1.Dispose();
+ c2.Dispose();
+ c3.Dispose();
+
+ Assert.AreEqual(3, _state.CreatedConnections);
+ Assert.AreEqual(2, _state.CloseCalls);
+ Assert.AreEqual(2, _state.DisposeCalls);
+ }
+
+ [Test]
+ public void TestDirectTransactionUsesSameThreadConnectionAndSeparateThreadGetsDifferentOne()
+ {
+ CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
+
+ using (var trans = _db.BeginTransaction())
+ {
+ IDbConnection threadLocalA = _db.Open();
+ IDbConnection threadLocalB = _db.Open();
+
+ try
+ {
+ Assert.AreSame(((DynamicConnection)threadLocalA).Connection, ((DynamicConnection)threadLocalB).Connection);
+
+ IDbConnection otherThreadConnection = null;
+ var task = Task.Run(() =>
+ {
+ using (var other = _db.Open())
+ otherThreadConnection = ((DynamicConnection)other).Connection;
+ });
+
+ Assert.IsTrue(task.Wait(TimeSpan.FromSeconds(2)));
+ Assert.AreNotSame(((DynamicConnection)threadLocalA).Connection, otherThreadConnection);
+ }
+ finally
+ {
+ threadLocalA.Dispose();
+ threadLocalB.Dispose();
+ }
+ }
+ }
+
+ [Test]
+ public void TestDirectTransactionUsesTransactionOnlyOnOwningThread()
+ {
+ CreateDatabase(DynamicDatabaseOptions.ConnectionPooling);
+
+ using (var transaction = _db.BeginTransaction())
+ {
+ ExecuteFakeCommand();
+
+ var otherThread = Task.Run(() => ExecuteFakeCommand());
+ Assert.IsTrue(otherThread.Wait(TimeSpan.FromSeconds(2)));
+
+ transaction.Commit();
+ }
+
+ lock (_state.SeenTransactions)
+ {
+ Assert.AreEqual(2, _state.SeenTransactions.Count);
+ Assert.NotNull(_state.SeenTransactions[0]);
+ Assert.IsNull(_state.SeenTransactions[1]);
+ }
+ }
+
+ [TestCase(DynamicDatabaseOptions.SingleConnection)]
+ [TestCase(DynamicDatabaseOptions.SingleTransaction)]
+ public void TestSingleModesSerializeCommandExecution(DynamicDatabaseOptions option)
+ {
+ CreateDatabase(option);
+ _state.BlockFirstExecution = true;
+ _state.AllowExecution.Reset();
+
+ Task task1 = Task.Run(() => ExecuteFakeCommand());
+ Assert.IsTrue(_state.FirstExecutionEntered.Wait(TimeSpan.FromSeconds(2)));
+
+ Task task2 = Task.Run(() => ExecuteFakeCommand());
+ Thread.Sleep(200);
+
+ Assert.AreEqual(1, _state.MaxConcurrentExecutions);
+
+ _state.AllowExecution.Set();
+
+ Assert.IsTrue(Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(5)));
+ Assert.AreEqual(1, _state.MaxConcurrentExecutions);
+ }
+
+ [TestCase(DynamicDatabaseOptions.SingleConnection)]
+ [TestCase(DynamicDatabaseOptions.SingleTransaction)]
+ public void TestSingleModesSerializeReaderLifetime(DynamicDatabaseOptions option)
+ {
+ CreateDatabase(option);
+ _state.BlockFirstReader = true;
+ _state.AllowReader.Reset();
+
+ IDataReader reader = null;
+ var task1 = Task.Run(() =>
+ {
+ using (var connection = _db.Open())
+ using (var command = connection.CreateCommand())
+ {
+ command.SetCommand("SELECT 1;");
+ reader = command.ExecuteReader();
+ }
+ });
+
+ Assert.IsTrue(_state.FirstReaderEntered.Wait(TimeSpan.FromSeconds(2)));
+
+ var secondCompleted = new ManualResetEventSlim(false);
+ var task2 = Task.Run(() =>
+ {
+ ExecuteFakeCommand();
+ secondCompleted.Set();
+ });
+
+ Assert.IsFalse(secondCompleted.Wait(TimeSpan.FromMilliseconds(200)));
+
+ _state.AllowReader.Set();
+ SpinWait.SpinUntil(() => reader != null, TimeSpan.FromSeconds(2));
+ reader.Dispose();
+
+ Assert.IsTrue(Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(5)));
+ Assert.AreEqual(1, _state.MaxConcurrentExecutions);
+ }
+
+ private void ExecuteFakeCommand()
+ {
+ using (var connection = _db.Open())
+ using (var command = connection.CreateCommand())
+ {
+ command.SetCommand("SELECT 1;");
+ command.ExecuteNonQuery();
+ }
+ }
+
+ private void CreateDatabase(DynamicDatabaseOptions options)
+ {
+ _state = new FakeProviderState();
+ _db = new DynamicDatabase(new FakeProviderFactory(_state), "fake", options);
+ }
+ }
+}
diff --git a/DynamORM.Tests/Helpers/FakeProviderFactory.cs b/DynamORM.Tests/Helpers/FakeProviderFactory.cs
new file mode 100644
index 0000000..aa4e85b
--- /dev/null
+++ b/DynamORM.Tests/Helpers/FakeProviderFactory.cs
@@ -0,0 +1,323 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DynamORM.Tests.Helpers
+{
+ internal sealed class FakeProviderState
+ {
+ public int CreatedConnections;
+ public int OpenCalls;
+ public int CloseCalls;
+ public int DisposeCalls;
+ public int BeginTransactionCalls;
+ public int ExecuteNonQueryCalls;
+ public int MaxConcurrentExecutions;
+ public int CurrentConcurrentExecutions;
+ public object LastTransactionSeen;
+ public readonly List