Add managed connection pooling and execution locking
This commit is contained in:
@@ -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; }
|
||||
|
||||
153
DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs
Normal file
153
DynamORM.Tests/Helpers/ConnectionPoolingAndLockingTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
236
DynamORM.Tests/Helpers/FakeProviderFactory.cs
Normal file
236
DynamORM.Tests/Helpers/FakeProviderFactory.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -92,9 +92,10 @@ namespace DynamORM.Tests.Helpers
|
||||
finally
|
||||
{
|
||||
// Remove for next tests
|
||||
Database.Dispose();
|
||||
if (Database != null)
|
||||
Database.Dispose();
|
||||
Database = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
101
DynamORM/Helpers/DynamicExecutionReader.cs
Normal file
101
DynamORM/Helpers/DynamicExecutionReader.cs
Normal 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(); }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user