-
Notifications
You must be signed in to change notification settings - Fork 317
Flatten | SqlInternalConnectionTds and SqlInternalConnection #3773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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
SqlInternalConnectionandSqlInternalConnectionTds - Renaming to
SqlConnectionInternalfor alphabetical alignment withSqlConnection - Introduction of
CachedContextshelper 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 callsbase.Dispose()as it did in the originalSqlInternalConnection.cs. This could lead to resource leaks if the base classDbConnectionInternalhas cleanup logic that needs to be executed. The base class'sDispose()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-6b6846079764to1C742CAF-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
IsAzureSqlConnectionwas originally aninternalproperty inSqlInternalConnection.cs, but is now marked asprivateinSqlConnectionInternal.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
IsGlobalTransactionandIsGlobalTransactionsEnabledForServerwere originallyinternalinSqlInternalConnection.cs, but are now marked asprivatein 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
IsEnlistedInTransactionwas originallyprivateinSqlInternalConnection.cs, but is now marked asinternalinSqlConnectionInternal.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 remainprivateto maintain encapsulation.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs:724 - The property
PromotedDtcTokenwas originallyinternalwith both getter and setter inSqlInternalConnection.cs, but is nowinternalwith only aprivatesetter. 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
CurrentDatabasewas originallyinternalwith both getter and setter inSqlInternalConnection.cs, but now has aprivatesetter. 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) |
Copilot
AI
Nov 14, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
Copilot
AI
Nov 14, 2025
There was a problem hiding this comment.
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.
| bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this); | |
| bool valid = _active | |
| && (connection == _connection) | |
| && (connection != null && connection.DelegatedTransaction == this); |
There was a problem hiding this comment.
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; |
Copilot
AI
Nov 14, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
Copilot
AI
Nov 14, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
| protected override void CleanupTransactionOnCompletion(Transaction transaction) => | ||
| DelegatedTransaction?.TransactionEnded(transaction); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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
…te into Deactivate
…qlInternalConnectionTds
…SqlInternalConnection into that. Migrate usages of ExecuteReaderAsyncCallContext to the new class.
…the usages of it are basically doing the same thing
…action, HasLocalTransactionFromAPI
…ce, DelegatedTransaction, Is2008OrNewer, IsAzureSqlConnection, IsEnlistedInTransaction, IsGlobalTransaction, IsGlobalTransactionEnabledForServer, IsLockedForBulkCopy, IsTransactionRoot, PromotedDtcToken
….Data.SqlClient.Connection.SqlInternalConnectionTds
65e24d1 to
d010d48
Compare
There was a problem hiding this 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 shows1c742caf-6680-40ea-9c26-6b6846079764(lowercase) but the code uses1C742CAF-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. |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| /// 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> |
…use a using block. Also removing redundant if statement condition.
There was a problem hiding this 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
PromotedDtcTokensetter visibility has been changed from implicit (which would have been internal in the base class) to explicitlyprivate 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
IsAzureSqlConnectionhas been changed frominternaltoprivate. 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
IsGlobalTransactionsetter has been changed from implicit (likely internal in base class) to explicitlyprivate 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
IsGlobalTransactionsEnabledForServersetter has been changed from implicit (likely internal in base class) to explicitlyprivate 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
IsEnlistedInTransactionis now defined here with both getter and setter, but it was previously defined as aprivateproperty in the base classSqlInternalConnection. The setter visibility has changed fromprivatetointernal. 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
CurrentDatabasesetter visibility has been changed from implicit (which would be internal in the base class) to explicitlyprivate 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();
}
| 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; | ||
| } |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| // 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) | ||
| { |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
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.
| { | |
| { | |
| // TrySetDataReaderSnapshot will only cache the snapshot if no snapshot is already present. |
| /// Indicates whether the connection is currently enlisted in a transaction. | ||
| /// </summary> | ||
| internal bool IsEnlistedInTransaction { get; private set; } | ||
| internal bool IsEnlistedInTransaction { get; set; } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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...
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.