Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
66c2cd6
Change all possible references from SqlInternalConnection to SqlInter…
benrr101 Nov 12, 2025
2d664a6
Move BeginTransaction and BeginSqlTransaction into SqlInternalConnect…
benrr101 Nov 12, 2025
c796212
Move ChangeDatabase into SqlInternalConnectionTds, move ChangeDatabas…
benrr101 Nov 12, 2025
13c373c
Move EnlistTransaction to SqlInternalConnectionTds
benrr101 Nov 12, 2025
a176e2f
Remove abstract ValidateConnectionForExecute
benrr101 Nov 12, 2025
c5863e3
Move Enlist into SqlInternalConnectionTds
benrr101 Nov 12, 2025
8002ec7
Move EnlistNonNull into SqlInternalConnectionTds
benrr101 Nov 12, 2025
202beb5
Move EnlistNull into SqlInternalConnectionTds
benrr101 Nov 12, 2025
bb3d541
Removed no longer necessary abstractions in SqlInternalConnection
benrr101 Nov 12, 2025
4d39842
Move OnError into SqlInternalConnectionTds
benrr101 Nov 13, 2025
58c358f
Move Deactivate into SqlInternalConnectionTds, merge InternalDeactiva…
benrr101 Nov 13, 2025
07750f8
Move GetTransactionCookie into SqlInternalConnectionTds
benrr101 Nov 13, 2025
dbe9d9e
Move FindLiveReader into SqlInternalConnectionTds
benrr101 Nov 13, 2025
355f2d2
Merge SqlInternalConnection.Dispose into SqlInternalConnectionTds
benrr101 Nov 13, 2025
74b0c92
Move CleanupTransactionOnCompletion, CreateReferenceCollection into S…
benrr101 Nov 13, 2025
ce46515
Introduce a CachedContexts class, move the cached call contexts from …
benrr101 Nov 13, 2025
79cf052
Migrate usages of CachedCommandExecuteNonQueryAsyncContext to the new…
benrr101 Nov 13, 2025
0a45c97
Migrate usages of CachedCommandExecuteXmlReaderAsyncContext to the ne…
benrr101 Nov 13, 2025
7b5a637
Migrate usage of ReadAsyncCallContext to the new class
benrr101 Nov 13, 2025
ba4c7df
Migrate usage of IsDBNullAsyncCallContext to the new class.
benrr101 Nov 13, 2025
a0a1f24
Comments on CachedContexts
benrr101 Nov 13, 2025
41b7f2a
Sure why not move the reader snapshot into the CachedContexts class? …
benrr101 Nov 13, 2025
1b52999
Move _whereAbouts and s_globalTransactionTMID
benrr101 Nov 13, 2025
8fb7b0e
Merge constructors
benrr101 Nov 13, 2025
0e78a6e
Merge AvailableInternalTransaction, CurrentTransaction, HasLocalTrans…
benrr101 Nov 13, 2025
858d98e
Merge Connection, ConnectionOptions, CurrentDatabase, CurrentDataSour…
benrr101 Nov 13, 2025
6dfbd5d
Remove SqlInternalConnection
benrr101 Nov 13, 2025
a83bd7c
Move SqlInternalConnectionTds.cs to SqlConnectionInternal.cs
benrr101 Nov 13, 2025
c4db055
Renane Microsoft.Data.SqlClient.SqlInternalConnectionTds to Microsoft…
benrr101 Nov 13, 2025
d010d48
Fix one reference to SqlInternalConnectionTds in unit tests
benrr101 Nov 14, 2025
4d02cc2
As per copilot comment, rewriting a block in SqlConnectionFactory to …
benrr101 Nov 18, 2025
04e5cdf
As per copilot comment, adding more comments to CachedContexts class
benrr101 Nov 18, 2025
64766f4
Addressing some feedback from @mdiagle
benrr101 Dec 1, 2025
89b92b9
Merge branch 'main' into dev/russellben/flatten/sqlinternalconnection
benrr101 Dec 2, 2025
3b795ae
Rename Clear*Context to Take*Context
benrr101 Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ internal abstract class SqlInternalConnection : DbConnectionInternal
/// <summary>
/// Cache the whereabouts (DTC Address) for exporting.
/// </summary>
private byte[] _whereAbouts;
protected byte[] _whereAbouts;

/// <summary>
/// ID of the Azure SQL DB Transaction Manager (Non-MSDTC)
/// </summary>
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;
Expand Down Expand Up @@ -132,7 +132,7 @@ internal bool HasLocalTransactionFromAPI
/// <summary>
/// Indicates whether the connection is currently enlisted in a transaction.
/// </summary>
internal bool IsEnlistedInTransaction { get; private set; }
internal bool IsEnlistedInTransaction { get; set; }

/// <summary>
/// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down