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..d94481b
--- /dev/null
+++ b/DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs
@@ -0,0 +1,153 @@
+/*
+ * 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 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();
+ }
+ }
+ }
+
+ [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);
+ }
+
+ 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..a904e74
--- /dev/null
+++ b/DynamORM.Tests/Helpers/FakeProviderFactory.cs
@@ -0,0 +1,236 @@
+/*
+ * 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;
+
+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 Connections = new List();
+ public ManualResetEventSlim FirstExecutionEntered = new ManualResetEventSlim(false);
+ public ManualResetEventSlim AllowExecution = new ManualResetEventSlim(true);
+ public bool BlockFirstExecution;
+ private int _blocked;
+
+ public void RecordExecution(IDbTransaction transaction)
+ {
+ int current = Interlocked.Increment(ref CurrentConcurrentExecutions);
+ int snapshot;
+ while ((snapshot = MaxConcurrentExecutions) < current)
+ Interlocked.CompareExchange(ref MaxConcurrentExecutions, current, snapshot);
+
+ Interlocked.Increment(ref ExecuteNonQueryCalls);
+ if (transaction != null)
+ LastTransactionSeen = transaction;
+
+ if (BlockFirstExecution && Interlocked.CompareExchange(ref _blocked, 1, 0) == 0)
+ {
+ FirstExecutionEntered.Set();
+ AllowExecution.Wait(TimeSpan.FromSeconds(5));
+ }
+ }
+
+ public void ExitExecution()
+ {
+ Interlocked.Decrement(ref CurrentConcurrentExecutions);
+ }
+ }
+
+ internal sealed class FakeProviderFactory : DbProviderFactory
+ {
+ private readonly FakeProviderState _state;
+
+ public FakeProviderFactory(FakeProviderState state)
+ {
+ _state = state;
+ }
+
+ public override DbConnection CreateConnection()
+ {
+ var connection = new FakeProviderConnection(_state);
+ lock (_state.Connections)
+ _state.Connections.Add(connection);
+ Interlocked.Increment(ref _state.CreatedConnections);
+ return connection;
+ }
+ }
+
+ internal sealed class FakeProviderConnection : DbConnection
+ {
+ private readonly FakeProviderState _state;
+ private ConnectionState _connectionState = ConnectionState.Closed;
+
+ public FakeProviderConnection(FakeProviderState state)
+ {
+ _state = state;
+ }
+
+ public override string ConnectionString { get; set; }
+ public override string Database { get { return "fake"; } }
+ public override string DataSource { get { return "fake"; } }
+ public override string ServerVersion { get { return "1.0"; } }
+ public override ConnectionState State { get { return _connectionState; } }
+
+ public override void ChangeDatabase(string databaseName) { }
+
+ public override void Close()
+ {
+ if (_connectionState != ConnectionState.Closed)
+ {
+ _connectionState = ConnectionState.Closed;
+ Interlocked.Increment(ref _state.CloseCalls);
+ }
+ }
+
+ public override void Open()
+ {
+ if (_connectionState != ConnectionState.Open)
+ {
+ _connectionState = ConnectionState.Open;
+ Interlocked.Increment(ref _state.OpenCalls);
+ }
+ }
+
+ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel)
+ {
+ Interlocked.Increment(ref _state.BeginTransactionCalls);
+ return new FakeProviderTransaction(this, isolationLevel);
+ }
+
+ protected override DbCommand CreateDbCommand()
+ {
+ return new FakeProviderCommand(_state) { Connection = this };
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ Interlocked.Increment(ref _state.DisposeCalls);
+
+ base.Dispose(disposing);
+ }
+ }
+
+ internal sealed class FakeProviderTransaction : DbTransaction
+ {
+ private readonly FakeProviderConnection _connection;
+ private readonly IsolationLevel _isolationLevel;
+
+ public FakeProviderTransaction(FakeProviderConnection connection, IsolationLevel isolationLevel)
+ {
+ _connection = connection;
+ _isolationLevel = isolationLevel;
+ }
+
+ public override IsolationLevel IsolationLevel { get { return _isolationLevel; } }
+ protected override DbConnection DbConnection { get { return _connection; } }
+ public override void Commit() { }
+ public override void Rollback() { }
+ }
+
+ internal sealed class FakeProviderCommand : DbCommand
+ {
+ private readonly FakeProviderState _state;
+ private readonly FakeProviderParameterCollection _parameters = new FakeProviderParameterCollection();
+
+ public FakeProviderCommand(FakeProviderState state)
+ {
+ _state = state;
+ }
+
+ public override string CommandText { get; set; }
+ public override int CommandTimeout { get; set; }
+ public override CommandType CommandType { get; set; }
+ public override bool DesignTimeVisible { get; set; }
+ public override UpdateRowSource UpdatedRowSource { get; set; }
+ protected override DbConnection DbConnection { get; set; }
+ protected override DbParameterCollection DbParameterCollection { get { return _parameters; } }
+ protected override DbTransaction DbTransaction { get; set; }
+
+ public override void Cancel() { }
+ public override int ExecuteNonQuery()
+ {
+ _state.RecordExecution(DbTransaction);
+ try
+ {
+ return 1;
+ }
+ finally
+ {
+ _state.ExitExecution();
+ }
+ }
+
+ public override object ExecuteScalar()
+ {
+ return ExecuteNonQuery();
+ }
+
+ public override void Prepare() { }
+
+ protected override DbParameter CreateDbParameter()
+ {
+ return new FakeProviderParameter();
+ }
+
+ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ internal sealed class FakeProviderParameter : DbParameter
+ {
+ public override DbType DbType { get; set; }
+ public override ParameterDirection Direction { get; set; }
+ public override bool IsNullable { get; set; }
+ public override string ParameterName { get; set; }
+ public override string SourceColumn { get; set; }
+ public override object Value { get; set; }
+ public override bool SourceColumnNullMapping { get; set; }
+ public override int Size { get; set; }
+ public override void ResetDbType() { }
+ }
+
+ internal sealed class FakeProviderParameterCollection : DbParameterCollection
+ {
+ private readonly List _items = new List();
+
+ public override int Count { get { return _items.Count; } }
+ public override object SyncRoot { get { return this; } }
+ public override int Add(object value) { _items.Add((DbParameter)value); return _items.Count - 1; }
+ public override void AddRange(Array values) { foreach (object value in values) Add(value); }
+ public override void Clear() { _items.Clear(); }
+ public override bool Contains(object value) { return _items.Contains((DbParameter)value); }
+ public override bool Contains(string value) { return IndexOf(value) >= 0; }
+ public override void CopyTo(Array array, int index) { _items.ToArray().CopyTo(array, index); }
+ public override System.Collections.IEnumerator GetEnumerator() { return _items.GetEnumerator(); }
+ protected override DbParameter GetParameter(int index) { return _items[index]; }
+ protected override DbParameter GetParameter(string parameterName) { return _items[IndexOf(parameterName)]; }
+ public override int IndexOf(object value) { return _items.IndexOf((DbParameter)value); }
+ public override int IndexOf(string parameterName) { return _items.FindIndex(x => x.ParameterName == parameterName); }
+ public override void Insert(int index, object value) { _items.Insert(index, (DbParameter)value); }
+ public override void Remove(object value) { _items.Remove((DbParameter)value); }
+ public override void RemoveAt(int index) { _items.RemoveAt(index); }
+ public override void RemoveAt(string parameterName) { int i = IndexOf(parameterName); if (i >= 0) _items.RemoveAt(i); }
+ protected override void SetParameter(int index, DbParameter value) { _items[index] = value; }
+ protected override void SetParameter(string parameterName, DbParameter value) { _items[IndexOf(parameterName)] = value; }
+ }
+}
diff --git a/DynamORM.Tests/Helpers/PoolingTests.cs b/DynamORM.Tests/Helpers/PoolingTests.cs
index a5e28c9..40084f6 100644
--- a/DynamORM.Tests/Helpers/PoolingTests.cs
+++ b/DynamORM.Tests/Helpers/PoolingTests.cs
@@ -92,9 +92,10 @@ namespace DynamORM.Tests.Helpers
finally
{
// Remove for next tests
- Database.Dispose();
+ if (Database != null)
+ Database.Dispose();
Database = null;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/DynamORM/DynamicCommand.cs b/DynamORM/DynamicCommand.cs
index 657b37e..6b71ab9 100644
--- a/DynamORM/DynamicCommand.cs
+++ b/DynamORM/DynamicCommand.cs
@@ -159,17 +159,24 @@ namespace DynamORM
/// Executes an SQL statement against the Connection object of a
/// data provider, and returns the number of rows affected.
/// The number of rows affected.
- public int ExecuteNonQuery()
- {
- try
- {
- return PrepareForExecution().ExecuteNonQuery();
- }
- catch (Exception ex)
- {
- throw new DynamicQueryException(ex, this);
- }
- }
+ 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 ,
@@ -178,49 +185,64 @@ namespace DynamORM
/// One of the
/// values.
/// An object.
- public IDataReader ExecuteReader(CommandBehavior behavior)
- {
- try
- {
- return PrepareForExecution().ExecuteReader(behavior);
- }
- catch (Exception ex)
- {
- throw new DynamicQueryException(ex, this);
- }
- }
+ public IDataReader ExecuteReader(CommandBehavior behavior)
+ {
+ IDisposable scope = null;
+ try
+ {
+ 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);
+ }
+ }
/// Executes the
/// against the and
/// builds an .
/// An object.
- public IDataReader ExecuteReader()
- {
- try
- {
- return PrepareForExecution().ExecuteReader();
- }
- catch (Exception ex)
- {
- throw new DynamicQueryException(ex, this);
- }
- }
+ public IDataReader ExecuteReader()
+ {
+ IDisposable scope = null;
+ try
+ {
+ 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);
+ }
+ }
/// Executes the query, and returns the first column of the
/// first row in the result set returned by the query. Extra columns or
/// rows are ignored.
/// The first column of the first row in the result set.
- public object ExecuteScalar()
- {
- try
- {
- return PrepareForExecution().ExecuteScalar();
- }
- catch (Exception ex)
- {
- throw new DynamicQueryException(ex, this);
- }
- }
+ 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
@@ -281,7 +303,7 @@ namespace DynamORM
IsDisposed = true;
- if (_con != null)
+ if (_con != null && db.CommandsPool != null)
{
List pool = db.CommandsPool.TryGetValue(_con.Connection);
diff --git a/DynamORM/DynamicConnection.cs b/DynamORM/DynamicConnection.cs
index a948786..5435f14 100644
--- a/DynamORM/DynamicConnection.cs
+++ b/DynamORM/DynamicConnection.cs
@@ -60,10 +60,10 @@ namespace DynamORM
/// Custom parameter describing transaction options.
/// This action is invoked when transaction is disposed.
/// Returns representation.
- internal DynamicTransaction BeginTransaction(IsolationLevel? il, object custom, Action disposed)
- {
- return new DynamicTransaction(_db, this, _singleTransaction, il, disposed, null);
- }
+ internal DynamicTransaction BeginTransaction(IsolationLevel? il, object custom, Action disposed)
+ {
+ return new DynamicTransaction(_db, this, _singleTransaction, il, disposed, custom);
+ }
#region IDbConnection Members
diff --git a/DynamORM/DynamicDatabase.cs b/DynamORM/DynamicDatabase.cs
index 4e61f39..c626c6a 100644
--- a/DynamORM/DynamicDatabase.cs
+++ b/DynamORM/DynamicDatabase.cs
@@ -31,11 +31,12 @@ using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
-using System.Dynamic;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using DynamORM.Builders;
+using System.Dynamic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using DynamORM.Builders;
using DynamORM.Builders.Extensions;
using DynamORM.Builders.Implementation;
using DynamORM.Helpers;
@@ -45,16 +46,25 @@ using DynamORM.Objects;
namespace DynamORM
{
/// Dynamic database is a class responsible for managing database.
- public class DynamicDatabase : IExtendedDisposable
- {
- #region Internal fields and properties
+ 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;
private DynamicProcedureInvoker _proc;
private string _connectionString;
- private bool _singleConnection;
- private bool _singleTransaction;
- private string _leftDecorator = "\"";
+ private bool _singleConnection;
+ private bool _singleTransaction;
+ private bool _connectionPooling;
+ private string _leftDecorator = "\"";
private string _rightDecorator = "\"";
private bool _leftDecoratorIsInInvalidMembersChars = true;
private bool _rightDecoratorIsInInvalidMembersChars = true;
@@ -62,10 +72,12 @@ namespace DynamORM
private int? _commandTimeout = null;
private long _poolStamp = 0;
- private DynamicConnection _tempConn = null;
-
- /// Provides lock object for this database instance.
- internal readonly object SyncLock = new object();
+ 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();
/// Gets or sets timestamp of last transaction pool or configuration change.
/// This property is used to allow commands to determine if
@@ -90,11 +102,17 @@ namespace DynamORM
}
/// Gets pool of connections and transactions.
- internal Dictionary> TransactionPool { get; private set; }
+ internal Dictionary> TransactionPool { get; private set; }
/// Gets pool of connections and commands.
/// Pool should contain dynamic commands instead of native ones.
- internal Dictionary> CommandsPool { get; private set; }
+ 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; }
@@ -116,8 +134,20 @@ namespace DynamORM
/// Gets database options.
public DynamicDatabaseOptions Options { get; private set; }
- /// Gets or sets command timeout.
- public int? CommandTimeout { get { return _commandTimeout; } set { _commandTimeout = value; _poolStamp = DateTime.Now.Ticks; } }
+ /// 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; } }
@@ -313,10 +343,18 @@ namespace DynamORM
IsDisposed = false;
InitCommon(connection.ConnectionString, options);
- TransactionPool.Add(connection, new Stack());
-
- if (!_singleConnection)
- throw new InvalidOperationException("This constructor accepts only connections with DynamicDatabaseOptions.SingleConnection option.");
+ 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.");
}
private void InitCommon(string connectionString, DynamicDatabaseOptions options)
@@ -324,14 +362,20 @@ namespace DynamORM
_connectionString = connectionString;
Options = options;
- _singleConnection = (options & DynamicDatabaseOptions.SingleConnection) == DynamicDatabaseOptions.SingleConnection;
- _singleTransaction = (options & DynamicDatabaseOptions.SingleTransaction) == DynamicDatabaseOptions.SingleTransaction;
- DumpCommands = (options & DynamicDatabaseOptions.DumpCommands) == DynamicDatabaseOptions.DumpCommands;
-
- TransactionPool = new Dictionary>();
- CommandsPool = new Dictionary>();
- Schema = new Dictionary>();
- RemainingBuilders = new List();
+ _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
TablesCache = new Dictionary();
#endif
@@ -1966,98 +2010,299 @@ namespace DynamORM
#endregion Decorators
- #region Connection
-
- /// Open managed connection.
- /// Opened connection.
- public IDbConnection Open()
- {
- IDbConnection conn = null;
- DynamicConnection ret = null;
- bool opened = false;
-
- 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;
- }
-
- if (opened)
- ExecuteInitCommands(ret);
+ #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()
+ {
+ IDbConnection conn = null;
+ DynamicConnection ret = null;
+ bool opened = false;
+
+ lock (SyncLock)
+ {
+ conn = GetOrCreateManagedConnectionUnderLock(out opened);
+ ret = new DynamicConnection(this, conn, _singleTransaction);
+ }
+
+ if (opened)
+ ExecuteInitCommands(ret);
return ret;
}
/// Close connection if we are allowed to.
/// Connection to manage.
- internal void Close(IDbConnection connection)
- {
- if (connection == null)
- return;
-
- if (!_singleConnection && connection != null && TransactionPool.ContainsKey(connection))
- {
- // Close all commands
- if (CommandsPool.ContainsKey(connection))
- {
- List tmp = CommandsPool[connection].ToList();
- tmp.ForEach(cmd => cmd.Dispose());
-
- CommandsPool[connection].Clear();
- }
-
- // Rollback remaining transactions
- while (TransactionPool[connection].Count > 0)
- {
- IDbTransaction trans = TransactionPool[connection].Pop();
- trans.Rollback();
- trans.Dispose();
- }
-
- // Close 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;
- }
- }
+ internal void Close(IDbConnection connection)
+ {
+ if (connection == null)
+ return;
+
+ lock (SyncLock)
+ {
+ if (IsDisposed || ConnectionEntries == null || TransactionPool == null || CommandsPool == null)
+ return;
+
+ 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)
+ {
+ Monitor.PulseAll(SyncLock);
+ return;
+ }
+
+ 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();
+
+ _poolStamp = DateTime.Now.Ticks;
+ Monitor.PulseAll(SyncLock);
+ }
+
+ connection.Dispose();
+ }
/// Gets or sets contains commands executed when connection is opened.
public List InitCommands { get; set; }
@@ -2076,65 +2321,66 @@ namespace DynamORM
#region Transaction
- /// Begins a global database transaction.
- /// Using this method connection is set to single open
- /// connection until all transactions are finished.
- /// Returns representation.
- public IDbTransaction BeginTransaction()
- {
- _tempConn = Open() as DynamicConnection;
+ /// Begins a global database transaction.
+ ///
+ /// 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()
+ {
+ return BeginAmbientTransaction(null, null);
+ }
- return _tempConn.BeginTransaction(null, null, () =>
- {
- Stack t = TransactionPool.TryGetValue(_tempConn.Connection);
+ /// 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)
+ {
+ return BeginAmbientTransaction(il, null);
+ }
- if (t == null | t.Count == 0)
- {
- _tempConn.Dispose();
- _tempConn = null;
- }
- });
- }
-
- /// Begins a database transaction with the specified
- /// value.
- /// 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;
- }
- });
- }
-
- /// Begins a database transaction with the specified
- /// value.
- /// Custom parameter describing transaction options.
- /// Returns representation.
- public IDbTransaction BeginTransaction(object custom)
- {
- _tempConn = Open() as DynamicConnection;
-
- return _tempConn.BeginTransaction(null, custom, () =>
- {
- Stack t = TransactionPool.TryGetValue(_tempConn.Connection);
-
- if (t == null | t.Count == 0)
- {
- _tempConn.Dispose();
- _tempConn = null;
- }
- });
- }
+ /// Begins a database transaction with the specified
+ /// 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)
+ {
+ return BeginAmbientTransaction(null, custom);
+ }
+
+ private IDbTransaction BeginAmbientTransaction(IsolationLevel? il, object custom)
+ {
+ DynamicConnection connection = Open() as DynamicConnection;
+ int threadId = Thread.CurrentThread.ManagedThreadId;
+
+ lock (SyncLock)
+ AmbientTransactionConnections[threadId] = connection.Connection;
+
+ return connection.BeginTransaction(il, custom, () =>
+ {
+ bool releaseConnection = false;
+
+ lock (SyncLock)
+ {
+ 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
@@ -2156,8 +2402,8 @@ namespace DynamORM
tables = null;
#endif
- foreach (KeyValuePair> con in TransactionPool)
- {
+ foreach (KeyValuePair> con in TransactionPool)
+ {
// Close all commands
if (CommandsPool.ContainsKey(con.Key))
{
@@ -2189,25 +2435,28 @@ namespace DynamORM
RemainingBuilders.First().Dispose();
// Clear pools
- lock (SyncLock)
- {
- TransactionPool.Clear();
- CommandsPool.Clear();
- RemainingBuilders.Clear();
-
- TransactionPool = null;
- CommandsPool = null;
- RemainingBuilders = null;
- }
+ lock (SyncLock)
+ {
+ TransactionPool.Clear();
+ CommandsPool.Clear();
+ ConnectionEntries.Clear();
+ AmbientTransactionConnections.Clear();
+ RemainingBuilders.Clear();
+
+ TransactionPool = null;
+ CommandsPool = null;
+ ConnectionEntries = null;
+ AmbientTransactionConnections = null;
+ RemainingBuilders = null;
+ }
ClearSchema();
- if (_proc != null)
- _proc.Dispose();
-
- _proc = null;
- _tempConn = null;
- IsDisposed = true;
- }
+ if (_proc != null)
+ _proc.Dispose();
+
+ _proc = null;
+ IsDisposed = true;
+ }
/// Gets a value indicating whether this instance is disposed.
public bool IsDisposed { get; private set; }
diff --git a/DynamORM/DynamicDatabaseOptions.cs b/DynamORM/DynamicDatabaseOptions.cs
index a561167..ca7c091 100644
--- a/DynamORM/DynamicDatabaseOptions.cs
+++ b/DynamORM/DynamicDatabaseOptions.cs
@@ -38,11 +38,17 @@ namespace DynamORM
/// No specific options.
None = 0x00000000,
- /// Only single persistent database connection.
- SingleConnection = 0x00000001,
-
- /// Only one transaction.
- SingleTransaction = 0x00000002,
+ /// 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,
@@ -68,4 +74,4 @@ namespace DynamORM
/// Debug option allowing to enable command dumps by default.
DumpCommands = 0x01000000,
}
-}
\ No newline at end of file
+}
diff --git a/DynamORM/DynamicTransaction.cs b/DynamORM/DynamicTransaction.cs
index 6f0a697..1e43713 100644
--- a/DynamORM/DynamicTransaction.cs
+++ b/DynamORM/DynamicTransaction.cs
@@ -102,10 +102,16 @@ namespace DynamORM
}
/// Commits the database transaction.
- public void Commit()
- {
- lock (_db.SyncLock)
- {
+ public void Commit()
+ {
+ if (_db == null)
+ {
+ _isOperational = false;
+ return;
+ }
+
+ lock (_db.SyncLock)
+ {
if (_isOperational)
{
Stack t = _db.TransactionPool.TryGetValue(_con.Connection);
@@ -131,10 +137,16 @@ namespace DynamORM
}
/// Rolls back a transaction from a pending state.
- public void Rollback()
- {
- lock (_db.SyncLock)
- {
+ public void Rollback()
+ {
+ if (_db == null)
+ {
+ _isOperational = false;
+ return;
+ }
+
+ lock (_db.SyncLock)
+ {
if (_isOperational)
{
Stack t = _db.TransactionPool.TryGetValue(_con.Connection);
diff --git a/DynamORM/Helpers/DynamicExecutionReader.cs b/DynamORM/Helpers/DynamicExecutionReader.cs
new file mode 100644
index 0000000..5bbd927
--- /dev/null
+++ b/DynamORM/Helpers/DynamicExecutionReader.cs
@@ -0,0 +1,101 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+using System;
+using System.Data;
+
+namespace DynamORM.Helpers
+{
+ 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(); }
+ }
+}
diff --git a/docs/quick-start.md b/docs/quick-start.md
index 1600931..01b316c 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -31,6 +31,14 @@ using (var db = new DynamicDatabase(
This setup mirrors `DynamORM.Tests/TestsBase.cs`.
+If you do not want `SingleConnection`, but still want managed reuse of open connections, enable `DynamicDatabaseOptions.ConnectionPooling` and configure:
+
+- `db.ConnectionPoolingKeepOpenCount`
+- `db.ConnectionPoolingMaximumOpenCount`
+- `db.ConnectionPoolingConnectionLifetime`
+
+Full details are documented in [Transactions and Disposal](transactions-and-disposal.md).
+
## First Query (Dynamic API)
```csharp
diff --git a/docs/transactions-and-disposal.md b/docs/transactions-and-disposal.md
index 694b835..95728ba 100644
--- a/docs/transactions-and-disposal.md
+++ b/docs/transactions-and-disposal.md
@@ -8,6 +8,7 @@ DynamORM manages connections, command pools, and transaction stacks internally.
- `SingleConnection`
- `SingleTransaction`
+- `ConnectionPooling`
- `SupportSchema`
- `SupportStoredProcedures`
- `SupportNoLock`
@@ -23,6 +24,42 @@ var options =
DynamicDatabaseOptions.SupportSchema;
```
+## Internal Connection Pooling
+
+Without `SingleConnection`, DynamORM normally opens and closes a managed connection for each operation.
+
+That is fine when the provider already has efficient connection pooling. It is not enough for providers that do not.
+
+`ConnectionPooling` enables internal managed connection reuse:
+
+```csharp
+var options =
+ DynamicDatabaseOptions.ConnectionPooling |
+ DynamicDatabaseOptions.SupportLimitOffset |
+ DynamicDatabaseOptions.SupportSchema;
+
+using (var db = new DynamicDatabase(factory, connectionString, options))
+{
+ db.ConnectionPoolingKeepOpenCount = 32;
+ db.ConnectionPoolingMaximumOpenCount = 128;
+ db.ConnectionPoolingConnectionLifetime = TimeSpan.FromHours(1);
+}
+```
+
+Behavior:
+
+- idle connections are reused instead of opened and closed repeatedly
+- up to `ConnectionPoolingKeepOpenCount` idle connections are kept open
+- no more than `ConnectionPoolingMaximumOpenCount` managed connections are kept at once
+- if the maximum is reached, callers wait until a connection is released
+- idle pooled connections older than `ConnectionPoolingConnectionLifetime` are retired even if the preferred idle count has not been exceeded
+
+Default values:
+
+- `ConnectionPoolingKeepOpenCount = 32`
+- `ConnectionPoolingMaximumOpenCount = 128`
+- `ConnectionPoolingConnectionLifetime = 1 hour`
+
## Transaction Usage
```csharp
@@ -38,6 +75,50 @@ using (var cmd = conn.CreateCommand())
Global transaction mode is also available via `db.BeginTransaction()`.
+## Direct Database Transactions and Thread Ownership
+
+`db.BeginTransaction()` is different from `conn.BeginTransaction()` on an arbitrary opened connection.
+
+When you call `db.BeginTransaction()`:
+
+- DynamORM binds one managed connection to the current thread
+- commands executed through that `DynamicDatabase` on the same thread reuse that connection and transaction
+- other threads do not join that transaction automatically
+- other threads use another managed connection or wait for one to become available
+
+This is important because it prevents unrelated work from another thread from accidentally running inside the transaction started on the original thread.
+
+Example:
+
+```csharp
+using (var tx = db.BeginTransaction())
+{
+ db.Execute("UPDATE sample_users SET code = 'A' WHERE id = 1");
+ db.Execute("UPDATE sample_users SET code = 'B' WHERE id = 2");
+ tx.Commit();
+}
+```
+
+All commands above run on the same transaction-bound connection because they execute on the same thread.
+
+If another thread uses the same `db` instance while that transaction is active, it does not participate in this transaction unless user code explicitly routes work back to the owning thread.
+
+## Single Connection and Single Transaction Locking
+
+`SingleConnection` and `SingleTransaction` are now guarded by internal execution serialization.
+
+That means:
+
+- only one command executes at a time per `DynamicDatabase` instance when either option is enabled
+- if another thread tries to execute a command at the same time, it waits until the current execution finishes
+- this reduces the amount of user-side locking needed for providers such as SQLite
+
+Important limit:
+
+- a reader keeps the execution slot until the reader is disposed
+
+So if one thread executes `ExecuteReader()` and holds the reader open, another thread waits until that reader is closed or disposed before executing the next command on the same `DynamicDatabase` instance.
+
## Disposal Guarantees
Current disposal behavior includes idempotent guards on key objects:
@@ -58,8 +139,18 @@ Behavior validated by `DynamORM.Tests/Helpers/PoolingTests.cs`:
- Disposing the database invalidates active commands.
- Open transactions are rolled back during disposal when not committed.
+Behavior validated by `DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs`:
+
+- pooled connections are reused
+- pooled connections wait when the configured maximum is reached
+- expired idle connections are retired
+- direct database transactions are isolated to the owning thread
+- `SingleConnection` and `SingleTransaction` serialize command execution across threads
+
## Practices
- Prefer `using` blocks for `DynamicDatabase`, connections, commands, transactions, and builders.
- Do not manually re-dispose the same object from multiple ownership paths unless `IsDisposed` is checked.
- Keep transaction scope short and explicit.
+- Use `db.BeginTransaction()` when you want DynamORM-managed transactional flow across multiple commands on the current thread.
+- Use `ConnectionPooling` when the underlying provider does not give you adequate pooling on its own.