diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/IsolationLevelContractTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/IsolationLevelContractTest.cs
new file mode 100644
index 0000000000..a306f91016
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/IsolationLevelContractTest.cs
@@ -0,0 +1,2675 @@
+// 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.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Transactions;
+using Microsoft.Data.SqlClient.ManualTesting.Tests.SystemDataInternals;
+using Microsoft.Data.SqlClient.Tests.Common;
+using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
+using Microsoft.SqlServer.TDS;
+using Microsoft.SqlServer.TDS.Done;
+using Microsoft.SqlServer.TDS.EndPoint;
+using Microsoft.SqlServer.TDS.Error;
+using Microsoft.SqlServer.TDS.Servers;
+using Microsoft.SqlServer.TDS.SQLBatch;
+using Xunit;
+using DataIsolationLevel = System.Data.IsolationLevel;
+using TransactionsIsolationLevel = System.Transactions.IsolationLevel;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests
+{
+ ///
+ /// Serializes the isolation contract because its tests mutate cached pool-selection switches.
+ ///
+ [CollectionDefinition(Name, DisableParallelization = true)]
+ public sealed class IsolationLevelContractTestCollection
+ {
+ public const string Name = "IsolationLevelContractTests";
+ }
+
+ ///
+ /// Verifies the isolation-level contract across logical connections, transactions, and pools.
+ ///
+ [Collection(IsolationLevelContractTestCollection.Name)]
+ [Trait("Set", "3")]
+ public sealed class IsolationLevelContractTest
+ {
+ private const string SessionOptionsSql =
+ "DBCC USEROPTIONS WITH NO_INFOMSGS;";
+
+ private static readonly DataIsolationLevel[] s_nonSnapshotLevels =
+ {
+ DataIsolationLevel.ReadUncommitted,
+ DataIsolationLevel.ReadCommitted,
+ DataIsolationLevel.RepeatableRead,
+ DataIsolationLevel.Serializable
+ };
+
+ private static readonly DataIsolationLevel[] s_nonDefaultNonSnapshotLevels =
+ {
+ DataIsolationLevel.ReadUncommitted,
+ DataIsolationLevel.RepeatableRead,
+ DataIsolationLevel.Serializable
+ };
+
+ private static readonly CompletionMode[] s_completionModes =
+ {
+ CompletionMode.Commit,
+ CompletionMode.Rollback,
+ CompletionMode.Dispose
+ };
+
+ private static readonly Lazy s_snapshotIsolationEnabled =
+ new(ProbeSnapshotIsolation);
+
+ ///
+ /// Supplies sync/async and MARS combinations for tests that do not use pooling.
+ ///
+ public static TheoryData ConnectionModes =>
+ new()
+ {
+ { false, false },
+ { false, true },
+ { true, false },
+ { true, true }
+ };
+
+ ///
+ /// Supplies sync/async, MARS, and pool implementation combinations.
+ ///
+ public static TheoryData PooledModes =>
+ new()
+ {
+ { false, false, false },
+ { false, false, true },
+ { false, true, false },
+ { false, true, true },
+ { true, false, false },
+ { true, false, true },
+ { true, true, false },
+ { true, true, true }
+ };
+
+ ///
+ /// Supplies both pool implementations for endpoint-specific tests.
+ ///
+ public static TheoryData PoolVersions =>
+ new()
+ {
+ false,
+ true
+ };
+
+ ///
+ /// Reports whether the configured database can run Snapshot transaction cases.
+ ///
+ public static bool IsSnapshotIsolationEnabled() =>
+ DataTestUtility.AreConnStringsSetup() &&
+ DataTestUtility.IsNotAzureSynapse() &&
+ s_snapshotIsolationEnabled.Value;
+
+ ///
+ /// Reports whether the configured endpoint is an Azure Synapse dedicated SQL pool.
+ ///
+ public static bool IsAzureSynapseConfigured() =>
+ DataTestUtility.AreConnStringsSetup() &&
+ DataTestUtility.IsAzureSynapse;
+
+ ///
+ /// Reports whether the proposed legacy isolation switch exists and can be tested.
+ ///
+ public static bool IsLegacyIsolationSwitchTestAvailable() =>
+ DataTestUtility.AreConnStringsSetup() &&
+ DataTestUtility.IsNotAzureServer() &&
+ DataTestUtility.IsNotAzureSynapse() &&
+ GetLegacyIsolationSwitchField() != null;
+
+ ///
+ /// Reports whether driver-managed isolation reset tracking exists.
+ ///
+ public static bool IsIsolationResetTrackingAvailable() =>
+ GetIsolationDirtyField() != null;
+
+ ///
+ /// Reports whether the configured Synapse endpoint can exercise isolation reset tracking.
+ ///
+ public static bool IsSynapseIsolationResetTestAvailable() =>
+ IsAzureSynapseConfigured() &&
+ IsIsolationResetTrackingAvailable();
+
+ ///
+ /// Verifies that a local transaction applies each requested standard isolation level and
+ /// that commit, rollback, or disposal leaves that level active while the logical connection
+ /// remains open.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task LocalTransactionLevelPersistsAfterCompletionWhileConnectionStaysOpen(
+ bool async,
+ bool mars)
+ {
+ foreach (DataIsolationLevel level in s_nonSnapshotLevels)
+ {
+ foreach (CompletionMode completion in s_completionModes)
+ {
+ string cs = NonPooled(
+ $"LocalSameOpen-{level}-{completion}",
+ mars);
+
+ using SqlConnection connection = new(cs);
+ await Open(connection, async);
+
+ SqlTransaction transaction = connection.BeginTransaction(level);
+ try
+ {
+ Assert.Equal(
+ level,
+ await SessionLevel(connection, async, transaction));
+ Complete(transaction, completion);
+ }
+ finally
+ {
+ transaction.Dispose();
+ }
+
+ Assert.Equal(level, await SessionLevel(connection, async));
+ }
+ }
+ }
+
+ ///
+ /// Verifies that a Snapshot local transaction applies Snapshot and leaves it active after
+ /// commit, rollback, or disposal while the logical connection remains open.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task SnapshotPersistsAfterLocalTransactionCompletionWhileConnectionStaysOpen(
+ bool async,
+ bool mars)
+ {
+ foreach (CompletionMode completion in s_completionModes)
+ {
+ string cs = NonPooled(
+ $"SnapshotSameOpen-{completion}",
+ mars);
+ using SqlConnection connection = new(cs);
+ await Open(connection, async);
+
+ SqlTransaction transaction =
+ connection.BeginTransaction(DataIsolationLevel.Snapshot);
+ try
+ {
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(connection, async, transaction));
+ Complete(transaction, completion);
+ }
+ finally
+ {
+ transaction.Dispose();
+ }
+
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(connection, async));
+ }
+ }
+
+ ///
+ /// Verifies that parameterless and Unspecified local transactions select SqlClient's
+ /// ReadCommitted default instead of inheriting a prior session-level RepeatableRead setting.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task UnspecifiedLocalTransactionUsesReadCommittedDefault(
+ bool async,
+ bool mars)
+ {
+ foreach (bool parameterless in new[] { false, true })
+ {
+ using SqlConnection connection =
+ new(NonPooled(
+ $"LocalUnspecified-{parameterless}",
+ mars));
+ await Open(connection, async);
+ await SetLevel(
+ connection,
+ DataIsolationLevel.RepeatableRead,
+ async);
+
+ using SqlTransaction transaction = parameterless
+ ? connection.BeginTransaction()
+ : connection.BeginTransaction(DataIsolationLevel.Unspecified);
+
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(connection, async, transaction));
+ transaction.Rollback();
+ }
+ }
+
+ ///
+ /// Verifies that SET TRANSACTION ISOLATION LEVEL can legally change an active local
+ /// transaction from ReadCommitted to Serializable for subsequent statements.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task SetIsolationLevelInsideLocalTransactionChangesSubsequentLevel(
+ bool async,
+ bool mars)
+ {
+ using SqlConnection connection =
+ new(NonPooled("LocalMidTransactionSet", mars));
+ await Open(connection, async);
+
+ using SqlTransaction transaction =
+ connection.BeginTransaction(DataIsolationLevel.ReadCommitted);
+
+ await SetLevel(
+ connection,
+ DataIsolationLevel.Serializable,
+ async,
+ transaction);
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(connection, async, transaction));
+ transaction.Rollback();
+ }
+
+ ///
+ /// Verifies the documented SQL Server rule that a transaction started at Snapshot can
+ /// change to ReadCommitted and then return to Snapshot.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task SnapshotStartedTransactionCanLeaveAndReturnToSnapshot(
+ bool async,
+ bool mars)
+ {
+ using SqlConnection connection =
+ new(NonPooled("SnapshotLeaveAndReturn", mars));
+ await Open(connection, async);
+ using Table table = new(connection, "IsolationTransition", "(Value int NOT NULL)");
+ await ExecuteNonQuery(
+ connection,
+ $"INSERT {table.Name} VALUES (1);",
+ async);
+
+ using SqlTransaction transaction =
+ connection.BeginTransaction(DataIsolationLevel.Snapshot);
+
+ await ExecuteScalar(
+ connection,
+ $"SELECT Value FROM {table.Name};",
+ async,
+ transaction);
+ await SetLevel(
+ connection,
+ DataIsolationLevel.ReadCommitted,
+ async,
+ transaction);
+ await SetLevel(
+ connection,
+ DataIsolationLevel.Snapshot,
+ async,
+ transaction);
+
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(connection, async, transaction));
+ transaction.Rollback();
+ }
+
+ ///
+ /// Verifies that changing a transaction that started at ReadCommitted to Snapshot causes
+ /// SQL error 3951 on the next data access and aborts the transaction.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task NonSnapshotStartedTransactionAbortsWhenChangedToSnapshot(
+ bool async,
+ bool mars)
+ {
+ using SqlConnection connection =
+ new(NonPooled("InvalidSnapshotTransition", mars));
+ await Open(connection, async);
+ using Table table = new(connection, "IsolationTransition", "(Value int NOT NULL)");
+ await ExecuteNonQuery(
+ connection,
+ $"INSERT {table.Name} VALUES (1);",
+ async);
+
+ using SqlTransaction transaction =
+ connection.BeginTransaction(DataIsolationLevel.ReadCommitted);
+
+ await ExecuteScalar(
+ connection,
+ $"SELECT Value FROM {table.Name};",
+ async,
+ transaction);
+ await SetLevel(
+ connection,
+ DataIsolationLevel.Snapshot,
+ async,
+ transaction);
+
+ SqlException error;
+ if (async)
+ {
+ error = await Assert.ThrowsAsync(
+ () => ExecuteScalar(
+ connection,
+ $"SELECT Value FROM {table.Name};",
+ async: true,
+ transaction));
+ }
+ else
+ {
+ error = Assert.Throws(
+ () => ExecuteScalar(
+ connection,
+ $"SELECT Value FROM {table.Name};",
+ async: false,
+ transaction).GetAwaiter().GetResult());
+ }
+
+ Assert.Equal(3951, error.Number);
+ Assert.Equal(
+ 0,
+ Convert.ToInt32(
+ await ExecuteScalar(
+ connection,
+ "SELECT XACT_STATE();",
+ async,
+ transaction)));
+ }
+
+ ///
+ /// Verifies that completing an ambient Serializable transaction does not reset the
+ /// connection-wide isolation level while the same logical connection remains open.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task AmbientCompletionDoesNotResetStillOpenConnection(
+ bool async,
+ bool mars)
+ {
+ string cs = Pooled("AmbientSameOpen", mars);
+ using SqlConnection connection = new(cs);
+
+ using (TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Serializable))
+ {
+ await Open(connection, async);
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(connection, async));
+ scope.Complete();
+ }
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(connection, async));
+ }
+
+ ///
+ /// Verifies that non-default isolation selected by a completed local transaction is reset
+ /// to ReadCommitted before the same physical connection reaches an unrelated pooled consumer.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task CompletedLocalTransactionDoesNotLeakIsolationAcrossPool(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (DataIsolationLevel level in s_nonDefaultNonSnapshotLevels)
+ {
+ foreach (CompletionMode completion in s_completionModes)
+ {
+ string cs = Pooled(
+ $"LocalPoolBoundary-{level}-{completion}",
+ mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ SqlTransaction transaction = first.BeginTransaction(level);
+ try
+ {
+ Assert.Equal(
+ level,
+ await SessionLevel(first, async, transaction));
+ Complete(transaction, completion);
+ }
+ finally
+ {
+ transaction.Dispose();
+ }
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+ }
+ }
+
+ ///
+ /// Verifies that Snapshot selected by a completed local transaction is reset to
+ /// ReadCommitted before the same physical connection reaches an unrelated pooled consumer.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(PooledModes))]
+ public async Task CompletedSnapshotTransactionDoesNotLeakIsolationAcrossPool(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (CompletionMode completion in s_completionModes)
+ {
+ string cs = Pooled(
+ $"SnapshotPoolBoundary-{completion}",
+ mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ SqlTransaction transaction =
+ first.BeginTransaction(DataIsolationLevel.Snapshot);
+ try
+ {
+ Complete(transaction, completion);
+ }
+ finally
+ {
+ transaction.Dispose();
+ }
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+ }
+
+ ///
+ /// Verifies that a completed or rolled-back ambient transaction cannot leak its
+ /// non-default isolation level into an unrelated consumer of the same physical connection.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task CompletedAmbientTransactionDoesNotLeakIsolationAcrossPool(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (DataIsolationLevel level in s_nonDefaultNonSnapshotLevels)
+ {
+ foreach (bool completeScope in new[] { false, true })
+ {
+ string cs = Pooled(
+ $"AmbientPoolBoundary-{level}-{completeScope}",
+ mars);
+ Guid originalConnectionId;
+
+ using (TransactionScope scope = CreateScope(
+ ToTransactionsLevel(level)))
+ {
+ using SqlConnection first = new(cs);
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ Assert.Equal(level, await SessionLevel(first, async));
+ if (completeScope)
+ {
+ scope.Complete();
+ }
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+ }
+ }
+
+ ///
+ /// Verifies that a completed or rolled-back Snapshot scope cannot leak Snapshot into an
+ /// unrelated consumer of the same physical connection.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(PooledModes))]
+ public async Task CompletedSnapshotScopeDoesNotLeakIsolationAcrossPool(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (bool completeScope in new[] { false, true })
+ {
+ string cs = Pooled(
+ $"SnapshotScopeBoundary-{completeScope}",
+ mars);
+ Guid originalConnectionId;
+
+ using (TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Snapshot))
+ {
+ using SqlConnection first = new(cs);
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ if (completeScope)
+ {
+ scope.Complete();
+ }
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+ }
+
+ ///
+ /// Verifies that isolation selected through top-level command text follows the server's
+ /// pooled-reset behavior: ReadCommitted on reset-clearing servers or Serializable on
+ /// reset-preserving servers.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task TopLevelSetAcrossPoolUsesServerResetBehavior(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("TopLevelSetBoundary", mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ await SetLevel(
+ first,
+ DataIsolationLevel.Serializable,
+ async);
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ AssertServerDependentDirectSqlLevel(
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that command text which changes a ReadCommitted local transaction to
+ /// Serializable remains outside driver tracking and follows the server's pooled-reset result.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task SetInsideReadCommittedLocalTransactionUsesServerResetBehavior(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("LocalDirectSetBoundary", mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(DataIsolationLevel.ReadCommitted);
+ await SetLevel(
+ first,
+ DataIsolationLevel.Serializable,
+ async,
+ transaction);
+ transaction.Commit();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ AssertServerDependentDirectSqlLevel(
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that closing a connection with an active non-default local transaction rolls
+ /// back the transaction and resets the reused physical connection to ReadCommitted.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ClosingActiveLocalTransactionRollsBackAndResetsIsolation(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (DataIsolationLevel level in s_nonDefaultNonSnapshotLevels)
+ {
+ string cs = Pooled($"ActiveLocalClose-{level}", mars);
+ Guid originalConnectionId;
+ using SqlConnection setup = new(
+ NonPooled($"ActiveLocalCloseSetup-{level}", mars));
+ await Open(setup, async);
+ using Table table = new(
+ setup,
+ "ActiveLocalRollback",
+ "(Value int NOT NULL)");
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(level);
+ await ExecuteNonQuery(
+ first,
+ $"INSERT {table.Name} VALUES (1);",
+ async,
+ transaction);
+ first.Close();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ Assert.Equal(
+ 0,
+ Convert.ToInt32(
+ await ExecuteScalar(
+ next,
+ $"SELECT COUNT(*) FROM {table.Name};",
+ async)));
+ }
+ }
+
+ ///
+ /// Verifies that closing a connection with an active Snapshot transaction rolls back the
+ /// transaction and resets the reused physical connection to ReadCommitted.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ClosingActiveSnapshotTransactionRollsBackAndResetsIsolation(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("ActiveSnapshotClose", mars);
+ Guid originalConnectionId;
+ using SqlConnection setup = new(
+ NonPooled("ActiveSnapshotCloseSetup", mars));
+ await Open(setup, async);
+ using Table table = new(
+ setup,
+ "ActiveSnapshotRollback",
+ "(Value int NOT NULL)");
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(DataIsolationLevel.Snapshot);
+ await ExecuteNonQuery(
+ first,
+ $"INSERT {table.Name} VALUES (1);",
+ async,
+ transaction);
+ first.Close();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ Assert.Equal(
+ 0,
+ Convert.ToInt32(
+ await ExecuteScalar(
+ next,
+ $"SELECT COUNT(*) FROM {table.Name};",
+ async)));
+ }
+
+ ///
+ /// Verifies that Close followed by Open on the same SqlConnection object creates a new
+ /// logical session and resets completed non-default transaction state to ReadCommitted.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ReopeningSameConnectionObjectResetsIsolation(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (DataIsolationLevel level in s_nonDefaultNonSnapshotLevels)
+ {
+ foreach (CompletionMode completion in s_completionModes)
+ {
+ string cs = Pooled(
+ $"SameObjectReopen-{level}-{completion}",
+ mars);
+ using SqlConnection connection = new(cs);
+ await Open(connection, async);
+ Guid originalConnectionId =
+ PhysicalConnectionId(connection);
+
+ SqlTransaction transaction =
+ connection.BeginTransaction(level);
+ try
+ {
+ Complete(transaction, completion);
+ }
+ finally
+ {
+ transaction.Dispose();
+ }
+
+ connection.Close();
+ await Open(connection, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(connection));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(connection, async));
+ }
+ }
+ }
+
+ ///
+ /// Verifies that Close followed by Open on the same SqlConnection object resets completed
+ /// Snapshot transaction state to ReadCommitted.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ReopeningSameConnectionObjectResetsSnapshotIsolation(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (CompletionMode completion in s_completionModes)
+ {
+ string cs = Pooled(
+ $"SameObjectSnapshot-{completion}",
+ mars);
+ using SqlConnection connection = new(cs);
+ await Open(connection, async);
+ Guid originalConnectionId =
+ PhysicalConnectionId(connection);
+
+ SqlTransaction transaction =
+ connection.BeginTransaction(DataIsolationLevel.Snapshot);
+ try
+ {
+ Complete(transaction, completion);
+ }
+ finally
+ {
+ transaction.Dispose();
+ }
+
+ connection.Close();
+ await Open(connection, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(connection));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(connection, async));
+ }
+ }
+
+ ///
+ /// Verifies that ambient completion preserves Serializable on a still-open connection, but
+ /// the later logical close resets the reused physical connection for an unrelated consumer.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task CloseAfterAmbientCompletionResetsNextPooledConsumer(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("AmbientCloseBoundary", mars);
+ using SqlConnection connection = new(cs);
+ Guid originalConnectionId;
+
+ using (TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Serializable))
+ {
+ await Open(connection, async);
+ originalConnectionId = PhysicalConnectionId(connection);
+ scope.Complete();
+ }
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(connection, async));
+ connection.Close();
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that a TransactionScope created without explicit options uses the documented
+ /// Serializable default when SqlClient enlists.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task DefaultTransactionScopeUsesSerializable(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("DefaultAmbientLevel", mars);
+
+ using TransactionScope scope = new(
+ TransactionScopeOption.RequiresNew,
+ TransactionScopeAsyncFlowOption.Enabled);
+ using SqlConnection connection = new(cs);
+ await Open(connection, async);
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(connection, async));
+ scope.Complete();
+ }
+
+ ///
+ /// Verifies that closing and reopening within one live ambient transaction reuses the same
+ /// physical connection and observes the transaction's declared standard isolation level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task LiveAmbientRecheckoutRestoresDeclaredIsolationLevel(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (DataIsolationLevel level in s_nonSnapshotLevels)
+ {
+ string cs = Pooled(
+ $"LiveAmbientRecheckout-{level}",
+ mars);
+ using TransactionScope scope = CreateScope(
+ ToTransactionsLevel(level));
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ Assert.Equal(level, await SessionLevel(first, async));
+ }
+
+ using (SqlConnection second = new(cs))
+ {
+ await Open(second, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(second));
+ Assert.Equal(level, await SessionLevel(second, async));
+ }
+
+ scope.Complete();
+ }
+ }
+
+ ///
+ /// Verifies that closing and reopening within one live Snapshot transaction reuses the
+ /// same physical connection and continues to observe Snapshot.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(PooledModes))]
+ public async Task LiveSnapshotRecheckoutRestoresSnapshot(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("LiveSnapshotRecheckout", mars);
+ using TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Snapshot);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(first, async));
+ }
+
+ using (SqlConnection second = new(cs))
+ {
+ await Open(second, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(second));
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(second, async));
+ }
+
+ scope.Complete();
+ }
+
+ ///
+ /// Verifies that a Snapshot-started ambient transaction is reasserted as Snapshot when the
+ /// same physical connection is checked out after a session-local ReadCommitted override.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSnapshotIsolationEnabled))]
+ [MemberData(nameof(PooledModes))]
+ public async Task SnapshotRecheckoutRestoresSnapshotAfterSessionOverride(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("SnapshotOverrideRecheckout", mars);
+ using TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Snapshot);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(first, async));
+ await SetLevel(
+ first,
+ DataIsolationLevel.ReadCommitted,
+ async);
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(first, async));
+ }
+
+ using (SqlConnection second = new(cs))
+ {
+ await Open(second, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(second));
+ Assert.Equal(
+ DataIsolationLevel.Snapshot,
+ await SessionLevel(second, async));
+ }
+
+ scope.Complete();
+ }
+
+ ///
+ /// Verifies that a session-local command-text override lasts until logical close, then
+ /// recheckout restores the live ambient transaction's declared non-Snapshot level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task LiveAmbientRecheckoutRestoresDeclaredLevelAfterSessionOverride(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+ foreach (DataIsolationLevel ambientLevel in s_nonSnapshotLevels)
+ {
+ DataIsolationLevel overrideLevel =
+ ambientLevel == DataIsolationLevel.ReadCommitted
+ ? DataIsolationLevel.Serializable
+ : DataIsolationLevel.ReadCommitted;
+
+ string cs = Pooled(
+ $"AmbientOverrideRecheckout-{ambientLevel}",
+ mars);
+ using TransactionScope scope = CreateScope(
+ ToTransactionsLevel(ambientLevel));
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ await SetLevel(first, overrideLevel, async);
+ Assert.Equal(
+ overrideLevel,
+ await SessionLevel(first, async));
+ }
+
+ using (SqlConnection second = new(cs))
+ {
+ await Open(second, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(second));
+ Assert.Equal(
+ ambientLevel,
+ await SessionLevel(second, async));
+ }
+
+ scope.Complete();
+ }
+ }
+
+ ///
+ /// Verifies that a direct Serializable override after ReadCommitted ambient enlistment
+ /// follows the server's normal pooled-reset behavior once the ambient transaction completes.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task DirectOverrideAfterAmbientCompletionUsesServerResetBehavior(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("AmbientDirectSetCompletion", mars);
+ Guid originalConnectionId;
+
+ using (TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.ReadCommitted))
+ {
+ using SqlConnection first = new(cs);
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ await SetLevel(
+ first,
+ DataIsolationLevel.Serializable,
+ async);
+ scope.Complete();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ AssertServerDependentDirectSqlLevel(
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that Enlist=false prevents automatic ambient enlistment and leaves a clean
+ /// connection at ReadCommitted even while a Serializable transaction is ambient.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task EnlistFalseIgnoresAmbientTransaction(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled(
+ "EnlistFalse",
+ mars,
+ enlist: false);
+
+ using TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Serializable);
+ using SqlConnection connection = new(cs);
+ await Open(connection, async);
+
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(connection, async));
+ scope.Complete();
+ }
+
+ ///
+ /// Verifies that creating an ambient transaction after SqlConnection.Open does not
+ /// retroactively enlist the already-open connection or change its ReadCommitted level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task OpenConnectionIsNotRetroactivelyAutoEnlisted(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using SqlConnection connection =
+ new(Pooled("OpenBeforeScope", mars));
+ await Open(connection, async);
+
+ using TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Serializable);
+
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(connection, async));
+ }
+
+ ///
+ /// Verifies that explicitly enlisting an open Enlist=false connection applies the
+ /// CommittableTransaction's declared Serializable level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ManualEnlistmentAppliesTransactionIsolationLevel(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using CommittableTransaction transaction = new(
+ new TransactionOptions
+ {
+ IsolationLevel = TransactionsIsolationLevel.Serializable
+ });
+
+ using SqlConnection connection =
+ new(Pooled(
+ "ManualEnlistment",
+ mars,
+ enlist: false));
+ await Open(connection, async);
+ connection.EnlistTransaction(transaction);
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(connection, async));
+ transaction.Commit();
+ }
+
+ ///
+ /// Verifies that TransactionScopeOption.Suppress removes the outer Serializable ambient
+ /// transaction so a connection opened inside the suppressed region uses ReadCommitted.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task SuppressedScopeUsesNonTransactedReadCommittedConnection(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("SuppressedScope", mars);
+
+ using TransactionScope outer = CreateScope(
+ TransactionsIsolationLevel.Serializable);
+ using (TransactionScope suppressed = new(
+ TransactionScopeOption.Suppress,
+ TransactionScopeAsyncFlowOption.Enabled))
+ using (SqlConnection connection = new(cs))
+ {
+ await Open(connection, async);
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(connection, async));
+ suppressed.Complete();
+ }
+
+ outer.Complete();
+ }
+
+ ///
+ /// Verifies that two simultaneously open connections both observe the Serializable
+ /// ambient level when distributed transaction promotion is supported.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse),
+ nameof(DataTestUtility.IsSupportingDistributedTransactions))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ConcurrentEnlistedConnectionsObserveAmbientIsolationLevel(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+#if NET8_0_OR_GREATER
+ TransactionManager.ImplicitDistributedTransactions = true;
+#endif
+ string cs = Pooled(
+ "ConcurrentAmbientConnections",
+ mars,
+ maxPoolSize: 2);
+
+ using TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Serializable);
+ using SqlConnection first = new(cs);
+ using SqlConnection second = new(cs);
+ await Open(first, async);
+ await Open(second, async);
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(first, async));
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(second, async));
+ scope.Complete();
+ }
+
+ ///
+ /// Verifies that Explicit Unbind keeps a connection attached to its completed ambient
+ /// transaction until close, rejects intervening commands, and then resets isolation.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ExplicitUnbindRemainsBoundUntilCloseThenResetsIsolation(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled(
+ "ExplicitUnbind",
+ mars,
+ transactionBinding: "Explicit Unbind");
+ using SqlConnection connection = new(cs);
+ Guid originalConnectionId;
+
+ using (TransactionScope scope = CreateScope(
+ TransactionsIsolationLevel.Serializable))
+ {
+ await Open(connection, async);
+ originalConnectionId = PhysicalConnectionId(connection);
+ scope.Complete();
+ }
+
+ using (SqlCommand command = new("SELECT 1;", connection))
+ {
+ if (async)
+ {
+ await Assert.ThrowsAsync(
+ () => command.ExecuteScalarAsync());
+ }
+ else
+ {
+ Assert.Throws(
+ () => command.ExecuteScalar());
+ }
+ }
+
+ connection.Close();
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that nested RequiresNew uses a different physical connection and transaction
+ /// pool subdivision, then restores the outer transaction's original connection and level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task RequiresNewUsesIndependentTransactionPoolSubdivision(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled(
+ "NestedRequiresNew",
+ mars,
+ maxPoolSize: 2);
+ Guid outerConnectionId;
+
+ using (TransactionScope outer = CreateScope(
+ TransactionsIsolationLevel.ReadCommitted))
+ {
+ using (SqlConnection outerFirst = new(cs))
+ {
+ await Open(outerFirst, async);
+ outerConnectionId =
+ PhysicalConnectionId(outerFirst);
+ }
+
+ using (TransactionScope inner = CreateScope(
+ TransactionsIsolationLevel.Serializable))
+ using (SqlConnection innerConnection = new(cs))
+ {
+ await Open(innerConnection, async);
+ Assert.NotEqual(
+ outerConnectionId,
+ PhysicalConnectionId(innerConnection));
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(innerConnection, async));
+ inner.Complete();
+ }
+
+ using (SqlConnection outerSecond = new(cs))
+ {
+ await Open(outerSecond, async);
+ Assert.Equal(
+ outerConnectionId,
+ PhysicalConnectionId(outerSecond));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(outerSecond, async));
+ }
+
+ outer.Complete();
+ }
+ }
+
+ ///
+ /// Verifies that Pooling=false creates a different physical connection whose isolation
+ /// starts at ReadCommitted rather than inheriting a prior Serializable command-text setting.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task PoolingFalsePreventsSessionIsolationTransfer(
+ bool async,
+ bool mars)
+ {
+ string cs = NonPooled("PoolingDisabled", mars);
+ Guid firstConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ firstConnectionId = PhysicalConnectionId(first);
+ await SetLevel(
+ first,
+ DataIsolationLevel.Serializable,
+ async);
+ }
+
+ using SqlConnection second = new(cs);
+ await Open(second, async);
+ Assert.NotEqual(
+ firstConnectionId,
+ PhysicalConnectionId(second));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(second, async));
+ }
+
+ ///
+ /// Verifies that System.Transactions rejects a nested Required scope whose Serializable
+ /// level conflicts with the existing ReadCommitted ambient transaction.
+ ///
+ [Fact]
+ public void NestedRequiredScopeCannotChangeAmbientIsolationLevel()
+ {
+ using TransactionScope outer = CreateScope(
+ TransactionsIsolationLevel.ReadCommitted);
+
+ Assert.Throws(
+ () => new TransactionScope(
+ TransactionScopeOption.Required,
+ new TransactionOptions
+ {
+ IsolationLevel =
+ TransactionsIsolationLevel.Serializable
+ }));
+ }
+
+ ///
+ /// Verifies that SqlConnection.BeginTransaction rejects the unsupported Chaos isolation
+ /// level before sending a transaction request to SQL Server.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task LocalTransactionRejectsChaosIsolationLevel(
+ bool async,
+ bool mars)
+ {
+ using SqlConnection connection =
+ new(NonPooled("LocalChaos", mars));
+ await Open(connection, async);
+
+ Assert.Throws(
+ () => connection.BeginTransaction(DataIsolationLevel.Chaos));
+ }
+
+ ///
+ /// Verifies that a stored procedure can observe its temporary Serializable setting while
+ /// SQL Server restores the caller's RepeatableRead level when the procedure returns.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task StoredProcedureIsolationChangeIsProcedureScoped(
+ bool async,
+ bool mars)
+ {
+ using SqlConnection connection =
+ new(NonPooled("ProcedureScopedIsolation", mars));
+ await Open(connection, async);
+ await SetLevel(
+ connection,
+ DataIsolationLevel.RepeatableRead,
+ async);
+
+ using StoredProcedure procedure = new(
+ connection,
+ "IsolationScope",
+ $@"
+AS
+BEGIN
+ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
+ {SessionOptionsSql}
+END");
+
+ using SqlCommand command = new(procedure.Name, connection)
+ {
+ CommandType = CommandType.StoredProcedure
+ };
+
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await ReadIsolationLevel(command, async));
+ Assert.Equal(
+ DataIsolationLevel.RepeatableRead,
+ await SessionLevel(connection, async));
+ }
+
+#if NETFRAMEWORK
+ ///
+ /// Verifies on .NET Framework that the obsolete Connection Reset=false setting is ignored
+ /// and a reused physical connection still resets completed Serializable state.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ConnectionResetFalseDoesNotDisablePooledReset(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+
+#pragma warning disable CS0618
+ string cs = new SqlConnectionStringBuilder(
+ Pooled("ConnectionResetFalse", mars))
+ {
+ ConnectionReset = false
+ }.ConnectionString;
+#pragma warning restore CS0618
+
+ Guid originalConnectionId;
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(DataIsolationLevel.Serializable);
+ transaction.Rollback();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+#endif
+
+ ///
+ /// Verifies on a reset-preserving SQL Server that the legacy isolation switch disables
+ /// driver compensation and lets completed Serializable state survive pooled reuse.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsLegacyIsolationSwitchTestAvailable))]
+ [MemberData(nameof(PooledModes))]
+ public async Task LegacyIsolationSwitchRestoresResetPreservingServerBehavior(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(
+ usePoolV2,
+ useLegacyIsolationBehavior: true);
+ string cs = Pooled("LegacyIsolationSwitch", mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId = PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(DataIsolationLevel.Serializable);
+ transaction.Rollback();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.Serializable,
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that System.Transactions normalizes an explicitly requested Unspecified level
+ /// to its Serializable default before SqlClient observes the transaction.
+ ///
+ [Fact]
+ public void SystemTransactionsUnspecifiedUsesSerializableDefault()
+ {
+ using CommittableTransaction transaction = new(
+ new TransactionOptions
+ {
+ IsolationLevel = TransactionsIsolationLevel.Unspecified
+ });
+
+ Assert.Equal(
+ TransactionsIsolationLevel.Serializable,
+ transaction.IsolationLevel);
+ }
+
+ ///
+ /// Verifies that SqlClient rejects manual enlistment in a Chaos transaction because TDS has
+ /// no transaction-manager mapping for that isolation level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ManualEnlistmentRejectsChaosIsolationLevel(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using CommittableTransaction transaction = new(
+ new TransactionOptions
+ {
+ IsolationLevel = TransactionsIsolationLevel.Chaos
+ });
+
+ using SqlConnection connection =
+ new(Pooled(
+ "AmbientChaos",
+ mars,
+ enlist: false));
+ await Open(connection, async);
+
+ Assert.Throws(
+ () => connection.EnlistTransaction(transaction));
+ }
+
+ ///
+ /// Verifies that SqlClient rejects a second local transaction instead of changing the
+ /// isolation level of the active ReadCommitted transaction.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(ConnectionModes))]
+ public async Task SecondLocalTransactionCannotReplaceActiveTransaction(
+ bool async,
+ bool mars)
+ {
+ using SqlConnection connection =
+ new(NonPooled("SecondLocalTransaction", mars));
+ await Open(connection, async);
+ using SqlTransaction transaction =
+ connection.BeginTransaction(DataIsolationLevel.ReadCommitted);
+
+ Assert.Throws(
+ () => connection.BeginTransaction(
+ DataIsolationLevel.Serializable));
+ transaction.Rollback();
+ }
+
+ ///
+ /// Verifies that ClearPool discards the marked physical connection so the next Open gets a
+ /// different connection identifier and the default ReadCommitted isolation level.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task ClearPoolForcesNewReadCommittedPhysicalConnection(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("ClearPool", mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId =
+ PhysicalConnectionId(first);
+ await SetLevel(
+ first,
+ DataIsolationLevel.Serializable,
+ async);
+ SqlConnection.ClearPool(first);
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.NotEqual(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that a successful ReadCommitted compensation batch resets isolation without
+ /// discarding the pooled physical connection.
+ ///
+ [ConditionalTheory(
+ typeof(DataTestUtility),
+ nameof(DataTestUtility.AreConnStringsSetup),
+ nameof(DataTestUtility.IsNotAzureSynapse))]
+ [MemberData(nameof(PooledModes))]
+ public async Task SuccessfulIsolationResetRetainsPhysicalConnection(
+ bool async,
+ bool mars,
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ string cs = Pooled("SuccessfulIsolationReset", mars);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await Open(first, async);
+ originalConnectionId =
+ PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(DataIsolationLevel.Serializable);
+ transaction.Rollback();
+ }
+
+ using SqlConnection next = new(cs);
+ await Open(next, async);
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ DataIsolationLevel.ReadCommitted,
+ await SessionLevel(next, async));
+ }
+
+ ///
+ /// Verifies that a healthy SQL rejection of the ReadCommitted compensation batch is
+ /// swallowed and the same pooled physical connection remains usable.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsIsolationResetTrackingAvailable))]
+ [MemberData(nameof(PoolVersions))]
+ public void HealthyIsolationResetRejectionKeepsConnectionUsable(
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using IsolationFaultTdsServer server = new(
+ IsolationFaultMode.Reject,
+ "SET TRANSACTION ISOLATION LEVEL READ COMMITTED;");
+ server.Start();
+ string cs = FaultServerConnectionString(
+ server,
+ "HealthyResetRejection");
+
+ using (SqlConnection first = new(cs))
+ {
+ first.Open();
+ MarkIsolationDirty(first);
+ }
+
+ using SqlConnection next = new(cs);
+ next.Open();
+
+ Assert.Equal(1, server.Login7Count);
+ Assert.Equal(1, server.FaultCount);
+ using SqlCommand ping = new("SELECT 1;", next);
+ Assert.Equal(1, Convert.ToInt32(ping.ExecuteScalar()));
+ }
+
+ ///
+ /// Verifies that a timeout while awaiting the isolation-reset response fails Open with the
+ /// original timeout, dooms the uncertain physical connection, and creates a replacement.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsIsolationResetTrackingAvailable))]
+ [MemberData(nameof(PoolVersions))]
+ public void IsolationResetTimeoutPreservesOriginalErrorAndDoomsConnection(
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using IsolationFaultTdsServer server = new(
+ IsolationFaultMode.Stall,
+ "SET TRANSACTION ISOLATION LEVEL READ COMMITTED;");
+ server.Start();
+ string cs = FaultServerConnectionString(
+ server,
+ "ResetTimeout");
+
+ using (SqlConnection first = new(cs))
+ {
+ first.Open();
+ MarkIsolationDirty(first);
+ }
+
+ using (SqlConnection failedCheckout = new(cs))
+ {
+ SqlException error = Assert.Throws(
+ () => failedCheckout.Open());
+
+ Assert.Equal(-2, error.Number);
+ Assert.Equal(
+ ConnectionState.Closed,
+ failedCheckout.State);
+ }
+
+ Assert.Equal(1, server.FaultCount);
+
+ using SqlConnection replacement = new(cs);
+ replacement.Open();
+ Assert.Equal(2, server.Login7Count);
+ }
+
+ ///
+ /// Verifies that rejecting a Serializable reassertion fails Open and dooms the physical
+ /// connection rather than handing a live ambient transaction an unverified isolation level.
+ ///
+ [Theory]
+ [MemberData(nameof(PoolVersions))]
+ public void AmbientIsolationReassertionFailureDoomsConnection(
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using IsolationFaultTdsServer server = new(
+ IsolationFaultMode.Reject,
+ "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;");
+ server.Start();
+ string cs = FaultServerConnectionString(
+ server,
+ "AmbientReassertionFailure");
+ using (CommittableTransaction transaction = new(
+ new TransactionOptions
+ {
+ IsolationLevel =
+ TransactionsIsolationLevel.Serializable
+ }))
+ {
+ using (SqlConnection first = new(cs))
+ {
+ first.Open();
+ AttachTransactionForPoolTest(first, transaction);
+ }
+
+ using (TransactionScope scope = new(
+ transaction,
+ TransactionScopeAsyncFlowOption.Enabled))
+ using (SqlConnection failedCheckout = new(cs))
+ {
+ SqlException error = Assert.Throws(
+ () => failedCheckout.Open());
+
+ Assert.Equal(
+ IsolationFaultTdsServer.InjectedErrorNumber,
+ error.Number);
+ Assert.Equal(
+ ConnectionState.Closed,
+ failedCheckout.State);
+ Assert.Equal(1, server.Login7Count);
+ Assert.Equal(1, server.FaultCount);
+ scope.Complete();
+ }
+
+ transaction.Rollback();
+ }
+
+ using SqlConnection replacement = new(cs);
+ replacement.Open();
+ Assert.Equal(2, server.Login7Count);
+ }
+
+ ///
+ /// Verifies that Azure Synapse dedicated pools skip the unsupported ReadCommitted
+ /// compensation after a ReadUncommitted transaction and keep the pooled connection usable.
+ ///
+ [ConditionalTheory(
+ typeof(IsolationLevelContractTest),
+ nameof(IsSynapseIsolationResetTestAvailable))]
+ [MemberData(nameof(PoolVersions))]
+ public async Task SynapseDedicatedPoolSkipsUnsupportedReadCommittedCompensation(
+ bool usePoolV2)
+ {
+ using PoolVersionScope _ = new(usePoolV2);
+ using DataTestUtility.MDSEventListener eventListener = new();
+ string cs = Pooled(
+ "SynapseDedicatedPool",
+ mars: false);
+ Guid originalConnectionId;
+
+ using (SqlConnection first = new(cs))
+ {
+ await first.OpenAsync();
+ Assert.Equal(
+ 6,
+ Convert.ToInt32(
+ await ExecuteScalar(
+ first,
+ "SELECT CONVERT(int, SERVERPROPERTY('EngineEdition'));",
+ async: true)));
+ originalConnectionId = PhysicalConnectionId(first);
+ using SqlTransaction transaction =
+ first.BeginTransaction(
+ DataIsolationLevel.ReadUncommitted);
+ transaction.Rollback();
+ }
+
+ using SqlConnection next = new(cs);
+ await next.OpenAsync();
+ Assert.Equal(
+ originalConnectionId,
+ PhysicalConnectionId(next));
+ Assert.Equal(
+ 1,
+ Convert.ToInt32(
+ await ExecuteScalar(
+ next,
+ "SELECT 1;",
+ async: true)));
+
+ Assert.DoesNotContain(
+ eventListener.EventData,
+ eventData =>
+ eventData.Payload?.Any(
+ value =>
+ value?.ToString()?.IndexOf(
+ "ResetSessionIsolationLevel",
+ StringComparison.Ordinal) >= 0) == true);
+ }
+
+ ///
+ /// Creates a unique pooled connection string for one contract scenario.
+ ///
+ /// Short scenario identifier.
+ /// Whether Multiple Active Result Sets is enabled.
+ /// Whether connections auto-enlist.
+ /// Maximum number of physical connections.
+ /// Optional transaction-binding mode.
+ /// A connection string isolated from every other test pool.
+ private static string Pooled(
+ string caseName,
+ bool mars,
+ bool enlist = true,
+ int maxPoolSize = 1,
+ string transactionBinding = null)
+ {
+ SqlConnectionStringBuilder builder = new(
+ DataTestUtility.TCPConnectionString)
+ {
+ Pooling = true,
+ MaxPoolSize = maxPoolSize,
+ MultipleActiveResultSets = mars,
+ Enlist = enlist,
+ ApplicationName =
+ $"IsolationContract-{caseName}-{Guid.NewGuid():N}"
+ };
+
+ if (transactionBinding != null)
+ {
+ builder.TransactionBinding = transactionBinding;
+ }
+
+ return builder.ConnectionString;
+ }
+
+ ///
+ /// Creates a unique non-pooled connection string for one contract scenario.
+ ///
+ /// Short scenario identifier.
+ /// Whether Multiple Active Result Sets is enabled.
+ /// A non-pooled connection string.
+ private static string NonPooled(string caseName, bool mars) =>
+ new SqlConnectionStringBuilder(
+ DataTestUtility.TCPConnectionString)
+ {
+ Pooling = false,
+ MultipleActiveResultSets = mars,
+ ApplicationName =
+ $"IsolationContract-{caseName}-{Guid.NewGuid():N}"
+ }.ConnectionString;
+
+ ///
+ /// Opens a connection through the selected sync or async API.
+ ///
+ /// Connection to open.
+ /// Whether to use the async API.
+ private static async Task Open(
+ SqlConnection connection,
+ bool async)
+ {
+ if (async)
+ {
+ await connection.OpenAsync();
+ }
+ else
+ {
+ connection.Open();
+ }
+ }
+
+ ///
+ /// Executes a scalar command through the selected sync or async API.
+ ///
+ /// Command to execute.
+ /// Whether to use the async API.
+ /// The first value returned by the command.
+ private static async Task