diff --git a/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs
new file mode 100644
index 0000000000..a5ca8e639a
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient.Tests.Common
+{
+ ///
+ /// Utility class that enables disposal of a collection of objects
+ /// with a single using statement.
+ ///
+ /// Type of the elements contained within.
+ public class DisposableArray : IDisposable, IEnumerable
+ where T : IDisposable?
+ {
+ private readonly T[] _elements;
+
+ ///
+ /// Constructs a new instance with elements.
+ ///
+ ///
+ /// Remember when using this constructor that the underlying array will be initialized to
+ /// default(T). If is a reference type, this will be
+ /// null - even if is not nullable!
+ ///
+ /// Number of elements the new instance will contain.
+ public DisposableArray(int size)
+ {
+ _elements = new T[size];
+ }
+
+ ///
+ /// Constructs a new instance from an existing array of elements.
+ ///
+ /// Array of elements to store within the current instance.
+ public DisposableArray(T[] elements)
+ {
+ _elements = elements;
+ }
+
+ ///
+ /// Gets or sets the element at index .
+ ///
+ /// The element to get/set will be at this position in the array
+ public T this[int i]
+ {
+ get => _elements[i];
+ set => _elements[i] = value;
+ }
+
+ ///
+ /// Gets the number of elements in the array.
+ ///
+ public int Length => _elements.Length;
+
+ ///
+ /// Disposes all elements in the current instance. Each element will be checked for
+ /// null before disposing it.
+ ///
+ public void Dispose()
+ {
+ foreach (T element in _elements)
+ {
+ element?.Dispose();
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public IEnumerator GetEnumerator() =>
+ ((IEnumerable)_elements).GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() =>
+ _elements.GetEnumerator();
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
index 733b483e75..bfa2080260 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
@@ -181,7 +181,7 @@
-
+
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs
deleted file mode 100644
index c32bbeb668..0000000000
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs
+++ /dev/null
@@ -1,276 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Data;
-using System.Runtime.CompilerServices;
-using Xunit;
-
-namespace Microsoft.Data.SqlClient.ManualTesting.Tests
-{
- public static class MARSSessionPoolingTest
- {
- private const string COMMAND_STATUS = "select count(*) as ConnectionCount, @@spid as spid from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " +
- "select count(*) as ActiveRequestCount, @@spid as spid from sys.dm_exec_requests where session_id=@@spid and status='running' or session_id=@@spid and status='suspended'";
- private const string COMMAND_SPID = "select @@spid";
- private const int CONCURRENT_COMMANDS = 5;
-
- private const string _COMMAND_RPC = "sp_who";
- private const string _COMMAND_SQL =
- "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " +
- "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " +
- "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " +
- "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " +
- "select * from sys.databases; print 'THIS IS THE END!'";
-
- private static readonly string _testConnString =
- (new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
- {
- PacketSize = 512,
- MaxPoolSize = 1,
- MultipleActiveResultSets = true
- }).ConnectionString;
-
- // Synapse: Catalog view 'dm_exec_connections' is not supported in this version.
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
- public static void MarsExecuteScalar_AllFlavors()
- {
- TestMARSSessionPooling("Case: Text, ExecuteScalar", _testConnString, CommandType.Text, ExecuteType.ExecuteScalar, ReaderTestType.ReaderClose, GCType.Wait);
- TestMARSSessionPooling("Case: RPC, ExecuteScalar", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteScalar, ReaderTestType.ReaderClose, GCType.Wait);
- }
-
- // Synapse: Catalog view 'dm_exec_connections' is not supported in this version.
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
- public static void MarsExecuteNonQuery_AllFlavors()
- {
- TestMARSSessionPooling("Case: Text, ExecuteNonQuery", _testConnString, CommandType.Text, ExecuteType.ExecuteNonQuery, ReaderTestType.ReaderClose, GCType.Wait);
- TestMARSSessionPooling("Case: RPC, ExecuteNonQuery", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteNonQuery, ReaderTestType.ReaderClose, GCType.Wait);
- }
-
- // Synapse: Catalog view 'dm_exec_connections' is not supported in this version.
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
- public static void MarsExecuteReader_Text_NoGC()
- {
- TestMARSSessionPooling("Case: Text, ExecuteReader, ReaderClose", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderClose, GCType.Wait);
- TestMARSSessionPooling("Case: Text, ExecuteReader, ReaderDispose", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderDispose, GCType.Wait);
- TestMARSSessionPooling("Case: Text, ExecuteReader, ConnectionClose", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ConnectionClose, GCType.Wait);
- }
-
- // Synapse: Stored procedure sp_who does not exist or is not supported.
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
- public static void MarsExecuteReader_RPC_NoGC()
- {
- TestMARSSessionPooling("Case: RPC, ExecuteReader, ReaderClose", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderClose, GCType.Wait);
- TestMARSSessionPooling("Case: RPC, ExecuteReader, ReaderDispose", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderDispose, GCType.Wait);
- TestMARSSessionPooling("Case: RPC, ExecuteReader, ConnectionClose", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ConnectionClose, GCType.Wait);
- }
-
- // Synapse: Catalog view 'dm_exec_connections' is not supported in this version.
- [ActiveIssue("11167")]
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
- public static void MarsExecuteReader_Text_WithGC()
- {
- TestMARSSessionPooling("Case: Text, ExecuteReader, GC-Wait", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.Wait);
- TestMARSSessionPooling("Case: Text, ExecuteReader, GC-NoWait", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.NoWait);
- }
-
- // Synapse: Stored procedure sp_who does not exist or is not supported.
- [ActiveIssue("8959")]
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
- public static void MarsExecuteReader_StoredProcedure_WithGC()
- {
- TestMARSSessionPooling("Case: RPC, ExecuteReader, GC-Wait", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.Wait);
- TestMARSSessionPooling("Case: RPC, ExecuteReader, GC-NoWait", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.NoWait);
-
- TestMARSSessionPooling("Case: Text, ExecuteReader, NoCloses", _testConnString + " ", CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.NoCloses, GCType.Wait);
- TestMARSSessionPooling("Case: RPC, ExecuteReader, NoCloses", _testConnString + " ", CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.NoCloses, GCType.Wait);
- }
-
- private enum ExecuteType
- {
- ExecuteScalar,
- ExecuteNonQuery,
- ExecuteReader,
- }
-
- private enum ReaderTestType
- {
- ReaderClose,
- ReaderDispose,
- ReaderGC,
- ConnectionClose,
- NoCloses,
- }
-
- private enum GCType
- {
- Wait,
- NoWait,
- }
-
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static void TestMARSSessionPooling(string caseName, string connectionString, CommandType commandType,
- ExecuteType executeType, ReaderTestType readerTestType, GCType gcType)
- {
- SqlCommand[] cmd = new SqlCommand[CONCURRENT_COMMANDS];
- SqlDataReader[] gch = new SqlDataReader[CONCURRENT_COMMANDS];
-
- using (SqlConnection con = new SqlConnection(connectionString))
- {
- con.Open();
-
- for (int i = 0; i < CONCURRENT_COMMANDS; i++)
- {
- // Prepare all commands
- cmd[i] = con.CreateCommand();
- switch (commandType)
- {
- case CommandType.Text:
- cmd[i].CommandText = _COMMAND_SQL;
- cmd[i].CommandTimeout = 120;
- break;
- case CommandType.StoredProcedure:
- cmd[i].CommandText = _COMMAND_RPC;
- cmd[i].CommandTimeout = 120;
- cmd[i].CommandType = CommandType.StoredProcedure;
- break;
- }
- }
-
- for (int i = 0; i < CONCURRENT_COMMANDS; i++)
- {
- switch (executeType)
- {
- case ExecuteType.ExecuteScalar:
- cmd[i].ExecuteScalar();
- break;
- case ExecuteType.ExecuteNonQuery:
- cmd[i].ExecuteNonQuery();
- break;
- case ExecuteType.ExecuteReader:
- if (readerTestType != ReaderTestType.ReaderGC)
- {
- gch[i] = cmd[i].ExecuteReader();
- }
-
- switch (readerTestType)
- {
- case ReaderTestType.ReaderClose:
- {
- gch[i].Dispose();
- break;
- }
- case ReaderTestType.ReaderDispose:
- gch[i].Dispose();
- break;
- case ReaderTestType.ReaderGC:
- gch[i] = null;
- WeakReference weak = OpenReaderThenNullify(cmd[i]);
- GC.Collect();
-
- if (gcType == GCType.Wait)
- {
- GC.WaitForPendingFinalizers();
- Assert.False(weak.IsAlive, "Error - target still alive!");
- }
- break;
- case ReaderTestType.ConnectionClose:
- GC.SuppressFinalize(gch[i]);
- con.Close();
- con.Open();
- break;
- case ReaderTestType.NoCloses:
- GC.SuppressFinalize(gch[i]);
- break;
- }
- break;
- }
-
- if (readerTestType != ReaderTestType.NoCloses)
- {
- con.Close();
- con.Open(); // Close and open, to re-assure collection!
- }
-
- using (SqlCommand verificationCmd = con.CreateCommand())
- {
-
- verificationCmd.CommandText = COMMAND_STATUS;
- using (SqlDataReader rdr = verificationCmd.ExecuteReader())
- {
- rdr.Read();
- int connections = (int)rdr.GetValue(0);
- int spid1 = (Int16)rdr.GetValue(1);
- rdr.NextResult();
- rdr.Read();
- int requests = (int)rdr.GetValue(0);
- int spid2 = (Int16)rdr.GetValue(1);
-
- switch (executeType)
- {
- case ExecuteType.ExecuteScalar:
- case ExecuteType.ExecuteNonQuery:
- // 1 for connection, 1 for command
- Assert.True(connections == 2, "Failure - incorrect number of connections for ExecuteScalar! #connections: " + connections);
-
- // only 1 executing
- Assert.True(requests == 1, "Failure - incorrect number of requests for ExecuteScalar! #requests: " + requests);
- break;
- case ExecuteType.ExecuteReader:
- switch (readerTestType)
- {
- case ReaderTestType.ReaderClose:
- case ReaderTestType.ReaderDispose:
- case ReaderTestType.ConnectionClose:
- // 1 for connection, 1 for command
- Assert.True(connections == 2, "Failure - Incorrect number of connections for ReaderClose / ReaderDispose / ConnectionClose! #connections: " + connections);
-
- // only 1 executing
- Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderClose/ReaderDispose/ConnectionClose! #requests: " + requests);
- break;
- case ReaderTestType.ReaderGC:
- switch (gcType)
- {
- case GCType.Wait:
- // 1 for connection, 1 for open reader
- Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCWait! #connections: " + connections);
- // only 1 executing
- Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCWait! #requests: " + requests);
- break;
- case GCType.NoWait:
- // 1 for connection, 1 for open reader
- Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCNoWait! #connections: " + connections);
-
- // only 1 executing
- Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCNoWait! #requests: " + requests);
- break;
- }
- break;
- case ReaderTestType.NoCloses:
- // 1 for connection, 1 for current command, 1 for 0 based array offset, plus i for open readers
- Assert.True(connections == (3 + i), "Failure - incorrect number of connections for NoCloses: " + connections +
- "\ni: " + i + " :::: requests: " + requests + " :::: spid1: " + spid1 + " ::::: spid2: " + spid2);
-
- // 1 for current command, 1 for 0 based array offset, plus i open readers
- Assert.True(requests == (2 + i), "Failure - incorrect number of requests for NoCloses: " + requests +
- "\ni: " + i + " :::: connections: " + connections + " :::: spid1: " + spid1 + " ::::: spid2: " + spid2);
- break;
- }
- break;
- }
- }
- }
- }
- }
- }
-
- private static WeakReference OpenReaderThenNullify(SqlCommand command)
- {
- SqlDataReader reader = command.ExecuteReader();
- WeakReference weak = new WeakReference(reader);
- reader = null;
- return weak;
- }
- }
-}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs
new file mode 100644
index 0000000000..06af375019
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs
@@ -0,0 +1,463 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Data;
+using System.Linq;
+using System.Threading;
+using Microsoft.Data.SqlClient.Tests.Common;
+using Xunit;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests
+{
+ public class MarsSessionPoolingTest
+ {
+ private const int ConcurrentCommands = 5;
+
+ // Synapse: Catalog view 'dm_exec_connections' is not supported in this version.
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteScalar_DisposeCommand(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ connection.Open();
+
+ // Act / Assert
+ foreach (SqlCommand command in commands)
+ {
+ // Act
+ // - Run command
+ command.ExecuteScalar();
+
+ // - Dispose command
+ command.Dispose();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Each request runs to completion, none are running concurrently, so additional
+ // MARS sessions do not need to be opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteScalar_CloseConnection(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ connection.Open();
+
+ // Act / Assert
+ foreach (SqlCommand command in commands)
+ {
+ // Act
+ // - Run command
+ command.ExecuteScalar();
+
+ // - Close and reopen connection (return to pool)
+ connection.Close();
+ connection.Open();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Each request runs to completion, none are running concurrently, so additional
+ // MARS sessions do not need to be opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteNonQuery_DisposeCommand(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ connection.Open();
+
+ // Act / Assert
+ foreach (SqlCommand command in commands)
+ {
+ // Act
+ // - Run command
+ command.ExecuteNonQuery();
+
+ // - Dispose command
+ command.Dispose();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Each request runs to completion, none are running concurrently, so additional
+ // MARS sessions do not need to be opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteNonQuery_CloseConnection(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ connection.Open();
+
+ // Act / Assert
+ foreach (SqlCommand command in commands)
+ {
+ // Act
+ // - Run command
+ command.ExecuteNonQuery();
+
+ // - Close and reopen connection (return to pool)
+ connection.Close();
+ connection.Open();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Each request runs to completion, none are running concurrently, so additional
+ // MARS sessions do not need to be opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteReader_CloseReader(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ using DisposableArray readers = new DisposableArray(commands.Length);
+ connection.Open();
+
+ // Act / Assert
+ for (int i = 0; i < commands.Length; i++)
+ {
+ // Act
+ // - Run command
+ readers[i] = commands[i].ExecuteReader();
+
+ // - Close reader
+ readers[i].Close();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Closing the reader completes the request, so no requests are running
+ // concurrently, so no additional MARS sessions should have been opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteReader_DisposeReader(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ using DisposableArray readers = new DisposableArray(commands.Length);
+ connection.Open();
+
+ // Act / Assert
+ for (int i = 0; i < commands.Length; i++)
+ {
+ // Act
+ // - Run command
+ readers[i] = commands[i].ExecuteReader();
+
+ // - Dispose reader
+ readers[i].Dispose();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Disposing the reader completes the request, so no requests are running
+ // concurrently, so no additional MARS sessions should have been opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteReader_GarbageCollectReader(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ connection.Open();
+
+ // Act / Assert
+ for (int i = 0; i < commands.Length; i++)
+ {
+ // Act
+ // - Run command and get weak reference to reader.
+ // This must happen in another scope otherwise the reader will not be marked for
+ // garbage collection.
+ WeakReference readerWeakReference = OpenReaderThenNullify(commands[i]);
+
+ // - Run the garbage collector
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ // Assert
+ // - Make sure reader has been collected by now, otherwise results are invalid
+ Assert.False(readerWeakReference.IsAlive);
+
+ // - Count of open sessions/requests will increase with each iteration
+ // Finalizing a data reader does *not* close it, meaning the MARS session is left
+ // in an incomplete state. As such, with each command that's executed, a new
+ // session is opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: i + 1, openRequests: i + 1);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteReader_DisposeCommand(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ using DisposableArray readers = new DisposableArray(commands.Length);
+ connection.Open();
+
+ // Act / Assert
+ for (int i = 0; i < commands.Length; i++)
+ {
+ // Act
+ // - Run command
+ readers[i] = commands[i].ExecuteReader();
+
+ // - Dispose the command
+ commands[i].Dispose();
+
+ // Assert
+ // - Count of open sessions/requests will increase with each iteration
+ // Disposing of the command does *not* close the reader, meaning the MARS session
+ // is left in an incomplete state. As such, with each command that's executed, a
+ // new session is opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: i + 1, openRequests: i + 1);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteReader_CloseConnection(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ using DisposableArray readers = new DisposableArray(commands.Length);
+ connection.Open();
+
+ // Act / Assert
+ for (int i = 0; i < commands.Length; i++)
+ {
+ // Act
+ // - Run command
+ readers[i] = commands[i].ExecuteReader();
+ // - Close and reopen connection (return to pool)
+ connection.Close();
+ connection.Open();
+
+ // Assert
+ // - Count of sessions/requests should stay the same
+ // Closing the connection completes any pending requests, so no requests are
+ // running concurrently, so no additional MARS sessions should have been opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0);
+ }
+ }
+
+ [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))]
+ [InlineData(CommandType.Text)]
+ [InlineData(CommandType.StoredProcedure)]
+ public void ExecuteReader_NoCloses(CommandType commandType)
+ {
+ // Arrange
+ using SqlConnection connection = GetConnection();
+ using DisposableArray commands = GetCommands(connection, commandType);
+ using DisposableArray readers = new DisposableArray(commands.Length);
+ connection.Open();
+
+ // Act / Assert
+ for (int i = 0; i < commands.Length; i++)
+ {
+ // Act
+ // - Run command, close nothing!
+ readers[i] = commands[i].ExecuteReader();
+
+ // Assert
+ // - Count of open sessions/requests will increase with each iteration
+ // Leaving a data reader open leaves the MARS session in an incomplete state. As
+ // such, with each command that's executed, a new session is opened.
+ AssertSessionsAndRequests(connection, openMarsSessions: i + 1, openRequests: i + 1);
+ }
+ }
+
+ ///
+ /// Asserts the number of open sessions and pending requests on the connection.
+ ///
+ /// Connection to check open sessions and pending requests on
+ ///
+ /// Number of MARS sessions expected to be open/in use by the test. The sessions for the
+ /// main connection and validation query will be added before assertion.
+ ///
+ ///
+ /// Number of open requests expected to be pending by use of the test. The request for the
+ /// validation query will be added before assertion.
+ ///
+ ///
+ /// Thrown if any of the validation result sets are missing or empty.
+ ///
+ private static void AssertSessionsAndRequests(
+ SqlConnection connection,
+ int openMarsSessions,
+ int openRequests)
+ {
+ const int maxAttempts = 5;
+
+ // For these tests, the expected session count will always be at least 2 in MARS mode:
+ // 1 for the main connection
+ // 1 for the verification command we just executed
+ int? observedSessions = null;
+ int expectedSessions = openMarsSessions + 2;
+
+ // For these tests, the expected request count will always be at least 1:
+ // 1 for the verification command we just executed
+ int? observedRequests = null;
+ int expectedRequests = openRequests + 1;
+
+ // There is a race between opening new sessions and them appearing in the DMV tables.
+ // As such, we want to poll the DMV a few times before declaring the wrong behavior was
+ // observed.
+ for (int attempt = 0; attempt < maxAttempts; attempt++)
+ {
+ (observedSessions, observedRequests) = QuerySessionCounters(connection);
+ if (observedSessions == expectedSessions && observedRequests == expectedRequests)
+ {
+ // We observed the expected values.
+ return;
+ }
+
+ // Back off and wait before trying again
+ Thread.SpinWait(20 << attempt);
+ }
+
+ // If we make it to here, we never saw the expected numbers, so fail the test with the
+ // last value we observed.
+ Assert.Equal(expectedSessions, observedSessions);
+ Assert.Equal(expectedRequests, observedRequests);
+ }
+
+ private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType)
+ {
+ DisposableArray result = new(ConcurrentCommands);
+ for (int i = 0; i < result.Length; i++)
+ {
+ switch (commandType)
+ {
+ case CommandType.Text:
+ string commandText = string.Join(" ", Enumerable.Repeat(@"SELECT * FROM sys.databases;", 20));
+ commandText += @" PRINT 'THIS IS THE END!'";
+
+ result[i] = new SqlCommand
+ {
+ CommandText = commandText,
+ CommandTimeout = 120,
+ CommandType = CommandType.Text,
+ Connection = connection
+ };
+ break;
+
+ case CommandType.StoredProcedure:
+ result[i] = new SqlCommand
+ {
+ CommandText = "sp_who",
+ CommandTimeout = 120,
+ CommandType = CommandType.StoredProcedure,
+ Connection = connection
+ };
+ break;
+
+ default:
+ throw new InvalidOperationException("Not supported test type");
+ }
+ }
+
+ return result;
+ }
+
+ private static SqlConnection GetConnection()
+ {
+ // Generate a unique name for the application to ensure pool isolation between tests
+ string applicationName = $"SqlClientMarsPoolingTests:{Guid.NewGuid()}";
+ string connectionString = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
+ {
+ ApplicationName = applicationName,
+ PacketSize = 512,
+ MaxPoolSize = 1,
+ MultipleActiveResultSets = true
+ }.ConnectionString;
+
+ return new SqlConnection(connectionString);
+ }
+
+ private static WeakReference OpenReaderThenNullify(SqlCommand command)
+ {
+ SqlDataReader? reader = command.ExecuteReader();
+ WeakReference weak = new WeakReference(reader);
+ reader = null;
+ return weak;
+ }
+
+ private static (int sessions, int requests) QuerySessionCounters(SqlConnection connection)
+ {
+ using SqlCommand verificationCommand = new SqlCommand();
+ verificationCommand.CommandText =
+ @"SELECT COUNT(*) AS SessionCount " +
+ @"FROM sys.dm_exec_connections " +
+ @"WHERE session_id=@@spid AND net_transport='Session'; " +
+ @"SELECT COUNT(*) AS RequestCount " +
+ @"FROM sys.dm_exec_requests " +
+ @"WHERE session_id=@@spid AND (status='running' OR status='suspended')";
+ verificationCommand.CommandType = CommandType.Text;
+ verificationCommand.Connection = connection;
+
+ // Result 1) Count of active sessions from sys.dm_exec_connections
+ using SqlDataReader reader = verificationCommand.ExecuteReader();
+ if (!reader.Read())
+ {
+ throw new Exception("Expected dm_exec_connections results from verification command");
+ }
+
+ int sessions = reader.GetInt32(0);
+
+ // Result 2) Count of active requests from sys.dm_exec_requests
+ if (!reader.NextResult() || !reader.Read())
+ {
+ throw new Exception("Expected dm_exec_requests results from verification command");
+ }
+
+ int requests = reader.GetInt32(0);
+
+ return (sessions, requests);
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs
index 0928a71569..9f20521f08 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs
@@ -152,15 +152,15 @@ public static void MultiThreadedCancel_AsyncNP()
// Synapse: WAITFOR not supported + ';' not supported.
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
- public static void TimeoutCancel()
+ public static void TimeoutCancelTcp()
{
TimeoutCancel(tcp_connStr);
}
- [ActiveIssue("12167")]
+ [ActiveIssue("https://github.com/dotnet/SqlClient/issues/3755")]
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
[PlatformSpecific(TestPlatforms.Windows)]
- public static void TimeoutCancelNP()
+ public static void TimeoutCancelNamedPipe()
{
TimeoutCancel(np_connStr);
}
@@ -178,17 +178,15 @@ public static void CancelAndDisposePreparedCommandNP()
CancelAndDisposePreparedCommand(np_connStr);
}
- [ActiveIssue("5541")]
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
- public static void TimeOutDuringRead()
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotNamedInstance))]
+ public static void TimeOutDuringReadTcp()
{
TimeOutDuringRead(tcp_connStr);
}
- [ActiveIssue("5541")]
- [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotNamedInstance))]
[PlatformSpecific(TestPlatforms.Windows)]
- public static void TimeOutDuringReadNP()
+ public static void TimeOutDuringReadNamedPipe()
{
TimeOutDuringRead(np_connStr);
}
@@ -348,26 +346,27 @@ private static void MultiThreadedCancel(string constr, bool async)
private static void TimeoutCancel(string constr)
{
- using (SqlConnection con = new SqlConnection(constr))
- {
- con.Open();
- using (SqlCommand cmd = con.CreateCommand())
- {
- cmd.CommandTimeout = 1;
- cmd.CommandText = "WAITFOR DELAY '00:00:20';select * from Customers";
+ // Arrange
+ using SqlConnection connection = new SqlConnection(constr);
+ connection.Open();
- string errorMessage = SystemDataResourceManager.Instance.SQL_Timeout_Execution;
- DataTestUtility.ExpectFailure(() => ExecuteReaderOnCmd(cmd), new string[] { errorMessage });
+ using SqlCommand command = new SqlCommand();
+ command.CommandTimeout = 1;
+ command.CommandText = @"WAITFOR DELAY '00:01:00';" +
+ @"SELECT * FROM customers";
+ command.CommandType = CommandType.Text;
+ command.Connection = connection;
- VerifyConnection(cmd);
- }
- }
- }
+ // Act
+ Action action = () => { using SqlDataReader reader = command.ExecuteReader(); };
- private static void ExecuteReaderOnCmd(SqlCommand cmd)
- {
- using (SqlDataReader reader = cmd.ExecuteReader())
- { }
+ // Assert
+ // - Action throws timeout exception with timeout message
+ Exception e = Assert.Throws(action);
+ Assert.Contains(SystemDataResourceManager.Instance.SQL_Timeout_Execution, e.Message);
+
+ // - Connection has not faulted
+ VerifyConnection(command);
}
//InvalidOperationException from connection.Dispose if that connection has prepared command cancelled during reading of data
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs
index ae656f11a4..dd3132a41c 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs
@@ -16,11 +16,11 @@ public static class SqlCredentialTest
public static void CreateSqlConnectionWithCredential()
{
var user = "u" + Guid.NewGuid().ToString().Replace("-", "");
- var passStr = "Pax561O$T5K#jD";
+ const string passStr = "Pax561O$T5K#jD";
try
{
- createTestUser(user, passStr);
+ CreateTestUser(user, passStr);
var csb = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString);
csb.Remove("User ID");
@@ -40,21 +40,20 @@ public static void CreateSqlConnectionWithCredential()
}
finally
{
- dropTestUser(user);
+ DropTestUser(user);
}
}
- [ActiveIssue("9196")]
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
public static void SqlConnectionChangePasswordPlaintext()
{
var user = "u" + Guid.NewGuid().ToString().Replace("-", "");
- var pass = "!21Ja3Ims7LI&n";
- var newPass = "fmVCNf@24Dg*8j";
+ const string pass = "!21Ja3Ims7LI&n";
+ const string newPass = "fmVCNf@24Dg*8j";
try
{
- createTestUser(user, pass);
+ CreateTestUser(user, pass);
var csb = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString);
csb.UserID = user;
@@ -74,21 +73,20 @@ public static void SqlConnectionChangePasswordPlaintext()
}
finally
{
- dropTestUser(user);
+ DropTestUser(user);
}
}
- [ActiveIssue("9196")]
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
public static void SqlConnectionChangePasswordSecureString()
{
var user = "u" + Guid.NewGuid().ToString().Replace("-", "");
- var passStr = "tcM0qB^izt%3u7";
- var newPassStr = "JSG2e(Vp0WCXE&";
+ const string passStr = "tcM0qB^izt%3u7";
+ const string newPassStr = "JSG2e(Vp0WCXE&";
try
{
- createTestUser(user, passStr);
+ CreateTestUser(user, passStr);
var csb = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString);
csb.Remove("User ID");
@@ -115,19 +113,19 @@ public static void SqlConnectionChangePasswordSecureString()
}
finally
{
- dropTestUser(user);
+ DropTestUser(user);
}
}
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))]
public static void OldCredentialsShouldFail()
{
- String user = "u" + Guid.NewGuid().ToString().Replace("-", "");
- String passStr = "Pax561O$T5K#jD";
+ string user = "u" + Guid.NewGuid().ToString().Replace("-", "");
+ const string passStr = "Pax561O$T5K#jD";
try
{
- createTestUser(user, passStr);
+ CreateTestUser(user, passStr);
SqlConnectionStringBuilder sqlConnectionStringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString);
sqlConnectionStringBuilder.Remove("User ID");
@@ -178,11 +176,11 @@ public static void OldCredentialsShouldFail()
}
finally
{
- dropTestUser(user);
+ DropTestUser(user);
}
}
- private static void createTestUser(string username, string password)
+ private static void CreateTestUser(string username, string password)
{
// Creates a test user with read permissions.
string createUserCmd = $"CREATE LOGIN {username} WITH PASSWORD = '{password}', CHECK_POLICY=OFF;"
@@ -196,7 +194,7 @@ private static void createTestUser(string username, string password)
}
}
- private static void dropTestUser(string username)
+ private static void DropTestUser(string username)
{
// Removes a created test user.
string dropUserCmd = $"IF EXISTS (SELECT * FROM sys.schemas WHERE name = '{username}') BEGIN DROP SCHEMA {username} END;"