Skip to content

Conversation

@benrr101
Copy link
Contributor

Description

This PR is part of an ongoing (undocumented) effort to clean up, and modernize the codebase. In this installment, we have the SqlInternalConnection and SqlInternalConnectionTds class. These are a remnant of when context connections existed - two classes inherited from SqlInternalConnection: SqlInternalConnectionTds and SqlInternalConnectionSmi (I think). The SMI version was removed a while ago as part of removing functionally dead codepaths. As such, there's no need for all this inheritance. We can merge SqlInternalConnectionTds into SqlInternalConnection.

At the same time, we also move SqlInternalConnectionTds into the Connection namespace, and rename it to SqlConnectionInternal to alphabetically organize it with SqlConnection.

Comments have been added to the PR to highlight key changes in the code.

Issues

N/A

Testing

Code continues to compile, CI will validate.

@benrr101 benrr101 added this to the 7.0.0-preview3 milestone Nov 14, 2025
@benrr101 benrr101 added the Code Health 💊 Issues/PRs that are targeted to source code quality improvements. label Nov 14, 2025
Copilot AI review requested due to automatic review settings November 14, 2025 20:20
@benrr101 benrr101 changed the title Dev/russellben/flatten/sqlinternalconnection Flatten | SqlInternalConnectionTds and SqlInternalConnection Nov 14, 2025
Copilot finished reviewing on behalf of benrr101 November 14, 2025 20:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the internal connection class hierarchy by merging SqlInternalConnectionTds into SqlInternalConnection and renaming the merged class to SqlConnectionInternal. The class has been moved to the Microsoft.Data.SqlClient.Connection namespace for better organization. Additionally, a new CachedContexts class has been introduced to centralize the management of cached asynchronous call contexts.

Key changes:

  • Elimination of unnecessary inheritance between SqlInternalConnection and SqlInternalConnectionTds
  • Renaming to SqlConnectionInternal for alphabetical alignment with SqlConnection
  • Introduction of CachedContexts helper class to manage async call context caching
  • Updates to all references across the codebase to use the new type name and namespace

Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
SqlConnectionInternal.cs New merged class combining SqlInternalConnection and SqlInternalConnectionTds
CachedContexts.cs New helper class for managing cached async contexts
SqlTransaction.cs Updated type references and added namespace import
SqlInternalTransaction.cs Updated type references and added namespace import
TdsParser.cs Updated type references throughout
SqlUtil.cs Updated method signatures to use new type name
SqlException.cs Updated method signatures to use new type name
SqlDelegatedTransaction.cs Updated type references and added namespace import
SqlDataReader.cs Updated to use CachedContexts for async context management
SqlConnection.cs Updated all internal connection references
SqlCommand.cs Updated type references and added namespace import
SqlCommand.*.cs Updated async context management to use CachedContexts
SqlBulkCopy.cs Updated type references and added namespace import
SqlConnectionFactory.cs Updated instantiation to use new type name
AdapterUtil.cs Updated method signatures and added namespace import
*.csproj Updated project files to reference new file locations
Comments suppressed due to low confidence (10)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:1036

  • The Dispose() method no longer calls base.Dispose() as it did in the original SqlInternalConnection.cs. This could lead to resource leaks if the base class DbConnectionInternal has cleanup logic that needs to be executed. The base class's Dispose() method should be called to ensure proper cleanup of inherited resources.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:68
  • The GUID value has been changed from lowercase to uppercase (from 1c742caf-6680-40ea-9c26-6b6846079764 to 1C742CAF-6680-40EA-9C26-6B6846079764). While GUIDs are case-insensitive, this change appears unintentional and could lead to confusion. If this was intentional, it should be documented in the PR description.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:821
  • The property IsAzureSqlConnection was originally an internal property in SqlInternalConnection.cs, but is now marked as private in SqlConnectionInternal.cs. This could be a breaking change if other parts of the codebase outside this class relied on accessing this property. Please verify that this access modifier change is intentional and doesn't break existing functionality.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:651
  • The properties IsGlobalTransaction and IsGlobalTransactionsEnabledForServer were originally internal in SqlInternalConnection.cs, but are now marked as private in the new merged class. This appears to be an unintentional access modifier change that could break code that depends on these properties being accessible from outside the class.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:639
  • The property IsEnlistedInTransaction was originally private in SqlInternalConnection.cs, but is now marked as internal in SqlConnectionInternal.cs. While this expands access rather than restricting it, this change should be intentional and documented. If the expanded access is not needed, it should remain private to maintain encapsulation.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:724
  • The property PromotedDtcToken was originally internal with both getter and setter in SqlInternalConnection.cs, but is now internal with only a private setter. This could be a breaking change if other code sets this property from outside the class. Please verify this change is intentional.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:532
  • The property CurrentDatabase was originally internal with both getter and setter in SqlInternalConnection.cs, but now has a private setter. This could be a breaking change if other code sets this property from outside the class. Please verify this change is intentional.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs:1065
  • Variable this._activeConnection may be null at this access as suggested by this null check.
                $"Client Connection Id {_activeConnection.ClientConnectionId}, " +

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:2438

  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs:5805
  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
                    if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection)
                    {
                        _snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot();
                    }
                    else
                    {
                        _snapshot = new Snapshot();
                    }

// 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 SqlConnectionInternal && !_isFromApi)
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable internalConnection is declared but never used in this code block. The original code used this variable, but after the type pattern matching change, it's no longer referenced. Consider removing the variable declaration or using a discard pattern if (_connection.InnerConnection is SqlConnectionInternal and not _isFromApi) if the type check is intentional.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhhh what?

private void ValidateActiveOnConnection(SqlInternalConnection connection)
private void ValidateActiveOnConnection(SqlConnectionInternal connection)
{
bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this);
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable connection may be null at this access as suggested by this null check.

Suggested change
bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this);
bool valid = _active
&& (connection == _connection)
&& (connection != null && connection.DelegatedTransaction == this);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically true, though I'm not sure the implications of making this change.

if (pool == null || (pool != null && pool.Count <= 0))
{ // Non-pooled or pooled and no connections in the pool.
SqlInternalConnectionTds sseConnection = null;
SqlConnectionInternal sseConnection = null;
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks odd to me. Will investigate...

// 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;
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not wrong, but because the internal connection is immediately disposed after construction, the cleanest way to write it is with the manual disposal. I'm too unsure what magic happens with the pools when the internal connection is constructed to want to mess with when the internal connection is disposed (ie, via a using declaration)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this method is opening a new connection (including a round trip) just to seed a new connection pool. Then it's explicitly clearing the old pool so that it's no longer used...
I want to look into this more because it feels fishy.

Comment on lines +2045 to +2046
protected override void CleanupTransactionOnCompletion(Transaction transaction) =>
DelegatedTransaction?.TransactionEnded(transaction);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This movement is missing a comment, and it highlights a slight change in approach: rather than getting the value of a property into a local variable and null-coalescing on the local variable, we null-coalesce on the property directly. If the value of the property changes halfway through the operation, we'll be operating on two different values (or might get a NullReferenceException.)

This might be fine - SqlConnection isn't supposed to be used in a non-thread-safe manner, and this is the first way I can see which would trigger that behaviour - but I want to make sure it's being done intentionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, the compile will implicitly create a temporary variable for this. Thus, we get the same behavior as before, but in four less lines :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just checked in SharpLab - you're right!

…eInternal into ChangeDatabase, remove abstract ChangeDatabaseInternal
…SqlInternalConnection into that. Migrate usages of ExecuteReaderAsyncCallContext to the new class.
…the usages of it are basically doing the same thing
Copilot AI review requested due to automatic review settings November 18, 2025 16:46
@benrr101 benrr101 force-pushed the dev/russellben/flatten/sqlinternalconnection branch from 65e24d1 to d010d48 Compare November 18, 2025 16:46
Copilot finished reviewing on behalf of benrr101 November 18, 2025 16:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:68

  • [nitpick] The GUID value in the comment has a different casing than the string literal used to initialize s_globalTransactionTMID. The comment shows 1c742caf-6680-40ea-9c26-6b6846079764 (lowercase) but the code uses 1C742CAF-6680-40EA-9C26-6B6846079764 (uppercase). While functionally equivalent, they should match for consistency.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:1501
  • [nitpick] Inconsistent formatting in the trace message. The log message ends with a period in $"Object ID {ObjectID}." on line 1501, but most other similar trace messages in this file don't use trailing periods (e.g., lines 1041-1043, 1060-1063). Consider removing the period for consistency.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs:1065
  • Variable this._activeConnection may be null at this access as suggested by this null check.
                $"Client Connection Id {_activeConnection.ClientConnectionId}, " +

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:2439

  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs:5805
  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
                    if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection)
                    {
                        _snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot();
                    }
                    else
                    {
                        _snapshot = new Snapshot();
                    }

namespace Microsoft.Data.SqlClient.Connection
{
/// <summary>
/// Provides cached asynchronous call contexts shared between objects in a connection's context.
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing XML documentation for the CachedContexts class. As a newly introduced internal class with public-facing methods, it should have a summary describing its purpose and usage pattern, especially since it deals with thread-safe context caching using Interlocked operations.

Suggested change
/// 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.
/// <para>
/// 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.
/// </para>
/// <para>
/// Thread safety is ensured via atomic operations using <see cref="Interlocked"/> methods, allowing concurrent access and updates
/// without explicit locking. All accessors and mutators are designed to be safe for use by multiple threads.
/// </para>
/// <para>
/// Intended for internal use by connection management infrastructure; not part of the public API.
/// </para>

Copilot uses AI. Check for mistakes.
@benrr101 benrr101 marked this pull request as ready for review November 18, 2025 22:44
@benrr101 benrr101 requested a review from a team as a code owner November 18, 2025 22:44
Copilot AI review requested due to automatic review settings November 18, 2025 22:44
Copilot finished reviewing on behalf of benrr101 November 18, 2025 22:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (9)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:724

  • The property PromotedDtcToken setter visibility has been changed from implicit (which would have been internal in the base class) to explicitly private set. This could be a breaking change if there are internal consumers that set this property. Please verify that no code within the assembly depends on setting this property externally.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:821
  • The property IsAzureSqlConnection has been changed from internal to private. This is a breaking change that could affect other code within the assembly that depends on accessing this property. Please verify that no internal code accesses this property.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:645
  • The property IsGlobalTransaction setter has been changed from implicit (likely internal in base class) to explicitly private set. This could be a breaking change if there are internal consumers that set this property. Please verify that no code within the assembly depends on setting this property externally.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:651
  • The property IsGlobalTransactionsEnabledForServer setter has been changed from implicit (likely internal in base class) to explicitly private set. This could be a breaking change if there are internal consumers that set this property. Please verify that no code within the assembly depends on setting this property externally.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:639
  • The property IsEnlistedInTransaction is now defined here with both getter and setter, but it was previously defined as a private property in the base class SqlInternalConnection. The setter visibility has changed from private to internal. This could be an intentional visibility expansion, but should be verified as it may affect thread safety or transaction state management.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:532
  • The property CurrentDatabase setter visibility has been changed from implicit (which would be internal in the base class) to explicitly private set. This could be a breaking change if there were any internal consumers that set this property. Please verify that no code within the assembly depends on setting this property externally.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs:1065
  • Variable this._activeConnection may be null at this access as suggested by this null check.
                $"Client Connection Id {_activeConnection.ClientConnectionId}, " +

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:2439

  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
    src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs:5805
  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
                    if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection)
                    {
                        _snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot();
                    }
                    else
                    {
                        _snapshot = new Snapshot();
                    }

Comment on lines +169 to +178
private static bool TrySetContext<TContext>(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;
}
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TrySetContext method throws ArgumentNullException when value is null. However, the public-facing methods that call this helper (e.g., TrySetCommandExecuteNonQueryAsyncContext, TrySetDataReaderSnapshot) don't document this behavior. Consider adding XML documentation to these methods indicating that they will throw ArgumentNullException if a null value is provided, or handle the null case gracefully by returning false instead of throwing.

Copilot uses AI. Check for mistakes.
// 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 SqlConnectionInternal sqlInternalConnection)
{
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logic for caching the snapshot has changed subtly. Previously, the code checked sqlInternalConnection.CachedDataReaderSnapshot is null before attempting to cache. Now it unconditionally calls TrySetDataReaderSnapshot. While functionally equivalent due to the CompareExchange inside TrySetContext, the semantic meaning is less clear. The previous pattern explicitly checked for null, making the intent clearer. Consider adding a comment explaining that TrySetDataReaderSnapshot will only cache if no snapshot is already present.

Suggested change
{
{
// TrySetDataReaderSnapshot will only cache the snapshot if no snapshot is already present.

Copilot uses AI. Check for mistakes.
/// Indicates whether the connection is currently enlisted in a transaction.
/// </summary>
internal bool IsEnlistedInTransaction { get; private set; }
internal bool IsEnlistedInTransaction { get; set; }
Copy link
Contributor

@mdaigle mdaigle Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this back to private set

}

base.Dispose();
_whereAbouts = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a call to base.Dispose()

if (_connection?.InnerConnection is SqlInternalConnectionTds sqlInternalConnection)
{
_snapshot = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderSnapshot, null) ?? new Snapshot();
_snapshot = sqlInternalConnection.CachedContexts.ClearDataReaderSnapshot() ?? new Snapshot();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines like this make me think that "pop" might make more sense than "clear". The ultimate intent often isn't to wipe out the cache, it's to check out the cache value.

IDbConnectionPool pool = null,
Func<SqlAuthenticationParameters, CancellationToken, Task<SqlAuthenticationToken>> accessTokenCallback = null,
SspiContextProvider sspiContextProvider = null) : base(connectionOptions)
SspiContextProvider sspiContextProvider = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep this call to the base constructor, we still inherit from DbConnectionInternal

// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this method is opening a new connection (including a round trip) just to seed a new connection pool. Then it's explicitly clearing the old pool so that it's no longer used...
I want to look into this more because it feels fishy.

if (pool == null || (pool != null && pool.Count <= 0))
{ // Non-pooled or pooled and no connections in the pool.
SqlInternalConnectionTds sseConnection = null;
SqlConnectionInternal sseConnection = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks odd to me. Will investigate...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Code Health 💊 Issues/PRs that are targeted to source code quality improvements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants