Add managed connection pooling and execution locking

This commit is contained in:
2026-02-27 17:28:50 +01:00
parent e43d7863ec
commit aedb97e879
12 changed files with 1554 additions and 348 deletions

View File

@@ -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
/// <returns>The number of rows affected.</returns>
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();
}
}
/// <summary>Executes the <see cref="P:System.Data.IDbCommand.CommandText"/>
/// against the <see cref="P:System.Data.IDbCommand.Connection"/>,
@@ -1289,12 +1297,16 @@ namespace DynamORM
/// <returns>An <see cref="T:System.Data.IDataReader"/> object.</returns>
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
/// <returns>An <see cref="T:System.Data.IDataReader"/> object.</returns>
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
/// <returns>The first column of the first row in the result set.</returns>
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();
}
}
/// <summary>Gets the <see cref="T:System.Data.IDataParameterCollection"/>.</summary>
public IDataParameterCollection Parameters
@@ -1384,7 +1407,7 @@ namespace DynamORM
IsDisposed = true;
if (_con != null)
if (_con != null && db.CommandsPool != null)
{
List<IDbCommand> pool = db.CommandsPool.TryGetValue(_con.Connection);
@@ -1436,7 +1459,7 @@ namespace DynamORM
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
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
/// <summary>Dynamic database is a class responsible for managing database.</summary>
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;
/// <summary>Provides lock object for this database instance.</summary>
internal readonly object SyncLock = new object();
@@ -1593,6 +1626,12 @@ namespace DynamORM
/// <remarks>Pool should contain dynamic commands instead of native ones.</remarks>
internal Dictionary<IDbConnection, List<IDbCommand>> CommandsPool { get; private set; }
/// <summary>Gets connection metadata tracked by the database instance.</summary>
private Dictionary<IDbConnection, ConnectionEntry> ConnectionEntries { get; set; }
/// <summary>Gets ambient transaction-bound connections keyed by managed thread identifier.</summary>
private Dictionary<int, IDbConnection> AmbientTransactionConnections { get; set; }
/// <summary>Gets schema columns cache.</summary>
internal Dictionary<string, Dictionary<string, DynamicSchemaColumn>> Schema { get; private set; }
@@ -1616,6 +1655,18 @@ namespace DynamORM
/// <summary>Gets or sets command timeout.</summary>
public int? CommandTimeout { get { return _commandTimeout; } set { _commandTimeout = value; _poolStamp = DateTime.Now.Ticks; } }
/// <summary>Gets or sets the preferred number of idle open connections kept in the internal pool.</summary>
/// <remarks>Default value is <c>32</c>. Applies only when <see cref="DynamicDatabaseOptions.ConnectionPooling"/> is enabled.</remarks>
public int ConnectionPoolingKeepOpenCount { get; set; }
/// <summary>Gets or sets the maximum number of connections that may remain managed by the internal pool at once.</summary>
/// <remarks>Default value is <c>128</c>. When this limit is reached, callers wait for a pooled connection to be released.</remarks>
public int ConnectionPoolingMaximumOpenCount { get; set; }
/// <summary>Gets or sets how long an idle pooled connection may stay open before it is retired.</summary>
/// <remarks>Default value is one hour. Applies only when <see cref="DynamicDatabaseOptions.ConnectionPooling"/> is enabled.</remarks>
public TimeSpan ConnectionPoolingConnectionLifetime { get; set; }
/// <summary>Gets the database provider.</summary>
public DbProviderFactory Provider { get { return _provider; } }
@@ -1804,6 +1855,14 @@ namespace DynamORM
IsDisposed = false;
InitCommon(connection.ConnectionString, options);
TransactionPool.Add(connection, new Stack<IDbTransaction>());
CommandsPool.Add(connection, new List<IDbCommand>());
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<IDbConnection, Stack<IDbTransaction>>();
CommandsPool = new Dictionary<IDbConnection, List<IDbCommand>>();
ConnectionEntries = new Dictionary<IDbConnection, ConnectionEntry>();
AmbientTransactionConnections = new Dictionary<int, IDbConnection>();
Schema = new Dictionary<string, Dictionary<string, DynamicSchemaColumn>>();
RemainingBuilders = new List<IDynamicQueryBuilder>();
#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<IDbTransaction>());
CommandsPool.Add(connection, new List<IDbCommand>());
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<IDbConnection> toRemove = new List<IDbConnection>();
foreach (KeyValuePair<IDbConnection, ConnectionEntry> 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);
}
}
/// <summary>Open managed connection.</summary>
/// <remarks>
/// When <see cref="DynamicDatabaseOptions.ConnectionPooling"/> is enabled, DynamORM reuses idle managed connections instead of opening and closing a new one for every operation.
/// When a transaction is started through <see cref="BeginTransaction()"/>, <see cref="BeginTransaction(IsolationLevel)"/>, or <see cref="BeginTransaction(object)"/>,
/// 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.
/// </remarks>
/// <returns>Opened connection.</returns>
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<IDbTransaction>());
CommandsPool.Add(conn, new List<IDbCommand>());
}
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<IDbCommand> 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();
}
/// <summary>Gets or sets contains commands executed when connection is opened.</summary>
public List<string> InitCommands { get; set; }
@@ -3457,60 +3702,59 @@ namespace DynamORM
#region Transaction
/// <summary>Begins a global database transaction.</summary>
/// <remarks>Using this method connection is set to single open
/// connection until all transactions are finished.</remarks>
/// <remarks>
/// 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 <see cref="DynamicDatabase"/> 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.
/// </remarks>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction()
{
_tempConn = Open() as DynamicConnection;
return _tempConn.BeginTransaction(null, null, () =>
{
Stack<IDbTransaction> t = TransactionPool.TryGetValue(_tempConn.Connection);
if (t == null | t.Count == 0)
{
_tempConn.Dispose();
_tempConn = null;
}
});
return BeginAmbientTransaction(null, null);
}
/// <summary>Begins a database transaction with the specified
/// <see cref="System.Data.IsolationLevel"/> value.</summary>
/// <remarks>Thread ownership and connection binding follow the same rules as <see cref="BeginTransaction()"/>.</remarks>
/// <param name="il">One of the <see cref="System.Data.IsolationLevel"/> values.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction(IsolationLevel il)
{
_tempConn = Open() as DynamicConnection;
return _tempConn.BeginTransaction(il, null, () =>
{
Stack<IDbTransaction> t = TransactionPool.TryGetValue(_tempConn.Connection);
if (t == null | t.Count == 0)
{
_tempConn.Dispose();
_tempConn = null;
}
});
return BeginAmbientTransaction(il, null);
}
/// <summary>Begins a database transaction with the specified
/// <see cref="System.Data.IsolationLevel"/> value.</summary>
/// custom provider argument.</summary>
/// <remarks>Thread ownership and connection binding follow the same rules as <see cref="BeginTransaction()"/>.</remarks>
/// <param name="custom">Custom parameter describing transaction options.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
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<IDbTransaction> t = TransactionPool.TryGetValue(_tempConn.Connection);
bool releaseConnection = false;
if (t == null | t.Count == 0)
lock (SyncLock)
{
_tempConn.Dispose();
_tempConn = null;
Stack<IDbTransaction> 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;
}
/// <summary>Gets a value indicating whether this instance is disposed.</summary>
@@ -3595,11 +3842,17 @@ namespace DynamORM
None = 0x00000000,
/// <summary>Only single persistent database connection.</summary>
/// <remarks>Command execution is serialized inside one <see cref="DynamicDatabase"/> instance when this option is enabled.</remarks>
SingleConnection = 0x00000001,
/// <summary>Only one transaction.</summary>
/// <remarks>Command execution is serialized inside one <see cref="DynamicDatabase"/> instance when this option is enabled.</remarks>
SingleTransaction = 0x00000002,
/// <summary>Use internal connection pooling when connections are not kept as a single shared connection.</summary>
/// <remarks>Pooling reuses idle managed connections and is configured through <see cref="DynamicDatabase.ConnectionPoolingKeepOpenCount"/>, <see cref="DynamicDatabase.ConnectionPoolingMaximumOpenCount"/>, and <see cref="DynamicDatabase.ConnectionPoolingConnectionLifetime"/>.</remarks>
ConnectionPooling = 0x00000004,
/// <summary>Database supports top syntax (SELECT TOP x ... FROM ...).</summary>
SupportTop = 0x00000080,
@@ -6902,6 +7155,11 @@ namespace DynamORM
/// <summary>Commits the database transaction.</summary>
public void Commit()
{
if (_db == null)
{
_isOperational = false;
return;
}
lock (_db.SyncLock)
{
if (_isOperational)
@@ -6929,6 +7187,11 @@ namespace DynamORM
/// <summary>Rolls back a transaction from a pending state.</summary>
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; }

View File

@@ -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);
}
}
}

View File

@@ -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<FakeProviderConnection> Connections = new List<FakeProviderConnection>();
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<DbParameter> _items = new List<DbParameter>();
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; }
}
}

View File

@@ -92,9 +92,10 @@ namespace DynamORM.Tests.Helpers
finally
{
// Remove for next tests
Database.Dispose();
if (Database != null)
Database.Dispose();
Database = null;
}
}
}
}
}

View File

@@ -159,17 +159,24 @@ namespace DynamORM
/// <summary>Executes an SQL statement against the Connection object of a
/// data provider, and returns the number of rows affected.</summary>
/// <returns>The number of rows affected.</returns>
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();
}
}
/// <summary>Executes the <see cref="P:System.Data.IDbCommand.CommandText"/>
/// against the <see cref="P:System.Data.IDbCommand.Connection"/>,
@@ -178,49 +185,64 @@ namespace DynamORM
/// </summary><param name="behavior">One of the
/// <see cref="T:System.Data.CommandBehavior"/> values.</param>
/// <returns>An <see cref="T:System.Data.IDataReader"/> object.</returns>
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);
}
}
/// <summary>Executes the <see cref="P:System.Data.IDbCommand.CommandText"/>
/// against the <see cref="P:System.Data.IDbCommand.Connection"/> and
/// builds an <see cref="T:System.Data.IDataReader"/>.</summary>
/// <returns>An <see cref="T:System.Data.IDataReader"/> object.</returns>
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);
}
}
/// <summary>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.</summary>
/// <returns>The first column of the first row in the result set.</returns>
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();
}
}
/// <summary>Gets the <see cref="T:System.Data.IDataParameterCollection"/>.</summary>
public IDataParameterCollection Parameters
@@ -281,7 +303,7 @@ namespace DynamORM
IsDisposed = true;
if (_con != null)
if (_con != null && db.CommandsPool != null)
{
List<IDbCommand> pool = db.CommandsPool.TryGetValue(_con.Connection);

View File

@@ -60,10 +60,10 @@ namespace DynamORM
/// <param name="custom">Custom parameter describing transaction options.</param>
/// <param name="disposed">This action is invoked when transaction is disposed.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
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

View File

@@ -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
{
/// <summary>Dynamic database is a class responsible for managing database.</summary>
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;
/// <summary>Provides lock object for this database instance.</summary>
internal readonly object SyncLock = new object();
private readonly object _executionSyncRoot = new object();
private int _executionOwnerThreadId = -1;
private int _executionLockDepth = 0;
/// <summary>Provides lock object for this database instance.</summary>
internal readonly object SyncLock = new object();
/// <summary>Gets or sets timestamp of last transaction pool or configuration change.</summary>
/// <remarks>This property is used to allow commands to determine if
@@ -90,11 +102,17 @@ namespace DynamORM
}
/// <summary>Gets pool of connections and transactions.</summary>
internal Dictionary<IDbConnection, Stack<IDbTransaction>> TransactionPool { get; private set; }
internal Dictionary<IDbConnection, Stack<IDbTransaction>> TransactionPool { get; private set; }
/// <summary>Gets pool of connections and commands.</summary>
/// <remarks>Pool should contain dynamic commands instead of native ones.</remarks>
internal Dictionary<IDbConnection, List<IDbCommand>> CommandsPool { get; private set; }
internal Dictionary<IDbConnection, List<IDbCommand>> CommandsPool { get; private set; }
/// <summary>Gets connection metadata tracked by the database instance.</summary>
private Dictionary<IDbConnection, ConnectionEntry> ConnectionEntries { get; set; }
/// <summary>Gets ambient transaction-bound connections keyed by managed thread identifier.</summary>
private Dictionary<int, IDbConnection> AmbientTransactionConnections { get; set; }
/// <summary>Gets schema columns cache.</summary>
internal Dictionary<string, Dictionary<string, DynamicSchemaColumn>> Schema { get; private set; }
@@ -116,8 +134,20 @@ namespace DynamORM
/// <summary>Gets database options.</summary>
public DynamicDatabaseOptions Options { get; private set; }
/// <summary>Gets or sets command timeout.</summary>
public int? CommandTimeout { get { return _commandTimeout; } set { _commandTimeout = value; _poolStamp = DateTime.Now.Ticks; } }
/// <summary>Gets or sets command timeout.</summary>
public int? CommandTimeout { get { return _commandTimeout; } set { _commandTimeout = value; _poolStamp = DateTime.Now.Ticks; } }
/// <summary>Gets or sets the preferred number of idle open connections kept in the internal pool.</summary>
/// <remarks>Default value is <c>32</c>. Applies only when <see cref="DynamicDatabaseOptions.ConnectionPooling"/> is enabled.</remarks>
public int ConnectionPoolingKeepOpenCount { get; set; }
/// <summary>Gets or sets the maximum number of connections that may remain managed by the internal pool at once.</summary>
/// <remarks>Default value is <c>128</c>. When this limit is reached, callers wait for a pooled connection to be released.</remarks>
public int ConnectionPoolingMaximumOpenCount { get; set; }
/// <summary>Gets or sets how long an idle pooled connection may stay open before it is retired.</summary>
/// <remarks>Default value is one hour. Applies only when <see cref="DynamicDatabaseOptions.ConnectionPooling"/> is enabled.</remarks>
public TimeSpan ConnectionPoolingConnectionLifetime { get; set; }
/// <summary>Gets the database provider.</summary>
public DbProviderFactory Provider { get { return _provider; } }
@@ -313,10 +343,18 @@ namespace DynamORM
IsDisposed = false;
InitCommon(connection.ConnectionString, options);
TransactionPool.Add(connection, new Stack<IDbTransaction>());
if (!_singleConnection)
throw new InvalidOperationException("This constructor accepts only connections with DynamicDatabaseOptions.SingleConnection option.");
TransactionPool.Add(connection, new Stack<IDbTransaction>());
CommandsPool.Add(connection, new List<IDbCommand>());
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<IDbConnection, Stack<IDbTransaction>>();
CommandsPool = new Dictionary<IDbConnection, List<IDbCommand>>();
Schema = new Dictionary<string, Dictionary<string, DynamicSchemaColumn>>();
RemainingBuilders = new List<IDynamicQueryBuilder>();
_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<IDbConnection, Stack<IDbTransaction>>();
CommandsPool = new Dictionary<IDbConnection, List<IDbCommand>>();
ConnectionEntries = new Dictionary<IDbConnection, ConnectionEntry>();
AmbientTransactionConnections = new Dictionary<int, IDbConnection>();
Schema = new Dictionary<string, Dictionary<string, DynamicSchemaColumn>>();
RemainingBuilders = new List<IDynamicQueryBuilder>();
#if !DYNAMORM_OMMIT_OLDSYNTAX
TablesCache = new Dictionary<string, DynamicTable>();
#endif
@@ -1966,98 +2010,299 @@ namespace DynamORM
#endregion Decorators
#region Connection
/// <summary>Open managed connection.</summary>
/// <returns>Opened connection.</returns>
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<IDbTransaction>());
CommandsPool.Add(conn, new List<IDbCommand>());
}
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<IDbTransaction>());
CommandsPool.Add(connection, new List<IDbCommand>());
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<IDbConnection> toRemove = new List<IDbConnection>();
foreach (KeyValuePair<IDbConnection, ConnectionEntry> 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);
}
}
/// <summary>Open managed connection.</summary>
/// <remarks>
/// When <see cref="DynamicDatabaseOptions.ConnectionPooling"/> is enabled, DynamORM reuses idle managed connections instead of opening and closing a new one for every operation.
/// When a transaction is started through <see cref="BeginTransaction()"/>, <see cref="BeginTransaction(IsolationLevel)"/>, or <see cref="BeginTransaction(object)"/>,
/// 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.
/// </remarks>
/// <returns>Opened connection.</returns>
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;
}
/// <summary>Close connection if we are allowed to.</summary>
/// <param name="connection">Connection to manage.</param>
internal void Close(IDbConnection connection)
{
if (connection == null)
return;
if (!_singleConnection && connection != null && TransactionPool.ContainsKey(connection))
{
// Close all commands
if (CommandsPool.ContainsKey(connection))
{
List<IDbCommand> 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();
}
/// <summary>Gets or sets contains commands executed when connection is opened.</summary>
public List<string> InitCommands { get; set; }
@@ -2076,65 +2321,66 @@ namespace DynamORM
#region Transaction
/// <summary>Begins a global database transaction.</summary>
/// <remarks>Using this method connection is set to single open
/// connection until all transactions are finished.</remarks>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction()
{
_tempConn = Open() as DynamicConnection;
/// <summary>Begins a global database transaction.</summary>
/// <remarks>
/// 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 <see cref="DynamicDatabase"/> 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.
/// </remarks>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction()
{
return BeginAmbientTransaction(null, null);
}
return _tempConn.BeginTransaction(null, null, () =>
{
Stack<IDbTransaction> t = TransactionPool.TryGetValue(_tempConn.Connection);
/// <summary>Begins a database transaction with the specified
/// <see cref="System.Data.IsolationLevel"/> value.</summary>
/// <remarks>Thread ownership and connection binding follow the same rules as <see cref="BeginTransaction()"/>.</remarks>
/// <param name="il">One of the <see cref="System.Data.IsolationLevel"/> values.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction(IsolationLevel il)
{
return BeginAmbientTransaction(il, null);
}
if (t == null | t.Count == 0)
{
_tempConn.Dispose();
_tempConn = null;
}
});
}
/// <summary>Begins a database transaction with the specified
/// <see cref="System.Data.IsolationLevel"/> value.</summary>
/// <param name="il">One of the <see cref="System.Data.IsolationLevel"/> values.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction(IsolationLevel il)
{
_tempConn = Open() as DynamicConnection;
return _tempConn.BeginTransaction(il, null, () =>
{
Stack<IDbTransaction> t = TransactionPool.TryGetValue(_tempConn.Connection);
if (t == null | t.Count == 0)
{
_tempConn.Dispose();
_tempConn = null;
}
});
}
/// <summary>Begins a database transaction with the specified
/// <see cref="System.Data.IsolationLevel"/> value.</summary>
/// <param name="custom">Custom parameter describing transaction options.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
public IDbTransaction BeginTransaction(object custom)
{
_tempConn = Open() as DynamicConnection;
return _tempConn.BeginTransaction(null, custom, () =>
{
Stack<IDbTransaction> t = TransactionPool.TryGetValue(_tempConn.Connection);
if (t == null | t.Count == 0)
{
_tempConn.Dispose();
_tempConn = null;
}
});
}
/// <summary>Begins a database transaction with the specified
/// custom provider argument.</summary>
/// <remarks>Thread ownership and connection binding follow the same rules as <see cref="BeginTransaction()"/>.</remarks>
/// <param name="custom">Custom parameter describing transaction options.</param>
/// <returns>Returns <see cref="DynamicTransaction"/> representation.</returns>
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<IDbTransaction> 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<IDbConnection, Stack<IDbTransaction>> con in TransactionPool)
{
foreach (KeyValuePair<IDbConnection, Stack<IDbTransaction>> 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;
}
/// <summary>Gets a value indicating whether this instance is disposed.</summary>
public bool IsDisposed { get; private set; }

View File

@@ -38,11 +38,17 @@ namespace DynamORM
/// <summary>No specific options.</summary>
None = 0x00000000,
/// <summary>Only single persistent database connection.</summary>
SingleConnection = 0x00000001,
/// <summary>Only one transaction.</summary>
SingleTransaction = 0x00000002,
/// <summary>Only single persistent database connection.</summary>
/// <remarks>Command execution is serialized inside one <see cref="DynamicDatabase"/> instance when this option is enabled.</remarks>
SingleConnection = 0x00000001,
/// <summary>Only one transaction.</summary>
/// <remarks>Command execution is serialized inside one <see cref="DynamicDatabase"/> instance when this option is enabled.</remarks>
SingleTransaction = 0x00000002,
/// <summary>Use internal connection pooling when connections are not kept as a single shared connection.</summary>
/// <remarks>Pooling reuses idle managed connections and is configured through <see cref="DynamicDatabase.ConnectionPoolingKeepOpenCount"/>, <see cref="DynamicDatabase.ConnectionPoolingMaximumOpenCount"/>, and <see cref="DynamicDatabase.ConnectionPoolingConnectionLifetime"/>.</remarks>
ConnectionPooling = 0x00000004,
/// <summary>Database supports top syntax (SELECT TOP x ... FROM ...).</summary>
SupportTop = 0x00000080,
@@ -68,4 +74,4 @@ namespace DynamORM
/// <summary>Debug option allowing to enable command dumps by default.</summary>
DumpCommands = 0x01000000,
}
}
}

View File

@@ -102,10 +102,16 @@ namespace DynamORM
}
/// <summary>Commits the database transaction.</summary>
public void Commit()
{
lock (_db.SyncLock)
{
public void Commit()
{
if (_db == null)
{
_isOperational = false;
return;
}
lock (_db.SyncLock)
{
if (_isOperational)
{
Stack<IDbTransaction> t = _db.TransactionPool.TryGetValue(_con.Connection);
@@ -131,10 +137,16 @@ namespace DynamORM
}
/// <summary>Rolls back a transaction from a pending state.</summary>
public void Rollback()
{
lock (_db.SyncLock)
{
public void Rollback()
{
if (_db == null)
{
_isOperational = false;
return;
}
lock (_db.SyncLock)
{
if (_isOperational)
{
Stack<IDbTransaction> t = _db.TransactionPool.TryGetValue(_con.Connection);

View File

@@ -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(); }
}
}

View File

@@ -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

View File

@@ -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.