From 66c2cd6c9b66ab6d5255a2c4623908f1b0e81492 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 13:54:19 -0600 Subject: [PATCH 01/34] Change all possible references from SqlInternalConnection to SqlInternalConnectionTds --- .../Data/SqlClient/SqlCommand.NonQuery.cs | 2 +- .../Data/SqlClient/SqlCommand.Reader.cs | 4 ++-- .../Data/SqlClient/SqlCommand.Xml.cs | 4 ++-- .../Microsoft/Data/SqlClient/SqlConnection.cs | 10 ++++------ .../Microsoft/Data/SqlClient/SqlDataReader.cs | 18 ++++++++--------- .../Data/SqlClient/SqlDelegatedTransaction.cs | 20 +++++++++---------- .../Data/SqlClient/SqlInternalTransaction.cs | 10 +++++----- .../Data/SqlClient/SqlTransaction.cs | 7 +++++-- 8 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs index 28bd1cf2ef..9137f11667 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs @@ -906,7 +906,7 @@ private void RunExecuteNonQueryTdsSetupReconnnectContinuation( private void SetCachedCommandExecuteNonQueryAsyncContext(ExecuteNonQueryAsyncCallContext instance) { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { // @TODO: Add this to SqlInternalConnection Interlocked.CompareExchange( diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs index 332195efb5..0495095e08 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -979,7 +979,7 @@ private Task InternalExecuteReaderAsync( { returnedTask = RegisterForConnectionCloseNotification(returnedTask); - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { context = Interlocked.Exchange( ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, @@ -1787,7 +1787,7 @@ private SqlDataReader RunExecuteReaderWithRetry( private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallContext instance) { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { // @TODO: This should be part of the sql internal connection class. Interlocked.CompareExchange( diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs index 6a18b6b1b6..0fdc4d9d14 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs @@ -486,7 +486,7 @@ private Task InternalExecuteXmlReaderAsync(CancellationToken cancella // @TODO: This can be cleaned up to lines if InnerConnection is always SqlInternalConnection ExecuteXmlReaderAsyncCallContext context = null; - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { context = Interlocked.Exchange( ref sqlInternalConnection.CachedCommandExecuteXmlReaderAsyncContext, @@ -549,7 +549,7 @@ private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken private void SetCachedCommandExecuteXmlReaderContext(ExecuteXmlReaderAsyncCallContext instance) { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { // @TODO: Move this compare exchange into the SqlInternalConnection class (or better yet, do away with this context) Interlocked.CompareExchange( diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs index 4f9d9ca14f..bac785b9e7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -816,10 +816,8 @@ public override string Database // just return what the connection string had. get { - SqlInternalConnection innerConnection = (InnerConnection as SqlInternalConnection); string result; - - if (innerConnection != null) + if (InnerConnection is SqlInternalConnectionTds innerConnection) { result = innerConnection.CurrentDatabase; } @@ -828,6 +826,7 @@ public override string Database SqlConnectionString constr = (SqlConnectionString)ConnectionOptions; result = constr != null ? constr.InitialCatalog : DbConnectionStringDefaults.InitialCatalog; } + return result; } } @@ -889,10 +888,8 @@ public override string DataSource { get { - SqlInternalConnection innerConnection = (InnerConnection as SqlInternalConnection); string result; - - if (innerConnection != null) + if (InnerConnection is SqlInternalConnectionTds innerConnection) { result = innerConnection.CurrentDataSource; } @@ -901,6 +898,7 @@ public override string DataSource SqlConnectionString constr = (SqlConnectionString)ConnectionOptions; result = constr != null ? constr.DataSource : DbConnectionStringDefaults.DataSource; } + return result; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 22da8a07a3..357c044de3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -4101,7 +4101,7 @@ internal TdsOperationStatus TryReadColumnInternal(int i, bool readHeaderOnly = f { // reset snapshot to save memory use. We can safely do that here because all SqlDataReader values are stable. // The retry logic can use the current values to get back to the right state. - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) { sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; } @@ -5038,7 +5038,7 @@ public override Task ReadAsync(CancellationToken cancellationToken) } ReadAsyncCallContext context = null; - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, null); } @@ -5085,7 +5085,7 @@ private static Task ReadAsyncExecute(Task task, object state) if (!hasReadRowToken) { hasReadRowToken = true; - if (reader.Connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (reader.Connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) { sqlInternalConnection.CachedDataReaderSnapshot = reader._snapshot; } @@ -5108,7 +5108,7 @@ private static Task ReadAsyncExecute(Task task, object state) private void SetCachedReadAsyncCallContext(ReadAsyncCallContext instance) { - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, instance, null); } @@ -5214,7 +5214,7 @@ override public Task IsDBNullAsync(int i, CancellationToken cancellationTo } IsDBNullAsyncCallContext context = null; - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, null); } @@ -5259,7 +5259,7 @@ private static Task IsDBNullAsyncExecute(Task task, object state) private void SetCachedIDBNullAsyncCallContext(IsDBNullAsyncCallContext instance) { - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, instance, null); } @@ -5800,7 +5800,7 @@ private void PrepareAsyncInvocation(bool useSnapshot) if (_snapshot == null) { - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { _snapshot = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderSnapshot, null) ?? new Snapshot(); } @@ -5880,7 +5880,7 @@ private void CleanupAfterAsyncInvocationInternal(TdsParserStateObject stateObj, stateObj._permitReplayStackTraceToDiffer = false; #endif - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) { sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; } @@ -5922,7 +5922,7 @@ private void SwitchToAsyncWithoutSnapshot() Debug.Assert(_snapshot != null, "Should currently have a snapshot"); Debug.Assert(_stateObj != null && !_stateObj._asyncReadWithoutSnapshot, "Already in async without snapshot"); - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) { sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs index eb7b65a497..d23bc4bd9f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs @@ -24,7 +24,7 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati // or notifications of same. Updates to the connection's association with the transaction or to the connection pool // may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy. - private SqlInternalConnection _connection; // the internal connection that is the root of the transaction + private SqlInternalConnectionTds _connection; // the internal connection that is the root of the transaction private System.Data.IsolationLevel _isolationLevel; // the IsolationLevel of the transaction we delegated to the server private SqlInternalTransaction _internalTransaction; // the SQL Server transaction we're delegating to @@ -35,7 +35,7 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati internal SqlDelegatedTransaction(SqlInternalConnection connection, Transaction tx) { Debug.Assert(connection != null, "null connection?"); - _connection = connection; + _connection = (SqlInternalConnectionTds)connection; _atomicTransaction = tx; _active = false; System.Transactions.IsolationLevel systxIsolationLevel = tx.IsolationLevel; @@ -78,7 +78,7 @@ public void Initialize() { // if we get here, then we know for certain that we're the delegated // transaction. - SqlInternalConnection connection = _connection; + SqlInternalConnectionTds connection = _connection; SqlConnection usersConnection = connection.Connection; SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | Object Id {0}, Client Connection Id {1}, delegating transaction.", ObjectID, usersConnection?.ClientConnectionId); @@ -119,7 +119,7 @@ public byte[] Promote() // Don't read values off of the connection outside the lock unless it doesn't really matter // from an operational standpoint (i.e. logging connection's ObjectID should be fine, // but the PromotedDTCToken can change over calls. so that must be protected). - SqlInternalConnection connection = GetValidConnection(); + SqlInternalConnectionTds connection = GetValidConnection(); Exception promoteException; byte[] returnValue = null; @@ -212,7 +212,7 @@ public byte[] Promote() public void Rollback(SinglePhaseEnlistment enlistment) { Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnection connection = GetValidConnection(); + SqlInternalConnectionTds connection = GetValidConnection(); if (connection != null) { @@ -280,7 +280,7 @@ public void Rollback(SinglePhaseEnlistment enlistment) public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) { Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnection connection = GetValidConnection(); + SqlInternalConnectionTds connection = GetValidConnection(); if (connection != null) { @@ -385,7 +385,7 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) // the transaction). internal void TransactionEnded(Transaction transaction) { - SqlInternalConnection connection = _connection; + SqlInternalConnectionTds connection = _connection; if (connection != null) { @@ -406,9 +406,9 @@ internal void TransactionEnded(Transaction transaction) } // Check for connection validity - private SqlInternalConnection GetValidConnection() + private SqlInternalConnectionTds GetValidConnection() { - SqlInternalConnection connection = _connection; + SqlInternalConnectionTds connection = _connection; if (connection == null && Transaction.TransactionInformation.Status != TransactionStatus.Aborted) { throw ADP.ObjectDisposed(this); @@ -420,7 +420,7 @@ private SqlInternalConnection GetValidConnection() // Dooms connection and throws and error if not a valid, active, delegated transaction for the given // connection. Designed to be called AFTER a lock is placed on the connection, otherwise a normal return // may not be trusted. - private void ValidateActiveOnConnection(SqlInternalConnection connection) + private void ValidateActiveOnConnection(SqlInternalConnectionTds connection) { bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs index 5cb7b990dc..e43170fca0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs @@ -36,7 +36,7 @@ sealed internal class SqlInternalTransaction private readonly TransactionType _transactionType; private long _transactionId; // passed in the MARS headers private int _openResultCount; // passed in the MARS headers - private SqlInternalConnection _innerConnection; + private SqlInternalConnectionTds _innerConnection; private bool _disposing; // used to prevent us from throwing exceptions while we're disposing private WeakReference _parent; // weak ref to the outer transaction object; needs to be weak to allow GC to occur. @@ -46,11 +46,11 @@ sealed internal class SqlInternalTransaction internal bool RestoreBrokenConnection { get; set; } internal bool ConnectionHasBeenRestored { get; set; } - internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction) : this(innerConnection, type, outerTransaction, NullTransactionId) + internal SqlInternalTransaction(SqlInternalConnectionTds innerConnection, TransactionType type, SqlTransaction outerTransaction) : this(innerConnection, type, outerTransaction, NullTransactionId) { } - internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction, long transactionId) + internal SqlInternalTransaction(SqlInternalConnectionTds innerConnection, TransactionType type, SqlTransaction outerTransaction, long transactionId) { SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.ctor | RES | CPOOL | Object Id {0}, Created for connection {1}, outer transaction {2}, Type {3}", ObjectID, innerConnection.ObjectID, outerTransaction?.ObjectId, (int)type); _innerConnection = innerConnection; @@ -187,7 +187,7 @@ private void CheckTransactionLevelAndZombie() internal void CloseFromConnection() { - SqlInternalConnection innerConnection = _innerConnection; + SqlInternalConnectionTds innerConnection = _innerConnection; Debug.Assert(innerConnection != null, "How can we be here if the connection is null?"); SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.CloseFromConnection | RES | CPOOL | Object Id {0}, Closing transaction", ObjectID); @@ -452,7 +452,7 @@ internal void Zombie() ZombieParent(); - SqlInternalConnection innerConnection = _innerConnection; + SqlInternalConnectionTds innerConnection = _innerConnection; _innerConnection = null; if (innerConnection != null) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs index 011a5ab364..35f52c4ff4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs @@ -41,7 +41,10 @@ internal SqlTransaction( if (internalTransaction == null) { - InternalTransaction = new SqlInternalTransaction(internalConnection, TransactionType.LocalFromAPI, this); + InternalTransaction = new SqlInternalTransaction( + (SqlInternalConnectionTds)internalConnection, + TransactionType.LocalFromAPI, + this); } else { @@ -289,7 +292,7 @@ internal void Zombie() // For Yukon, we have to defer "zombification" until we get past the users' next // rollback, else we'll throw an exception there that is a breaking change. Of course, // if the connection is already closed, then we're free to zombify... - if (_connection.InnerConnection is SqlInternalConnection internalConnection && !_isFromApi) + if (_connection.InnerConnection is SqlInternalConnectionTds && !_isFromApi) { SqlClientEventSource.Log.TryAdvancedTraceEvent( "SqlTransaction.Zombie | ADV | Object Id {0} yukon deferred zombie", From 2d664a68af3f949da2a93ed5149bf5c1a331dadc Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:10:34 -0600 Subject: [PATCH 02/34] Move BeginTransaction and BeginSqlTransaction into SqlInternalConnectionTds.cs --- .../Data/SqlClient/SqlInternalConnection.cs | 41 ----------------- .../SqlClient/SqlInternalConnectionTds.cs | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 46852a5df4..2640c93335 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -175,47 +175,6 @@ override internal bool IsTransactionRoot #endregion - override public DbTransaction BeginTransaction(System.Data.IsolationLevel iso) - { - return BeginSqlTransaction(iso, null, false); - } - - virtual internal SqlTransaction BeginSqlTransaction(System.Data.IsolationLevel iso, string transactionName, bool shouldReconnect) - { - SqlStatistics statistics = null; - try - { - statistics = SqlStatistics.StartTimer(Connection.Statistics); - - #if NETFRAMEWORK - SqlConnection.ExecutePermission.Demand(); // MDAC 81476 - #endif - - ValidateConnectionForExecute(null); - - if (HasLocalTransactionFromAPI) - { - throw ADP.ParallelTransactionsNotSupported(Connection); - } - - if (iso == System.Data.IsolationLevel.Unspecified) - { - iso = System.Data.IsolationLevel.ReadCommitted; // Default to ReadCommitted if unspecified. - } - - SqlTransaction transaction = new(this, Connection, iso, AvailableInternalTransaction); - transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect; - ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false); - transaction.InternalTransaction.RestoreBrokenConnection = false; - return transaction; - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - finally - { - SqlStatistics.StopTimer(statistics); - } - } - override public void ChangeDatabase(string database) { if (string.IsNullOrEmpty(database)) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 80864716a3..a9732da193 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -20,6 +20,7 @@ using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Identity.Client; +using IsolationLevel = System.Data.IsolationLevel; namespace Microsoft.Data.SqlClient { @@ -701,6 +702,49 @@ private int accessTokenExpirationBufferTime #region Public and Internal Methods + public override DbTransaction BeginTransaction(IsolationLevel iso) => + BeginSqlTransaction(iso, transactionName: null, shouldReconnect: false); + + internal SqlTransaction BeginSqlTransaction( + IsolationLevel iso, + string transactionName, + bool shouldReconnect) + { + SqlStatistics statistics = null; + try + { + statistics = SqlStatistics.StartTimer(Connection.Statistics); + + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); // MDAC 81476 + #endif + + ValidateConnectionForExecute(null); + + if (HasLocalTransactionFromAPI) + { + throw ADP.ParallelTransactionsNotSupported(Connection); + } + + if (iso == IsolationLevel.Unspecified) + { + // Default to ReadCommitted if unspecified. + iso = IsolationLevel.ReadCommitted; + } + + SqlTransaction transaction = new(this, Connection, iso, AvailableInternalTransaction); + transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect; + ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false); + transaction.InternalTransaction.RestoreBrokenConnection = false; + return transaction; + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + finally + { + SqlStatistics.StopTimer(statistics); + } + } + internal void BreakConnection() { SqlClientEventSource.Log.TryTraceEvent( From c796212b314579cfb145e2fc2b563b70620ec966 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:15:31 -0600 Subject: [PATCH 03/34] Move ChangeDatabase into SqlInternalConnectionTds, move ChangeDatabaseInternal into ChangeDatabase, remove abstract ChangeDatabaseInternal --- .../Data/SqlClient/SqlInternalConnection.cs | 14 ----- .../SqlClient/SqlInternalConnectionTds.cs | 51 ++++++++++--------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 2640c93335..b9fcc51775 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -175,20 +175,6 @@ override internal bool IsTransactionRoot #endregion - override public void ChangeDatabase(string database) - { - if (string.IsNullOrEmpty(database)) - { - throw ADP.EmptyDatabaseName(); - } - - ValidateConnectionForExecute(null); - - ChangeDatabaseInternal(database); // do the real work... - } - - abstract protected void ChangeDatabaseInternal(string database); - override protected void CleanupTransactionOnCompletion(Transaction transaction) { // Note: unlocked, potentially multi-threaded code, so pull delegate to local to diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index a9732da193..f27616cc8d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -705,6 +705,34 @@ private int accessTokenExpirationBufferTime public override DbTransaction BeginTransaction(IsolationLevel iso) => BeginSqlTransaction(iso, transactionName: null, shouldReconnect: false); + public override void ChangeDatabase(string database) + { + if (string.IsNullOrEmpty(database)) + { + throw ADP.EmptyDatabaseName(); + } + + ValidateConnectionForExecute(null); + + // MDAC 73598 - add brackets around database + database = SqlConnection.FixupDatabaseTransactionName(database); // @TODO: Should go to a utility method + Task executeTask = _parser.TdsExecuteSQLBatch( + $@"USE {database}", + ConnectionOptions.ConnectTimeout, + notificationRequest: null, + _parser._physicalStateObj, + sync: true); + + Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); + + _parser.Run( + RunBehavior.UntilDone, + cmdHandler: null, + dataStream: null, + bulkCopyHandler: null, + _parser._physicalStateObj); + } + internal SqlTransaction BeginSqlTransaction( IsolationLevel iso, string transactionName, @@ -1790,29 +1818,6 @@ protected override void Activate(Transaction transaction) } } - // @TODO: Is this suppression still required - [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs - protected override void ChangeDatabaseInternal(string database) - { - // MDAC 73598 - add brackets around database - database = SqlConnection.FixupDatabaseTransactionName(database); // @TODO: Should go to a utility method - Task executeTask = _parser.TdsExecuteSQLBatch( - $@"USE {database}", - ConnectionOptions.ConnectTimeout, - notificationRequest: null, - _parser._physicalStateObj, - sync: true); - - Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); - - _parser.Run( - RunBehavior.UntilDone, - cmdHandler: null, - dataStream: null, - bulkCopyHandler: null, - _parser._physicalStateObj); - } - // @TODO: Rename to match guidelines protected override byte[] GetDTCAddress() { From 13c373c9a729c6382f2a667aeec89f4a60f823b6 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:21:56 -0600 Subject: [PATCH 04/34] Move EnlistTransaction to SqlInternalConnectionTds --- .../Data/SqlClient/SqlInternalConnection.cs | 35 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 34 ++++++++++++++++++ 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index b9fcc51775..91de2ea6bb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -463,41 +463,6 @@ internal void EnlistNull() Debug.Assert(CurrentTransaction == null, "unenlisted transaction with non-null current transaction?"); // verify it! } - override public void EnlistTransaction(Transaction transaction) - { -#if NETFRAMEWORK - SqlConnection.VerifyExecutePermission(); -#endif - ValidateConnectionForExecute(null); - - // If a connection has a local transaction outstanding and you try - // to enlist in a DTC transaction, SQL Server will rollback the - // local transaction and then do the enlist (7.0 and 2000). So, if - // the user tries to do this, throw. - if (HasLocalTransaction) - { - throw ADP.LocalTransactionPresent(); - } - - if (transaction != null && transaction.Equals(EnlistedTransaction)) - { - // No-op if this is the current transaction - return; - } - - // If a connection is already enlisted in a DTC transaction and you - // try to enlist in another one, in 7.0 the existing DTC transaction - // would roll back and then the connection would enlist in the new - // one. In SQL 2000 & 2005, when you enlist in a DTC transaction - // while the connection is already enlisted in a DTC transaction, - // the connection simply switches enlistments. Regardless, simply - // enlist in the user specified distributed transaction. This - // behavior matches OLEDB and ODBC. - - Enlist(transaction); - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest); internal SqlDataReader FindLiveReader(SqlCommand command) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index f27616cc8d..e09e6a0be1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -733,6 +733,40 @@ public override void ChangeDatabase(string database) _parser._physicalStateObj); } + public override void EnlistTransaction(Transaction transaction) + { + #if NETFRAMEWORK + SqlConnection.VerifyExecutePermission(); + #endif + + ValidateConnectionForExecute(null); + + // If a connection has a local transaction outstanding, and you try to enlist in a DTC + // transaction, SQL Server will roll back the local transaction and then enlist (7.0 and + // 2000). So, if the user tries to do this, throw. + if (HasLocalTransaction) + { + throw ADP.LocalTransactionPresent(); + } + + if (transaction != null && transaction.Equals(EnlistedTransaction)) + { + // No-op if this is the current transaction + return; + } + + // If a connection is already enlisted in a DTC transaction, and you try to enlist in + // another one, in 7.0 the existing DTC transaction would roll back and then the + // connection would enlist in the new one. In SQL 2000 & 2005, when you enlist in a DTC + // transaction while the connection is already enlisted in a DTC transaction, the + // connection simply switches enlistments. Regardless, simply enlist in the user + // specified distributed transaction. This behavior matches OLEDB and ODBC. + + Enlist(transaction); + // @TODO: CER Exception Handling was removed here (see GH#3581) + } + + internal SqlTransaction BeginSqlTransaction( IsolationLevel iso, string transactionName, From a176e2f7cd7a247659b92afd6d40f1df06ac34bd Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:23:27 -0600 Subject: [PATCH 05/34] Remove abstract ValidateConnectionForExecute --- .../src/Microsoft/Data/SqlClient/SqlInternalConnection.cs | 2 -- .../src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 91de2ea6bb..5219472829 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -515,7 +515,5 @@ internal void OnError(SqlException exception, bool breakConnection, Action Date: Wed, 12 Nov 2025 17:29:52 -0600 Subject: [PATCH 06/34] Move Enlist into SqlInternalConnectionTds --- .../Data/SqlClient/SqlInternalConnection.cs | 49 +---------------- .../SqlClient/SqlInternalConnectionTds.cs | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 5219472829..c74b05523e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -233,54 +233,7 @@ override public void Dispose() base.Dispose(); } - protected void Enlist(Transaction tx) - { - // This method should not be called while the connection has a - // reference to an active delegated transaction. - // Manual enlistment via SqlConnection.EnlistTransaction - // should catch this case and throw an exception. - // - // Automatic enlistment isn't possible because - // Sys.Tx keeps the connection alive until the transaction is completed. - // TODO: why do we assert pooling status? shouldn't we just be checking - // whether the connection is the root of the transaction? - Debug.Assert(!(IsTransactionRoot && Pool == null), "cannot defect an active delegated transaction!"); // potential race condition, but it's an assert - - if (tx == null) - { - if (IsEnlistedInTransaction) - { - EnlistNull(); - } - else - { - // When IsEnlistedInTransaction is false, it means we are in one of two states: - // 1. EnlistTransaction is null, so the connection is truly not enlisted in a transaction, or - // 2. Connection is enlisted in a SqlDelegatedTransaction. - // - // For #2, we have to consider whether or not the delegated transaction is active. - // If it is not active, we allow the enlistment in the NULL transaction. - // - // If it is active, technically this is an error. - // However, no exception is thrown as this was the precedent (and this case is silently ignored, no error, but no enlistment either). - // There are two mitigations for this: - // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has completed before allowing a different enlistment. - // 2. For debug builds, the assert at the beginning of this method checks for an enlistment in an active delegated transaction. - Transaction enlistedTransaction = EnlistedTransaction; - if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != TransactionStatus.Active) - { - EnlistNull(); - } - } - } - // Only enlist if it's different... - else if (!tx.Equals(EnlistedTransaction)) - { // WebData 20000024 - Must use Equals, not != - EnlistNonNull(tx); - } - } - - private void EnlistNonNull(Transaction tx) + protected void EnlistNonNull(Transaction tx) { Debug.Assert(tx != null, "null transaction?"); SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object {0}, Transaction Id {1}, attempting to delegate.", ObjectID, tx?.TransactionInformation?.LocalIdentifier); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index aba03d206d..7a68d18bf7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2098,6 +2098,58 @@ private void CompleteLogin(bool enlistOK) // @TODO: Rename as per guidelines _parser._physicalStateObj.SniContext = SniContext.Snix_Login; } + private void Enlist(Transaction transaction) + { + // This method should not be called while the connection has a reference to an active + // delegated transaction. Manual enlistment via SqlConnection.EnlistTransaction should + // catch this case and throw an exception. + + // Automatic enlistment isn't possible because Sys.Tx keeps the connection alive until + // the transaction is completed. + // @TODO: What does the above mean? Is it still valid in a post-SDS world? + + // TODO: why do we assert pooling status? shouldn't we just be checking whether the connection is the root of the transaction? + // @TODO: potential race condition, but it's an assert + Debug.Assert(!(IsTransactionRoot && Pool == null), "cannot defect an active delegated transaction!"); + + if (transaction is null) + { + if (IsEnlistedInTransaction) + { + EnlistNull(); + } + else + { + // When IsEnlistedInTransaction is false, it means we are in one of two states: + // 1. EnlistTransaction is null, so the connection is truly not enlisted in a + // transaction + // 2. Connection is enlisted in a SqlDelegatedTransaction. + // + // For #2, we have to consider whether the delegated transaction is active. If + // it is not active, we allow the enlistment in the NULL transaction. If it is + // active, technically this is an error. + // + // However, no exception is thrown as this was the precedent (and this case is + // silently ignored, no error, but no enlistment either). There are two + // mitigations for this: + // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has + // completed before allowing a different enlistment. + // 2. For debug builds, the assertion at the beginning of this method checks + // for an enlistment in an active delegated transaction. + Transaction enlistedTransaction = EnlistedTransaction; + if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != TransactionStatus.Active) + { + EnlistNull(); + } + } + } + else if (!transaction.Equals(EnlistedTransaction)) + { + // Only enlist if it's different... + EnlistNonNull(transaction); + } + } + // @TODO: Rename to ExecuteTransactionInternal ... we don't have multiple server version implementations of this private void ExecuteTransaction2005( TransactionRequest transactionRequest, From 8002ec705e47fc1eade53187419ff4617ffb619d Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:45:31 -0600 Subject: [PATCH 07/34] Move EnlistNonNull into SqlInternalConnectionTds --- .../Data/SqlClient/SqlInternalConnection.cs | 159 +----------------- .../SqlClient/SqlInternalConnectionTds.cs | 155 +++++++++++++++++ 2 files changed, 159 insertions(+), 155 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index c74b05523e..2b32de0874 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -21,12 +21,12 @@ internal abstract class SqlInternalConnection : DbConnectionInternal /// /// Cache the whereabouts (DTC Address) for exporting. /// - private byte[] _whereAbouts; + protected byte[] _whereAbouts; /// /// ID of the Azure SQL DB Transaction Manager (Non-MSDTC) /// - private static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); + protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); internal SqlCommand.ExecuteReaderAsyncCallContext CachedCommandExecuteReaderAsyncContext; internal SqlCommand.ExecuteNonQueryAsyncCallContext CachedCommandExecuteNonQueryAsyncContext; @@ -132,7 +132,7 @@ internal bool HasLocalTransactionFromAPI /// /// Indicates whether the connection is currently enlisted in a transaction. /// - internal bool IsEnlistedInTransaction { get; private set; } + internal bool IsEnlistedInTransaction { get; set; } /// /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) @@ -233,157 +233,6 @@ override public void Dispose() base.Dispose(); } - protected void EnlistNonNull(Transaction tx) - { - Debug.Assert(tx != null, "null transaction?"); - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object {0}, Transaction Id {1}, attempting to delegate.", ObjectID, tx?.TransactionInformation?.LocalIdentifier); - bool hasDelegatedTransaction = false; - - // Promotable transactions are only supported on 2005 - // servers or newer. - SqlDelegatedTransaction delegatedTransaction = new(this, tx); - - try - { - // NOTE: System.Transactions claims to resolve all - // potential race conditions between multiple delegate - // requests of the same transaction to different - // connections in their code, such that only one - // attempt to delegate will succeed. - - // NOTE: PromotableSinglePhaseEnlist will eventually - // make a round trip to the server; doing this inside - // a lock is not the best choice. We presume that you - // aren't trying to enlist concurrently on two threads - // and leave it at that -- We don't claim any thread - // safety with regard to multiple concurrent requests - // to enlist the same connection in different - // transactions, which is good, because we don't have - // it anyway. - - // PromotableSinglePhaseEnlist may not actually promote - // the transaction when it is already delegated (this is - // the way they resolve the race condition when two - // threads attempt to delegate the same Lightweight - // Transaction) In that case, we can safely ignore - // our delegated transaction, and proceed to enlist - // in the promoted one. - - // NOTE: Global Transactions is an Azure SQL DB only - // feature where the Transaction Manager (TM) is not - // MS-DTC. Sys.Tx added APIs to support Non MS-DTC - // promoter types/TM in .NET 4.6.2. Following directions - // from .NETFX shiproom, to avoid a "hard-dependency" - // (compile time) on Sys.Tx, we use reflection to invoke - // the new APIs. Further, the IsGlobalTransaction flag - // indicates that this is an Azure SQL DB Transaction - // that could be promoted to a Global Transaction (it's - // always false for on-prem Sql Server). The Promote() - // call in SqlDelegatedTransaction makes sure that the - // right Sys.Tx.dll is loaded and that Global Transactions - // are actually allowed for this Azure SQL DB. - - if (IsGlobalTransaction) - { - if (SysTxForGlobalTransactions.EnlistPromotableSinglePhase == null) - { - // This could be a local Azure SQL DB transaction. - hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction); - } - else - { - hasDelegatedTransaction = (bool)SysTxForGlobalTransactions.EnlistPromotableSinglePhase.Invoke(tx, new object[] { delegatedTransaction, s_globalTransactionTMID }); - } - } - else - { - // This is an MS-DTC distributed transaction - hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction); - } - - if (hasDelegatedTransaction) - { - DelegatedTransaction = delegatedTransaction; - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object Id {0}, Client Connection Id {1} delegated to transaction {1} with transactionId {2}", ObjectID, Connection?.ClientConnectionId, delegatedTransaction?.ObjectID, delegatedTransaction?.Transaction?.TransactionInformation?.LocalIdentifier); - } - } - catch (SqlException e) - { - // we do not want to eat the error if it is a fatal one - if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) - { - throw; - } - - // if the parser is null or its state is not openloggedin, the connection is no longer good. - if (this is SqlInternalConnectionTds tdsConnection) - { - TdsParser parser = tdsConnection.Parser; - if (parser == null || parser.State != TdsParserState.OpenLoggedIn) - { - throw; - } - } - -#if NETFRAMEWORK - ADP.TraceExceptionWithoutRethrow(e); -#endif - // In this case, SqlDelegatedTransaction.Initialize - // failed and we don't necessarily want to reject - // things -- there may have been a legitimate reason - // for the failure. - } - - if (!hasDelegatedTransaction) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object Id {0}, delegation not possible, enlisting.", ObjectID); - byte[] cookie = null; - - if (IsGlobalTransaction) - { - if (SysTxForGlobalTransactions.GetPromotedToken == null) - { - throw SQL.UnsupportedSysTxForGlobalTransactions(); - } - - cookie = (byte[])SysTxForGlobalTransactions.GetPromotedToken.Invoke(tx, null); - } - else - { - if (_whereAbouts == null) - { - byte[] dtcAddress = GetDTCAddress(); - _whereAbouts = dtcAddress ?? throw SQL.CannotGetDTCAddress(); - } - cookie = GetTransactionCookie(tx, _whereAbouts); - } - - // send cookie to server to finish enlistment - PropagateTransactionCookie(cookie); - - IsEnlistedInTransaction = true; - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object Id {0}, Client Connection Id {1}, Enlisted in transaction with transactionId {2}", ObjectID, Connection?.ClientConnectionId, tx?.TransactionInformation?.LocalIdentifier); - } - - EnlistedTransaction = tx; // Tell the base class about our enlistment - - - // If we're on a 2005 or newer server, and we delegate the - // transaction successfully, we will have done a begin transaction, - // which produces a transaction id that we should execute all requests - // on. The TdsParser or SmiEventSink will store this information as - // the current transaction. - // - // Likewise, propagating a transaction to a 2005 or newer server will - // produce a transaction id that The TdsParser or SmiEventSink will - // store as the current transaction. - // - // In either case, when we're working with a 2005 or newer server - // we better have a current transaction by now. - - Debug.Assert(CurrentTransaction != null, "delegated/enlisted transaction with null current transaction?"); - } - internal void EnlistNull() { SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNull | ADV | Object Id {0}, unenlisting.", ObjectID); @@ -431,7 +280,7 @@ internal SqlDataReader FindLiveReader(SqlCommand command) abstract protected byte[] GetDTCAddress(); - static private byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) + static protected byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) { byte[] transactionCookie = null; if (transaction != null) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 7a68d18bf7..970ed450f6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2150,6 +2150,161 @@ private void Enlist(Transaction transaction) } } + private void EnlistNonNull(Transaction transaction) + { + Debug.Assert(transaction != null, "null transaction?"); + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"Transaction Id {transaction?.TransactionInformation?.LocalIdentifier}, " + + $"attempting to delegate."); + + bool hasDelegatedTransaction = false; + SqlDelegatedTransaction delegatedTransaction = new(this, transaction); + + try + { + // NOTE: System.Transactions claims to resolve all potential race conditions + // between multiple delegate requests of the same transaction to different + // connections in their code, such that only one attempt to delegate will succeed. + + // NOTE: PromotableSinglePhaseEnlist will eventually make a round trip to the + // server; doing this inside a lock is not the best choice. We presume that you + // aren't trying to enlist concurrently on two threads and leave it at that. We + // don't claim any thread safety with regard to multiple concurrent requests to + // enlist the same connection in different transactions, which is good, because we + // don't have it anyway. + + // PromotableSinglePhaseEnlist may not actually promote the transaction when it is + // already delegated (this is the way they resolve the race condition when two + // threads attempt to delegate the same Lightweight Transaction). In that case, we + // can safely ignore our delegated transaction, and proceed to enlist in the + // promoted one. + + // NOTE: Global Transactions is an Azure SQL DB only feature where the Transaction + // Manager (TM) is not MS-DTC. Sys.Tx added APIs to support Non MS-DTC promoter + // types/TM in .NET 4.6.2. Following directions from .NETFX shiproom, to avoid a + // "hard-dependency" (compile time) on Sys.Tx, we use reflection to invoke the new + // APIs. Further, the IsGlobalTransaction flag indicates that this is an Azure SQL + // DB Transaction that could be promoted to a Global Transaction (it's always false + // for on-prem SQL Server). The Promote() call in SqlDelegatedTransaction makes + // sure that the right Sys.Tx.dll is loaded and that Global Transactions are + // actually allowed for this Azure SQL DB. + // @TODO: Revisit these comments and see if they are still necessary/desirable. + + if (IsGlobalTransaction) + { + if (SysTxForGlobalTransactions.EnlistPromotableSinglePhase == null) + { + // This could be a local Azure SQL DB transaction. + hasDelegatedTransaction = transaction.EnlistPromotableSinglePhase(delegatedTransaction); + } + else + { + hasDelegatedTransaction = (bool)SysTxForGlobalTransactions.EnlistPromotableSinglePhase.Invoke( + obj: transaction, + parameters: [delegatedTransaction, s_globalTransactionTMID]); + } + } + else + { + // This is an MS-DTC distributed transaction + hasDelegatedTransaction = transaction.EnlistPromotableSinglePhase(delegatedTransaction); + } + + if (hasDelegatedTransaction) + { + DelegatedTransaction = delegatedTransaction; + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"Client Connection Id {Connection?.ClientConnectionId} " + + $"delegated to transaction {delegatedTransaction?.ObjectID} " + + $"with transactionId {delegatedTransaction?.Transaction?.TransactionInformation?.LocalIdentifier}"); + } + } + catch (SqlException e) + { + // we do not want to eat the error if it is a fatal one + if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) + { + throw; + } + + if (Parser?.State is not TdsParserState.OpenLoggedIn) + { + // If the parser is null or its state is not openloggedin, the connection is no + // longer good. + throw; + } + + #if NETFRAMEWORK + ADP.TraceExceptionWithoutRethrow(e); + #endif + + // In this case, SqlDelegatedTransaction.Initialize failed, and we don't + // necessarily want to reject things - there may have been a legitimate reason for + // the failure. + } + + if (!hasDelegatedTransaction) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"delegation not possible, enlisting."); + + byte[] cookie = null; + + if (IsGlobalTransaction) + { + if (SysTxForGlobalTransactions.GetPromotedToken == null) + { + throw SQL.UnsupportedSysTxForGlobalTransactions(); + } + + cookie = (byte[])SysTxForGlobalTransactions.GetPromotedToken.Invoke(transaction, null); + } + else + { + if (_whereAbouts == null) + { + byte[] dtcAddress = GetDTCAddress(); + _whereAbouts = dtcAddress ?? throw SQL.CannotGetDTCAddress(); + } + + cookie = GetTransactionCookie(transaction, _whereAbouts); + } + + // send cookie to server to finish enlistment + PropagateTransactionCookie(cookie); + + IsEnlistedInTransaction = true; + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"Client Connection Id {Connection?.ClientConnectionId}, " + + $"Enlisted in transaction with transactionId {transaction?.TransactionInformation?.LocalIdentifier}"); + } + + // Tell the base class about our enlistment + EnlistedTransaction = transaction; + + // If we're on a 2005 or newer server, and we delegate the transaction successfully, we + // will have begun a transaction, which produces a transaction ID that we should + // execute all requests on. The TdsParser will store this information as the current + // transaction. + + // Likewise, propagating a transaction to a 2005 or newer server will produce a + // transaction id that The TdsParser will store as the current transaction. + + // In either case, when we're working with a 2005 or newer server we better have a + // current transaction by now. + + Debug.Assert(CurrentTransaction != null, "delegated/enlisted transaction with null current transaction?"); + } + // @TODO: Rename to ExecuteTransactionInternal ... we don't have multiple server version implementations of this private void ExecuteTransaction2005( TransactionRequest transactionRequest, From 202beb5fc06234dbad321ad7c14b696e3f477ceb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:54:32 -0600 Subject: [PATCH 08/34] Move EnlistNull into SqlInternalConnectionTds --- .../Data/SqlClient/SqlInternalConnection.cs | 32 ------------------ .../SqlClient/SqlInternalConnectionTds.cs | 33 +++++++++++++++++++ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 2b32de0874..bbbf582cee 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -233,38 +233,6 @@ override public void Dispose() base.Dispose(); } - internal void EnlistNull() - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNull | ADV | Object Id {0}, unenlisting.", ObjectID); - // We were in a transaction, but now we are not - so send - // message to server with empty transaction - confirmed proper - // behavior from Sameet Agarwal - // - // The connection pooler maintains separate pools for enlisted - // transactions, and only when that transaction is committed or - // rolled back will those connections be taken from that - // separate pool and returned to the general pool of connections - // that are not affiliated with any transactions. When this - // occurs, we will have a new transaction of null and we are - // required to send an empty transaction payload to the server. - - PropagateTransactionCookie(null); - - IsEnlistedInTransaction = false; - EnlistedTransaction = null; // Tell the base class about our enlistment - - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNull | ADV | Object Id {0}, unenlisted.", ObjectID); - - // The EnlistTransaction above will return an TransactionEnded event, - // which causes the TdsParser or SmiEventSink should to clear the - // current transaction. - // - // In either case, when we're working with a 2005 or newer server - // we better not have a current transaction at this point. - - Debug.Assert(CurrentTransaction == null, "unenlisted transaction with non-null current transaction?"); // verify it! - } - abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest); internal SqlDataReader FindLiveReader(SqlCommand command) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 970ed450f6..a29a3ba029 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -914,6 +914,39 @@ public override void Dispose() base.Dispose(); } + internal void EnlistNull() + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNull | ADV | " + + $"Object ID {ObjectID}, " + + $"unenlisting."); + + // We were in a transaction, but now we are not - so send message to server with empty + // transaction - confirmed proper behavior from Sameet Agarwal. + + // The connection pooler maintains separate pools for enlisted transactions. Only when + // that transaction is committed or rolled back will those connections be taken from + // that separate pool and returned to the general pool of connections that are not + // affiliated with any transactions. When this occurs, we will have a new transaction + // of null, and we are required to send an empty transaction payload to the server. + + PropagateTransactionCookie(null); + + // Tell the base class about our enlistment + IsEnlistedInTransaction = false; + EnlistedTransaction = null; + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNull | ADV | " + + $"Object ID {ObjectID}, " + + $"unenlisted."); + + // The EnlistTransaction above will return an TransactionEnded event, which causes the + // TdsParser to clear the current transaction. In either case, when we're working with + // a 2005 or newer server we better not have a current transaction at this point. + Debug.Assert(CurrentTransaction == null, "unenlisted transaction with non-null current transaction?"); + } + internal override void ExecuteTransaction( TransactionRequest transactionRequest, string name, From bb3d541f0b1398686906233de5cb45e6ce8b41c2 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 17:58:40 -0600 Subject: [PATCH 09/34] Removed no longer necessary abstractions in SqlInternalConnection --- .../Data/SqlClient/SqlInternalConnection.cs | 13 ------------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 17 ++++++++++------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index bbbf582cee..5f9b6af7c7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -163,11 +163,6 @@ override internal bool IsTransactionRoot } } - /// - /// TODO: need to understand this property better - /// - abstract internal SqlInternalTransaction PendingTransaction { get; } - /// /// A token returned by the server when we promote transaction. /// @@ -225,16 +220,12 @@ override protected void Deactivate() } } - abstract internal void DisconnectTransaction(SqlInternalTransaction internalTransaction); - override public void Dispose() { _whereAbouts = null; base.Dispose(); } - abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest); - internal SqlDataReader FindLiveReader(SqlCommand command) { SqlDataReader reader = null; @@ -246,8 +237,6 @@ internal SqlDataReader FindLiveReader(SqlCommand command) return reader; } - abstract protected byte[] GetDTCAddress(); - static protected byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) { byte[] transactionCookie = null; @@ -283,7 +272,5 @@ internal void OnError(SqlException exception, bool breakConnection, Action _parser; } - internal override SqlInternalTransaction PendingTransaction + /// + /// TODO: need to understand this property better + /// + internal SqlInternalTransaction PendingTransaction { get => _parser.PendingTransaction; } @@ -880,7 +883,7 @@ internal void DecrementAsyncCount() Interlocked.Decrement(ref _asyncCommandCount); } - internal override void DisconnectTransaction(SqlInternalTransaction internalTransaction) => + internal void DisconnectTransaction(SqlInternalTransaction internalTransaction) => _parser?.DisconnectTransaction(internalTransaction); // @TODO: Make internal by making the SqlInternalConnection implementation internal @@ -947,7 +950,7 @@ internal void EnlistNull() Debug.Assert(CurrentTransaction == null, "unenlisted transaction with non-null current transaction?"); } - internal override void ExecuteTransaction( + internal void ExecuteTransaction( TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, @@ -1340,8 +1343,8 @@ internal void OnFeatureExtAck(int featureId, byte[] data) SqlClientEventSource.Log.TryTraceEvent( $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + $"Object ID {ObjectID }, " + - $"AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, but it did not update the new value.", - ObjectID); + $"AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, " + + $"but it did not update the new value."); } #endif } @@ -1886,7 +1889,7 @@ protected override void Activate(Transaction transaction) } // @TODO: Rename to match guidelines - protected override byte[] GetDTCAddress() + protected byte[] GetDTCAddress() { byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); @@ -1940,7 +1943,7 @@ protected override bool ObtainAdditionalLocksForClose() return obtainParserLock; } - protected override void PropagateTransactionCookie(byte[] cookie) + protected void PropagateTransactionCookie(byte[] cookie) { _parser.PropagateDistributedTransaction( cookie, From 4d398422f22d1b86b77c039a6b33b599478ee586 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 18:02:36 -0600 Subject: [PATCH 10/34] Move OnError into SqlInternalConnectionTds --- .../Data/SqlClient/SqlInternalConnection.cs | 22 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 5f9b6af7c7..03dc008ce2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -250,27 +250,5 @@ static protected byte[] GetTransactionCookie(Transaction transaction, byte[] whe virtual protected void InternalDeactivate() { } - - // If wrapCloseInAction is defined, then the action it defines will be run with the connection close action passed in as a parameter - // The close action also supports being run asynchronously - internal void OnError(SqlException exception, bool breakConnection, Action wrapCloseInAction = null) - { - if (breakConnection) - { - DoomThisConnection(); - } - - SqlConnection connection = Connection; - if (connection != null) - { - connection.OnError(exception, breakConnection, wrapCloseInAction); - } - else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) - { - // It is an error, and should be thrown. Class of TdsEnums.MIN_ERROR_CLASS - // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message. - throw exception; - } - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0e410eb4cd..3690404aac 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1159,6 +1159,31 @@ internal void OnEnvChange(SqlEnvChange rec) } } + /// + /// If wrapCloseInAction is defined, then the action it defines will be run with the + /// connection close action passed in as a parameter. The close action also supports being + /// run asynchronously. + /// + internal void OnError(SqlException exception, bool breakConnection, Action wrapCloseInAction = null) + { + if (breakConnection) + { + DoomThisConnection(); + } + + SqlConnection connection = Connection; + if (connection != null) + { + connection.OnError(exception, breakConnection, wrapCloseInAction); + } + else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) + { + // It is an error, and should be thrown. Class of TdsEnums.MIN_ERROR_CLASS + // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message. + throw exception; + } + } + // @TODO: This feature is *far* too big, and has the same issues as the above OnEnvChange // @TODO: Consider individual callbacks for the supported features and perhaps an interface of feature callbacks. Or registering with the parser what features are handleable. // @TODO: This class should not do low-level parsing of data from the server. From 58c358f176d426d907904f75f8b94d3d9f06bea5 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 18:08:05 -0600 Subject: [PATCH 11/34] Move Deactivate into SqlInternalConnectionTds, merge InternalDeactivate into Deactivate --- .../Data/SqlClient/SqlInternalConnection.cs | 38 --------- .../SqlClient/SqlInternalConnectionTds.cs | 84 ++++++++++++------- 2 files changed, 56 insertions(+), 66 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 03dc008ce2..e46cdcabeb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -186,40 +186,6 @@ override protected DbReferenceCollection CreateReferenceCollection() return new SqlReferenceCollection(); } - /// - override protected void Deactivate() - { - try - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.Deactivate | ADV | Object Id {0} deactivating, Client Connection Id {1}", ObjectID, Connection?.ClientConnectionId); - - SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; - if (referenceCollection != null) - { - referenceCollection.Deactivate(); - } - - // Invoke subclass-specific deactivation logic - InternalDeactivate(); - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - catch (Exception e) - { - if (!ADP.IsCatchableExceptionType(e)) - { - throw; - } - - // if an exception occurred, the inner connection will be - // marked as unusable and destroyed upon returning to the - // pool - DoomThisConnection(); -#if NETFRAMEWORK - ADP.TraceExceptionWithoutRethrow(e); -#endif - } - } - override public void Dispose() { _whereAbouts = null; @@ -246,9 +212,5 @@ static protected byte[] GetTransactionCookie(Transaction transaction, byte[] whe } return transactionCookie; } - - virtual protected void InternalDeactivate() - { - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 3690404aac..e602eb9130 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1913,43 +1913,71 @@ protected override void Activate(Transaction transaction) } } - // @TODO: Rename to match guidelines - protected byte[] GetDTCAddress() - { - byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); - - Debug.Assert(dtcAddress != null, "null dtcAddress?"); - return dtcAddress; - } - - protected override void InternalDeactivate() + /// + protected override void Deactivate() { - // When we're deactivated, the user must have called End on all the async commands, or - // we don't know that we're in a state that we can recover from. We doom the connection - // in this case, to prevent odd cases when we go to the wire. - if (_asyncCommandCount != 0) + try { - DoomThisConnection(); - } + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.Deactivate | ADV | " + + $"Object ID {ObjectID} deactivating, " + + $"Client Connection Id {Connection?.ClientConnectionId}"); - // If we're deactivating with a delegated transaction, we should not be cleaning up the - // parser just yet, that will cause our transaction to be rolled back and the - // connection to be reset. We'll get called again once the delegated transaction is - // completed, and we can do it all then. - // TODO: I think this logic cares about pooling because the pool will handle deactivation of pool-associated trasaction roots? - if (!(IsTransactionRoot && Pool == null)) - { - Debug.Assert(_parser != null || IsConnectionDoomed, "Deactivating a disposed connection?"); - if (_parser != null) + SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; + referenceCollection?.Deactivate(); + + // When we're deactivated, the user must have called End on all the async commands, + // or we don't know that we're in a state that we can recover from. We doom the + // connection in this case, to prevent odd cases when we go to the wire. + if (_asyncCommandCount != 0) { - _parser.Deactivate(IsConnectionDoomed); + DoomThisConnection(); + } - if (!IsConnectionDoomed) + // If we're deactivating with a delegated transaction, we should not be cleaning up + // the parser just yet, that will cause our transaction to be rolled back and the + // connection to be reset. We'll get called again once the delegated transaction is + // completed, and we can do it all then. + // TODO: I think this logic cares about pooling because the pool will handle deactivation of pool-associated trasaction roots? + if (!(IsTransactionRoot && Pool == null)) + { + Debug.Assert(_parser != null || IsConnectionDoomed, "Deactivating a disposed connection?"); + if (_parser != null) { - ResetConnection(); + _parser.Deactivate(IsConnectionDoomed); + + if (!IsConnectionDoomed) + { + ResetConnection(); + } } } } + // @TODO: CER Exception Handling was removed here (see GH#3581) + catch (Exception e) + { + if (!ADP.IsCatchableExceptionType(e)) + { + throw; + } + + // If an exception occurred, the inner connection will be marked as unusable and + // destroyed upon returning to the pool + DoomThisConnection(); + + #if NETFRAMEWORK + ADP.TraceExceptionWithoutRethrow(e); + #endif + } + } + + // @TODO: Rename to match guidelines + protected byte[] GetDTCAddress() + { + byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); + + Debug.Assert(dtcAddress != null, "null dtcAddress?"); + return dtcAddress; } protected override bool ObtainAdditionalLocksForClose() From 07750f8d807947c4ce6ac6be2c991fc1c32c54a9 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 18:10:52 -0600 Subject: [PATCH 12/34] Move GetTransactionCookie into SqlInternalConnectionTds --- .../Microsoft/Data/SqlClient/SqlInternalConnection.cs | 10 ---------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 7 +++++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index e46cdcabeb..fac56894d4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -202,15 +202,5 @@ internal SqlDataReader FindLiveReader(SqlCommand command) } return reader; } - - static protected byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) - { - byte[] transactionCookie = null; - if (transaction != null) - { - transactionCookie = TransactionInterop.GetExportCookie(transaction, whereAbouts); - } - return transactionCookie; - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index e602eb9130..3e8ddadb7c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2017,6 +2017,13 @@ protected override void ReleaseAdditionalLocksForClose(bool lockToken) #region Private Methods + private static byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) + { + return transaction is not null + ? TransactionInterop.GetExportCookie(transaction, whereAbouts) + : null; + } + /// /// Common code path for making one attempt to establish a connection and log in to server. /// From dbe9d9e4f99a5ada92a5ea3aa50eb70ee83b4b5b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 18:12:59 -0600 Subject: [PATCH 13/34] Move FindLiveReader into SqlInternalConnectionTds --- .../Microsoft/Data/SqlClient/SqlInternalConnection.cs | 11 ----------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index fac56894d4..2c68d16c75 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -191,16 +191,5 @@ override public void Dispose() _whereAbouts = null; base.Dispose(); } - - internal SqlDataReader FindLiveReader(SqlCommand command) - { - SqlDataReader reader = null; - SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; - if (referenceCollection != null) - { - reader = referenceCollection.FindLiveReader(command); - } - return reader; - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 3e8ddadb7c..5e3e58c1ed 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -984,6 +984,17 @@ TransactionRequest.Rollback or ExecuteTransaction2005(transactionRequest, transactionName, iso, internalTransaction, isDelegateControlRequest); } + internal SqlDataReader FindLiveReader(SqlCommand command) + { + SqlDataReader reader = null; + SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; + if (referenceCollection != null) + { + reader = referenceCollection.FindLiveReader(command); + } + return reader; + } + /// /// Called by SqlConnection.RepairConnection which is a relatively expensive way of repair /// inner connection prior to execution of request, used from EnlistTransaction, From 355f2d278cb2ca44d7fea057b4478e36ed05ba92 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 18:13:45 -0600 Subject: [PATCH 14/34] Merge SqlInternalConnection.Dispose into SqlInternalConnectionTds --- .../src/Microsoft/Data/SqlClient/SqlInternalConnection.cs | 6 ------ .../Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 2c68d16c75..ebde3d829c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -185,11 +185,5 @@ override protected DbReferenceCollection CreateReferenceCollection() { return new SqlReferenceCollection(); } - - override public void Dispose() - { - _whereAbouts = null; - base.Dispose(); - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 5e3e58c1ed..2e43b91f5c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -914,7 +914,7 @@ public override void Dispose() _fConnectionOpen = false; } - base.Dispose(); + _whereAbouts = null; } internal void EnlistNull() From 74b0c92b4193e958a6069e668f2602a56885b44a Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 18:24:02 -0600 Subject: [PATCH 15/34] Move CleanupTransactionOnCompletion, CreateReferenceCollection into SqlInternalConnectionTds --- .../Data/SqlClient/SqlInternalConnection.cs | 16 ---------------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 9 +++++++-- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index ebde3d829c..8d05b3f783 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -169,21 +169,5 @@ override internal bool IsTransactionRoot internal byte[] PromotedDtcToken { get; set; } #endregion - - override protected void CleanupTransactionOnCompletion(Transaction transaction) - { - // Note: unlocked, potentially multi-threaded code, so pull delegate to local to - // ensure it doesn't change between test and call. - SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction; - if (delegatedTransaction != null) - { - delegatedTransaction.TransactionEnded(transaction); - } - } - - override protected DbReferenceCollection CreateReferenceCollection() - { - return new SqlReferenceCollection(); - } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 2e43b91f5c..751eb61c4e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -769,7 +769,6 @@ public override void EnlistTransaction(Transaction transaction) // @TODO: CER Exception Handling was removed here (see GH#3581) } - internal SqlTransaction BeginSqlTransaction( IsolationLevel iso, string transactionName, @@ -886,7 +885,7 @@ internal void DecrementAsyncCount() internal void DisconnectTransaction(SqlInternalTransaction internalTransaction) => _parser?.DisconnectTransaction(internalTransaction); - // @TODO: Make internal by making the SqlInternalConnection implementation internal + // @TODO: Make internal by making the DbConnectionInternal implementation internal public override void Dispose() { SqlClientEventSource.Log.TryAdvancedTraceEvent( @@ -1924,6 +1923,12 @@ protected override void Activate(Transaction transaction) } } + protected override void CleanupTransactionOnCompletion(Transaction transaction) => + DelegatedTransaction?.TransactionEnded(transaction); + + protected override DbReferenceCollection CreateReferenceCollection() => + new SqlReferenceCollection(); + /// protected override void Deactivate() { From ce46515d6e612bb323539f50f41bc4e9dcb5f34b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 22:27:10 -0600 Subject: [PATCH 16/34] Introduce a CachedContexts class, move the cached call contexts from SqlInternalConnection into that. Migrate usages of ExecuteReaderAsyncCallContext to the new class. --- .../src/Microsoft.Data.SqlClient.csproj | 3 + .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 + .../SqlClient/Connection/CachedContexts.cs | 58 +++++++++++++++++++ .../Data/SqlClient/SqlCommand.Reader.cs | 23 +++----- .../Data/SqlClient/SqlInternalConnection.cs | 1 - .../SqlClient/SqlInternalConnectionTds.cs | 5 ++ 6 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 950d7f83b1..15beeb6e9f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -210,6 +210,9 @@ Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs + + Microsoft\Data\SqlClient\Connection\CachedContexts.cs + Microsoft\Data\SqlClient\Connection\ServerInfo.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 104b9261e7..499792bc91 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -479,6 +479,9 @@ Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs + + Microsoft\Data\SqlClient\Connection\CachedContexts.cs + Microsoft\Data\SqlClient\Connection\ServerInfo.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs new file mode 100644 index 0000000000..b78d0624de --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -0,0 +1,58 @@ +// 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.Threading; + +#nullable enable + +namespace Microsoft.Data.SqlClient.Connection +{ + internal class CachedContexts + { + #region Fields + + private SqlCommand.ExecuteNonQueryAsyncCallContext? _commandExecuteNonQueryAsyncContext; + private SqlCommand.ExecuteReaderAsyncCallContext? _commandExecuteReaderAsyncContext; + private SqlCommand.ExecuteXmlReaderAsyncCallContext? _commandExecuteXmlReaderAsyncContext; + // private SqlDataReader.IsDBNullAsyncCallContext? _dataReaderIsDbNullContext; + // private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; + // private SqlDataReader.Snapshot? _dataReaderSnapshot; + + #endregion + + #region Access Methods + + internal SqlCommand.ExecuteNonQueryAsyncCallContext? ClearCommandExecuteNonQueryAsyncContext() => + Interlocked.Exchange(ref _commandExecuteNonQueryAsyncContext, null); + + internal SqlCommand.ExecuteReaderAsyncCallContext? ClearCommandExecuteReaderAsyncContext() => + Interlocked.Exchange(ref _commandExecuteReaderAsyncContext, null); + + internal SqlCommand.ExecuteXmlReaderAsyncCallContext? ClearCommandExecuteXmlReaderAsyncContext() => + Interlocked.Exchange(ref _commandExecuteXmlReaderAsyncContext, null); + + internal bool TrySetCommandExecuteNonQueryAsyncContext(SqlCommand.ExecuteNonQueryAsyncCallContext value) => + TrySetContext(value, ref _commandExecuteNonQueryAsyncContext); + + internal bool TrySetCommandExecuteReaderAsyncContext(SqlCommand.ExecuteReaderAsyncCallContext value) => + TrySetContext(value, ref _commandExecuteReaderAsyncContext); + + internal bool TrySetCommandExecuteXmlReaderAsyncContext(SqlCommand.ExecuteXmlReaderAsyncCallContext value) => + TrySetContext(value, ref _commandExecuteXmlReaderAsyncContext); + + #endregion + + private static bool TrySetContext(TContext value, ref TContext? location) + where TContext : class + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + return Interlocked.CompareExchange(ref location, value, null) is null; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs index 0495095e08..b8d95ca159 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; #if NETFRAMEWORK using System.Security.Permissions; @@ -981,9 +982,7 @@ private Task InternalExecuteReaderAsync( if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - context = Interlocked.Exchange( - ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, - null); + context = sqlInternalConnection.CachedContexts.ClearCommandExecuteReaderAsyncContext(); } context ??= new ExecuteReaderAsyncCallContext(); @@ -1785,18 +1784,6 @@ private SqlDataReader RunExecuteReaderWithRetry( this, () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); - private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) - { - // @TODO: This should be part of the sql internal connection class. - Interlocked.CompareExchange( - ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, - instance, - null); - } - } - #endregion internal sealed class ExecuteReaderAsyncCallContext @@ -1824,7 +1811,11 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { - owner?.SetCachedCommandExecuteReaderAsyncContext(this); + DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; + if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetCommandExecuteReaderAsyncContext(this); + } } protected override void Clear() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 8d05b3f783..bc10c57e12 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -28,7 +28,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal /// protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); - internal SqlCommand.ExecuteReaderAsyncCallContext CachedCommandExecuteReaderAsyncContext; internal SqlCommand.ExecuteNonQueryAsyncCallContext CachedCommandExecuteNonQueryAsyncContext; internal SqlCommand.ExecuteXmlReaderAsyncCallContext CachedCommandExecuteXmlReaderAsyncContext; internal SqlDataReader.Snapshot CachedDataReaderSnapshot; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 751eb61c4e..c3b6d1b31a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -493,6 +493,11 @@ internal override SqlInternalTransaction AvailableInternalTransaction get => _parser._fResetConnection ? null : CurrentTransaction; } + /// + /// Gets the collection of async call contexts that belong to this connection. + /// + internal CachedContexts CachedContexts { get; private set; } = new CachedContexts(); + // @TODO: Make auto-property internal Guid ClientConnectionId { From 79cf0527134cb54559bb29615e321a57ac900106 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 22:30:28 -0600 Subject: [PATCH 17/34] Migrate usages of CachedCommandExecuteNonQueryAsyncContext to the new class. --- .../Data/SqlClient/SqlCommand.NonQuery.cs | 19 ++++++------------- .../Data/SqlClient/SqlInternalConnection.cs | 1 - 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs index 9137f11667..a49d4c119d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; #if NETFRAMEWORK using System.Security.Permissions; @@ -904,18 +905,6 @@ private void RunExecuteNonQueryTdsSetupReconnnectContinuation( }); } - private void SetCachedCommandExecuteNonQueryAsyncContext(ExecuteNonQueryAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) - { - // @TODO: Add this to SqlInternalConnection - Interlocked.CompareExchange( - ref sqlInternalConnection.CachedCommandExecuteNonQueryAsyncContext, - instance, - comparand: null); - } - } - #endregion internal sealed class ExecuteNonQueryAsyncCallContext @@ -939,7 +928,11 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { - owner?.SetCachedCommandExecuteNonQueryAsyncContext(this); + DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; + if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetCommandExecuteNonQueryAsyncContext(this); + } } protected override void Clear() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index bc10c57e12..b207038989 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -28,7 +28,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal /// protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); - internal SqlCommand.ExecuteNonQueryAsyncCallContext CachedCommandExecuteNonQueryAsyncContext; internal SqlCommand.ExecuteXmlReaderAsyncCallContext CachedCommandExecuteXmlReaderAsyncContext; internal SqlDataReader.Snapshot CachedDataReaderSnapshot; internal SqlDataReader.IsDBNullAsyncCallContext CachedDataReaderIsDBNullContext; From 0a45c97d7ac57b8242c5c6bb5eab6ea7854953c6 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 22:34:53 -0600 Subject: [PATCH 18/34] Migrate usages of CachedCommandExecuteXmlReaderAsyncContext to the new class --- .../Data/SqlClient/SqlCommand.Xml.cs | 23 ++++++------------- .../Data/SqlClient/SqlInternalConnection.cs | 1 - 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs index 0fdc4d9d14..099122c39f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.Server; #if NETFRAMEWORK @@ -488,9 +489,7 @@ private Task InternalExecuteXmlReaderAsync(CancellationToken cancella ExecuteXmlReaderAsyncCallContext context = null; if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - context = Interlocked.Exchange( - ref sqlInternalConnection.CachedCommandExecuteXmlReaderAsyncContext, - null); + context = sqlInternalConnection.CachedContexts.ClearCommandExecuteXmlReaderAsyncContext(); } context ??= new ExecuteXmlReaderAsyncCallContext(); @@ -546,18 +545,6 @@ private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken sender: this, () => InternalExecuteXmlReaderAsync(cancellationToken), cancellationToken); - - private void SetCachedCommandExecuteXmlReaderContext(ExecuteXmlReaderAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) - { - // @TODO: Move this compare exchange into the SqlInternalConnection class (or better yet, do away with this context) - Interlocked.CompareExchange( - ref sqlInternalConnection.CachedCommandExecuteXmlReaderAsyncContext, - instance, - comparand: null); - } - } #endregion @@ -582,7 +569,11 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { - owner?.SetCachedCommandExecuteXmlReaderContext(this); + DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; + if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetCommandExecuteXmlReaderAsyncContext(this); + } } protected override void Clear() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index b207038989..04510403c2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -28,7 +28,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal /// protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); - internal SqlCommand.ExecuteXmlReaderAsyncCallContext CachedCommandExecuteXmlReaderAsyncContext; internal SqlDataReader.Snapshot CachedDataReaderSnapshot; internal SqlDataReader.IsDBNullAsyncCallContext CachedDataReaderIsDBNullContext; internal SqlDataReader.ReadAsyncCallContext CachedDataReaderReadAsyncContext; From 7b5a637f0c284fa5252adac095ca4fd0363c30b8 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 22:42:59 -0600 Subject: [PATCH 19/34] Migrate usage of ReadAsyncCallContext to the new class --- .../Data/SqlClient/Connection/CachedContexts.cs | 8 +++++++- .../Microsoft/Data/SqlClient/SqlDataReader.cs | 16 ++++++---------- .../Data/SqlClient/SqlInternalConnection.cs | 1 - 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs index b78d0624de..e5746d732e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -17,7 +17,7 @@ internal class CachedContexts private SqlCommand.ExecuteReaderAsyncCallContext? _commandExecuteReaderAsyncContext; private SqlCommand.ExecuteXmlReaderAsyncCallContext? _commandExecuteXmlReaderAsyncContext; // private SqlDataReader.IsDBNullAsyncCallContext? _dataReaderIsDbNullContext; - // private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; + private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; // private SqlDataReader.Snapshot? _dataReaderSnapshot; #endregion @@ -33,6 +33,9 @@ internal class CachedContexts internal SqlCommand.ExecuteXmlReaderAsyncCallContext? ClearCommandExecuteXmlReaderAsyncContext() => Interlocked.Exchange(ref _commandExecuteXmlReaderAsyncContext, null); + internal SqlDataReader.ReadAsyncCallContext? ClearDataReaderReadAsyncContext() => + Interlocked.Exchange(ref _dataReaderReadAsyncContext, null); + internal bool TrySetCommandExecuteNonQueryAsyncContext(SqlCommand.ExecuteNonQueryAsyncCallContext value) => TrySetContext(value, ref _commandExecuteNonQueryAsyncContext); @@ -42,6 +45,9 @@ internal bool TrySetCommandExecuteReaderAsyncContext(SqlCommand.ExecuteReaderAsy internal bool TrySetCommandExecuteXmlReaderAsyncContext(SqlCommand.ExecuteXmlReaderAsyncCallContext value) => TrySetContext(value, ref _commandExecuteXmlReaderAsyncContext); + internal bool TrySetDataReaderReadAsyncContext(SqlDataReader.ReadAsyncCallContext value) => + TrySetContext(value, ref _dataReaderReadAsyncContext); + #endregion private static bool TrySetContext(TContext value, ref TContext? location) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 357c044de3..9d0e301040 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -5040,7 +5040,7 @@ public override Task ReadAsync(CancellationToken cancellationToken) ReadAsyncCallContext context = null; if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, null); + context = sqlInternalConnection.CachedContexts.ClearDataReaderReadAsyncContext(); } if (context is null) { @@ -5106,14 +5106,6 @@ private static Task ReadAsyncExecute(Task task, object state) return reader.ExecuteAsyncCall(context); } - private void SetCachedReadAsyncCallContext(ReadAsyncCallContext instance) - { - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) - { - Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, instance, null); - } - } - /// override public Task IsDBNullAsync(int i, CancellationToken cancellationToken) { @@ -5503,7 +5495,11 @@ internal ReadAsyncCallContext() protected override void AfterCleared(SqlDataReader owner) { - owner.SetCachedReadAsyncCallContext(this); + DbConnectionInternal internalConnection = owner?._connection?.InnerConnection; + if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetDataReaderReadAsyncContext(this); + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 04510403c2..c279395613 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -30,7 +30,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal internal SqlDataReader.Snapshot CachedDataReaderSnapshot; internal SqlDataReader.IsDBNullAsyncCallContext CachedDataReaderIsDBNullContext; - internal SqlDataReader.ReadAsyncCallContext CachedDataReaderReadAsyncContext; /// /// Constructs a new SqlInternalConnection object using the provided connection options. From ba4c7dfc8d0e164d1336ba0f5be24cecb9647753 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 22:50:41 -0600 Subject: [PATCH 20/34] Migrate usage of IsDBNullAsyncCallContext to the new class. --- .../Data/SqlClient/Connection/CachedContexts.cs | 9 +++++++-- .../Microsoft/Data/SqlClient/SqlDataReader.cs | 16 ++++++---------- .../Data/SqlClient/SqlInternalConnection.cs | 1 - 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs index e5746d732e..5a94b2b8d2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -16,9 +16,8 @@ internal class CachedContexts private SqlCommand.ExecuteNonQueryAsyncCallContext? _commandExecuteNonQueryAsyncContext; private SqlCommand.ExecuteReaderAsyncCallContext? _commandExecuteReaderAsyncContext; private SqlCommand.ExecuteXmlReaderAsyncCallContext? _commandExecuteXmlReaderAsyncContext; - // private SqlDataReader.IsDBNullAsyncCallContext? _dataReaderIsDbNullContext; + private SqlDataReader.IsDBNullAsyncCallContext? _dataReaderIsDbNullContext; private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; - // private SqlDataReader.Snapshot? _dataReaderSnapshot; #endregion @@ -36,6 +35,9 @@ internal class CachedContexts internal SqlDataReader.ReadAsyncCallContext? ClearDataReaderReadAsyncContext() => Interlocked.Exchange(ref _dataReaderReadAsyncContext, null); + internal SqlDataReader.IsDBNullAsyncCallContext? ClearDataReaderIsDbNullContext() => + Interlocked.Exchange(ref _dataReaderIsDbNullContext, null); + internal bool TrySetCommandExecuteNonQueryAsyncContext(SqlCommand.ExecuteNonQueryAsyncCallContext value) => TrySetContext(value, ref _commandExecuteNonQueryAsyncContext); @@ -48,6 +50,9 @@ internal bool TrySetCommandExecuteXmlReaderAsyncContext(SqlCommand.ExecuteXmlRea internal bool TrySetDataReaderReadAsyncContext(SqlDataReader.ReadAsyncCallContext value) => TrySetContext(value, ref _dataReaderReadAsyncContext); + internal bool TrySetDataReaderIsDbNullContext(SqlDataReader.IsDBNullAsyncCallContext value) => + TrySetContext(value, ref _dataReaderIsDbNullContext); + #endregion private static bool TrySetContext(TContext value, ref TContext? location) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 9d0e301040..2ee373544a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -5208,7 +5208,7 @@ override public Task IsDBNullAsync(int i, CancellationToken cancellationTo IsDBNullAsyncCallContext context = null; if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, null); + context = sqlInternalConnection.CachedContexts.ClearDataReaderIsDbNullContext(); } if (context is null) { @@ -5249,14 +5249,6 @@ private static Task IsDBNullAsyncExecute(Task task, object state) } } - private void SetCachedIDBNullAsyncCallContext(IsDBNullAsyncCallContext instance) - { - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) - { - Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, instance, null); - } - } - /// override public Task GetFieldValueAsync(int i, CancellationToken cancellationToken) { @@ -5515,7 +5507,11 @@ internal IsDBNullAsyncCallContext() { } protected override void AfterCleared(SqlDataReader owner) { - owner.SetCachedIDBNullAsyncCallContext(this); + DbConnectionInternal internalConnection = owner?._connection?.InnerConnection; + if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetDataReaderIsDbNullContext(this); + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index c279395613..2b97b20571 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -29,7 +29,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); internal SqlDataReader.Snapshot CachedDataReaderSnapshot; - internal SqlDataReader.IsDBNullAsyncCallContext CachedDataReaderIsDBNullContext; /// /// Constructs a new SqlInternalConnection object using the provided connection options. From a0a1f2467e8411ebcc398ef46f434f7326fc9981 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 11:53:31 -0600 Subject: [PATCH 21/34] Comments on CachedContexts --- .../SqlClient/Connection/CachedContexts.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs index 5a94b2b8d2..4571cbf98a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -9,47 +9,124 @@ namespace Microsoft.Data.SqlClient.Connection { + /// + /// Provides cached asynchronous call contexts shared between objects in a connection's context. + /// internal class CachedContexts { #region Fields + /// + /// Stores reusable context for ExecuteNonQueryAsync invocations. + /// private SqlCommand.ExecuteNonQueryAsyncCallContext? _commandExecuteNonQueryAsyncContext; + + /// + /// Stores reusable context for ExecuteReaderAsync invocations. + /// private SqlCommand.ExecuteReaderAsyncCallContext? _commandExecuteReaderAsyncContext; + + /// + /// Stores reusable context for ExecuteXmlReaderAsync invocations. + /// private SqlCommand.ExecuteXmlReaderAsyncCallContext? _commandExecuteXmlReaderAsyncContext; + + /// + /// Stores reusable context for IsDBNullAsync invocations. + /// private SqlDataReader.IsDBNullAsyncCallContext? _dataReaderIsDbNullContext; + + /// + /// Stores reusable context for ReadAsync invocations. + /// private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; #endregion #region Access Methods + /// + /// Removes and returns the cached ExecuteNonQueryAsync context. + /// + /// The previously cached context or null when empty. internal SqlCommand.ExecuteNonQueryAsyncCallContext? ClearCommandExecuteNonQueryAsyncContext() => Interlocked.Exchange(ref _commandExecuteNonQueryAsyncContext, null); + /// + /// Removes and returns the cached ExecuteReaderAsync context. + /// + /// The previously cached context or null when empty. internal SqlCommand.ExecuteReaderAsyncCallContext? ClearCommandExecuteReaderAsyncContext() => Interlocked.Exchange(ref _commandExecuteReaderAsyncContext, null); + /// + /// Removes and returns the cached ExecuteXmlReaderAsync context. + /// + /// The previously cached context or null when empty. internal SqlCommand.ExecuteXmlReaderAsyncCallContext? ClearCommandExecuteXmlReaderAsyncContext() => Interlocked.Exchange(ref _commandExecuteXmlReaderAsyncContext, null); + /// + /// Removes and returns the cached ReadAsync context. + /// + /// The previously cached context or null when empty. internal SqlDataReader.ReadAsyncCallContext? ClearDataReaderReadAsyncContext() => Interlocked.Exchange(ref _dataReaderReadAsyncContext, null); + /// + /// Removes and returns the cached IsDBNullAsync context. + /// + /// The previously cached context or null when empty. internal SqlDataReader.IsDBNullAsyncCallContext? ClearDataReaderIsDbNullContext() => Interlocked.Exchange(ref _dataReaderIsDbNullContext, null); + /// + /// Attempts to cache the provided ExecuteNonQueryAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// internal bool TrySetCommandExecuteNonQueryAsyncContext(SqlCommand.ExecuteNonQueryAsyncCallContext value) => TrySetContext(value, ref _commandExecuteNonQueryAsyncContext); + /// + /// Attempts to cache the provided ExecuteReaderAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// internal bool TrySetCommandExecuteReaderAsyncContext(SqlCommand.ExecuteReaderAsyncCallContext value) => TrySetContext(value, ref _commandExecuteReaderAsyncContext); + /// + /// Attempts to cache the provided ExecuteXmlReaderAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// internal bool TrySetCommandExecuteXmlReaderAsyncContext(SqlCommand.ExecuteXmlReaderAsyncCallContext value) => TrySetContext(value, ref _commandExecuteXmlReaderAsyncContext); + /// + /// Attempts to cache the provided ReadAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// internal bool TrySetDataReaderReadAsyncContext(SqlDataReader.ReadAsyncCallContext value) => TrySetContext(value, ref _dataReaderReadAsyncContext); + /// + /// Attempts to cache the provided IsDBNullAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// internal bool TrySetDataReaderIsDbNullContext(SqlDataReader.IsDBNullAsyncCallContext value) => TrySetContext(value, ref _dataReaderIsDbNullContext); From 41b7f2a9bca2fe05627de16aaff83f7daf9f146c Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 12:32:54 -0600 Subject: [PATCH 22/34] Sure why not move the reader snapshot into the CachedContexts class? the usages of it are basically doing the same thing --- .../SqlClient/Connection/CachedContexts.cs | 22 +++++++++++++++++++ .../Microsoft/Data/SqlClient/SqlDataReader.cs | 22 +++++++++++-------- .../Data/SqlClient/SqlInternalConnection.cs | 2 -- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs index 4571cbf98a..9d9de46ac1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -41,6 +41,11 @@ internal class CachedContexts /// private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; + /// + /// Stores a data reader snapshot. + /// + private SqlDataReader.Snapshot? _dataReaderSnapshot; + #endregion #region Access Methods @@ -80,6 +85,13 @@ internal class CachedContexts internal SqlDataReader.IsDBNullAsyncCallContext? ClearDataReaderIsDbNullContext() => Interlocked.Exchange(ref _dataReaderIsDbNullContext, null); + /// + /// Removes and returns the cached data reader snapshot. + /// + /// The previously cached snapshot or null when empty. + internal SqlDataReader.Snapshot? ClearDataReaderSnapshot() => + Interlocked.Exchange(ref _dataReaderSnapshot, null); + /// /// Attempts to cache the provided ExecuteNonQueryAsync context. /// @@ -130,6 +142,16 @@ internal bool TrySetDataReaderReadAsyncContext(SqlDataReader.ReadAsyncCallContex internal bool TrySetDataReaderIsDbNullContext(SqlDataReader.IsDBNullAsyncCallContext value) => TrySetContext(value, ref _dataReaderIsDbNullContext); + /// + /// Attempts to cache the provided data reader snapshot context. + /// + /// Context instance to store. + /// + /// True when the snapshot is cached; false if an existing snapshot is preserved. + /// + internal bool TrySetDataReaderSnapshot(SqlDataReader.Snapshot value) => + TrySetContext(value, ref _dataReaderSnapshot); + #endregion private static bool TrySetContext(TContext value, ref TContext? location) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 2ee373544a..2d94822099 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -4101,10 +4101,11 @@ internal TdsOperationStatus TryReadColumnInternal(int i, bool readHeaderOnly = f { // reset snapshot to save memory use. We can safely do that here because all SqlDataReader values are stable. // The retry logic can use the current values to get back to the right state. - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } + _snapshot = null; PrepareAsyncInvocation(useSnapshot: true); } @@ -5085,10 +5086,11 @@ private static Task ReadAsyncExecute(Task task, object state) if (!hasReadRowToken) { hasReadRowToken = true; - if (reader.Connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (reader.Connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = reader._snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(reader._snapshot); } + reader._snapshot = null; reader.PrepareAsyncInvocation(useSnapshot: true); } @@ -5794,7 +5796,7 @@ private void PrepareAsyncInvocation(bool useSnapshot) { if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - _snapshot = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderSnapshot, null) ?? new Snapshot(); + _snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot(); } else { @@ -5872,10 +5874,11 @@ private void CleanupAfterAsyncInvocationInternal(TdsParserStateObject stateObj, stateObj._permitReplayStackTraceToDiffer = false; #endif - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } + // We are setting this to null inside the if-statement because stateObj==null means that the reader hasn't been initialized or has been closed (either way _snapshot should already be null) _snapshot = null; } @@ -5914,10 +5917,11 @@ private void SwitchToAsyncWithoutSnapshot() Debug.Assert(_snapshot != null, "Should currently have a snapshot"); Debug.Assert(_stateObj != null && !_stateObj._asyncReadWithoutSnapshot, "Already in async without snapshot"); - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } + _snapshot = null; _stateObj.ResetSnapshot(); _stateObj._asyncReadWithoutSnapshot = true; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 2b97b20571..9b75f2811b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -28,8 +28,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal /// protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); - internal SqlDataReader.Snapshot CachedDataReaderSnapshot; - /// /// Constructs a new SqlInternalConnection object using the provided connection options. /// From 1b52999ee7efa411ab83b0c3f5d6cd9d8b61ebf0 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 12:53:54 -0600 Subject: [PATCH 23/34] Move _whereAbouts and s_globalTransactionTMID --- .../Data/SqlClient/SqlInternalConnection.cs | 10 ---------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 9b75f2811b..bb3029edc3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -18,16 +18,6 @@ namespace Microsoft.Data.SqlClient { internal abstract class SqlInternalConnection : DbConnectionInternal { - /// - /// Cache the whereabouts (DTC Address) for exporting. - /// - protected byte[] _whereAbouts; - - /// - /// ID of the Azure SQL DB Transaction Manager (Non-MSDTC) - /// - protected static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); - /// /// Constructs a new SqlInternalConnection object using the provided connection options. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c3b6d1b31a..4e4b4a2527 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -58,7 +58,14 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable /// same. /// // @TODO: Rename to match naming conventions (s_camelCase or PascalCase) - private static readonly TimeSpan _dbAuthenticationContextUnLockedRefreshTimeSpan = new TimeSpan(hours: 0, minutes: 10, seconds: 00); + private static readonly TimeSpan _dbAuthenticationContextUnLockedRefreshTimeSpan = + new TimeSpan(hours: 0, minutes: 10, seconds: 00); + + /// + /// ID of the Azure SQL DB Transaction Manager (Non-MSDTC) + /// + // @TODO: Rename to a match naming conventions (s_camelCase or PascalCase) + private static readonly Guid s_globalTransactionTMID = new("1C742CAF-6680-40EA-9C26-6B6846079764"); private static readonly HashSet s_transientErrors = [ @@ -307,6 +314,12 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable private readonly SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; + /// + /// Cache the whereabouts (DTC Address) for exporting. + /// + // @TODO: This name ... doesn't make a whole lot of sense. + private byte[] _whereAbouts; + #endregion #region Constructors From 8fb7b0e2cab2c5292c46850ae16679725ad61e23 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 12:56:15 -0600 Subject: [PATCH 24/34] Merge constructors --- .../Microsoft/Data/SqlClient/SqlInternalConnection.cs | 10 ---------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index bb3029edc3..2aa0d2838b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -18,16 +18,6 @@ namespace Microsoft.Data.SqlClient { internal abstract class SqlInternalConnection : DbConnectionInternal { - /// - /// Constructs a new SqlInternalConnection object using the provided connection options. - /// - /// The options to use for this connection. - internal SqlInternalConnection(SqlConnectionString connectionOptions) : base() - { - Debug.Assert(connectionOptions != null, "null connectionOptions?"); - ConnectionOptions = connectionOptions; - } - #region Properties // SQLBU 415870 diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 4e4b4a2527..1716677be7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,8 +346,12 @@ internal SqlInternalConnectionTds( string accessToken = null, IDbConnectionPool pool = null, Func> accessTokenCallback = null, - SspiContextProvider sspiContextProvider = null) : base(connectionOptions) + SspiContextProvider sspiContextProvider = null) { + Debug.Assert(connectionOptions is not null, "null connectionOptions"); + + ConnectionOptions = connectionOptions; + #if DEBUG if (reconnectSessionData != null) { From 0e78a6eb76e5a2112882d27ae89fe054642ad01f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 13:08:54 -0600 Subject: [PATCH 25/34] Merge AvailableInternalTransaction, CurrentTransaction, HasLocalTransaction, HasLocalTransactionFromAPI --- .../Data/SqlClient/SqlInternalConnection.cs | 41 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 38 ++++++++++++++--- .../Data/SqlClient/SqlTransaction.cs | 3 +- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 2aa0d2838b..59b9d889c2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -20,15 +20,6 @@ internal abstract class SqlInternalConnection : DbConnectionInternal { #region Properties - // SQLBU 415870 - // Get the internal transaction that should be hooked to a new outer transaction - // during a BeginTransaction API call. In some cases (i.e. connection is going to - // be reset), CurrentTransaction should not be hooked up this way. - /// - /// TODO: need to understand this property better - /// - virtual internal SqlInternalTransaction AvailableInternalTransaction => CurrentTransaction; - /// /// A reference to the SqlConnection that owns this internal connection. /// @@ -55,43 +46,11 @@ internal abstract class SqlInternalConnection : DbConnectionInternal /// internal string CurrentDataSource { get; set; } - /// - /// The Transaction currently associated with this connection. - /// - abstract internal SqlInternalTransaction CurrentTransaction { get; } - /// /// The delegated (or promoted) transaction this connection is responsible for. /// internal SqlDelegatedTransaction DelegatedTransaction { get; set; } - /// - /// Whether this connection has a local (non-delegated) transaction. - /// - internal bool HasLocalTransaction - { - get - { - SqlInternalTransaction currentTransaction = CurrentTransaction; - bool result = currentTransaction != null && currentTransaction.IsLocal; - return result; - } - } - - /// - /// Whether this connection has a local transaction started from the API (i.e., SqlConnection.BeginTransaction) - /// or had a TSQL transaction and later got wrapped by an API transaction. - /// - internal bool HasLocalTransactionFromAPI - { - get - { - SqlInternalTransaction currentTransaction = CurrentTransaction; - bool result = currentTransaction != null && currentTransaction.HasParentTransaction; - return result; - } - } - /// /// Whether the server version is SQL Server 2008 or newer. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 1716677be7..c55dd4f886 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -505,11 +505,6 @@ public override string ServerVersion get => $"{_loginAck.majorVersion:00}.{(short)_loginAck.minorVersion:00}.{_loginAck.buildNum:0000}"; } - internal override SqlInternalTransaction AvailableInternalTransaction - { - get => _parser._fResetConnection ? null : CurrentTransaction; - } - /// /// Gets the collection of async call contexts that belong to this connection. /// @@ -534,11 +529,32 @@ internal SessionData CurrentSessionData } } - internal override SqlInternalTransaction CurrentTransaction + /// + /// The Transaction currently associated with this connection. + /// + internal SqlInternalTransaction CurrentTransaction { get => _parser.CurrentTransaction; } + /// + /// Whether this connection has a local (non-delegated) transaction. + /// + internal bool HasLocalTransaction + { + get => CurrentTransaction?.IsLocal == true; + } + + /// + /// Whether this connection has a local transaction started from the API (i.e., + /// SqlConnection.BeginTransaction) or had a TSQL transaction and later got wrapped by an + /// API transaction. + /// + internal bool HasLocalTransactionFromAPI + { + get => CurrentTransaction?.HasParentTransaction == true; + } + // @TODO: Make auto-property internal DbConnectionPoolIdentity Identity { @@ -723,6 +739,16 @@ private int accessTokenExpirationBufferTime : ConnectionOptions.ConnectTimeout; } + // SQLBU 415870 + // Get the internal transaction that should be hooked to a new outer transaction + // during a BeginTransaction API call. In some cases (i.e. connection is going to + // be reset), CurrentTransaction should not be hooked up this way. + // TODO: (mdaigle) need to understand this property better + private SqlInternalTransaction AvailableInternalTransaction + { + get => _parser._fResetConnection ? null : CurrentTransaction; + } + #endregion #region Public and Internal Methods diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs index 35f52c4ff4..10b1cb6b75 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs @@ -48,7 +48,8 @@ internal SqlTransaction( } else { - Debug.Assert(internalConnection.CurrentTransaction == internalTransaction, "Unexpected Parser.CurrentTransaction state!"); + Debug.Assert(((SqlInternalConnectionTds)internalConnection).CurrentTransaction == internalTransaction, + "Unexpected Parser.CurrentTransaction state!"); InternalTransaction = internalTransaction; InternalTransaction.InitParent(this); } From 858d98e32a397ac7e744b514926f8971aa48063d Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 13:24:34 -0600 Subject: [PATCH 26/34] Merge Connection, ConnectionOptions, CurrentDatabase, CurrentDataSource, DelegatedTransaction, Is2008OrNewer, IsAzureSqlConnection, IsEnlistedInTransaction, IsGlobalTransaction, IsGlobalTransactionEnabledForServer, IsLockedForBulkCopy, IsTransactionRoot, PromotedDtcToken --- .../Data/SqlClient/SqlInternalConnection.cs | 78 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 77 +++++++++++++++++- 2 files changed, 74 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index 59b9d889c2..022dc82d3d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -20,85 +20,7 @@ internal abstract class SqlInternalConnection : DbConnectionInternal { #region Properties - /// - /// A reference to the SqlConnection that owns this internal connection. - /// - internal SqlConnection Connection => (SqlConnection)Owner; - /// - /// The connection options to be used for this connection. - /// - internal SqlConnectionString ConnectionOptions { get; init; } - - /// - /// The current database for this connection. - /// Null if the connection is not open yet. - /// - internal string CurrentDatabase { get; set; } - - /// - /// The current data source for this connection. - /// - /// if connection is not open yet, CurrentDataSource is null - /// if connection is open: - /// * for regular connections, it is set to the Data Source value from connection string - /// * for failover connections, it is set to the FailoverPartner value from the connection string - /// - internal string CurrentDataSource { get; set; } - - /// - /// The delegated (or promoted) transaction this connection is responsible for. - /// - internal SqlDelegatedTransaction DelegatedTransaction { get; set; } - - /// - /// Whether the server version is SQL Server 2008 or newer. - /// - abstract internal bool Is2008OrNewer { get; } - - /// - /// Whether this connection is to an Azure SQL Database. - /// - internal bool IsAzureSqlConnection { get; set; } - - /// - /// Indicates whether the connection is currently enlisted in a transaction. - /// - internal bool IsEnlistedInTransaction { get; set; } - - /// - /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) - /// TODO: overlaps with IsGlobalTransactionsEnabledForServer, need to consolidate to avoid bugs - /// - internal bool IsGlobalTransaction { get; set; } - - /// - /// Whether Global Transactions are enabled. Only supported by Azure SQL. - /// False if disabled or connected to on-prem SQL Server. - /// - internal bool IsGlobalTransactionsEnabledForServer { get; set; } - - /// - /// Whether this connection is locked for bulk copy operations. - /// - abstract internal bool IsLockedForBulkCopy { get; } - - /// - /// Whether this connection is the root of a delegated or promoted transaction. - /// - override internal bool IsTransactionRoot - { - get - { - SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction; - return delegatedTransaction != null && (delegatedTransaction.IsActive); - } - } - - /// - /// A token returned by the server when we promote transaction. - /// - internal byte[] PromotedDtcToken { get; set; } #endregion } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c55dd4f886..6ae819ded7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -516,6 +516,32 @@ internal Guid ClientConnectionId get => _clientConnectionId; } + /// + /// A reference to the SqlConnection that owns this internal connection. + /// + internal SqlConnection Connection => (SqlConnection)Owner; + + /// + /// The connection options to be used for this connection. + /// + internal SqlConnectionString ConnectionOptions { get; } + + /// + /// The current database for this connection. Null if the connection is not open yet. + /// + internal string CurrentDatabase { get; private set; } + + /// + /// The current data source for this connection. + /// + /// + /// If connection is not open yet, CurrentDataSource is null + /// If connection is open: + /// * for regular connections, it is set to the Data Source value from connection string + /// * for failover connections, it is set to the FailoverPartner value from the connection string + /// + internal string CurrentDataSource { get; set; } + internal SessionData CurrentSessionData { get @@ -537,6 +563,11 @@ internal SqlInternalTransaction CurrentTransaction get => _parser.CurrentTransaction; } + /// + /// The delegated (or promoted) transaction this connection is responsible for. + /// + internal SqlDelegatedTransaction DelegatedTransaction { get; set; } + /// /// Whether this connection has a local (non-delegated) transaction. /// @@ -576,7 +607,7 @@ internal string InstanceName get => _instanceName; } - internal override bool Is2008OrNewer + internal bool Is2008OrNewer { get => _parser.Is2008OrNewer; } @@ -592,7 +623,8 @@ internal override bool IsAccessTokenExpired } /// - /// Get or set if the control ring send redirect token and feature ext ack with true for DNSCaching + /// Get or set if the control ring send redirect token and feature ext ack with true for + /// DNSCaching. /// /// @TODO: Make auto-property internal bool IsDNSCachingBeforeRedirectSupported @@ -601,7 +633,27 @@ internal bool IsDNSCachingBeforeRedirectSupported set => _dnsCachingBeforeRedirect = value; } - internal override bool IsLockedForBulkCopy + /// + /// Indicates whether the connection is currently enlisted in a transaction. + /// + internal bool IsEnlistedInTransaction { get; set; } + + /// + /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) + /// TODO: overlaps with IsGlobalTransactionsEnabledForServer, need to consolidate to avoid bugs + /// + internal bool IsGlobalTransaction { get; private set; } + + /// + /// Whether Global Transactions are enabled. Only supported by Azure SQL. False if disabled + /// or connected to on-prem SQL Server. + /// + internal bool IsGlobalTransactionsEnabledForServer { get; private set; } + + /// + /// Whether this connection is locked for bulk copy operations. + /// + internal bool IsLockedForBulkCopy { get => !_parser.MARSOn && _parser._physicalStateObj.BcpLock; } @@ -626,6 +678,14 @@ internal bool IsSQLDNSRetryEnabled set => _SQLDNSRetryEnabled = value; } + /// + /// Whether this connection is the root of a delegated or promoted transaction. + /// + internal override bool IsTransactionRoot + { + get => DelegatedTransaction?.IsActive == true; + } + // @TODO: Make auto-property internal Guid OriginalClientConnectionId { @@ -658,6 +718,11 @@ internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo get => _poolGroupProviderInfo; } + /// + /// A token returned by the server when we promote transaction. + /// + internal byte[] PromotedDtcToken { get; private set; } + // @TODO: Make auto-property internal string RoutingDestination { @@ -749,6 +814,12 @@ private SqlInternalTransaction AvailableInternalTransaction get => _parser._fResetConnection ? null : CurrentTransaction; } + /// + /// Whether this connection is to an Azure SQL Database. + /// + // @TODO: Make private field. + private bool IsAzureSqlConnection { get; set; } + #endregion #region Public and Internal Methods From 6dfbd5db3de0c5f5e786943f195472541dcdc0b4 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 13:28:55 -0600 Subject: [PATCH 27/34] Remove SqlInternalConnection --- .../src/Microsoft.Data.SqlClient.csproj | 3 --- .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 --- .../Data/SqlClient/SqlDelegatedTransaction.cs | 4 +-- .../Data/SqlClient/SqlInternalConnection.cs | 27 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 2 +- .../Data/SqlClient/SqlTransaction.cs | 6 ++--- 6 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 15beeb6e9f..ddb86aa4b3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -702,9 +702,6 @@ Microsoft\Data\SqlClient\SqlInfoMessageEventHandler.cs - - Microsoft\Data\SqlClient\SqlInternalConnection.cs - Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 499792bc91..7d08296d90 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -866,9 +866,6 @@ Microsoft\Data\SqlClient\SqlInfoMessageEventHandler.cs - - Microsoft\Data\SqlClient\SqlInternalConnection.cs - Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs index d23bc4bd9f..fcef0af514 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs @@ -32,10 +32,10 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati private bool _active; // Is the transaction active? - internal SqlDelegatedTransaction(SqlInternalConnection connection, Transaction tx) + internal SqlDelegatedTransaction(SqlInternalConnectionTds connection, Transaction tx) { Debug.Assert(connection != null, "null connection?"); - _connection = (SqlInternalConnectionTds)connection; + _connection = connection; _atomicTransaction = tx; _active = false; System.Transactions.IsolationLevel systxIsolationLevel = tx.IsolationLevel; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs deleted file mode 100644 index 022dc82d3d..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ /dev/null @@ -1,27 +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.Common; -using System.Diagnostics; -using System.Transactions; -using Microsoft.Data.Common; -using Microsoft.Data.ProviderBase; - -#if NETFRAMEWORK -using System.Runtime.CompilerServices; -using System.Runtime.ConstrainedExecution; -#endif - -namespace Microsoft.Data.SqlClient -{ - internal abstract class SqlInternalConnection : DbConnectionInternal - { - #region Properties - - - - #endregion - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6ae819ded7..5fde6de773 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -24,7 +24,7 @@ namespace Microsoft.Data.SqlClient { - internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable + internal class SqlInternalConnectionTds : DbConnectionInternal, IDisposable { #region Constants diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs index 10b1cb6b75..c5ccbf5798 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs @@ -28,7 +28,7 @@ public sealed class SqlTransaction : DbTransaction private bool _isFromApi; internal SqlTransaction( - SqlInternalConnection internalConnection, + SqlInternalConnectionTds internalConnection, SqlConnection con, IsolationLevel iso, SqlInternalTransaction internalTransaction) @@ -42,13 +42,13 @@ internal SqlTransaction( if (internalTransaction == null) { InternalTransaction = new SqlInternalTransaction( - (SqlInternalConnectionTds)internalConnection, + internalConnection, TransactionType.LocalFromAPI, this); } else { - Debug.Assert(((SqlInternalConnectionTds)internalConnection).CurrentTransaction == internalTransaction, + Debug.Assert(internalConnection.CurrentTransaction == internalTransaction, "Unexpected Parser.CurrentTransaction state!"); InternalTransaction = internalTransaction; InternalTransaction.InitParent(this); From a83bd7ca4962c5d16bc5281a0c6331f21615d3eb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 13:33:48 -0600 Subject: [PATCH 28/34] Move SqlInternalConnectionTds.cs to SqlConnectionInternal.cs --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 6 +++--- .../netfx/src/Microsoft.Data.SqlClient.csproj | 6 +++--- .../SqlConnectionInternal.cs} | 0 3 files changed, 6 insertions(+), 6 deletions(-) rename src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/{SqlInternalConnectionTds.cs => Connection/SqlConnectionInternal.cs} (100%) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index ddb86aa4b3..80a22d6385 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -222,6 +222,9 @@ Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs + + Microsoft\Data\SqlClient\Connection\SqlConnectionInternal.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs @@ -702,9 +705,6 @@ Microsoft\Data\SqlClient\SqlInfoMessageEventHandler.cs - - Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs - Microsoft\Data\SqlClient\SqlInternalTransaction.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 7d08296d90..703c7ca1f3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -491,6 +491,9 @@ Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs + + Microsoft\Data\SqlClient\Connection\SqlConnectionInternal.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs @@ -866,9 +869,6 @@ Microsoft\Data\SqlClient\SqlInfoMessageEventHandler.cs - - Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs - Microsoft\Data\SqlClient\SqlInternalTransaction.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs similarity index 100% rename from src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs From c4db0556aae8905dd7d94651e1c98f048d597d4b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 13:51:27 -0600 Subject: [PATCH 29/34] Renane Microsoft.Data.SqlClient.SqlInternalConnectionTds to Microsoft.Data.SqlClient.Connection.SqlInternalConnectionTds --- .../src/Microsoft/Data/Common/AdapterUtil.cs | 7 ++- .../Connection/SqlConnectionInternal.cs | 6 +-- .../Microsoft/Data/SqlClient/SqlBulkCopy.cs | 23 +++++----- .../Data/SqlClient/SqlCommand.Encryption.cs | 8 ++-- .../Data/SqlClient/SqlCommand.NonQuery.cs | 3 +- .../Data/SqlClient/SqlCommand.Reader.cs | 7 +-- .../Data/SqlClient/SqlCommand.Xml.cs | 5 ++- .../Microsoft/Data/SqlClient/SqlCommand.cs | 17 ++++---- .../Microsoft/Data/SqlClient/SqlConnection.cs | 43 +++++++++++-------- .../Data/SqlClient/SqlConnectionFactory.cs | 16 +++++-- .../Microsoft/Data/SqlClient/SqlDataReader.cs | 19 ++++---- .../Data/SqlClient/SqlDelegatedTransaction.cs | 21 ++++----- .../Microsoft/Data/SqlClient/SqlException.cs | 7 +-- .../Data/SqlClient/SqlInternalTransaction.cs | 19 +++++--- .../Data/SqlClient/SqlTransaction.cs | 5 ++- .../src/Microsoft/Data/SqlClient/SqlUtil.cs | 27 +++++++----- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 42 ++++++++++-------- 17 files changed, 161 insertions(+), 114 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index b37f97ac17..0cf70a9d0b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -25,6 +25,7 @@ using Microsoft.SqlServer.Server; using System.Security.Authentication; using System.Collections.Generic; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Reflection; @@ -465,7 +466,11 @@ internal static ArgumentException InvalidArgumentLength(string argumentName, int internal static ArgumentException MustBeReadOnly(string argumentName) => Argument(StringsHelper.GetString(Strings.ADP_MustBeReadOnly, argumentName)); - internal static Exception CreateSqlException(MsalException msalException, SqlConnectionString connectionOptions, SqlInternalConnectionTds sender, string username) + internal static Exception CreateSqlException( + MsalException msalException, + SqlConnectionString connectionOptions, + SqlConnectionInternal sender, + string username) { // Error[0] SqlErrorCollection sqlErs = new(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 5fde6de773..2b62794702 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -22,9 +22,9 @@ using Microsoft.Identity.Client; using IsolationLevel = System.Data.IsolationLevel; -namespace Microsoft.Data.SqlClient +namespace Microsoft.Data.SqlClient.Connection { - internal class SqlInternalConnectionTds : DbConnectionInternal, IDisposable + internal class SqlConnectionInternal : DbConnectionInternal, IDisposable { #region Constants @@ -332,7 +332,7 @@ internal class SqlInternalConnectionTds : DbConnectionInternal, IDisposable /// has been expanded (see SqlConnectionString.Expand) /// // @TODO: We really really need simplify what we pass into this. All these optional parameters need to go! - internal SqlInternalConnectionTds( + internal SqlConnectionInternal( DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs index 9784117a5e..39e4f570c7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -230,7 +231,7 @@ private int RowNumber private bool _hasMoreRowToCopy = false; private bool _isAsyncBulkCopy = false; private bool _isBulkCopyingInProgress = false; - private SqlInternalConnectionTds.SyncAsyncLock _parserLock = null; + private SqlConnectionInternal.SyncAsyncLock _parserLock = null; private SourceColumnMetadata[] _currentRowMetadata; @@ -1156,8 +1157,8 @@ private Task ReadFromRowSourceAsync(CancellationToken cts) } else { // This will call Read for DataRows, DataTable and IDataReader (this includes all IDataReader except DbDataReader) - // Release lock to prevent possible deadlocks - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + // Release lock to prevent possible deadlocks + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); bool semaphoreLock = internalConnection._parserLock.CanBeReleasedFromAnyThread; internalConnection._parserLock.Release(); @@ -1366,7 +1367,7 @@ private void CreateOrValidateConnection(string method) private void RunParser(BulkCopySimpleResultSet bulkCopyHandler = null) { // In case of error while reading, we should let the connection know that we already own the _parserLock - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; try @@ -1384,7 +1385,7 @@ private void RunParser(BulkCopySimpleResultSet bulkCopyHandler = null) private void RunParserReliably(BulkCopySimpleResultSet bulkCopyHandler = null) { // In case of error while reading, we should let the connection know that we already own the _parserLock - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; try { @@ -1401,7 +1402,7 @@ private void CommitTransaction() { if (_internalTransaction != null) { - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; // In case of error, let the connection know that we have the lock try { @@ -1422,7 +1423,7 @@ private void AbortTransaction() { if (!_internalTransaction.IsZombied) { - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; // In case of error, let the connection know that we have the lock try { @@ -2069,7 +2070,7 @@ private Task WriteRowSourceToServerAsync(int columnCount, CancellationToken ctok CreateOrValidateConnection(nameof(WriteToServer)); - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); Debug.Assert(_parserLock == null, "Previous parser lock not cleaned"); _parserLock = internalConnection._parserLock; @@ -2222,7 +2223,7 @@ internal void OnConnectionClosed() private bool FireRowsCopiedEvent(long rowsCopied) { // Release lock to prevent possible deadlocks - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); bool semaphoreLock = internalConnection._parserLock.CanBeReleasedFromAnyThread; internalConnection._parserLock.Release(); @@ -2578,7 +2579,7 @@ private Task CopyBatchesAsync(BulkCopySimpleResultSet internalResults, string up while (_hasMoreRowToCopy) { //pre->before every batch: Transaction, BulkCmd and metadata are done. - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); if (IsCopyOption(SqlBulkCopyOptions.UseInternalTransaction)) { //internal transaction is started prior to each batch if the Option is set. @@ -2965,7 +2966,7 @@ private void WriteToServerInternalRestAsync(CancellationToken cts, TaskCompletio _hasMoreRowToCopy = true; Task internalResultsTask = null; BulkCopySimpleResultSet internalResults = new BulkCopySimpleResultSet(); - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); try { _parser = _connection.Parser; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index cf90d29632..ee2ee1aabd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -266,8 +267,7 @@ private SqlDataReader GetParameterEncryptionDataReader( // If it is async, then TryFetchInputParameterEncryptionInfo -> // RunExecuteReaderTds would have incremented the async count. Decrement it // when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = - command._activeConnection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnectionTds = command._activeConnection.GetOpenTdsConnection(); if (internalConnectionTds is not null) { internalConnectionTds.DecrementAsyncCount(); @@ -345,7 +345,7 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync( // If it is async, then TryFetchInputParameterEncryptionInfo -> // RunExecuteReaderTds would have incremented the async count. Decrement it // when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnectionTds = _activeConnection.GetOpenTdsConnection(); if (internalConnectionTds is not null) { internalConnectionTds.DecrementAsyncCount(); @@ -769,7 +769,7 @@ private void PrepareTransparentEncryptionFinallyBlock( if (decrementAsyncCount) { // Decrement the async count - SqlInternalConnectionTds internalConnection = _activeConnection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _activeConnection.GetOpenTdsConnection(); internalConnection?.DecrementAsyncCount(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs index a49d4c119d..5d4cba4976 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Security.Permissions; @@ -929,7 +930,7 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; - if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + if (internalConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetCommandExecuteNonQueryAsyncContext(this); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs index b8d95ca159..9e13c07c6d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Security.Permissions; @@ -980,7 +981,7 @@ private Task InternalExecuteReaderAsync( { returnedTask = RegisterForConnectionCloseNotification(returnedTask); - if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { context = sqlInternalConnection.CachedContexts.ClearCommandExecuteReaderAsyncContext(); } @@ -1576,7 +1577,7 @@ private SqlDataReader RunExecuteReaderTds( if (decrementAsyncCountOnFailure) { - if (_activeConnection.InnerConnection is SqlInternalConnectionTds innerConnectionTds) + if (_activeConnection.InnerConnection is SqlConnectionInternal innerConnectionTds) { // It may be closed innerConnectionTds.DecrementAsyncCount(); @@ -1812,7 +1813,7 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; - if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + if (internalConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetCommandExecuteReaderAsyncContext(this); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs index 099122c39f..9be2713a2d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs @@ -10,6 +10,7 @@ using System.Xml; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Server; #if NETFRAMEWORK @@ -487,7 +488,7 @@ private Task InternalExecuteXmlReaderAsync(CancellationToken cancella // @TODO: This can be cleaned up to lines if InnerConnection is always SqlInternalConnection ExecuteXmlReaderAsyncCallContext context = null; - if (_activeConnection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { context = sqlInternalConnection.CachedContexts.ClearCommandExecuteXmlReaderAsyncContext(); } @@ -570,7 +571,7 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; - if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + if (internalConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetCommandExecuteXmlReaderAsyncContext(this); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 784692cb2e..f3a85723c6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Sql; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Diagnostics; #if NETFRAMEWORK @@ -958,10 +959,10 @@ private int DefaultCommandTimeout } // @TODO: Should be used in more than one place to justify its existence - private SqlInternalConnectionTds InternalTdsConnection + private SqlConnectionInternal InternalTdsConnection { // @TODO: Should check for null? Should use Connection? - get => (SqlInternalConnectionTds)_activeConnection.InnerConnection; + get => (SqlConnectionInternal)_activeConnection.InnerConnection; } private bool IsColumnEncryptionEnabled @@ -1084,7 +1085,7 @@ public override void Cancel() // Note that this model is implementable because we only allow one active command // at any one time. This code will have to change we allow multiple outstanding // batches. - if (_activeConnection?.InnerConnection is not SqlInternalConnectionTds connection) + if (_activeConnection?.InnerConnection is not SqlConnectionInternal connection) { // @TODO: Really this case only applies if the connection is null. // Fail without locking @@ -1100,7 +1101,7 @@ public override void Cancel() { // Make sure the connection did not get changed getting the connection and // taking the lock. If it has, the connection has been closed. - if (connection != _activeConnection.InnerConnection as SqlInternalConnectionTds) + if (connection != _activeConnection.InnerConnection as SqlConnectionInternal) { return; } @@ -2380,9 +2381,8 @@ private void CheckNotificationStateAndAutoEnlist() // 3) database // Obtain identity from connection. - // @TODO: Remove cast when possible. - SqlInternalConnectionTds internalConnection = - (SqlInternalConnectionTds)_activeConnection.InnerConnection; + SqlConnectionInternal internalConnection = + (SqlConnectionInternal)_activeConnection.InnerConnection; SqlDependency.IdentityUserNamePair identityUserName = internalConnection.Identity is not null ? new SqlDependency.IdentityUserNamePair( @@ -2951,8 +2951,7 @@ private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "" } // Ensure that the connection is open and that the parser is in the correct state - // @TODO: Remove cast when possible. - SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; + SqlConnectionInternal tdsConnection = _activeConnection.InnerConnection as SqlConnectionInternal; // Ensure that if column encryption override was used then server supports it // @TODO: This is kinda clunky diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs index bac785b9e7..32ba9d7148 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -817,7 +817,7 @@ public override string Database get { string result; - if (InnerConnection is SqlInternalConnectionTds innerConnection) + if (InnerConnection is SqlConnectionInternal innerConnection) { result = innerConnection.CurrentDatabase; } @@ -839,7 +839,7 @@ internal string SQLDNSCachingSupportedState { get { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; string result; if (innerConnection != null) @@ -863,7 +863,7 @@ internal string SQLDNSCachingSupportedStateBeforeRedirect { get { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; string result; if (innerConnection != null) @@ -889,7 +889,7 @@ public override string DataSource get { string result; - if (InnerConnection is SqlInternalConnectionTds innerConnection) + if (InnerConnection is SqlConnectionInternal innerConnection) { result = innerConnection.CurrentDataSource; } @@ -916,7 +916,7 @@ public int PacketSize { int result; - if (InnerConnection is SqlInternalConnectionTds innerConnection) + if (InnerConnection is SqlConnectionInternal innerConnection) { result = innerConnection.PacketSize; } @@ -938,7 +938,7 @@ public Guid ClientConnectionId { get { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; if (innerConnection != null) { @@ -1502,7 +1502,7 @@ private void DisposeMe(bool disposing) // For non-pooled connections we need to make sure that if the SqlConnection was not closed, // then we release the GCHandle on the stateObject to allow it to be GCed // For pooled connections, we will rely on the pool reclaiming the connection - var innerConnection = (InnerConnection as SqlInternalConnectionTds); + var innerConnection = (InnerConnection as SqlConnectionInternal); if ((innerConnection != null) && (!innerConnection.ConnectionOptions.Pooling)) { var parser = innerConnection.Parser; @@ -1742,7 +1742,7 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) { if (_connectRetryCount > 0) { - SqlInternalConnectionTds tdsConn = GetOpenTdsConnection(); + SqlConnectionInternal tdsConn = GetOpenTdsConnection(); if (tdsConn._sessionRecoveryAcknowledged) { TdsParserStateObject stateObj = tdsConn.Parser._physicalStateObj; @@ -1843,7 +1843,7 @@ private void RepairInnerConnection() { return; } - SqlInternalConnectionTds tdsConn = InnerConnection as SqlInternalConnectionTds; + SqlConnectionInternal tdsConn = InnerConnection as SqlConnectionInternal; if (tdsConn != null) { tdsConn.ValidateConnectionForExecute(null); @@ -2214,7 +2214,7 @@ private bool TryOpenInner(TaskCompletionSource retry) } // does not require GC.KeepAlive(this) because of ReRegisterForFinalize below. - var tdsInnerConnection = (SqlInternalConnectionTds)InnerConnection; + var tdsInnerConnection = (SqlConnectionInternal)InnerConnection; Debug.Assert(tdsInnerConnection.Parser != null, "Where's the parser?"); @@ -2326,7 +2326,7 @@ internal TdsParser Parser { get { - SqlInternalConnectionTds tdsConnection = GetOpenTdsConnection(); + SqlConnectionInternal tdsConnection = GetOpenTdsConnection(); return tdsConnection.Parser; } } @@ -2427,7 +2427,7 @@ internal void ValidateConnectionForExecute(string method, SqlCommand command) return; // execution will wait for this task later } } - SqlInternalConnectionTds innerConnection = GetOpenTdsConnection(method); + SqlConnectionInternal innerConnection = GetOpenTdsConnection(method); innerConnection.ValidateConnectionForExecute(command); } @@ -2529,9 +2529,9 @@ private void ConnectionString_Set(DbConnectionPoolKey key) } } - internal SqlInternalConnectionTds GetOpenTdsConnection() + internal SqlConnectionInternal GetOpenTdsConnection() { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; if (innerConnection == null) { throw ADP.ClosedConnectionError(); @@ -2539,9 +2539,9 @@ internal SqlInternalConnectionTds GetOpenTdsConnection() return innerConnection; } - internal SqlInternalConnectionTds GetOpenTdsConnection(string method) + internal SqlConnectionInternal GetOpenTdsConnection(string method) { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; if (innerConnection == null) { throw ADP.OpenConnectionRequired(method, InnerConnection.State); @@ -2695,10 +2695,17 @@ private static void ChangePassword(string connectionString, SqlConnectionString // note: This is the only case where we directly construct the internal connection, passing in the new password. // Normally we would simply create a regular connection and open it, but there is no other way to pass the // new password down to the constructor. This would have an unwanted impact on the connection pool. - SqlInternalConnectionTds con = null; + SqlConnectionInternal con = null; try { - con = new SqlInternalConnectionTds(null, connectionOptions, credential, null, newPassword, newSecurePassword, false); + con = new SqlConnectionInternal( + identity: null, + connectionOptions, + credential, + providerInfo: null, + newPassword, + newSecurePassword, + redirectedUserInstance: false); } finally { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index 6f697d37e8..628cc92c20 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -645,7 +645,7 @@ protected virtual DbConnectionInternal CreateConnection( if (pool == null || (pool != null && pool.Count <= 0)) { // Non-pooled or pooled and no connections in the pool. - SqlInternalConnectionTds sseConnection = null; + SqlConnectionInternal sseConnection = null; try { // We throw an exception in case of a failure @@ -653,7 +653,17 @@ protected virtual DbConnectionInternal CreateConnection( // This first connection is established to SqlExpress to get the instance name // of the UserInstance. SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false); - sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling, sspiContextProvider: key.SspiContextProvider); + sseConnection = new SqlConnectionInternal( + identity, + sseopt, + key.Credential, + providerInfo: null, + newPassword: string.Empty, + newSecurePassword: null, + redirectedUserInstance: false, + applyTransientFaultHandling: applyTransientFaultHandling, + sspiContextProvider: key.SspiContextProvider); + // NOTE: Retrieve here. This user instance name will be used below to connect to the Sql Express User Instance. instanceName = sseConnection.InstanceName; @@ -694,7 +704,7 @@ protected virtual DbConnectionInternal CreateConnection( poolGroupProviderInfo = null; // null so we do not pass to constructor below... } - return new SqlInternalConnectionTds( + return new SqlConnectionInternal( identity, opt, key.Credential, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 2d94822099..3daac5055a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -24,6 +24,7 @@ using System.Xml; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.Server; using Microsoft.Data.SqlTypes; @@ -4101,7 +4102,7 @@ internal TdsOperationStatus TryReadColumnInternal(int i, bool readHeaderOnly = f { // reset snapshot to save memory use. We can safely do that here because all SqlDataReader values are stable. // The retry logic can use the current values to get back to the right state. - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } @@ -5039,7 +5040,7 @@ public override Task ReadAsync(CancellationToken cancellationToken) } ReadAsyncCallContext context = null; - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { context = sqlInternalConnection.CachedContexts.ClearDataReaderReadAsyncContext(); } @@ -5086,7 +5087,7 @@ private static Task ReadAsyncExecute(Task task, object state) if (!hasReadRowToken) { hasReadRowToken = true; - if (reader.Connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (reader.Connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(reader._snapshot); } @@ -5208,7 +5209,7 @@ override public Task IsDBNullAsync(int i, CancellationToken cancellationTo } IsDBNullAsyncCallContext context = null; - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { context = sqlInternalConnection.CachedContexts.ClearDataReaderIsDbNullContext(); } @@ -5490,7 +5491,7 @@ internal ReadAsyncCallContext() protected override void AfterCleared(SqlDataReader owner) { DbConnectionInternal internalConnection = owner?._connection?.InnerConnection; - if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + if (internalConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetDataReaderReadAsyncContext(this); } @@ -5510,7 +5511,7 @@ internal IsDBNullAsyncCallContext() { } protected override void AfterCleared(SqlDataReader owner) { DbConnectionInternal internalConnection = owner?._connection?.InnerConnection; - if (internalConnection is SqlInternalConnectionTds sqlInternalConnection) + if (internalConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetDataReaderIsDbNullContext(this); } @@ -5794,7 +5795,7 @@ private void PrepareAsyncInvocation(bool useSnapshot) if (_snapshot == null) { - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { _snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot(); } @@ -5874,7 +5875,7 @@ private void CleanupAfterAsyncInvocationInternal(TdsParserStateObject stateObj, stateObj._permitReplayStackTraceToDiffer = false; #endif - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } @@ -5917,7 +5918,7 @@ private void SwitchToAsyncWithoutSnapshot() Debug.Assert(_snapshot != null, "Should currently have a snapshot"); Debug.Assert(_stateObj != null && !_stateObj._asyncReadWithoutSnapshot, "Already in async without snapshot"); - if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs index fcef0af514..c463b2aeb4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Transactions; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -24,7 +25,7 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati // or notifications of same. Updates to the connection's association with the transaction or to the connection pool // may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy. - private SqlInternalConnectionTds _connection; // the internal connection that is the root of the transaction + private SqlConnectionInternal _connection; // the internal connection that is the root of the transaction private System.Data.IsolationLevel _isolationLevel; // the IsolationLevel of the transaction we delegated to the server private SqlInternalTransaction _internalTransaction; // the SQL Server transaction we're delegating to @@ -32,7 +33,7 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati private bool _active; // Is the transaction active? - internal SqlDelegatedTransaction(SqlInternalConnectionTds connection, Transaction tx) + internal SqlDelegatedTransaction(SqlConnectionInternal connection, Transaction tx) { Debug.Assert(connection != null, "null connection?"); _connection = connection; @@ -78,7 +79,7 @@ public void Initialize() { // if we get here, then we know for certain that we're the delegated // transaction. - SqlInternalConnectionTds connection = _connection; + SqlConnectionInternal connection = _connection; SqlConnection usersConnection = connection.Connection; SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | Object Id {0}, Client Connection Id {1}, delegating transaction.", ObjectID, usersConnection?.ClientConnectionId); @@ -119,7 +120,7 @@ public byte[] Promote() // Don't read values off of the connection outside the lock unless it doesn't really matter // from an operational standpoint (i.e. logging connection's ObjectID should be fine, // but the PromotedDTCToken can change over calls. so that must be protected). - SqlInternalConnectionTds connection = GetValidConnection(); + SqlConnectionInternal connection = GetValidConnection(); Exception promoteException; byte[] returnValue = null; @@ -212,7 +213,7 @@ public byte[] Promote() public void Rollback(SinglePhaseEnlistment enlistment) { Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnectionTds connection = GetValidConnection(); + SqlConnectionInternal connection = GetValidConnection(); if (connection != null) { @@ -280,7 +281,7 @@ public void Rollback(SinglePhaseEnlistment enlistment) public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) { Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnectionTds connection = GetValidConnection(); + SqlConnectionInternal connection = GetValidConnection(); if (connection != null) { @@ -385,7 +386,7 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) // the transaction). internal void TransactionEnded(Transaction transaction) { - SqlInternalConnectionTds connection = _connection; + SqlConnectionInternal connection = _connection; if (connection != null) { @@ -406,9 +407,9 @@ internal void TransactionEnded(Transaction transaction) } // Check for connection validity - private SqlInternalConnectionTds GetValidConnection() + private SqlConnectionInternal GetValidConnection() { - SqlInternalConnectionTds connection = _connection; + SqlConnectionInternal connection = _connection; if (connection == null && Transaction.TransactionInformation.Status != TransactionStatus.Aborted) { throw ADP.ObjectDisposed(this); @@ -420,7 +421,7 @@ private SqlInternalConnectionTds GetValidConnection() // Dooms connection and throws and error if not a valid, active, delegated transaction for the given // connection. Designed to be called AFTER a lock is placed on the connection, otherwise a normal return // may not be trusted. - private void ValidateActiveOnConnection(SqlInternalConnectionTds connection) + private void ValidateActiveOnConnection(SqlConnectionInternal connection) { bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs index e070bb7d2e..b392a94521 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.Runtime.Serialization; using System.Text; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -174,7 +175,7 @@ public override string ToString() internal static SqlException CreateException( SqlError error, string serverVersion, - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, Exception innerException = null) { SqlErrorCollection errorCollection = new() { error }; @@ -200,7 +201,7 @@ internal static SqlException CreateException( internal static SqlException CreateException( SqlErrorCollection errorCollection, string serverVersion, - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, Exception innerException = null) { return CreateException( @@ -214,7 +215,7 @@ internal static SqlException CreateException( internal static SqlException CreateException( SqlErrorCollection errorCollection, string serverVersion, - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, Exception innerException = null, SqlBatchCommand batchCommand = null) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs index e43170fca0..cc43c59e99 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Threading; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -36,7 +37,7 @@ sealed internal class SqlInternalTransaction private readonly TransactionType _transactionType; private long _transactionId; // passed in the MARS headers private int _openResultCount; // passed in the MARS headers - private SqlInternalConnectionTds _innerConnection; + private SqlConnectionInternal _innerConnection; private bool _disposing; // used to prevent us from throwing exceptions while we're disposing private WeakReference _parent; // weak ref to the outer transaction object; needs to be weak to allow GC to occur. @@ -46,11 +47,19 @@ sealed internal class SqlInternalTransaction internal bool RestoreBrokenConnection { get; set; } internal bool ConnectionHasBeenRestored { get; set; } - internal SqlInternalTransaction(SqlInternalConnectionTds innerConnection, TransactionType type, SqlTransaction outerTransaction) : this(innerConnection, type, outerTransaction, NullTransactionId) + internal SqlInternalTransaction( + SqlConnectionInternal innerConnection, + TransactionType type, + SqlTransaction outerTransaction) + : this(innerConnection, type, outerTransaction, NullTransactionId) { } - internal SqlInternalTransaction(SqlInternalConnectionTds innerConnection, TransactionType type, SqlTransaction outerTransaction, long transactionId) + internal SqlInternalTransaction( + SqlConnectionInternal innerConnection, + TransactionType type, + SqlTransaction outerTransaction, + long transactionId) { SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.ctor | RES | CPOOL | Object Id {0}, Created for connection {1}, outer transaction {2}, Type {3}", ObjectID, innerConnection.ObjectID, outerTransaction?.ObjectId, (int)type); _innerConnection = innerConnection; @@ -187,7 +196,7 @@ private void CheckTransactionLevelAndZombie() internal void CloseFromConnection() { - SqlInternalConnectionTds innerConnection = _innerConnection; + SqlConnectionInternal innerConnection = _innerConnection; Debug.Assert(innerConnection != null, "How can we be here if the connection is null?"); SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.CloseFromConnection | RES | CPOOL | Object Id {0}, Closing transaction", ObjectID); @@ -452,7 +461,7 @@ internal void Zombie() ZombieParent(); - SqlInternalConnectionTds innerConnection = _innerConnection; + SqlConnectionInternal innerConnection = _innerConnection; _innerConnection = null; if (innerConnection != null) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs index c5ccbf5798..28f8a3d6db 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Threading; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Diagnostics; #if NETFRAMEWORK using System.Runtime.CompilerServices; @@ -28,7 +29,7 @@ public sealed class SqlTransaction : DbTransaction private bool _isFromApi; internal SqlTransaction( - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, SqlConnection con, IsolationLevel iso, SqlInternalTransaction internalTransaction) @@ -293,7 +294,7 @@ internal void Zombie() // For Yukon, we have to defer "zombification" until we get past the users' next // rollback, else we'll throw an exception there that is a breaking change. Of course, // if the connection is already closed, then we're free to zombify... - if (_connection.InnerConnection is SqlInternalConnectionTds && !_isFromApi) + if (_connection.InnerConnection is SqlConnectionInternal && !_isFromApi) { SqlClientEventSource.Log.TryAdvancedTraceEvent( "SqlTransaction.Zombie | ADV | Object Id {0} yukon deferred zombie", diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index fd814370b5..5f674ff7ed 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -16,6 +16,7 @@ using System.Transactions; using Interop.Common.Sni; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Runtime.InteropServices; @@ -911,7 +912,9 @@ internal static Exception SqlDependencyNoMatchingServerDatabaseStart() // // SQL.SqlDelegatedTransaction // - static internal Exception CannotCompleteDelegatedTransactionWithOpenResults(SqlInternalConnectionTds internalConnection, bool marsOn) + static internal Exception CannotCompleteDelegatedTransactionWithOpenResults( + SqlConnectionInternal internalConnection, + bool marsOn) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(TdsEnums.TIMEOUT_EXPIRED, (byte)0x00, TdsEnums.MIN_ERROR_CLASS, null, (StringsHelper.GetString(Strings.ADP_OpenReaderExists, marsOn ? ADP.Command : ADP.Connection)), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); @@ -1167,7 +1170,9 @@ internal static Exception UnsupportedSysTxForGlobalTransactions() /// * server-provided failover partner - raising SqlException in this case /// * connection string with failover partner and MultiSubnetFailover=true - raising argument one in this case with the same message /// - internal static Exception MultiSubnetFailoverWithFailoverPartner(bool serverProvidedFailoverPartner, SqlInternalConnectionTds internalConnection) + internal static Exception MultiSubnetFailoverWithFailoverPartner( + bool serverProvidedFailoverPartner, + SqlConnectionInternal internalConnection) { string msg = StringsHelper.GetString(Strings.SQLMSF_FailoverPartnerNotSupported); if (serverProvidedFailoverPartner) @@ -1211,7 +1216,7 @@ internal static Exception ROR_FailoverNotSupportedConnString() return ADP.Argument(StringsHelper.GetString(Strings.SQLROR_FailoverNotSupported)); } - internal static Exception ROR_FailoverNotSupportedServer(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_FailoverNotSupportedServer(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_FailoverNotSupported)), "", 0)); @@ -1220,7 +1225,7 @@ internal static Exception ROR_FailoverNotSupportedServer(SqlInternalConnectionTd return exc; } - internal static Exception ROR_RecursiveRoutingNotSupported(SqlInternalConnectionTds internalConnection, int maxNumberOfRedirectRoute) + internal static Exception ROR_RecursiveRoutingNotSupported(SqlConnectionInternal internalConnection, int maxNumberOfRedirectRoute) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_RecursiveRoutingNotSupported, maxNumberOfRedirectRoute)), "", 0)); @@ -1229,7 +1234,7 @@ internal static Exception ROR_RecursiveRoutingNotSupported(SqlInternalConnection return exc; } - internal static Exception ROR_UnexpectedRoutingInfo(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_UnexpectedRoutingInfo(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_UnexpectedRoutingInfo)), "", 0)); @@ -1238,7 +1243,7 @@ internal static Exception ROR_UnexpectedRoutingInfo(SqlInternalConnectionTds int return exc; } - internal static Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_InvalidRoutingInfo(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_InvalidRoutingInfo)), "", 0)); @@ -1247,7 +1252,7 @@ internal static Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds intern return exc; } - internal static Exception ROR_TimeoutAfterRoutingInfo(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_TimeoutAfterRoutingInfo(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_TimeoutAfterRoutingInfo)), "", 0)); @@ -1287,7 +1292,7 @@ internal static Exception CR_NextAttemptWillExceedQueryTimeout(SqlException inne return exc; } - internal static Exception CR_EncryptionChanged(SqlInternalConnectionTds internalConnection) + internal static Exception CR_EncryptionChanged(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_EncryptionChanged), "", 0)); @@ -1303,7 +1308,7 @@ internal static SqlException CR_AllAttemptsFailed(SqlException innerException, G return exc; } - internal static SqlException CR_NoCRAckAtReconnection(SqlInternalConnectionTds internalConnection) + internal static SqlException CR_NoCRAckAtReconnection(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_NoCRAckAtReconnection), "", 0)); @@ -1311,7 +1316,7 @@ internal static SqlException CR_NoCRAckAtReconnection(SqlInternalConnectionTds i return exc; } - internal static SqlException CR_TDSVersionNotPreserved(SqlInternalConnectionTds internalConnection) + internal static SqlException CR_TDSVersionNotPreserved(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_TDSVersionNotPreserved), "", 0)); @@ -1373,7 +1378,7 @@ internal static Exception NetworkLibraryKeywordNotSupported() { return ADP.NotSupported(StringsHelper.GetString(Strings.SQL_NetworkLibraryNotSupported)); } - internal static Exception UnsupportedFeatureAndToken(SqlInternalConnectionTds internalConnection, string token) + internal static Exception UnsupportedFeatureAndToken(SqlConnectionInternal internalConnection, string token) { var innerException = ADP.NotSupported(StringsHelper.GetString(Strings.SQL_UnsupportedToken, token)); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 115c62f6c5..07ab80b96a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -101,7 +101,7 @@ internal sealed partial class TdsParser private int _nonTransactedOpenResultCount = 0; // Connection reference - private SqlInternalConnectionTds _connHandler; + private SqlConnectionInternal _connHandler; // Async/Mars variables private bool _fMARS = false; @@ -209,7 +209,7 @@ internal TdsParser(bool MARS, bool fAsynchronous) DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; } - internal SqlInternalConnectionTds Connection + internal SqlConnectionInternal Connection { get { @@ -359,17 +359,19 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) } } - internal void Connect(ServerInfo serverInfo, - SqlInternalConnectionTds connHandler, - TimeoutTimer timeout, - SqlConnectionString connectionOptions, -#if NETFRAMEWORK - bool withFailover, - bool isFirstTransparentAttempt, - bool disableTnir -#else - bool withFailover -#endif + internal void Connect( + ServerInfo serverInfo, + SqlConnectionInternal connHandler, + TimeoutTimer timeout, + SqlConnectionString connectionOptions, + + #if NETFRAMEWORK + bool withFailover, + bool isFirstTransparentAttempt, + bool disableTnir + #else + bool withFailover + #endif ) { SqlConnectionEncryptOption encrypt = connectionOptions.Encrypt; @@ -1689,7 +1691,7 @@ internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand if (asyncClose) { // Wait until we have the parser lock, then try to close - SqlInternalConnectionTds connHandler = _connHandler; + SqlConnectionInternal connHandler = _connHandler; Action wrapCloseAction = closeAction => { Task.Factory.StartNew(() => @@ -6236,7 +6238,9 @@ private TdsOperationStatus TryProcessRow(_SqlMetaDataSet columns, object[] buffe /// Determines if a column value should be transparently decrypted (based on SqlCommand and Connection String settings). /// /// true if the value should be transparently decrypted, false otherwise - internal static bool ShouldHonorTceForRead(SqlCommandColumnEncryptionSetting columnEncryptionSetting, SqlInternalConnectionTds connection) + internal static bool ShouldHonorTceForRead( + SqlCommandColumnEncryptionSetting columnEncryptionSetting, + SqlConnectionInternal connection) { // Command leve setting trumps all switch (columnEncryptionSetting) @@ -6259,7 +6263,7 @@ internal static bool ShouldHonorTceForRead(SqlCommandColumnEncryptionSetting col internal static object GetNullSqlValue(SqlBuffer nullVal, SqlMetaDataPriv md, SqlCommandColumnEncryptionSetting columnEncryptionSetting, - SqlInternalConnectionTds connection) + SqlConnectionInternal connection) { SqlDbType type = md.type; @@ -10000,10 +10004,10 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques static (Task task, object state) => { Debug.Assert(!task.IsCanceled, "Task should not be canceled"); - var parameters = (Tuple)state; + var parameters = (Tuple)state; TdsParser parser = parameters.Item1; TdsParserStateObject tdsParserStateObject = parameters.Item2; - SqlInternalConnectionTds internalConnectionTds = parameters.Item3; + SqlConnectionInternal internalConnectionTds = parameters.Item3; try { if (task.IsFaulted) @@ -10237,7 +10241,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout if (releaseConnectionLock) { task.ContinueWith( - static (Task _, object state) => ((SqlInternalConnectionTds)state)._parserLock.Release(), + static (Task _, object state) => ((SqlConnectionInternal)state)._parserLock.Release(), state: _connHandler, TaskScheduler.Default ); From d010d487e9c88db60ac285c9b69a9768254515b3 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 14 Nov 2025 17:30:51 -0600 Subject: [PATCH 30/34] Fix one reference to SqlInternalConnectionTds in unit tests --- .../UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs index dfc37d2720..435d9cd956 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs @@ -4,7 +4,7 @@ using System; using System.Data; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Tests.Common; using Microsoft.SqlServer.TDS.Servers; using Xunit; @@ -563,7 +563,7 @@ public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUs // Connect once to the primary to trigger it to send the failover partner connection.Open(); - Assert.Equal("invalidhost", (connection.InnerConnection as SqlInternalConnectionTds)!.ServerProvidedFailoverPartner); + Assert.Equal("invalidhost", (connection.InnerConnection as SqlConnectionInternal)!.ServerProvidedFailoverPartner); // Close the connection to return it to the pool connection.Close(); From 4d02cc22e488642d1b5b5f4d90162e6301ae38d3 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 18 Nov 2025 16:40:27 -0600 Subject: [PATCH 31/34] As per copilot comment, rewriting a block in SqlConnectionFactory to use a using block. Also removing redundant if statement condition. --- .../Data/SqlClient/SqlConnectionFactory.cs | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index 628cc92c20..5c7759d921 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -643,56 +643,58 @@ protected virtual DbConnectionInternal CreateConnection( redirectedUserInstance = true; string instanceName; - if (pool == null || (pool != null && pool.Count <= 0)) - { // Non-pooled or pooled and no connections in the pool. - SqlConnectionInternal sseConnection = null; - try + if (pool == null || pool.Count <= 0) + { + // Non-pooled or pooled and no connections in the pool. + + // NOTE: Cloning connection option opt to set 'UserInstance=True' and 'Enlist=False' + // This first connection is established to SqlExpress to get the instance name + // of the UserInstance. + SqlConnectionString sseopt = new SqlConnectionString( + opt, + opt.DataSource, + userInstance: true, + setEnlistValue: false); + + SqlConnectionInternal sseConnection = new SqlConnectionInternal( + identity, + sseopt, + key.Credential, + providerInfo: null, + newPassword: string.Empty, + newSecurePassword: null, + redirectedUserInstance: false, + applyTransientFaultHandling: applyTransientFaultHandling, + sspiContextProvider: key.SspiContextProvider); + using (sseConnection) { - // We throw an exception in case of a failure - // NOTE: Cloning connection option opt to set 'UserInstance=True' and 'Enlist=False' - // This first connection is established to SqlExpress to get the instance name - // of the UserInstance. - SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false); - sseConnection = new SqlConnectionInternal( - identity, - sseopt, - key.Credential, - providerInfo: null, - newPassword: string.Empty, - newSecurePassword: null, - redirectedUserInstance: false, - applyTransientFaultHandling: applyTransientFaultHandling, - sspiContextProvider: key.SspiContextProvider); - - // NOTE: Retrieve here. This user instance name will be used below to connect to the Sql Express User Instance. + // NOTE: Retrieve here. This user instance name will be + // used below to connect to the SQL Express User Instance. instanceName = sseConnection.InstanceName; // Set future transient fault handling based on connection options sqlOwningConnection._applyTransientFaultHandling = opt != null && opt.ConnectRetryCount > 0; - if (!instanceName.StartsWith("\\\\.\\", StringComparison.Ordinal)) + if (!instanceName.StartsWith(@"\\.\", StringComparison.Ordinal)) { throw SQL.NonLocalSSEInstance(); } if (pool != null) - { // Pooled connection - cache result + { + // Pooled connection - cache result SqlConnectionPoolProviderInfo providerInfo = (SqlConnectionPoolProviderInfo)pool.ProviderInfo; + // No lock since we are already in creation mutex providerInfo.InstanceName = instanceName; } } - finally - { - if (sseConnection != null) - { - sseConnection.Dispose(); - } - } } else - { // Cached info from pool. + { + // Cached info from pool. SqlConnectionPoolProviderInfo providerInfo = (SqlConnectionPoolProviderInfo)pool.ProviderInfo; + // No lock since we are already in creation mutex instanceName = providerInfo.InstanceName; } From 04e5cdfd040496a76700092f1855f594cb77d94b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 18 Nov 2025 16:40:43 -0600 Subject: [PATCH 32/34] As per copilot comment, adding more comments to CachedContexts class --- .../Data/SqlClient/Connection/CachedContexts.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs index 9d9de46ac1..46a2c54c11 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -10,8 +10,20 @@ namespace Microsoft.Data.SqlClient.Connection { /// - /// Provides cached asynchronous call contexts shared between objects in a connection's context. + /// Provides thread-safe caching and sharing of asynchronous call contexts between objects + /// within a single SQL connection context. /// + /// + /// This internal class manages reusable context objects for various asynchronous operations + /// such as ExecuteNonQueryAsync, ExecuteReaderAsync, etc, performed on a connection, enabling + /// efficient reuse and reducing allocations. + /// + /// Thread safety is ensured via interlocked operations, allowing concurrent access and + /// updates without explicit locking. All accessors and mutators are designed to be safe for + /// use by multiple threads. + /// + /// Intended for internal use by connection management infrastructure. + /// internal class CachedContexts { #region Fields From 64766f4d5214aa67b631cb3f7e31121f869fa5d4 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 1 Dec 2025 14:05:22 -0600 Subject: [PATCH 33/34] Addressing some feedback from @mdiagle --- .../Data/SqlClient/Connection/SqlConnectionInternal.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 2b62794702..5567dac6d5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -636,7 +636,7 @@ internal bool IsDNSCachingBeforeRedirectSupported /// /// Indicates whether the connection is currently enlisted in a transaction. /// - internal bool IsEnlistedInTransaction { get; set; } + internal bool IsEnlistedInTransaction { get; private set; } /// /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) @@ -1033,6 +1033,8 @@ public override void Dispose() } _whereAbouts = null; + + base.Dispose(); } internal void EnlistNull() From 3b795aea440e57e3ce8f9863c2ff5d58c4352221 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 1 Dec 2025 18:20:21 -0600 Subject: [PATCH 34/34] Rename Clear*Context to Take*Context --- .../Data/SqlClient/Connection/CachedContexts.cs | 12 ++++++------ .../Microsoft/Data/SqlClient/SqlCommand.Reader.cs | 2 +- .../src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs | 2 +- .../src/Microsoft/Data/SqlClient/SqlDataReader.cs | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs index 46a2c54c11..d7a6073d13 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -66,42 +66,42 @@ internal class CachedContexts /// Removes and returns the cached ExecuteNonQueryAsync context. /// /// The previously cached context or null when empty. - internal SqlCommand.ExecuteNonQueryAsyncCallContext? ClearCommandExecuteNonQueryAsyncContext() => + internal SqlCommand.ExecuteNonQueryAsyncCallContext? TakeCommandExecuteNonQueryAsyncContext() => Interlocked.Exchange(ref _commandExecuteNonQueryAsyncContext, null); /// /// Removes and returns the cached ExecuteReaderAsync context. /// /// The previously cached context or null when empty. - internal SqlCommand.ExecuteReaderAsyncCallContext? ClearCommandExecuteReaderAsyncContext() => + internal SqlCommand.ExecuteReaderAsyncCallContext? TakeCommandExecuteReaderAsyncContext() => Interlocked.Exchange(ref _commandExecuteReaderAsyncContext, null); /// /// Removes and returns the cached ExecuteXmlReaderAsync context. /// /// The previously cached context or null when empty. - internal SqlCommand.ExecuteXmlReaderAsyncCallContext? ClearCommandExecuteXmlReaderAsyncContext() => + internal SqlCommand.ExecuteXmlReaderAsyncCallContext? TakeCommandExecuteXmlReaderAsyncContext() => Interlocked.Exchange(ref _commandExecuteXmlReaderAsyncContext, null); /// /// Removes and returns the cached ReadAsync context. /// /// The previously cached context or null when empty. - internal SqlDataReader.ReadAsyncCallContext? ClearDataReaderReadAsyncContext() => + internal SqlDataReader.ReadAsyncCallContext? TakeDataReaderReadAsyncContext() => Interlocked.Exchange(ref _dataReaderReadAsyncContext, null); /// /// Removes and returns the cached IsDBNullAsync context. /// /// The previously cached context or null when empty. - internal SqlDataReader.IsDBNullAsyncCallContext? ClearDataReaderIsDbNullContext() => + internal SqlDataReader.IsDBNullAsyncCallContext? TakeDataReaderIsDbNullContext() => Interlocked.Exchange(ref _dataReaderIsDbNullContext, null); /// /// Removes and returns the cached data reader snapshot. /// /// The previously cached snapshot or null when empty. - internal SqlDataReader.Snapshot? ClearDataReaderSnapshot() => + internal SqlDataReader.Snapshot? TakeDataReaderSnapshot() => Interlocked.Exchange(ref _dataReaderSnapshot, null); /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs index 9e13c07c6d..dda6534047 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -983,7 +983,7 @@ private Task InternalExecuteReaderAsync( if (_activeConnection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = sqlInternalConnection.CachedContexts.ClearCommandExecuteReaderAsyncContext(); + context = sqlInternalConnection.CachedContexts.TakeCommandExecuteReaderAsyncContext(); } context ??= new ExecuteReaderAsyncCallContext(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs index 9be2713a2d..add7f90407 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs @@ -490,7 +490,7 @@ private Task InternalExecuteXmlReaderAsync(CancellationToken cancella ExecuteXmlReaderAsyncCallContext context = null; if (_activeConnection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = sqlInternalConnection.CachedContexts.ClearCommandExecuteXmlReaderAsyncContext(); + context = sqlInternalConnection.CachedContexts.TakeCommandExecuteXmlReaderAsyncContext(); } context ??= new ExecuteXmlReaderAsyncCallContext(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 3daac5055a..adb06498c1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -5042,7 +5042,7 @@ public override Task ReadAsync(CancellationToken cancellationToken) ReadAsyncCallContext context = null; if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = sqlInternalConnection.CachedContexts.ClearDataReaderReadAsyncContext(); + context = sqlInternalConnection.CachedContexts.TakeDataReaderReadAsyncContext(); } if (context is null) { @@ -5211,7 +5211,7 @@ override public Task IsDBNullAsync(int i, CancellationToken cancellationTo IsDBNullAsyncCallContext context = null; if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = sqlInternalConnection.CachedContexts.ClearDataReaderIsDbNullContext(); + context = sqlInternalConnection.CachedContexts.TakeDataReaderIsDbNullContext(); } if (context is null) { @@ -5797,7 +5797,7 @@ private void PrepareAsyncInvocation(bool useSnapshot) { if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - _snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot(); + _snapshot = sqlInternalConnection.CachedContexts.TakeDataReaderSnapshot() ?? new Snapshot(); } else {