/* * DynamORM - Dynamic Object-Relational Mapping library. * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) * All rights reserved. */ using System; using System.Data; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; namespace DynamORM.Tests.Helpers { [TestFixture] public class ConnectionPoolingAndLockingTests { private FakeProviderState _state; private DynamicDatabase _db; [TearDown] public void TearDown() { if (_db != null) _db.Dispose(); } [Test] public void TestConnectionPoolingReusesIdleConnection() { CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); using (_db.Open()) { } using (_db.Open()) { } Assert.AreEqual(1, _state.CreatedConnections); Assert.AreEqual(1, _state.OpenCalls); Assert.AreEqual(0, _state.CloseCalls); } [Test] public void TestConnectionPoolingWaitsForReleasedConnectionWhenAtMaximum() { CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); _db.ConnectionPoolingKeepOpenCount = 1; _db.ConnectionPoolingMaximumOpenCount = 1; var first = _db.Open(); var started = new ManualResetEventSlim(false); var completed = new ManualResetEventSlim(false); var task = Task.Run(() => { started.Set(); using (_db.Open()) { } completed.Set(); }); Assert.IsTrue(started.Wait(TimeSpan.FromSeconds(2))); Assert.IsFalse(completed.Wait(TimeSpan.FromMilliseconds(200))); first.Dispose(); Assert.IsTrue(completed.Wait(TimeSpan.FromSeconds(2))); task.Wait(TimeSpan.FromSeconds(2)); Assert.AreEqual(1, _state.CreatedConnections); } [Test] public void TestConnectionPoolingClosesExpiredIdleConnections() { CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); _db.ConnectionPoolingConnectionLifetime = TimeSpan.Zero; using (_db.Open()) { } using (_db.Open()) { } Assert.AreEqual(2, _state.CreatedConnections); Assert.GreaterOrEqual(_state.CloseCalls, 1); Assert.GreaterOrEqual(_state.DisposeCalls, 1); } [Test] public void TestConnectionPoolingTrimsIdleConnectionsAbovePreferredLimit() { CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); _db.ConnectionPoolingKeepOpenCount = 1; _db.ConnectionPoolingMaximumOpenCount = 3; var c1 = _db.Open(); var c2 = _db.Open(); var c3 = _db.Open(); c1.Dispose(); c2.Dispose(); c3.Dispose(); Assert.AreEqual(3, _state.CreatedConnections); Assert.AreEqual(2, _state.CloseCalls); Assert.AreEqual(2, _state.DisposeCalls); } [Test] public void TestDirectTransactionUsesSameThreadConnectionAndSeparateThreadGetsDifferentOne() { CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); using (var trans = _db.BeginTransaction()) { IDbConnection threadLocalA = _db.Open(); IDbConnection threadLocalB = _db.Open(); try { Assert.AreSame(((DynamicConnection)threadLocalA).Connection, ((DynamicConnection)threadLocalB).Connection); IDbConnection otherThreadConnection = null; var task = Task.Run(() => { using (var other = _db.Open()) otherThreadConnection = ((DynamicConnection)other).Connection; }); Assert.IsTrue(task.Wait(TimeSpan.FromSeconds(2))); Assert.AreNotSame(((DynamicConnection)threadLocalA).Connection, otherThreadConnection); } finally { threadLocalA.Dispose(); threadLocalB.Dispose(); } } } [Test] public void TestDirectTransactionUsesTransactionOnlyOnOwningThread() { CreateDatabase(DynamicDatabaseOptions.ConnectionPooling); using (var transaction = _db.BeginTransaction()) { ExecuteFakeCommand(); var otherThread = Task.Run(() => ExecuteFakeCommand()); Assert.IsTrue(otherThread.Wait(TimeSpan.FromSeconds(2))); transaction.Commit(); } lock (_state.SeenTransactions) { Assert.AreEqual(2, _state.SeenTransactions.Count); Assert.NotNull(_state.SeenTransactions[0]); Assert.IsNull(_state.SeenTransactions[1]); } } [TestCase(DynamicDatabaseOptions.SingleConnection)] [TestCase(DynamicDatabaseOptions.SingleTransaction)] public void TestSingleModesSerializeCommandExecution(DynamicDatabaseOptions option) { CreateDatabase(option); _state.BlockFirstExecution = true; _state.AllowExecution.Reset(); Task task1 = Task.Run(() => ExecuteFakeCommand()); Assert.IsTrue(_state.FirstExecutionEntered.Wait(TimeSpan.FromSeconds(2))); Task task2 = Task.Run(() => ExecuteFakeCommand()); Thread.Sleep(200); Assert.AreEqual(1, _state.MaxConcurrentExecutions); _state.AllowExecution.Set(); Assert.IsTrue(Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(5))); Assert.AreEqual(1, _state.MaxConcurrentExecutions); } [TestCase(DynamicDatabaseOptions.SingleConnection)] [TestCase(DynamicDatabaseOptions.SingleTransaction)] public void TestSingleModesSerializeReaderLifetime(DynamicDatabaseOptions option) { CreateDatabase(option); _state.BlockFirstReader = true; _state.AllowReader.Reset(); IDataReader reader = null; var task1 = Task.Run(() => { using (var connection = _db.Open()) using (var command = connection.CreateCommand()) { command.SetCommand("SELECT 1;"); reader = command.ExecuteReader(); } }); Assert.IsTrue(_state.FirstReaderEntered.Wait(TimeSpan.FromSeconds(2))); var secondCompleted = new ManualResetEventSlim(false); var task2 = Task.Run(() => { ExecuteFakeCommand(); secondCompleted.Set(); }); Assert.IsFalse(secondCompleted.Wait(TimeSpan.FromMilliseconds(200))); _state.AllowReader.Set(); SpinWait.SpinUntil(() => reader != null, TimeSpan.FromSeconds(2)); reader.Dispose(); Assert.IsTrue(Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(5))); Assert.AreEqual(1, _state.MaxConcurrentExecutions); } private void ExecuteFakeCommand() { using (var connection = _db.Open()) using (var command = connection.CreateCommand()) { command.SetCommand("SELECT 1;"); command.ExecuteNonQuery(); } } private void CreateDatabase(DynamicDatabaseOptions options) { _state = new FakeProviderState(); _db = new DynamicDatabase(new FakeProviderFactory(_state), "fake", options); } } }