From 67c381902d00a2e16cfca4de5ecb383889392872 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 18:12:40 -0500 Subject: [PATCH 01/56] Introduce partial for SqlInternalConnectionTds --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- .../Data/SqlClient/SqlInternalConnectionTds.cs | 11 +++++++++++ .../Data/SqlClient/SqlInternalConnectionTds.stub.cs | 2 +- 6 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.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 de98172898..bdddf73f37 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -693,6 +693,9 @@ Microsoft\Data\SqlClient\SqlInternalConnection.cs + + Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs + Microsoft\Data\SqlClient\SqlInternalTransaction.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 482551fe79..e8cf65db0b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -104,7 +104,7 @@ public void AssertUnrecoverableStateCountIsCorrect() } } - internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposable + internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 internal const int MsalHttpRetryStatusCode = 429; 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 be475493fb..e759baa926 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -857,6 +857,9 @@ Microsoft\Data\SqlClient\SqlInternalConnection.cs + + Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs + Microsoft\Data\SqlClient\SqlInternalTransaction.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 523f1288e5..871fdafd7c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -105,7 +105,7 @@ public void AssertUnrecoverableStateCountIsCorrect() } } - internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposable + internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 internal const int MsalHttpRetryStatusCode = 429; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs new file mode 100644 index 0000000000..77c55f51f6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -0,0 +1,11 @@ +// 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. + +namespace Microsoft.Data.SqlClient +{ + internal partial class SqlInternalConnectionTds + { + + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs index 1cc6d14ec6..d5fb3689fc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs @@ -12,7 +12,7 @@ namespace Microsoft.Data.SqlClient { - internal class SqlInternalConnectionTds + internal partial class SqlInternalConnectionTds { internal SyncAsyncLock _parserLock = null; From b7df614a2e75d3b06f6e27ff2ba4414af45a057e Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 18:28:17 -0500 Subject: [PATCH 02/56] Merge MaxNumberOfRedirectRoute, MsalHttpRetryStatusCode, _accessTokenInBytes, _accessTokenCallback, _cleanSQLDNSCaching, _currentSessionData, _sessionRecoveryAcknowledged, _fedAuthRequired, _federatedAuthenticationAcknowledged, _federatedAuthenticationInfoReceived, _federatedAuthenticationInfoRequested, _federatedAuthenticationRequested, _sspiContextProvider, _activeDirectoryAuthTimeoutRetryHelper, _credential, _fedAuthFeatureExtensionData, _fedAuthToken, _loginAck, _parser, _poolGroupProviderInfo, _recoverySessionData, _serverSupportsDNSCaching, _sessionRecoveryRequested --- .../SqlClient/SqlInternalConnectionTds.cs | 38 -------- .../SqlClient/SqlInternalConnectionTds.cs | 38 -------- .../SqlClient/SqlInternalConnectionTds.cs | 90 +++++++++++++++++++ 3 files changed, 90 insertions(+), 76 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index e8cf65db0b..37c0df7066 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,44 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 - internal const int MsalHttpRetryStatusCode = 429; - // Connection re-route limit - internal const int MaxNumberOfRedirectRoute = 10; - - // CONNECTION AND STATE VARIABLES - private readonly SqlConnectionPoolGroupProviderInfo _poolGroupProviderInfo; // will only be null when called for ChangePassword, or creating SSE User Instance - private TdsParser _parser; - - private SqlLoginAck _loginAck; - private SqlCredential _credential; - private FederatedAuthenticationFeatureExtensionData _fedAuthFeatureExtensionData; - - // Connection Resiliency - private bool _sessionRecoveryRequested; - internal bool _sessionRecoveryAcknowledged; - internal SessionData _currentSessionData; // internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language - private SessionData _recoverySessionData; - - // Federated Authentication - // Response obtained from the server for FEDAUTHREQUIRED prelogin option. - internal bool _fedAuthRequired; - internal bool _federatedAuthenticationRequested; - internal bool _federatedAuthenticationAcknowledged; - internal bool _federatedAuthenticationInfoRequested; // Keep this distinct from _federatedAuthenticationRequested, since some fedauth library types may not need more info - internal bool _federatedAuthenticationInfoReceived; - - // The Federated Authentication returned by TryGetFedAuthTokenLocked or GetFedAuthToken. - SqlFedAuthToken _fedAuthToken = null; - internal byte[] _accessTokenInBytes; - internal readonly Func> _accessTokenCallback; - internal readonly SspiContextProvider _sspiContextProvider; - - private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; - - internal bool _cleanSQLDNSCaching = false; - private bool _serverSupportsDNSCaching = false; - /// /// Returns buffer time allowed before access token expiry to continue using the access token. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 871fdafd7c..f5419db222 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,44 +107,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 - internal const int MsalHttpRetryStatusCode = 429; - // Connection re-route limit - internal const int MaxNumberOfRedirectRoute = 10; - - // CONNECTION AND STATE VARIABLES - private readonly SqlConnectionPoolGroupProviderInfo _poolGroupProviderInfo; // will only be null when called for ChangePassword, or creating SSE User Instance - private TdsParser _parser; - - private SqlLoginAck _loginAck; - private SqlCredential _credential; - private FederatedAuthenticationFeatureExtensionData _fedAuthFeatureExtensionData; - - // Connection Resiliency - private bool _sessionRecoveryRequested; - internal bool _sessionRecoveryAcknowledged; - internal SessionData _currentSessionData; // internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language - private SessionData _recoverySessionData; - - // Federated Authentication - // Response obtained from the server for FEDAUTHREQUIRED prelogin option. - internal bool _fedAuthRequired; - internal bool _federatedAuthenticationRequested; - internal bool _federatedAuthenticationAcknowledged; - internal bool _federatedAuthenticationInfoRequested; // Keep this distinct from _federatedAuthenticationRequested, since some fedauth library types may not need more info - internal bool _federatedAuthenticationInfoReceived; - - // The Federated Authentication returned by TryGetFedAuthTokenLocked or GetFedAuthToken. - SqlFedAuthToken _fedAuthToken = null; - internal byte[] _accessTokenInBytes; - internal readonly Func> _accessTokenCallback; - internal readonly SspiContextProvider _sspiContextProvider; - - private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; - - internal bool _cleanSQLDNSCaching = false; - private bool _serverSupportsDNSCaching = false; - /// /// Returns buffer time allowed before access token expiry to continue using the access token. /// 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 77c55f51f6..c2f72280bd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2,10 +2,100 @@ // 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; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient.ConnectionPool; + namespace Microsoft.Data.SqlClient { internal partial class SqlInternalConnectionTds { + #region Constants + + /// + /// Maximum number of times the connection should be rerouted. + /// + // @TODO: Can be private? + internal const int MaxNumberOfRedirectRoute = 10; + + /// + /// Status code that indicates MSAL request should be retried. + /// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 + /// + // @TODO: Can be private? + internal const int MsalHttpRetryStatusCode = 429; + + #endregion + + #region Fields + + // @TODO: Should be private and accessed via internal property + internal byte[] _accessTokenInBytes; + + // @TODO: Should be private and accessed via internal property + // @TODO: Probably a good idea to introduce a delegate type + internal readonly Func> _accessTokenCallback; + + // @TODO: Should be private and accessed via internal property + // @TODO: Rename to match naming conventions + internal bool _cleanSQLDNSCaching = false; + + /// + /// Internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language + /// @TODO: No... all external usages should be via property. + /// + internal SessionData _currentSessionData; + + // @TODO: Should be private and accessed via internal property + internal bool _sessionRecoveryAcknowledged; + + // @TODO: Should be private and accessed via internal property + // @TODO: Could these federated auth fields be contained in a single record/struct/object? + internal bool _fedAuthRequired; + + // @TODO: Should be private and accessed via internal property + internal bool _federatedAuthenticationAcknowledged; + + // @TODO: Should be private and accessed via internal property + internal bool _federatedAuthenticationInfoReceived; + + /// + /// Keep this distinct from _federatedAuthenticationRequested, since some fedauth library types may not need more info + /// + // @TODO: Should be private and accessed via internal property + internal bool _federatedAuthenticationInfoRequested; + + // @TODO: Should be private and accessed via internal property + internal bool _federatedAuthenticationRequested; + + // @TODO: Should be private and accessed via internal property + internal readonly SspiContextProvider _sspiContextProvider; + + private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; + + private SqlCredential _credential; + + private FederatedAuthenticationFeatureExtensionData _fedAuthFeatureExtensionData; + + private SqlFedAuthToken _fedAuthToken = null; + + private SqlLoginAck _loginAck; + + private TdsParser _parser; + + /// + /// Will only be null when called for ChangePassword, or creating SSE User Instance. + /// + private readonly SqlConnectionPoolGroupProviderInfo _poolGroupProviderInfo; + + private SessionData _recoverySessionData; + + // @TODO: Rename to match naming conventions + private bool _serverSupportsDNSCaching = false; + + private bool _sessionRecoveryRequested; + #endregion } } From 67848e47cdc5df9d32dd8102f6a6eea43fbbf2ed Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 18:32:19 -0500 Subject: [PATCH 03/56] Merge accessTokenExpirationBufferTime --- .../SqlClient/SqlInternalConnectionTds.cs | 12 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 12 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 19 ++++++++++++++++++- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 37c0df7066..3bf15d5cc4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,18 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - /// - /// Returns buffer time allowed before access token expiry to continue using the access token. - /// - private int accessTokenExpirationBufferTime - { - get - { - return (ConnectionOptions.ConnectTimeout == ADP.InfiniteConnectionTimeout || ConnectionOptions.ConnectTimeout >= ADP.MaxBufferAccessTokenExpiry) - ? ADP.MaxBufferAccessTokenExpiry : ConnectionOptions.ConnectTimeout; - } - } - /// /// Get or set if SQLDNSCaching is supported by the server. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index f5419db222..de1abbc695 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,18 +107,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - /// - /// Returns buffer time allowed before access token expiry to continue using the access token. - /// - private int accessTokenExpirationBufferTime - { - get - { - return (ConnectionOptions.ConnectTimeout == ADP.InfiniteConnectionTimeout || ConnectionOptions.ConnectTimeout >= ADP.MaxBufferAccessTokenExpiry) - ? ADP.MaxBufferAccessTokenExpiry : ConnectionOptions.ConnectTimeout; - } - } - /// /// Get or set if SQLDNSCaching is supported by the server. /// 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 c2f72280bd..7fdeb09095 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -5,11 +5,12 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Data.Common; using Microsoft.Data.SqlClient.ConnectionPool; namespace Microsoft.Data.SqlClient { - internal partial class SqlInternalConnectionTds + internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { #region Constants @@ -97,5 +98,21 @@ internal partial class SqlInternalConnectionTds private bool _sessionRecoveryRequested; #endregion + + #region Properties + + /// + /// Returns buffer time allowed before access token expiry to continue using the access token. + /// + // @TODO: Rename to match naming convention + private int accessTokenExpirationBufferTime + { + get => ConnectionOptions.ConnectTimeout == ADP.InfiniteConnectionTimeout || + ConnectionOptions.ConnectTimeout >= ADP.MaxBufferAccessTokenExpiry + ? ADP.MaxBufferAccessTokenExpiry + : ConnectionOptions.ConnectTimeout; + } + + #endregion } } From da2c73fafa140c4b52af83236a2bdc22714bca74 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 18:38:51 -0500 Subject: [PATCH 04/56] Merge _dnsCachingBeforeRedirect, _SQLDNSRetryEnabled, IsSqlDnsCachingSupported, IsDnsCachingBeforeRedirectSupported, IsSQLDNSRetryEnabled --- .../SqlClient/SqlInternalConnectionTds.cs | 48 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 47 ------------------ .../SqlClient/SqlInternalConnectionTds.cs | 36 ++++++++++++++ 3 files changed, 36 insertions(+), 95 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 3bf15d5cc4..2ba2031925 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,54 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - /// - /// Get or set if SQLDNSCaching is supported by the server. - /// - internal bool IsSQLDNSCachingSupported - { - get - { - return _serverSupportsDNSCaching; - } - set - { - _serverSupportsDNSCaching = value; - } - } - - private bool _SQLDNSRetryEnabled = false; - - /// - /// Get or set if we need retrying with IP received from FeatureExtAck. - /// - internal bool IsSQLDNSRetryEnabled - { - get - { - return _SQLDNSRetryEnabled; - } - set - { - _SQLDNSRetryEnabled = value; - } - } - - private bool _dnsCachingBeforeRedirect = false; - - /// - /// Get or set if the control ring send redirect token and feature ext ack with true for DNSCaching - /// - internal bool IsDNSCachingBeforeRedirectSupported - { - get - { - return _dnsCachingBeforeRedirect; - } - set - { - _dnsCachingBeforeRedirect = value; - } - } internal SQLDNSInfo pendingSQLDNSObject = null; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index de1abbc695..0f94271d50 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,54 +107,7 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - /// - /// Get or set if SQLDNSCaching is supported by the server. - /// - internal bool IsSQLDNSCachingSupported - { - get - { - return _serverSupportsDNSCaching; - } - set - { - _serverSupportsDNSCaching = value; - } - } - - private bool _SQLDNSRetryEnabled = false; - /// - /// Get or set if we need retrying with IP received from FeatureExtAck. - /// - internal bool IsSQLDNSRetryEnabled - { - get - { - return _SQLDNSRetryEnabled; - } - set - { - _SQLDNSRetryEnabled = value; - } - } - - private bool _dnsCachingBeforeRedirect = false; - - /// - /// Get or set if the control ring send redirect token and feature ext ack with true for DNSCaching - /// - internal bool IsDNSCachingBeforeRedirectSupported - { - get - { - return _dnsCachingBeforeRedirect; - } - set - { - _dnsCachingBeforeRedirect = value; - } - } internal SQLDNSInfo pendingSQLDNSObject = 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 7fdeb09095..5c113de082 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -77,6 +77,9 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private SqlCredential _credential; + // @TODO: Rename to match naming conventions + private bool _dnsCachingBeforeRedirect = false; + private FederatedAuthenticationFeatureExtensionData _fedAuthFeatureExtensionData; private SqlFedAuthToken _fedAuthToken = null; @@ -92,6 +95,9 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private SessionData _recoverySessionData; + // @TODO: Rename to match naming conventions + private bool _SQLDNSRetryEnabled = false; + // @TODO: Rename to match naming conventions private bool _serverSupportsDNSCaching = false; @@ -101,6 +107,36 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos #region Properties + /// + /// Get or set if SQLDNSCaching is supported by the server. + /// + // @TODO: Make auto-property + internal bool IsSQLDNSCachingSupported + { + get => _serverSupportsDNSCaching; + set => _serverSupportsDNSCaching = value; + } + + /// + /// 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 + { + get => _dnsCachingBeforeRedirect; + set => _dnsCachingBeforeRedirect = value; + } + + /// + /// Get or set if we need retrying with IP received from FeatureExtAck. + /// + // @TODO: Make auto-property + internal bool IsSQLDNSRetryEnabled + { + get => _SQLDNSRetryEnabled; + set => _SQLDNSRetryEnabled = value; + } + /// /// Returns buffer time allowed before access token expiry to continue using the access token. /// From 161f254bf016787b3ce2c33f935cc19aa5ad0b5b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 18:49:29 -0500 Subject: [PATCH 05/56] Merging _forceMsalRetry, _forceExpiryLocked, _forceExpiryUnLocked, IsJsonSupportEnabled, IsVectorSupportEnabled, pendingSQLDNSObject, _tceVersionSupported, _dbConnectionPool, _dbConnectionPoolAuthenticationContextKey, _newDbConnectionPoolAuthenticationContext --- .../SqlClient/SqlInternalConnectionTds.cs | 43 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 44 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 70 +++++++++++++++++++ 3 files changed, 70 insertions(+), 87 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 2ba2031925..31f30721fc 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,49 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - - internal SQLDNSInfo pendingSQLDNSObject = null; - - // Json Support Flag - internal bool IsJsonSupportEnabled = false; - - // Vector Support Flag - internal bool IsVectorSupportEnabled = false; - - // TCE flags - internal byte _tceVersionSupported; - - // The pool that this connection is associated with, if at all it is. - private IDbConnectionPool _dbConnectionPool; - - // This is used to preserve the authentication context object if we decide to cache it for subsequent connections in the same pool. - // This will finally end up in _dbConnectionPool.AuthenticationContexts, but only after 1 successful login to SQL Server using this context. - // This variable is to persist the context after we have generated it, but before we have successfully completed the login with this new context. - // If this connection attempt ended up re-using the existing context and not create a new one, this will be null (since the context is not new). - private DbConnectionPoolAuthenticationContext _newDbConnectionPoolAuthenticationContext; - - // The key of the authentication context, built from information found in the FedAuthInfoToken. - private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey; - -#if DEBUG - // This is a test hook to enable testing of the retry paths for MSAL get access token. - // Sample code to enable: - // - // Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient.SQLInternalConnectionTds"); - // System.Reflection.FieldInfo field = type.GetField("_forceMsalRetry", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - // if (field != null) { - // field.SetValue(null, true); - // } - // - internal static bool _forceMsalRetry = false; - - // This is a test hook to simulate a token expiring within the next 45 minutes. - private static bool _forceExpiryLocked = false; - - // This is a test hook to simulate a token expiring within the next 10 minutes. - private static bool _forceExpiryUnLocked = false; -#endif //DEBUG - // The timespan defining the amount of time the authentication context needs to be valid for at-least, to re-use the cached context, // without making an attempt to refresh it. IF the context is expiring within the next 45 mins, then try to take a lock and refresh // the context, if the lock is acquired. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0f94271d50..705366b836 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,50 +107,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - - - internal SQLDNSInfo pendingSQLDNSObject = null; - - // Json Support Flag - internal bool IsJsonSupportEnabled = false; - - // Vector Support Flag - internal bool IsVectorSupportEnabled = false; - - // TCE flags - internal byte _tceVersionSupported; - - // The pool that this connection is associated with, if at all it is. - private IDbConnectionPool _dbConnectionPool; - - // This is used to preserve the authentication context object if we decide to cache it for subsequent connections in the same pool. - // This will finally end up in _dbConnectionPool.AuthenticationContexts, but only after 1 successful login to SQL Server using this context. - // This variable is to persist the context after we have generated it, but before we have successfully completed the login with this new context. - // If this connection attempt ended up re-using the existing context and not create a new one, this will be null (since the context is not new). - private DbConnectionPoolAuthenticationContext _newDbConnectionPoolAuthenticationContext; - - // The key of the authentication context, built from information found in the FedAuthInfoToken. - private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey; - -#if DEBUG - // This is a test hook to enable testing of the retry paths for MSAL get access token. - // Sample code to enable: - // - // Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient.SQLInternalConnectionTds"); - // System.Reflection.FieldInfo field = type.GetField("_forceMsalRetry", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - // if (field != null) { - // field.SetValue(null, true); - // } - // - internal static bool _forceMsalRetry = false; - - // This is a test hook to simulate a token expiring within the next 45 minutes. - private static bool _forceExpiryLocked = false; - - // This is a test hook to simulate a token expiring within the next 10 minutes. - private static bool _forceExpiryUnLocked = false; -#endif //DEBUG - // The timespan defining the amount of time the authentication context needs to be valid for at-least, to re-use the cached context, // without making an attempt to refresh it. IF the context is expiring within the next 45 mins, then try to take a lock and refresh // the context, if the lock is acquired. 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 5c113de082..3932ef37a6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -31,6 +31,34 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos #region Fields + #region Debug/Test Behavior Overrides + #if DEBUG + /// + /// This is a test hook to enable testing of the retry paths for MSAL get access token. + /// + /// + /// Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient.SQLInternalConnectionTds"); + /// FieldInfo field = type.GetField("_forceMsalRetry", BindingFlags.NonPublic | BindingFlags.Static); + /// if (field != null) + /// { + /// field.SetValue(null, true); + /// } + /// + /// @TODO: For unit tests, it should not be necessary to do this via reflection. + internal static bool _forceMsalRetry = false; + + /// + /// This is a test hook to simulate a token expiring within the next 45 minutes. + /// + private static bool _forceExpiryLocked = false; + + /// + /// This is a test hook to simulate a token expiring within the next 10 minutes. + /// + private static bool _forceExpiryUnLocked = false; + #endif + #endregion + // @TODO: Should be private and accessed via internal property internal byte[] _accessTokenInBytes; @@ -70,13 +98,44 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos // @TODO: Should be private and accessed via internal property internal bool _federatedAuthenticationRequested; + /// + /// Flag indicating whether JSON objects are supported by the server. + /// + // @TODO: Should be private and accessed via internal property + internal bool IsJsonSupportEnabled = false; + + /// + /// Flag indicating whether vector objects are supported by the server. + /// + // @TODO: Should be private and accessed via internal property + internal bool IsVectorSupportEnabled = false; + + // @TODO: Should be private and accessed via internal property + internal SQLDNSInfo pendingSQLDNSObject = null; + // @TODO: Should be private and accessed via internal property internal readonly SspiContextProvider _sspiContextProvider; + /// + /// TCE flags supported by the server. + /// + // @TODO: Should be private and accessed via internal property + internal byte _tceVersionSupported; + private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; private SqlCredential _credential; + /// + /// Pool this connection is associated with, if any. + /// + private IDbConnectionPool _dbConnectionPool; + + /// + /// Ley of the authentication context, built from information found in the FedAuthInfoToken. + /// + private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey; + // @TODO: Rename to match naming conventions private bool _dnsCachingBeforeRedirect = false; @@ -86,6 +145,17 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private SqlLoginAck _loginAck; + /// + /// This is used to preserve the authentication context object if we decide to cache it for + /// subsequent connections in the same pool. This will finally end up in + /// _dbConnectionPool.AuthenticationContexts, but only after 1 successful login to SQL + /// Server using this context. This variable is to persist the context after we have + /// generated it, but before we have successfully completed the login with this new + /// context. If this connection attempt ended up re-using the existing context and not + /// create a new one, this will be null (since the context is not new). + /// + private DbConnectionPoolAuthenticationContext _newDbConnectionPoolAuthenticationContext; + private TdsParser _parser; /// From 1ea8e582b0bec9bc6a3a2fa33380db386397720e Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 19:04:19 -0500 Subject: [PATCH 06/56] Merge _dbAuthenticationContextLockedRefreshTimeSpan, _dbAuthenticationContextUnLockedRefreshTimeSpan, s_transientErrors --- .../SqlClient/SqlInternalConnectionTds.cs | 47 ----------- .../SqlClient/SqlInternalConnectionTds.cs | 58 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 79 +++++++++++++++++++ 3 files changed, 79 insertions(+), 105 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 31f30721fc..91b77c1ce4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,53 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // The timespan defining the amount of time the authentication context needs to be valid for at-least, to re-use the cached context, - // without making an attempt to refresh it. IF the context is expiring within the next 45 mins, then try to take a lock and refresh - // the context, if the lock is acquired. - private static readonly TimeSpan _dbAuthenticationContextLockedRefreshTimeSpan = new TimeSpan(hours: 0, minutes: 45, seconds: 00); - - // The timespan defining the minimum amount of time the authentication context needs to be valid for re-using the cached context. - // If the context is expiring within the next 10 mins, then create a new context, irrespective of if another thread is trying to do the same. - private static readonly TimeSpan _dbAuthenticationContextUnLockedRefreshTimeSpan = new TimeSpan(hours: 0, minutes: 10, seconds: 00); - - // The errors in the transient error set are contained in - // https://azure.microsoft.com/en-us/documentation/articles/sql-database-develop-error-messages/#transient-faults-connection-loss-and-other-temporary-errors - private static readonly HashSet s_transientErrors = new HashSet - { - // SQL Error Code: 4060 - // Cannot open database "%.*ls" requested by the login. The login failed. - 4060, - - // SQL Error Code: 10928 - // Resource ID: %d. The %s limit for the database is %d and has been reached. - 10928, - - // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. - // However, the server is currently too busy to support requests greater than %d for this database. - 10929, - - // SQL Error Code: 40197 - // You will receive this error, when the service is down due to software or hardware upgrades, hardware failures, - // or any other failover problems. The error code (%d) embedded within the message of error 40197 provides - // additional information about the kind of failure or failover that occurred. Some examples of the error codes are - // embedded within the message of error 40197 are 40020, 40143, 40166, and 40540. - 40197, - - // The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. - 40501, - - // Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. - // If the problem persists, contact customer support, and provide them the session tracing ID of '%.*ls'. - 40613, - - // Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. - 42108, - - // The SQL pool is warming up. Please try again. - 42109 - }; - internal SessionData CurrentSessionData { get diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 705366b836..acf2be75bc 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,64 +107,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // The timespan defining the amount of time the authentication context needs to be valid for at-least, to re-use the cached context, - // without making an attempt to refresh it. IF the context is expiring within the next 45 mins, then try to take a lock and refresh - // the context, if the lock is acquired. - private static readonly TimeSpan _dbAuthenticationContextLockedRefreshTimeSpan = new TimeSpan(hours: 0, minutes: 45, seconds: 00); - - // The timespan defining the minimum amount of time the authentication context needs to be valid for re-using the cached context. - // If the context is expiring within the next 10 mins, then create a new context, irrespective of if another thread is trying to do the same. - private static readonly TimeSpan _dbAuthenticationContextUnLockedRefreshTimeSpan = new TimeSpan(hours: 0, minutes: 10, seconds: 00); - - // The errors in the transient error set are contained in - // https://azure.microsoft.com/en-us/documentation/articles/sql-database-develop-error-messages/#transient-faults-connection-loss-and-other-temporary-errors - private static readonly HashSet s_transientErrors = new HashSet - { - // SQL Error Code: 4060 - // Cannot open database "%.*ls" requested by the login. The login failed. - 4060, - - // SQL Error Code: 10928 - // Resource ID: %d. The %s limit for the database is %d and has been reached. - 10928, - - // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. - // However, the server is currently too busy to support requests greater than %d for this database. - 10929, - - // SQL Error Code: 40197 - // You will receive this error, when the service is down due to software or hardware upgrades, hardware failures, - // or any other failover problems. The error code (%d) embedded within the message of error 40197 provides - // additional information about the kind of failure or failover that occurred. Some examples of the error codes are - // embedded within the message of error 40197 are 40020, 40143, 40166, and 40540. - 40197, - 40020, - 40143, - 40166, - - // The service has encountered an error processing your request. Please try again. - 40540, - - // The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. - 40501, - - // Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. - // If the problem persists, contact customer support, and provide them the session tracing ID of '%.*ls'. - 40613, - - // Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. - 42108, - - // The SQL pool is warming up. Please try again. - 42109 - - // Do federation errors deserve to be here ? - // Note: Federation errors 10053 and 10054 might also deserve inclusion in your retry logic. - // 10053 - // 10054 - }; - internal SessionData CurrentSessionData { get 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 3932ef37a6..95a3fe3b94 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; @@ -27,6 +28,84 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos // @TODO: Can be private? internal const int MsalHttpRetryStatusCode = 429; + /// + /// The timespan defining the amount of time the authentication context needs to be valid + /// for at-least, to re-use the cached context, without making an attempt to refresh it. IF + /// the context is expiring within the next 45 mins, then try to take a lock and refresh + /// the context, if the lock is acquired. + /// + // @TODO: Rename to match naming conventions (s_camelCase or PascalCase) + private static readonly TimeSpan _dbAuthenticationContextLockedRefreshTimeSpan = + new TimeSpan(hours: 0, minutes: 45, seconds: 00); + + /// + /// The timespan defining the minimum amount of time the authentication context needs to be + /// valid for re-using the cached context. If the context is expiring within the next 10 + /// mins, then create a new context, irrespective of if another thread is trying to do the + /// 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 HashSet s_transientErrors = + [ + // SQL Error Code: 4060 + // Cannot open database "%.*ls" requested by the login. The login failed. + 4060, + + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + 10928, + + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current + // usage for the database is %d. However, the server is currently too busy to support + // requests greater than %d for this database. + 10929, + + // @TODO: Why aren't these included in netcore? + #if NETFRAMEWORK + 40020, + 40143, + 40166, + #endif + + // SQL Error Code: 40197 + // You will receive this error, when the service is down due to software or hardware + // upgrades, hardware failures, or any other failover problems. The error code (%d) + // embedded within the message of error 40197 provides additional information about the + // kind of failure or failover that occurred. Some examples of the error codes are + // embedded within the message of error 40197 are 40020, 40143, 40166, and 40540. + 40197, + + // The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. + // Code: %d. + 40501, + + #if NETFRAMEWORK + // @TODO: Why isn't this one included in netcore? + // The service has encountered an error processing your request. Please try again. + 40540, + #endif + + // Database '%.*ls' on server '%.*ls' is not currently available. Please retry the + // connection later. If the problem persists, contact customer support, and provide + // them the session tracing ID of '%.*ls'. + 40613, + + // Can not connect to the SQL pool since it is paused. Please resume the SQL pool and + // try again. + 42108, + + // The SQL pool is warming up. Please try again. + 42109 + + // @TODO: From netfx: Do federation errors deserve to be here ? + // Note: Federation errors 10053 and 10054 might also deserve inclusion in your retry logic. + // 10053 + // 10054 + ]; + #endregion #region Fields From 73d8cb22aecd5ea101724e9bfbc25e253fbf2370 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 19:15:19 -0500 Subject: [PATCH 07/56] Merge _asyncCommandCount, _fConnectionOpen, _currentLanguage, _currentPacketSize, _identity, _instanceName, _originalDatabase, _originalLanguage, _fResetConnection, CurrentSessionData --- .../SqlClient/SqlInternalConnectionTds.cs | 30 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 30 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 39 +++++++++++++++++++ 3 files changed, 39 insertions(+), 60 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 91b77c1ce4..fd71b74908 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,36 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - internal SessionData CurrentSessionData - { - get - { - if (_currentSessionData != null) - { - _currentSessionData._database = CurrentDatabase; - _currentSessionData._language = _currentLanguage; - } - return _currentSessionData; - } - } - - // FOR POOLING - private bool _fConnectionOpen = false; - - // FOR CONNECTION RESET MANAGEMENT - private bool _fResetConnection; - private string _originalDatabase; - private string _originalLanguage; - private string _currentLanguage; - private int _currentPacketSize; - private int _asyncCommandCount; // number of async Begins minus number of async Ends. - - // FOR SSE - private string _instanceName = string.Empty; - - // FOR NOTIFICATIONS - private DbConnectionPoolIdentity _identity; // Used to lookup info for notification matching Start(). - // FOR SYNCHRONIZATION IN TdsParser // How to use these locks: // 1. Whenever writing to the connection (with the exception of Cancellation) the _parserLock MUST be taken diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index acf2be75bc..f855a40d67 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,36 +107,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - internal SessionData CurrentSessionData - { - get - { - if (_currentSessionData != null) - { - _currentSessionData._database = CurrentDatabase; - _currentSessionData._language = _currentLanguage; - } - return _currentSessionData; - } - } - - // FOR POOLING - private bool _fConnectionOpen = false; - - // FOR CONNECTION RESET MANAGEMENT - private bool _fResetConnection; - private string _originalDatabase; - private string _originalLanguage; - private string _currentLanguage; - private int _currentPacketSize; - private int _asyncCommandCount; // number of async Begins minus number of async Ends. - - // FOR SSE - private string _instanceName = string.Empty; - - // FOR NOTIFICATIONS - private DbConnectionPoolIdentity _identity; // Used to lookup info for notification matching Start(). - // FOR SYNCHRONIZATION IN TdsParser // How to use these locks: // 1. Whenever writing to the connection (with the exception of Cancellation) the _parserLock MUST be taken 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 95a3fe3b94..5a3fb8d879 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -203,8 +203,20 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; + /// + /// Number of async Begins minus number of async Ends. + /// + private int _asyncCommandCount; + + // @TODO: Rename for naming conventions (remove f prefix) + private bool _fConnectionOpen = false; + private SqlCredential _credential; + private string _currentLanguage; + + private int _currentPacketSize; + /// /// Pool this connection is associated with, if any. /// @@ -222,6 +234,13 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private SqlFedAuthToken _fedAuthToken = null; + /// + /// Used to lookup info for notification matching Start(). + /// + private DbConnectionPoolIdentity _identity; + + private string _instanceName = string.Empty; + private SqlLoginAck _loginAck; /// @@ -235,6 +254,10 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos /// private DbConnectionPoolAuthenticationContext _newDbConnectionPoolAuthenticationContext; + private string _originalDatabase; + + private string _originalLanguage; + private TdsParser _parser; /// @@ -244,6 +267,9 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private SessionData _recoverySessionData; + // @TODO: Rename to match naming conventions (remove f prefix) + private bool _fResetConnection; + // @TODO: Rename to match naming conventions private bool _SQLDNSRetryEnabled = false; @@ -256,6 +282,19 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos #region Properties + internal SessionData CurrentSessionData + { + get + { + if (_currentSessionData != null) + { + _currentSessionData._database = CurrentDatabase; + _currentSessionData._language = _currentLanguage; + } + return _currentSessionData; + } + } + /// /// Get or set if SQLDNSCaching is supported by the server. /// From 93debea6d3b75f32f2e3199cfbf200485eefd5dc Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 27 Oct 2025 19:22:30 -0500 Subject: [PATCH 08/56] Merge _clientConnectionId, _originalClientConnectionId, _routingDestination, _threadIdOwningParserLock, _timeout, _timeoutErrorInternal, TimeoutErrorInternal --- .../SqlClient/SqlInternalConnectionTds.cs | 17 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 17 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 20 +++++++++++++++++++ 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index fd71b74908..afc6379994 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -206,24 +206,7 @@ internal bool ThreadMayHaveLock() } } - internal SyncAsyncLock _parserLock = new SyncAsyncLock(); - private int _threadIdOwningParserLock = -1; - - private SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; - - internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal - { - get { return _timeoutErrorInternal; } - } - - // OTHER STATE VARIABLES AND REFERENCES - internal Guid _clientConnectionId = Guid.Empty; - - // Routing information (ROR) - private Guid _originalClientConnectionId = Guid.Empty; - private string _routingDestination = null; - private readonly TimeoutTimer _timeout; // although the new password is generally not used it must be passed to the ctor // the new Login7 packet will always write out the new password (or a length of zero and no bytes if not present) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index f855a40d67..802bf079ff 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -207,24 +207,7 @@ internal bool ThreadMayHaveLock() } } - internal SyncAsyncLock _parserLock = new SyncAsyncLock(); - private int _threadIdOwningParserLock = -1; - - private SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; - - internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal - { - get { return _timeoutErrorInternal; } - } - - // OTHER STATE VARIABLES AND REFERENCES - internal Guid _clientConnectionId = Guid.Empty; - - // Routing information (ROR) - private Guid _originalClientConnectionId = Guid.Empty; - private string _routingDestination = null; - private readonly TimeoutTimer _timeout; // although the new password is generally not used it must be passed to the ctor // the new Login7 packet will always write out the new password (or a length of zero and no bytes if not present) 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 5a3fb8d879..746c0d900c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; namespace Microsoft.Data.SqlClient @@ -149,6 +150,8 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos // @TODO: Rename to match naming conventions internal bool _cleanSQLDNSCaching = false; + internal Guid _clientConnectionId = Guid.Empty; + /// /// Internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language /// @TODO: No... all external usages should be via property. @@ -254,6 +257,8 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos /// private DbConnectionPoolAuthenticationContext _newDbConnectionPoolAuthenticationContext; + private Guid _originalClientConnectionId = Guid.Empty; + private string _originalDatabase; private string _originalLanguage; @@ -270,6 +275,8 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos // @TODO: Rename to match naming conventions (remove f prefix) private bool _fResetConnection; + private string _routingDestination = null; + // @TODO: Rename to match naming conventions private bool _SQLDNSRetryEnabled = false; @@ -278,6 +285,13 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos private bool _sessionRecoveryRequested; + private int _threadIdOwningParserLock = -1; + + // @TODO: Rename to indicate this has to do with routing + private readonly TimeoutTimer _timeout; + + private SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; + #endregion #region Properties @@ -325,6 +339,12 @@ internal bool IsSQLDNSRetryEnabled set => _SQLDNSRetryEnabled = value; } + // @TODO: Make auto-property + internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal + { + get => _timeoutErrorInternal; + } + /// /// Returns buffer time allowed before access token expiry to continue using the access token. /// From d46aee8db94faad2be039284650a431782aa8731 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 28 Oct 2025 17:42:00 -0500 Subject: [PATCH 09/56] Merge AvailableInternalTransaction, ClientConnectionid, CurrentTransaction, Identity, OriginalClientConnectionId, PendingTransaction, RoutingDestination, RoutingInfo, IsTransientError() --- .../SqlClient/SqlInternalConnectionTds.cs | 77 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 77 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 68 ++++++++++++++++ 3 files changed, 68 insertions(+), 154 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index afc6379994..7c6503e92f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,83 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - // Returns true if the Sql error is a transient. - private bool IsTransientError(SqlException exc) - { - if (exc == null) - { - return false; - } - foreach (SqlError error in exc.Errors) - { - if (s_transientErrors.Contains(error.Number)) - { - // When server timeouts, connection is doomed. Reset here to allow reconnect. - UnDoomThisConnection(); - return true; - } - } - return false; - } - - internal Guid ClientConnectionId - { - get - { - return _clientConnectionId; - } - } - - internal Guid OriginalClientConnectionId - { - get - { - return _originalClientConnectionId; - } - } - - internal string RoutingDestination - { - get - { - return _routingDestination; - } - } - - internal RoutingInfo RoutingInfo { get; private set; } = null; - - internal override SqlInternalTransaction CurrentTransaction - { - get - { - return _parser.CurrentTransaction; - } - } - - internal override SqlInternalTransaction AvailableInternalTransaction - { - get - { - return _parser._fResetConnection ? null : CurrentTransaction; - } - } - - internal override SqlInternalTransaction PendingTransaction - { - get - { - return _parser.PendingTransaction; - } - } - - internal DbConnectionPoolIdentity Identity - { - get - { - return _identity; - } - } - internal string InstanceName { get diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 802bf079ff..e421702772 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,83 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - // Returns true if the Sql error is a transient. - private bool IsTransientError(SqlException exc) - { - if (exc == null) - { - return false; - } - foreach (SqlError error in exc.Errors) - { - if (s_transientErrors.Contains(error.Number)) - { - // When server timeouts, connection is doomed. Reset here to allow reconnect. - UnDoomThisConnection(); - return true; - } - } - return false; - } - - internal Guid ClientConnectionId - { - get - { - return _clientConnectionId; - } - } - - internal Guid OriginalClientConnectionId - { - get - { - return _originalClientConnectionId; - } - } - - internal string RoutingDestination - { - get - { - return _routingDestination; - } - } - - internal RoutingInfo RoutingInfo { get; private set; } = null; - - internal override SqlInternalTransaction CurrentTransaction - { - get - { - return _parser.CurrentTransaction; - } - } - - internal override SqlInternalTransaction AvailableInternalTransaction - { - get - { - return _parser._fResetConnection ? null : CurrentTransaction; - } - } - - internal override SqlInternalTransaction PendingTransaction - { - get - { - return _parser.PendingTransaction; - } - } - - internal DbConnectionPoolIdentity Identity - { - get - { - return _identity; - } - } - internal string InstanceName { get 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 746c0d900c..fab8654e76 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -296,6 +296,17 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos #region Properties + internal override SqlInternalTransaction AvailableInternalTransaction + { + get => _parser._fResetConnection ? null : CurrentTransaction; + } + + // @TODO: Make auto-property + internal Guid ClientConnectionId + { + get => _clientConnectionId; + } + internal SessionData CurrentSessionData { get @@ -309,6 +320,17 @@ internal SessionData CurrentSessionData } } + internal override SqlInternalTransaction CurrentTransaction + { + get => _parser.CurrentTransaction; + } + + // @TODO: Make auto-property + internal DbConnectionPoolIdentity Identity + { + get => _identity; + } + /// /// Get or set if SQLDNSCaching is supported by the server. /// @@ -339,6 +361,25 @@ internal bool IsSQLDNSRetryEnabled set => _SQLDNSRetryEnabled = value; } + // @TODO: Make auto-property + internal Guid OriginalClientConnectionId + { + get => _originalClientConnectionId; + } + + internal override SqlInternalTransaction PendingTransaction + { + get => _parser.PendingTransaction; + } + + // @TODO: Make auto-property + internal string RoutingDestination + { + get => _routingDestination; + } + + internal RoutingInfo RoutingInfo { get; private set; } = null; + // @TODO: Make auto-property internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal { @@ -358,5 +399,32 @@ private int accessTokenExpirationBufferTime } #endregion + + #region Private Methods + + /// + /// Returns true if the SQL error is transient, as per . + /// + private bool IsTransientError(SqlException exc) + { + if (exc == null) + { + return false; + } + + foreach (SqlError error in exc.Errors) + { + if (s_transientErrors.Contains(error.Number)) + { + // When server timeouts, connection is doomed. Reset here to allow reconnect. + UnDoomThisConnection(); + return true; + } + } + + return false; + } + + #endregion } } From 8581a517f6b8c6b0e2290999fbfcb2d7d3be32b5 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 28 Oct 2025 17:56:55 -0500 Subject: [PATCH 10/56] Merge ServerVersion (rewrite using string interpolation), InstanceName, Is2008OrNewer, IsLockedForBulkCopy, PacketSize, Parser, PoolGroupProviderInfo, ServerProcessId (made internal), ServerProvidedFailoverPartner, ReadyToPrepareTransaction # Conflicts: # src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs # src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs --- .../SqlClient/SqlInternalConnectionTds.cs | 77 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 77 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 54 +++++++++++++ .../SqlInternalConnectionTds.stub.cs | 2 - 4 files changed, 54 insertions(+), 156 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 7c6503e92f..d6fc5ffa2c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,83 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - internal string InstanceName - { - get - { - return _instanceName; - } - } - - internal override bool IsLockedForBulkCopy - { - get - { - return (!Parser.MARSOn && Parser._physicalStateObj.BcpLock); - } - } - - internal override bool Is2008OrNewer - { - get - { - return _parser.Is2008OrNewer; - } - } - - internal int PacketSize - { - get - { - return _currentPacketSize; - } - } - - internal TdsParser Parser - { - get - { - return _parser; - } - } - - internal string ServerProvidedFailoverPartner { get; set; } - - internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo - { - get - { - return _poolGroupProviderInfo; - } - } - - protected override bool ReadyToPrepareTransaction - { - get - { - // TODO: probably need to use a different method, but that's a different bug - bool result = FindLiveReader(null) == null; // can't prepare with a live data reader... - return result; - } - } - - public override string ServerVersion - { - get - { - return (string.Format("{0:00}.{1:00}.{2:0000}", _loginAck.majorVersion, - (short)_loginAck.minorVersion, _loginAck.buildNum)); - } - } - - public int ServerProcessId - { - get - { - return Parser._physicalStateObj._spid; - } - } - /// /// Get boolean that specifies whether an enlisted transaction can be unbound from /// the connection when that transaction completes. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index e421702772..5ddef22767 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,83 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - internal string InstanceName - { - get - { - return _instanceName; - } - } - - internal override bool IsLockedForBulkCopy - { - get - { - return (!Parser.MARSOn && Parser._physicalStateObj.BcpLock); - } - } - - internal override bool Is2008OrNewer - { - get - { - return _parser.Is2008OrNewer; - } - } - - internal int PacketSize - { - get - { - return _currentPacketSize; - } - } - - internal TdsParser Parser - { - get - { - return _parser; - } - } - - internal string ServerProvidedFailoverPartner { get; set; } - - internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo - { - get - { - return _poolGroupProviderInfo; - } - } - - protected override bool ReadyToPrepareTransaction - { - get - { - // TODO: probably need to use a different method, but that's a different bug - bool result = FindLiveReader(null) == null; // can't prepare with a live data reader... - return result; - } - } - - public override string ServerVersion - { - get - { - return (string.Format("{0:00}.{1:00}.{2:0000}", _loginAck.majorVersion, - (short)_loginAck.minorVersion, _loginAck.buildNum)); - } - } - - public int ServerProcessId - { - get - { - return Parser._physicalStateObj._spid; - } - } - /// /// Get boolean that specifies whether an enlisted transaction can be unbound from /// the connection when that transaction completes. 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 fab8654e76..0b69f07597 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -296,6 +296,12 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos #region Properties + // @TODO: Make internal + public override string ServerVersion + { + get => $"{_loginAck.majorVersion:00}.{(short)_loginAck.minorVersion:00}.{_loginAck.buildNum:0000}"; + } + internal override SqlInternalTransaction AvailableInternalTransaction { get => _parser._fResetConnection ? null : CurrentTransaction; @@ -331,6 +337,17 @@ internal DbConnectionPoolIdentity Identity get => _identity; } + // @TODO: Make auto-property + internal string InstanceName + { + get => _instanceName; + } + + internal override bool Is2008OrNewer + { + get => _parser.Is2008OrNewer; + } + /// /// Get or set if SQLDNSCaching is supported by the server. /// @@ -351,6 +368,11 @@ internal bool IsDNSCachingBeforeRedirectSupported set => _dnsCachingBeforeRedirect = value; } + internal override bool IsLockedForBulkCopy + { + get => !_parser.MARSOn && _parser._physicalStateObj.BcpLock; + } + /// /// Get or set if we need retrying with IP received from FeatureExtAck. /// @@ -367,11 +389,29 @@ internal Guid OriginalClientConnectionId get => _originalClientConnectionId; } + // @TODO: Make auto-property + internal int PacketSize + { + get => _currentPacketSize; + } + + // @TODO: Make auto-property + internal TdsParser Parser + { + get => _parser; + } + internal override SqlInternalTransaction PendingTransaction { get => _parser.PendingTransaction; } + // @TODO: Make auto-property + internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo + { + get => _poolGroupProviderInfo; + } + // @TODO: Make auto-property internal string RoutingDestination { @@ -380,12 +420,26 @@ internal string RoutingDestination internal RoutingInfo RoutingInfo { get; private set; } = null; + internal int ServerProcessId + { + get => _parser._physicalStateObj._spid; + } + + internal string ServerProvidedFailoverPartner { get; set; } + // @TODO: Make auto-property internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal { get => _timeoutErrorInternal; } + // @TODO: Rename to be "IsReadyToPrepareTransaction" + protected override bool ReadyToPrepareTransaction + { + // TODO: probably need to use a different method but that's a different bug + get => FindLiveReader(null) is null; // Can't prepare with a live data reader... + } + /// /// Returns buffer time allowed before access token expiry to continue using the access token. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs index d5fb3689fc..9b2df299b6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs @@ -32,8 +32,6 @@ internal SqlInternalConnectionTds( Func> accessTokenCallback = null) { } - - internal string InstanceName => null; internal bool ThreadHasParserLockForClose { get; set; } From 5d74b8710951aed3b3fdbd9f5d94e033179e9a24 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 28 Oct 2025 23:05:15 -0500 Subject: [PATCH 11/56] Merge IsAccesTokenExpired, UnbindOnTransactionCompletion Move IsSqlDNSCachingSupported --- .../SqlClient/SqlInternalConnectionTds.cs | 23 ------------- .../SqlClient/SqlInternalConnectionTds.cs | 23 ------------- .../SqlClient/SqlInternalConnectionTds.cs | 33 ++++++++++++++++--- 3 files changed, 28 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index d6fc5ffa2c..2dc30b56d2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,29 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - /// - /// Get boolean that specifies whether an enlisted transaction can be unbound from - /// the connection when that transaction completes. - /// - /// - /// This override always returns false. - /// - /// - /// The SqlInternalConnectionTds.CheckEnlistedTransactionBinding method handles implicit unbinding for disposed transactions. - /// - protected override bool UnbindOnTransactionCompletion - { - get - { - return false; - } - } - - /// - /// Validates if federated authentication is used, Access Token used by this connection is active for the value of 'accessTokenExpirationBufferTime'. - /// - internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); - //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 5ddef22767..521fe9f28a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,29 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - /// - /// Get boolean that specifies whether an enlisted transaction can be unbound from - /// the connection when that transaction completes. - /// - /// - /// This override always returns false. - /// - /// - /// The SqlInternalConnectionTds.CheckEnlistedTransactionBinding method handles implicit unbinding for disposed transactions. - /// - protected override bool UnbindOnTransactionCompletion - { - get - { - return false; - } - } - - /// - /// Validates if federated authentication is used, Access Token used by this connection is active for the value of 'accessTokenExpirationBufferTime'. - /// - internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); - //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// 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 0b69f07597..2303710af1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -349,13 +349,13 @@ internal override bool Is2008OrNewer } /// - /// Get or set if SQLDNSCaching is supported by the server. + /// Validates if federated authentication is used, Access Token used by this connection is + /// active for the value of 'accessTokenExpirationBufferTime'. /// - // @TODO: Make auto-property - internal bool IsSQLDNSCachingSupported + internal override bool IsAccessTokenExpired { - get => _serverSupportsDNSCaching; - set => _serverSupportsDNSCaching = value; + get => _federatedAuthenticationInfoRequested && + DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); } /// @@ -373,6 +373,16 @@ internal override bool IsLockedForBulkCopy get => !_parser.MARSOn && _parser._physicalStateObj.BcpLock; } + /// + /// Get or set if SQLDNSCaching is supported by the server. + /// + // @TODO: Make auto-property + internal bool IsSQLDNSCachingSupported + { + get => _serverSupportsDNSCaching; + set => _serverSupportsDNSCaching = value; + } + /// /// Get or set if we need retrying with IP received from FeatureExtAck. /// @@ -440,6 +450,19 @@ protected override bool ReadyToPrepareTransaction get => FindLiveReader(null) is null; // Can't prepare with a live data reader... } + /// + /// Get boolean that specifies whether an enlisted transaction can be unbound from the + /// connection when that transaction completes. This override always returns false. + /// + /// + /// The SqlInternalConnectionTds.CheckEnlistedTransactionBinding method handles implicit + /// unbinding for disposed transactions. + /// + protected override bool UnbindOnTransactionCompletion + { + get => false; + } + /// /// Returns buffer time allowed before access token expiry to continue using the access token. /// From 144a9cdef6e68b8ce12dd82ac52bc93c127aa50f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 16:49:28 -0500 Subject: [PATCH 12/56] Merge IgnoreEnvChange, ThreadHasParserLockForClose --- .../SqlClient/SqlInternalConnectionTds.cs | 34 ------------------ .../SqlClient/SqlInternalConnectionTds.cs | 34 ------------------ .../SqlClient/SqlInternalConnectionTds.cs | 36 +++++++++++++++++++ .../SqlInternalConnectionTds.stub.cs | 2 -- 4 files changed, 36 insertions(+), 70 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 2dc30b56d2..6dfbd08029 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1752,14 +1752,6 @@ internal void BreakConnection() } } - internal bool IgnoreEnvChange - { // true if we are only draining environment change tokens, used by TdsParser - get - { - return RoutingInfo != null; // connection was routed, ignore rest of env change - } - } - internal void OnEnvChange(SqlEnvChange rec) { Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); @@ -2618,32 +2610,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) // Helper methods for Locks //////////////////////////////////////////////////////////////////////////////////////// - // Indicates if the current thread claims to hold the parser lock - internal bool ThreadHasParserLockForClose - { - get - { - return _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId; - } - set - { - Debug.Assert(_parserLock.ThreadMayHaveLock(), "Should not modify ThreadHasParserLockForClose without taking the lock first"); - Debug.Assert(_threadIdOwningParserLock == -1 || _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId, "Another thread already claims to own the parser lock"); - - if (value) - { - // If setting to true, then the thread owning the lock is the current thread - _threadIdOwningParserLock = Thread.CurrentThread.ManagedThreadId; - } - else if (_threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId) - { - // If setting to false and currently owns the lock, then no-one owns the lock - _threadIdOwningParserLock = -1; - } - // else This thread didn't own the parser lock and doesn't claim to own it, so do nothing - } - } - internal override bool TryReplaceConnection( DbConnection outerConnection, SqlConnectionFactory connectionFactory, diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 521fe9f28a..45edbfa4ed 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1803,14 +1803,6 @@ internal void BreakConnection() } } - internal bool IgnoreEnvChange - { // true if we are only draining environment change tokens, used by TdsParser - get - { - return RoutingInfo != null; // connection was routed, ignore rest of env change - } - } - internal void OnEnvChange(SqlEnvChange rec) { Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); @@ -2658,32 +2650,6 @@ internal void OnFeatureExtAck(int featureId, byte[] data) // Helper methods for Locks //////////////////////////////////////////////////////////////////////////////////////// - // Indicates if the current thread claims to hold the parser lock - internal bool ThreadHasParserLockForClose - { - get - { - return _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId; - } - set - { - Debug.Assert(_parserLock.ThreadMayHaveLock(), "Should not modify ThreadHasParserLockForClose without taking the lock first"); - Debug.Assert(_threadIdOwningParserLock == -1 || _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId, "Another thread already claims to own the parser lock"); - - if (value) - { - // If setting to true, then the thread owning the lock is the current thread - _threadIdOwningParserLock = Thread.CurrentThread.ManagedThreadId; - } - else if (_threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId) - { - // If setting to false and currently owns the lock, then no-one owns the lock - _threadIdOwningParserLock = -1; - } - // else This thread didn't own the parser lock and doesn't claim to own it, so do nothing - } - } - internal override bool TryReplaceConnection( DbConnection outerConnection, SqlConnectionFactory connectionFactory, 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 2303710af1..8dcdd24908 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; @@ -337,6 +338,15 @@ internal DbConnectionPoolIdentity Identity get => _identity; } + /// + /// Returns true if we are only draining environment change tokens, used by + /// . This is the case when the connection has been re-routed. + /// + internal bool IgnoreEnvChange + { + get => RoutingInfo != null; + } + // @TODO: Make auto-property internal string InstanceName { @@ -443,6 +453,32 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal get => _timeoutErrorInternal; } + /// + /// Indicates if the current thread claims to hold the parser lock. + /// + internal bool ThreadHasParserLockForClose + { + // @TODO: Replace with Environment.CurrentManagedThreadId in netcore + get => _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId; + set + { + Debug.Assert(_parserLock.ThreadMayHaveLock(), "Should not modify ThreadHasParserLockForClose without taking the lock first"); + Debug.Assert(_threadIdOwningParserLock == -1 || _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId, "Another thread already claims to own the parser lock"); + + if (value) + { + // If setting to true, then the thread owning the lock is the current thread + _threadIdOwningParserLock = Thread.CurrentThread.ManagedThreadId; + } + else if (_threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId) + { + // If setting to false and currently owns the lock, then no-one owns the lock + _threadIdOwningParserLock = -1; + } + // else This thread didn't own the parser lock and doesn't claim to own it, so do nothing + } + } + // @TODO: Rename to be "IsReadyToPrepareTransaction" protected override bool ReadyToPrepareTransaction { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs index 9b2df299b6..b2a450d90d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs @@ -32,8 +32,6 @@ internal SqlInternalConnectionTds( Func> accessTokenCallback = null) { } - - internal bool ThreadHasParserLockForClose { get; set; } internal void Dispose() { } From 282ecee41406f00ff9ea98566e2dca930d830c00 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 16:55:20 -0500 Subject: [PATCH 13/56] Merge ChangeDatabaseInternal (split long lines, added parameter labels to literals, replaced ad-hoc SQL with string interpolation) --- .../SqlClient/SqlInternalConnectionTds.cs | 10 ------- .../SqlClient/SqlInternalConnectionTds.cs | 10 ------- .../SqlClient/SqlInternalConnectionTds.cs | 28 +++++++++++++++++++ 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6dfbd08029..f128aacdc1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,16 +345,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - [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); - Task executeTask = _parser.TdsExecuteSQLBatch("use " + database, ConnectionOptions.ConnectTimeout, null, _parser._physicalStateObj, sync: true); - Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); - _parser.Run(RunBehavior.UntilDone, null, null, null, _parser._physicalStateObj); - } - public override void Dispose() { SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0} disposing", ObjectID); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 45edbfa4ed..a50b66f5d2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,16 +344,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - [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); - Task executeTask = _parser.TdsExecuteSQLBatch("use " + database, ConnectionOptions.ConnectTimeout, null, _parser._physicalStateObj, sync: true); - Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); - _parser.Run(RunBehavior.UntilDone, null, null, null, _parser._physicalStateObj); - } - public override void Dispose() { SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0} disposing", ObjectID); 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 8dcdd24908..1628b9d345 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; @@ -513,6 +514,33 @@ private int accessTokenExpirationBufferTime #endregion + #region Protected Methods + + // @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); + } + + #endregion + #region Private Methods /// From 8f9dab88f15818e1d5b10cd6c903c83f27dc504c Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:03:02 -0500 Subject: [PATCH 14/56] Merge Dispose (Split long lines, adopt netcore style trave event formatting, use null propagation for parser.disconnect, move comments around) --- .../SqlClient/SqlInternalConnectionTds.cs | 22 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 22 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 35 +++++++++++++++++++ 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index f128aacdc1..5439559d3e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,28 +345,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - public override void Dispose() - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0} disposing", ObjectID); - try - { - TdsParser parser = Interlocked.Exchange(ref _parser, null); // guard against multiple concurrent dispose calls -- Delegated Transactions might cause this. - - Debug.Assert(parser != null && _fConnectionOpen || parser == null && !_fConnectionOpen, "Unexpected state on dispose"); - if (parser != null) - { - parser.Disconnect(); - } - } - finally - { - // close will always close, even if exception is thrown - // remember to null out any object references - _loginAck = null; - _fConnectionOpen = false; // mark internal connection as closed - } - base.Dispose(); - } internal override void ValidateConnectionForExecute(SqlCommand command) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index a50b66f5d2..9bc5e415d4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,28 +344,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - public override void Dispose() - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0} disposing", ObjectID); - try - { - TdsParser parser = Interlocked.Exchange(ref _parser, null); // guard against multiple concurrent dispose calls -- Delegated Transactions might cause this. - - Debug.Assert(parser != null && _fConnectionOpen || parser == null && !_fConnectionOpen, "Unexpected state on dispose"); - if (parser != null) - { - parser.Disconnect(); - } - } - finally - { - // close will always close, even if exception is thrown - // remember to null out any object references - _loginAck = null; - _fConnectionOpen = false; // mark internal connection as closed - } - base.Dispose(); - } internal override void ValidateConnectionForExecute(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 1628b9d345..120bb7c626 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -514,6 +514,41 @@ private int accessTokenExpirationBufferTime #endregion + #region Public and Internal Methods + + // @TODO: Make internal by making the SqlInternalConnection implementation internal + public override void Dispose() + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.Dispose | ADV | " + + $"Object ID {ObjectID} disposing"); + + try + { + // Guard against multiple concurrent dispose calls -- Delegated Transactions might + // cause this. + TdsParser parser = Interlocked.Exchange(ref _parser, null); + + Debug.Assert(parser is not null && _fConnectionOpen || parser is null && !_fConnectionOpen, + "Unexpected state on dispose"); + + parser?.Disconnect(); + } + finally + { + // Close will always close, even if exception is thrown. + // Remember to null out any object references. + _loginAck = null; + + // Mark internal connection as closed + _fConnectionOpen = false; + } + + base.Dispose(); + } + + #endregion + #region Protected Methods // @TODO: Is this suppression still required From 3e0649a7bd8799c6472f9e9925d5a65a67cc328b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:08:56 -0500 Subject: [PATCH 15/56] Merge ValidateConnectionForExecute (remove redundant parantheses, split long lines, moved a couple comments) --- .../SqlClient/SqlInternalConnectionTds.cs | 42 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 42 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 48 +++++++++++++++++++ 3 files changed, 48 insertions(+), 84 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 5439559d3e..a5ca4ad925 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,48 +346,6 @@ internal SqlInternalConnectionTds( // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - internal override void ValidateConnectionForExecute(SqlCommand command) - { - TdsParser parser = _parser; - if ((parser == null) || (parser.State == TdsParserState.Broken) || (parser.State == TdsParserState.Closed)) - { - throw ADP.ClosedConnectionError(); - } - else - { - SqlDataReader reader = null; - if (parser.MARSOn) - { - if (command != null) - { // command can't have datareader already associated with it - reader = FindLiveReader(command); - } - } - else - { // single execution/datareader per connection - if (_asyncCommandCount > 0) - { - throw SQL.MARSUnsupportedOnConnection(); - } - - reader = FindLiveReader(null); - } - if (reader != null) - { - // if MARS is on, then a datareader associated with the command exists - // or if MARS is off, then a datareader exists - throw ADP.OpenReaderExists(parser.MARSOn); // MDAC 66411 - } - else if (!parser.MARSOn && parser._physicalStateObj.HasPendingData) - { - parser.DrainData(parser._physicalStateObj); - } - Debug.Assert(!parser._physicalStateObj.HasPendingData, "Should not have a busy physicalStateObject at this point!"); - - parser.RollbackOrphanedAPITransactions(); - } - } - /// /// Validate the enlisted transaction state, taking into consideration the ambient transaction and transaction unbinding mode. /// If there is no enlisted transaction, this method is a nop. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 9bc5e415d4..c284364fee 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,48 +345,6 @@ internal SqlInternalConnectionTds( // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - internal override void ValidateConnectionForExecute(SqlCommand command) - { - TdsParser parser = _parser; - if ((parser == null) || (parser.State == TdsParserState.Broken) || (parser.State == TdsParserState.Closed)) - { - throw ADP.ClosedConnectionError(); - } - else - { - SqlDataReader reader = null; - if (parser.MARSOn) - { - if (command != null) - { // command can't have datareader already associated with it - reader = FindLiveReader(command); - } - } - else - { // single execution/datareader per connection - if (_asyncCommandCount > 0) - { - throw SQL.MARSUnsupportedOnConnection(); - } - - reader = FindLiveReader(null); - } - if (reader != null) - { - // if MARS is on, then a datareader associated with the command exists - // or if MARS is off, then a datareader exists - throw ADP.OpenReaderExists(parser.MARSOn); // MDAC 66411 - } - else if (!parser.MARSOn && parser._physicalStateObj.HasPendingData) - { - parser.DrainData(parser._physicalStateObj); - } - Debug.Assert(!parser._physicalStateObj.HasPendingData, "Should not have a busy physicalStateObject at this point!"); - - parser.RollbackOrphanedAPITransactions(); - } - } - /// /// Validate the enlisted transaction state, taking into consideration the ambient transaction and transaction unbinding mode. /// If there is no enlisted transaction, this method is a nop. 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 120bb7c626..21eeb96435 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -547,6 +547,54 @@ public override void Dispose() base.Dispose(); } + internal override void ValidateConnectionForExecute(SqlCommand command) + { + TdsParser parser = _parser; + if (parser == null || parser.State is TdsParserState.Broken or TdsParserState.Closed) + { + throw ADP.ClosedConnectionError(); + } + else + { + SqlDataReader reader = null; + if (parser.MARSOn) + { + if (command != null) + { + // Command can't have datareader already associated with it + reader = FindLiveReader(command); + } + } + else + { + // Single execution/datareader per connection + if (_asyncCommandCount > 0) + { + throw SQL.MARSUnsupportedOnConnection(); + } + + reader = FindLiveReader(null); + } + + if (reader != null) + { + // If MARS is on, then a datareader associated with the command exists or if + // MARS is off, then a datareader exists + throw ADP.OpenReaderExists(parser.MARSOn); // MDAC 66411 + } + + if (!parser.MARSOn && parser._physicalStateObj.HasPendingData) + { + parser.DrainData(parser._physicalStateObj); + } + + Debug.Assert(!parser._physicalStateObj.HasPendingData, + "Should not have a busy physicalStateObject at this point!"); + + parser.RollbackOrphanedAPITransactions(); + } + } + #endregion #region Protected Methods From 89fa5ea3860e6eb09a46aac5e0ee386f62b7b0f2 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:15:06 -0500 Subject: [PATCH 16/56] Merge CheckEnlistedTransactionBinding (split long lines, flip enum comparison operands, add parameter labels for constant) --- .../SqlClient/SqlInternalConnectionTds.cs | 59 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 59 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 57 ++++++++++++++++++ 3 files changed, 57 insertions(+), 118 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index a5ca4ad925..917eaf7cd9 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,65 +346,6 @@ internal SqlInternalConnectionTds( // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Validate the enlisted transaction state, taking into consideration the ambient transaction and transaction unbinding mode. - /// If there is no enlisted transaction, this method is a nop. - /// - /// - /// - /// This method must be called while holding a lock on the SqlInternalConnection instance, - /// to ensure we don't accidentally execute after the transaction has completed on a different thread, - /// causing us to unwittingly execute in auto-commit mode. - /// - /// - /// - /// When using Explicit transaction unbinding, - /// verify that the enlisted transaction is active and equal to the current ambient transaction. - /// - /// - /// - /// When using Implicit transaction unbinding, - /// verify that the enlisted transaction is active. - /// If it is not active, and the transaction object has been disposed, unbind from the transaction. - /// If it is not active and not disposed, throw an exception. - /// - /// - internal void CheckEnlistedTransactionBinding() - { - // If we are enlisted in a transaction, check that transaction is active. - // When using explicit transaction unbinding, also verify that the enlisted transaction is the current transaction. - Transaction enlistedTransaction = EnlistedTransaction; - - if (enlistedTransaction != null) - { - bool requireExplicitTransactionUnbind = ConnectionOptions.TransactionBinding == SqlConnectionString.TransactionBindingEnum.ExplicitUnbind; - - if (requireExplicitTransactionUnbind) - { - Transaction currentTransaction = Transaction.Current; - - if (TransactionStatus.Active != enlistedTransaction.TransactionInformation.Status || !enlistedTransaction.Equals(currentTransaction)) - { - throw ADP.TransactionConnectionMismatch(); - } - } - else // implicit transaction unbind - { - if (TransactionStatus.Active != enlistedTransaction.TransactionInformation.Status) - { - if (EnlistedTransactionDisposed) - { - DetachTransaction(enlistedTransaction, true); - } - else - { - throw ADP.TransactionCompletedButNotDisposed(); - } - } - } - } - } - internal override bool IsConnectionAlive(bool throwOnException) => _parser._physicalStateObj.IsConnectionAlive(throwOnException); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c284364fee..7481d27cec 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,65 +345,6 @@ internal SqlInternalConnectionTds( // GENERAL METHODS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Validate the enlisted transaction state, taking into consideration the ambient transaction and transaction unbinding mode. - /// If there is no enlisted transaction, this method is a nop. - /// - /// - /// - /// This method must be called while holding a lock on the SqlInternalConnection instance, - /// to ensure we don't accidentally execute after the transaction has completed on a different thread, - /// causing us to unwittingly execute in auto-commit mode. - /// - /// - /// - /// When using Explicit transaction unbinding, - /// verify that the enlisted transaction is active and equal to the current ambient transaction. - /// - /// - /// - /// When using Implicit transaction unbinding, - /// verify that the enlisted transaction is active. - /// If it is not active, and the transaction object has been disposed, unbind from the transaction. - /// If it is not active and not disposed, throw an exception. - /// - /// - internal void CheckEnlistedTransactionBinding() - { - // If we are enlisted in a transaction, check that transaction is active. - // When using explicit transaction unbinding, also verify that the enlisted transaction is the current transaction. - Transaction enlistedTransaction = EnlistedTransaction; - - if (enlistedTransaction != null) - { - bool requireExplicitTransactionUnbind = ConnectionOptions.TransactionBinding == SqlConnectionString.TransactionBindingEnum.ExplicitUnbind; - - if (requireExplicitTransactionUnbind) - { - Transaction currentTransaction = Transaction.Current; - - if (TransactionStatus.Active != enlistedTransaction.TransactionInformation.Status || !enlistedTransaction.Equals(currentTransaction)) - { - throw ADP.TransactionConnectionMismatch(); - } - } - else // implicit transaction unbind - { - if (TransactionStatus.Active != enlistedTransaction.TransactionInformation.Status) - { - if (EnlistedTransactionDisposed) - { - DetachTransaction(enlistedTransaction, true); - } - else - { - throw ADP.TransactionCompletedButNotDisposed(); - } - } - } - } - } - internal override bool IsConnectionAlive(bool throwOnException) => _parser._physicalStateObj.IsConnectionAlive(throwOnException); 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 21eeb96435..fc0968b58e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; @@ -516,6 +517,62 @@ private int accessTokenExpirationBufferTime #region Public and Internal Methods + /// + /// Validate the enlisted transaction state, taking into consideration the ambient + /// transaction and transaction unbinding mode. If there is no enlisted transaction, this + /// method is a no-op. + /// + /// + /// + /// This method must be called while holding a lock on the SqlInternalConnection instance, + /// to ensure we don't accidentally execute after the transaction has completed on a + /// different thread, causing us to unwittingly execute in auto-commit mode. + /// + /// + /// When using Explicit transaction unbinding, verify that the enlisted transaction is + /// active and equal to the current ambient transaction. + /// + /// + /// When using Implicit transaction unbinding, verify that the enlisted transaction is + /// active. If it is not active, and the transaction object has been disposed, unbind from + /// the transaction. If it is not active and not disposed, throw an exception. + /// + /// + internal void CheckEnlistedTransactionBinding() + { + // If we are enlisted in a transaction, check that transaction is active. + // When using explicit transaction unbinding, also verify that the enlisted transaction + // is the current transaction. + Transaction enlistedTransaction = EnlistedTransaction; + + if (enlistedTransaction != null) + { + if (ConnectionOptions.TransactionBinding is SqlConnectionString.TransactionBindingEnum.ExplicitUnbind) + { + Transaction currentTransaction = Transaction.Current; + if (enlistedTransaction.TransactionInformation.Status != TransactionStatus.Active || !enlistedTransaction.Equals(currentTransaction)) + { + throw ADP.TransactionConnectionMismatch(); + } + } + else + { + // Implicit transaction unbind + if (enlistedTransaction.TransactionInformation.Status != TransactionStatus.Active) + { + if (EnlistedTransactionDisposed) + { + DetachTransaction(enlistedTransaction, isExplicitlyReleasing: true); + } + else + { + throw ADP.TransactionCompletedButNotDisposed(); + } + } + } + } + } + // @TODO: Make internal by making the SqlInternalConnection implementation internal public override void Dispose() { From 7deca1b724e1f2c7ce69925a9f221ee6ece14a8f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:18:50 -0500 Subject: [PATCH 17/56] Merge IsConnectionAlive, Activate (diff denoted by #if NETFRAMEWORK, rebalance comments) --- .../SqlClient/SqlInternalConnectionTds.cs | 34 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 32 ----------------- .../SqlClient/SqlInternalConnectionTds.cs | 28 +++++++++++++++ 3 files changed, 28 insertions(+), 66 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 917eaf7cd9..01db3c548b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,44 +342,10 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // GENERAL METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal override bool IsConnectionAlive(bool throwOnException) => - _parser._physicalStateObj.IsConnectionAlive(throwOnException); - //////////////////////////////////////////////////////////////////////////////////////// // POOLING METHODS //////////////////////////////////////////////////////////////////////////////////////// - protected override void Activate(Transaction transaction) - { -#if NETFRAMEWORK - FailoverPermissionDemand(); // Demand for unspecified failover pooled connections -#endif - - // When we're required to automatically enlist in transactions and - // there is one we enlist in it. On the other hand, if there isn't a - // transaction and we are currently enlisted in one, then we - // unenlist from it. - // - // Regardless of whether we're required to automatically enlist, - // when there is not a current transaction, we cannot leave the - // connection enlisted in a transaction. - if (transaction != null) - { - if (ConnectionOptions.Enlist) - { - Enlist(transaction); - } - } - else - { - Enlist(null); - } - } - protected override void InternalDeactivate() { // When we're deactivated, the user must have called End on all diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 7481d27cec..670da46b58 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,42 +341,10 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // GENERAL METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal override bool IsConnectionAlive(bool throwOnException) => - _parser._physicalStateObj.IsConnectionAlive(throwOnException); - //////////////////////////////////////////////////////////////////////////////////////// // POOLING METHODS //////////////////////////////////////////////////////////////////////////////////////// - protected override void Activate(Transaction transaction) - { - FailoverPermissionDemand(); // Demand for unspecified failover pooled connections - - // When we're required to automatically enlist in transactions and - // there is one we enlist in it. On the other hand, if there isn't a - // transaction and we are currently enlisted in one, then we - // unenlist from it. - // - // Regardless of whether we're required to automatically enlist, - // when there is not a current transaction, we cannot leave the - // connection enlisted in a transaction. - if (transaction != null) - { - if (ConnectionOptions.Enlist) - { - Enlist(transaction); - } - } - else - { - Enlist(null); - } - } - protected override void InternalDeactivate() { // When we're deactivated, the user must have called End on all 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 fc0968b58e..ea463de3eb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -604,6 +604,9 @@ public override void Dispose() base.Dispose(); } + internal override bool IsConnectionAlive(bool throwOnException) => + _parser._physicalStateObj.IsConnectionAlive(throwOnException); + internal override void ValidateConnectionForExecute(SqlCommand command) { TdsParser parser = _parser; @@ -656,6 +659,31 @@ internal override void ValidateConnectionForExecute(SqlCommand command) #region Protected Methods + protected override void Activate(Transaction transaction) + { + #if NETFRAMEWORK + // Demand for unspecified failover pooled connections + FailoverPermissionDemand(); + #endif + + // When we're required to automatically enlist in transactions and there is one we + // enlist in it. On the other hand, if there isn't a transaction, and we are + // currently enlisted in one, then we un-enlist from it. + // Regardless of whether we're required to automatically enlist, when there is not a + // current transaction, we cannot leave the connection enlisted in a transaction. + if (transaction != null) + { + if (ConnectionOptions.Enlist) + { + Enlist(transaction); + } + } + else + { + Enlist(null); + } + } + // @TODO: Is this suppression still required [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs protected override void ChangeDatabaseInternal(string database) From ae0f909255320cb46586b69fbe48ffc6b70febe1 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:21:20 -0500 Subject: [PATCH 18/56] Merge InternalDeactivate (rebalance comments, flip comparison with constant operands) --- .../SqlClient/SqlInternalConnectionTds.cs | 33 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 33 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 29 ++++++++++++++++ 3 files changed, 29 insertions(+), 66 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 01db3c548b..624bd497f0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,39 +346,6 @@ internal SqlInternalConnectionTds( // POOLING METHODS //////////////////////////////////////////////////////////////////////////////////////// - protected override void InternalDeactivate() - { - // 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 (0 != _asyncCommandCount) - { - DoomThisConnection(); - } - - // 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 transaction roots? - if (!(IsTransactionRoot && Pool == null)) - { - Debug.Assert(_parser != null || IsConnectionDoomed, "Deactivating a disposed connection?"); - if (_parser != null) - { - _parser.Deactivate(IsConnectionDoomed); - - if (!IsConnectionDoomed) - { - ResetConnection(); - } - } - } - } - [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs private void ResetConnection() { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 670da46b58..ab4db870a0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,39 +345,6 @@ internal SqlInternalConnectionTds( // POOLING METHODS //////////////////////////////////////////////////////////////////////////////////////// - protected override void InternalDeactivate() - { - // 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 (0 != _asyncCommandCount) - { - DoomThisConnection(); - } - - // 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 transaction roots? - if (!(IsTransactionRoot && Pool == null)) - { - Debug.Assert(_parser != null || IsConnectionDoomed, "Deactivating a disposed connection?"); - if (_parser != null) - { - _parser.Deactivate(IsConnectionDoomed); - - if (!IsConnectionDoomed) - { - ResetConnection(); - } - } - } - } - [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs private void ResetConnection() { 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 ea463de3eb..906f21b2dc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -707,6 +707,35 @@ protected override void ChangeDatabaseInternal(string database) _parser._physicalStateObj); } + protected override void InternalDeactivate() + { + // 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) + { + DoomThisConnection(); + } + + // 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. + if (!(IsTransactionRoot && Pool == null)) + { + Debug.Assert(_parser != null || IsConnectionDoomed, "Deactivating a disposed connection?"); + if (_parser != null) + { + _parser.Deactivate(IsConnectionDoomed); + + if (!IsConnectionDoomed) + { + ResetConnection(); + } + } + } + } + #endregion #region Private Methods From 8cec5c7f0fea6552c490d0190e69e98764edae0b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:24:07 -0500 Subject: [PATCH 19/56] Merge ResetConnection (split long lines, rebalanced comments) --- .../SqlClient/SqlInternalConnectionTds.cs | 25 ----------------- .../SqlClient/SqlInternalConnectionTds.cs | 25 ----------------- .../SqlClient/SqlInternalConnectionTds.cs | 27 +++++++++++++++++++ 3 files changed, 27 insertions(+), 50 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 624bd497f0..1e4ef57a89 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,31 +346,6 @@ internal SqlInternalConnectionTds( // POOLING METHODS //////////////////////////////////////////////////////////////////////////////////////// - [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs - private void ResetConnection() - { - // For implicit pooled connections, if connection reset behavior is specified, - // reset the database and language properties back to default. It is important - // to do this on activate so that the dictionary is correct before SqlConnection - // obtains a clone. - - Debug.Assert(!HasLocalTransactionFromAPI, "Upon ResetConnection SqlInternalConnectionTds has a currently ongoing local transaction."); - Debug.Assert(!_parser._physicalStateObj.HasPendingData, "Upon ResetConnection SqlInternalConnectionTds has pending data."); - - if (_fResetConnection) - { - // Pooled connections that are enlisted in a transaction must have their transaction - // preserved when reseting the connection state. Otherwise, future uses of the connection - // from the pool will execute outside of the transaction, in auto-commit mode. - // https://github.com/dotnet/SqlClient/issues/2970 - _parser.PrepareResetConnection(EnlistedTransaction is not null && Pool is not null); - - // Reset dictionary values, since calling reset will not send us env_changes. - CurrentDatabase = _originalDatabase; - _currentLanguage = _originalLanguage; - } - } - internal void DecrementAsyncCount() { Debug.Assert(_asyncCommandCount > 0); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index ab4db870a0..83465a2bb3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,31 +345,6 @@ internal SqlInternalConnectionTds( // POOLING METHODS //////////////////////////////////////////////////////////////////////////////////////// - [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs - private void ResetConnection() - { - // For implicit pooled connections, if connection reset behavior is specified, - // reset the database and language properties back to default. It is important - // to do this on activate so that the dictionary is correct before SqlConnection - // obtains a clone. - - Debug.Assert(!HasLocalTransactionFromAPI, "Upon ResetConnection SqlInternalConnectionTds has a currently ongoing local transaction."); - Debug.Assert(!_parser._physicalStateObj.HasPendingData, "Upon ResetConnection SqlInternalConnectionTds has pending data."); - - if (_fResetConnection) - { - // Pooled connections that are enlisted in a transaction must have their transaction - // preserved when reseting the connection state. Otherwise, future uses of the connection - // from the pool will execute outside of the transaction, in auto-commit mode. - // https://github.com/dotnet/SqlClient/issues/2970 - _parser.PrepareResetConnection(EnlistedTransaction is not null && Pool is not null); - - // Reset dictionary values, since calling reset will not send us env_changes. - CurrentDatabase = _originalDatabase; - _currentLanguage = _originalLanguage; - } - } - internal void DecrementAsyncCount() { Debug.Assert(_asyncCommandCount > 0); 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 906f21b2dc..1ebfe4c7ad 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -763,6 +763,33 @@ private bool IsTransientError(SqlException exc) return false; } + // @TODO: Is this suppression still required + [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs + private void ResetConnection() + { + // For implicit pooled connections, if connection reset behavior is specified, reset + // the database and language properties back to default. It is important to do this on + // activate so that the dictionary is correct before SqlConnection obtains a clone. + + Debug.Assert(!HasLocalTransactionFromAPI, + "Upon ResetConnection SqlInternalConnectionTds has a currently ongoing local transaction."); + Debug.Assert(!_parser._physicalStateObj.HasPendingData, + "Upon ResetConnection SqlInternalConnectionTds has pending data."); + + if (_fResetConnection) + { + // Pooled connections that are enlisted in a transaction must have their transaction + // preserved when resetting the connection state. Otherwise, future uses of the connection + // from the pool will execute outside the transaction, in auto-commit mode. + // https://github.com/dotnet/SqlClient/issues/2970 + _parser.PrepareResetConnection(EnlistedTransaction is not null && Pool is not null); + + // Reset dictionary values, since calling reset will not send us env_changes. + CurrentDatabase = _originalDatabase; + _currentLanguage = _originalLanguage; + } + } + #endregion } } From 0e089f015b72c023f1cbc5f27e456db5389e1051 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:29:12 -0500 Subject: [PATCH 20/56] Merge DecrementAsyncCount, DisconnectTransaction, IncrementAsyncCount (rewrote disconnectransaction into a single line) --- .../SqlClient/SqlInternalConnectionTds.cs | 30 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 30 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 14 +++++++++ 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 1e4ef57a89..b6ed706750 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,36 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // POOLING METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal void DecrementAsyncCount() - { - Debug.Assert(_asyncCommandCount > 0); - Interlocked.Decrement(ref _asyncCommandCount); - } - - internal void IncrementAsyncCount() - { - Interlocked.Increment(ref _asyncCommandCount); - } - - - //////////////////////////////////////////////////////////////////////////////////////// - // LOCAL TRANSACTION METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal override void DisconnectTransaction(SqlInternalTransaction internalTransaction) - { - TdsParser parser = Parser; - - if (parser != null) - { - parser.DisconnectTransaction(internalTransaction); - } - } - internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso) { ExecuteTransaction(transactionRequest, name, iso, null, false); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 83465a2bb3..980a669751 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,36 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // POOLING METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal void DecrementAsyncCount() - { - Debug.Assert(_asyncCommandCount > 0); - Interlocked.Decrement(ref _asyncCommandCount); - } - - internal void IncrementAsyncCount() - { - Interlocked.Increment(ref _asyncCommandCount); - } - - - //////////////////////////////////////////////////////////////////////////////////////// - // LOCAL TRANSACTION METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal override void DisconnectTransaction(SqlInternalTransaction internalTransaction) - { - TdsParser parser = Parser; - - if (parser != null) - { - parser.DisconnectTransaction(internalTransaction); - } - } - internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso) { ExecuteTransaction(transactionRequest, name, iso, null, false); 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 1ebfe4c7ad..b2af22e000 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -573,6 +573,15 @@ internal void CheckEnlistedTransactionBinding() } } + internal void DecrementAsyncCount() + { + Debug.Assert(_asyncCommandCount > 0); + Interlocked.Decrement(ref _asyncCommandCount); + } + + internal override void DisconnectTransaction(SqlInternalTransaction internalTransaction) => + _parser?.DisconnectTransaction(internalTransaction); + // @TODO: Make internal by making the SqlInternalConnection implementation internal public override void Dispose() { @@ -604,6 +613,11 @@ public override void Dispose() base.Dispose(); } + internal void IncrementAsyncCount() + { + Interlocked.Increment(ref _asyncCommandCount); + } + internal override bool IsConnectionAlive(bool throwOnException) => _parser._physicalStateObj.IsConnectionAlive(throwOnException); From f809dce0699c4044b69ba5025b004bee1637ee57 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:35:33 -0500 Subject: [PATCH 21/56] Merge ExecuteTransaction, Remove unused ExecuteTransaction overload (use `is Enum or Enum` pattern, chop argument list) --- .../SqlClient/SqlInternalConnectionTds.cs | 33 ------------------ .../SqlClient/SqlInternalConnectionTds.cs | 33 ------------------ .../SqlClient/SqlInternalConnectionTds.cs | 34 +++++++++++++++++++ 3 files changed, 34 insertions(+), 66 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index b6ed706750..a84d143aae 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,39 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso) - { - ExecuteTransaction(transactionRequest, name, iso, null, false); - } - - internal override void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest) - { - if (IsConnectionDoomed) - { // doomed means we can't do anything else... - if (transactionRequest == TransactionRequest.Rollback - || transactionRequest == TransactionRequest.IfRollback) - { - return; - } - throw SQL.ConnectionDoomed(); - } - - if (transactionRequest == TransactionRequest.Commit - || transactionRequest == TransactionRequest.Rollback - || transactionRequest == TransactionRequest.IfRollback) - { - if (!Parser.MARSOn && Parser._physicalStateObj.BcpLock) - { - throw SQL.ConnectionLockedForBcpEvent(); - } - } - - string transactionName = name == null ? string.Empty : name; - - ExecuteTransaction2005(transactionRequest, transactionName, iso, internalTransaction, isDelegateControlRequest); - } - - internal void ExecuteTransaction2005( TransactionRequest transactionRequest, string transactionName, diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 980a669751..ce9cd7cb4f 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,39 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso) - { - ExecuteTransaction(transactionRequest, name, iso, null, false); - } - - internal override void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest) - { - if (IsConnectionDoomed) - { // doomed means we can't do anything else... - if (transactionRequest == TransactionRequest.Rollback - || transactionRequest == TransactionRequest.IfRollback) - { - return; - } - throw SQL.ConnectionDoomed(); - } - - if (transactionRequest == TransactionRequest.Commit - || transactionRequest == TransactionRequest.Rollback - || transactionRequest == TransactionRequest.IfRollback) - { - if (!Parser.MARSOn && Parser._physicalStateObj.BcpLock) - { - throw SQL.ConnectionLockedForBcpEvent(); - } - } - - string transactionName = name == null ? string.Empty : name; - - ExecuteTransaction2005(transactionRequest, transactionName, iso, internalTransaction, isDelegateControlRequest); - } - - internal void ExecuteTransaction2005( TransactionRequest transactionRequest, string transactionName, 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 b2af22e000..fee1f5f8bf 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -613,6 +613,40 @@ public override void Dispose() base.Dispose(); } + internal override void ExecuteTransaction( + TransactionRequest transactionRequest, + string name, + System.Data.IsolationLevel iso, + SqlInternalTransaction internalTransaction, + bool isDelegateControlRequest) + { + if (IsConnectionDoomed) + { + // Doomed means we can't do anything else... + if (transactionRequest is TransactionRequest.Rollback or + TransactionRequest.IfRollback) + { + return; + } + + throw SQL.ConnectionDoomed(); + } + + if (transactionRequest is TransactionRequest.Commit or + TransactionRequest.Rollback or + TransactionRequest.IfRollback) + { + if (!Parser.MARSOn && Parser._physicalStateObj.BcpLock) + { + throw SQL.ConnectionLockedForBcpEvent(); + } + } + + string transactionName = name ?? string.Empty; + + ExecuteTransaction2005(transactionRequest, transactionName, iso, internalTransaction, isDelegateControlRequest); + } + internal void IncrementAsyncCount() { Interlocked.Increment(ref _asyncCommandCount); From 5f9143c96d596fb2af5266fad46826bb3c395577 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:45:29 -0500 Subject: [PATCH 22/56] Merge ExecuteTransaction2005 (make private, split long lines, rebalance comments) --- .../SqlClient/SqlInternalConnectionTds.cs | 146 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 146 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 160 ++++++++++++++++++ 3 files changed, 160 insertions(+), 292 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index a84d143aae..789e7ad6f5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,152 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - internal void ExecuteTransaction2005( - TransactionRequest transactionRequest, - string transactionName, - System.Data.IsolationLevel iso, - SqlInternalTransaction internalTransaction, - bool isDelegateControlRequest) - { - TdsEnums.TransactionManagerRequestType requestType = TdsEnums.TransactionManagerRequestType.Begin; - TdsEnums.TransactionManagerIsolationLevel isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadCommitted; - - switch (iso) - { - case System.Data.IsolationLevel.Unspecified: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.Unspecified; - break; - case System.Data.IsolationLevel.ReadCommitted: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadCommitted; - break; - case System.Data.IsolationLevel.ReadUncommitted: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadUncommitted; - break; - case System.Data.IsolationLevel.RepeatableRead: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.RepeatableRead; - break; - case System.Data.IsolationLevel.Serializable: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.Serializable; - break; - case System.Data.IsolationLevel.Snapshot: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.Snapshot; - break; - case System.Data.IsolationLevel.Chaos: - throw SQL.NotSupportedIsolationLevel(iso); - default: - throw ADP.InvalidIsolationLevel(iso); - } - - TdsParserStateObject stateObj = _parser._physicalStateObj; - TdsParser parser = _parser; - bool mustPutSession = false; - bool releaseConnectionLock = false; - - Debug.Assert(!ThreadHasParserLockForClose || _parserLock.ThreadMayHaveLock(), "Thread claims to have parser lock, but lock is not taken"); - if (!ThreadHasParserLockForClose) - { - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; // In case of error, let the connection know that we already own the parser lock - releaseConnectionLock = true; - } - try - { - switch (transactionRequest) - { - case TransactionRequest.Begin: - requestType = TdsEnums.TransactionManagerRequestType.Begin; - break; - case TransactionRequest.Promote: - requestType = TdsEnums.TransactionManagerRequestType.Promote; - break; - case TransactionRequest.Commit: - requestType = TdsEnums.TransactionManagerRequestType.Commit; - break; - case TransactionRequest.IfRollback: - // Map IfRollback to Rollback since with 2005 and beyond we should never need - // the if since the server will inform us when transactions have completed - // as a result of an error on the server. - case TransactionRequest.Rollback: - requestType = TdsEnums.TransactionManagerRequestType.Rollback; - break; - case TransactionRequest.Save: - requestType = TdsEnums.TransactionManagerRequestType.Save; - break; - default: - Debug.Fail("Unknown transaction type"); - break; - } - - // only restore if connection lock has been taken within the function - if (internalTransaction != null && internalTransaction.RestoreBrokenConnection && releaseConnectionLock) - { - Task reconnectTask = internalTransaction.Parent.Connection.ValidateAndReconnect(() => - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - releaseConnectionLock = false; - }, ADP.InfiniteConnectionTimeout); - if (reconnectTask != null) - { - AsyncHelper.WaitForCompletion(reconnectTask, ADP.InfiniteConnectionTimeout); // there is no specific timeout for BeginTransaction, uses ConnectTimeout - internalTransaction.ConnectionHasBeenRestored = true; - return; - } - } - - // SQLBUDT #20010853 - Promote, Commit and Rollback requests for - // delegated transactions often happen while there is an open result - // set, so we need to handle them by using a different MARS session, - // otherwise we'll write on the physical state objects while someone - // else is using it. When we don't have MARS enabled, we need to - // lock the physical state object to synchronize it's use at least - // until we increment the open results count. Once it's been - // incremented the delegated transaction requests will fail, so they - // won't stomp on anything. - // - // We need to keep this lock through the duration of the TM request - // so that we won't hijack a different request's data stream and a - // different request won't hijack ours, so we have a lock here on - // an object that the ExecTMReq will also lock, but since we're on - // the same thread, the lock is a no-op. - - if (internalTransaction != null && internalTransaction.IsDelegated) - { - if (_parser.MARSOn) - { - stateObj = _parser.GetSession(this); - mustPutSession = true; - } - if (internalTransaction.OpenResultsCount != 0) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection is marked to be doomed when closed. Transaction ended with OpenResultsCount {1} > 0, MARSOn {2}", - ObjectID, - internalTransaction.OpenResultsCount, - _parser.MARSOn); - throw SQL.CannotCompleteDelegatedTransactionWithOpenResults(this, _parser.MARSOn); - } - } - - // SQLBU #406778 - _parser may be nulled out during TdsExecuteTransactionManagerRequest. - // Only use local variable after this call. - _parser.TdsExecuteTransactionManagerRequest(null, requestType, transactionName, isoLevel, - ConnectionOptions.ConnectTimeout, internalTransaction, stateObj, isDelegateControlRequest); - } - finally - { - if (mustPutSession) - { - parser.PutSession(stateObj); - } - - if (releaseConnectionLock) - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - } - } - //////////////////////////////////////////////////////////////////////////////////////// // DISTRIBUTED TRANSACTION METHODS //////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index ce9cd7cb4f..1f482a5fa3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,152 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - internal void ExecuteTransaction2005( - TransactionRequest transactionRequest, - string transactionName, - System.Data.IsolationLevel iso, - SqlInternalTransaction internalTransaction, - bool isDelegateControlRequest) - { - TdsEnums.TransactionManagerRequestType requestType = TdsEnums.TransactionManagerRequestType.Begin; - TdsEnums.TransactionManagerIsolationLevel isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadCommitted; - - switch (iso) - { - case System.Data.IsolationLevel.Unspecified: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.Unspecified; - break; - case System.Data.IsolationLevel.ReadCommitted: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadCommitted; - break; - case System.Data.IsolationLevel.ReadUncommitted: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadUncommitted; - break; - case System.Data.IsolationLevel.RepeatableRead: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.RepeatableRead; - break; - case System.Data.IsolationLevel.Serializable: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.Serializable; - break; - case System.Data.IsolationLevel.Snapshot: - isoLevel = TdsEnums.TransactionManagerIsolationLevel.Snapshot; - break; - case System.Data.IsolationLevel.Chaos: - throw SQL.NotSupportedIsolationLevel(iso); - default: - throw ADP.InvalidIsolationLevel(iso); - } - - TdsParserStateObject stateObj = _parser._physicalStateObj; - TdsParser parser = _parser; - bool mustPutSession = false; - bool releaseConnectionLock = false; - - Debug.Assert(!ThreadHasParserLockForClose || _parserLock.ThreadMayHaveLock(), "Thread claims to have parser lock, but lock is not taken"); - if (!ThreadHasParserLockForClose) - { - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; // In case of error, let the connection know that we already own the parser lock - releaseConnectionLock = true; - } - try - { - switch (transactionRequest) - { - case TransactionRequest.Begin: - requestType = TdsEnums.TransactionManagerRequestType.Begin; - break; - case TransactionRequest.Promote: - requestType = TdsEnums.TransactionManagerRequestType.Promote; - break; - case TransactionRequest.Commit: - requestType = TdsEnums.TransactionManagerRequestType.Commit; - break; - case TransactionRequest.IfRollback: - // Map IfRollback to Rollback since with 2005 and beyond we should never need - // the if since the server will inform us when transactions have completed - // as a result of an error on the server. - case TransactionRequest.Rollback: - requestType = TdsEnums.TransactionManagerRequestType.Rollback; - break; - case TransactionRequest.Save: - requestType = TdsEnums.TransactionManagerRequestType.Save; - break; - default: - Debug.Fail("Unknown transaction type"); - break; - } - - // only restore if connection lock has been taken within the function - if (internalTransaction != null && internalTransaction.RestoreBrokenConnection && releaseConnectionLock) - { - Task reconnectTask = internalTransaction.Parent.Connection.ValidateAndReconnect(() => - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - releaseConnectionLock = false; - }, ADP.InfiniteConnectionTimeout); - if (reconnectTask != null) - { - AsyncHelper.WaitForCompletion(reconnectTask, ADP.InfiniteConnectionTimeout); // there is no specific timeout for BeginTransaction, uses ConnectTimeout - internalTransaction.ConnectionHasBeenRestored = true; - return; - } - } - - // SQLBUDT #20010853 - Promote, Commit and Rollback requests for - // delegated transactions often happen while there is an open result - // set, so we need to handle them by using a different MARS session, - // otherwise we'll write on the physical state objects while someone - // else is using it. When we don't have MARS enabled, we need to - // lock the physical state object to synchronize it's use at least - // until we increment the open results count. Once it's been - // incremented the delegated transaction requests will fail, so they - // won't stomp on anything. - // - // We need to keep this lock through the duration of the TM request - // so that we won't hijack a different request's data stream and a - // different request won't hijack ours, so we have a lock here on - // an object that the ExecTMReq will also lock, but since we're on - // the same thread, the lock is a no-op. - - if (internalTransaction != null && internalTransaction.IsDelegated) - { - if (_parser.MARSOn) - { - stateObj = _parser.GetSession(this); - mustPutSession = true; - } - if (internalTransaction.OpenResultsCount != 0) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection is marked to be doomed when closed. Transaction ended with OpenResultsCount {1} > 0, MARSOn {2}", - ObjectID, - internalTransaction.OpenResultsCount, - _parser.MARSOn); - throw SQL.CannotCompleteDelegatedTransactionWithOpenResults(this, _parser.MARSOn); - } - } - - // SQLBU #406778 - _parser may be nulled out during TdsExecuteTransactionManagerRequest. - // Only use local variable after this call. - _parser.TdsExecuteTransactionManagerRequest(null, requestType, transactionName, isoLevel, - ConnectionOptions.ConnectTimeout, internalTransaction, stateObj, isDelegateControlRequest); - } - finally - { - if (mustPutSession) - { - parser.PutSession(stateObj); - } - - if (releaseConnectionLock) - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - } - } - //////////////////////////////////////////////////////////////////////////////////////// // DISTRIBUTED TRANSACTION METHODS //////////////////////////////////////////////////////////////////////////////////////// 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 fee1f5f8bf..68a995ea44 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -788,6 +788,166 @@ protected override void InternalDeactivate() #region Private Methods + // @TODO: Rename to ExecuteTransactionInternal ... we don't have multiple server version implementations of this + private void ExecuteTransaction2005( + TransactionRequest transactionRequest, + string transactionName, + System.Data.IsolationLevel iso, // @TODO: We have three different IsolationTypes, maybe we can indicate which one is which a bit better. + SqlInternalTransaction internalTransaction, + bool isDelegateControlRequest) + { + TdsEnums.TransactionManagerRequestType requestType = TdsEnums.TransactionManagerRequestType.Begin; + TdsEnums.TransactionManagerIsolationLevel isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadCommitted; + + switch (iso) + { + case System.Data.IsolationLevel.Unspecified: + isoLevel = TdsEnums.TransactionManagerIsolationLevel.Unspecified; + break; + case System.Data.IsolationLevel.ReadCommitted: + isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadCommitted; + break; + case System.Data.IsolationLevel.ReadUncommitted: + isoLevel = TdsEnums.TransactionManagerIsolationLevel.ReadUncommitted; + break; + case System.Data.IsolationLevel.RepeatableRead: + isoLevel = TdsEnums.TransactionManagerIsolationLevel.RepeatableRead; + break; + case System.Data.IsolationLevel.Serializable: + isoLevel = TdsEnums.TransactionManagerIsolationLevel.Serializable; + break; + case System.Data.IsolationLevel.Snapshot: + isoLevel = TdsEnums.TransactionManagerIsolationLevel.Snapshot; + break; + case System.Data.IsolationLevel.Chaos: + throw SQL.NotSupportedIsolationLevel(iso); + default: + throw ADP.InvalidIsolationLevel(iso); + } + + TdsParserStateObject stateObj = _parser._physicalStateObj; + TdsParser parser = _parser; + bool mustPutSession = false; + bool releaseConnectionLock = false; + + Debug.Assert(!ThreadHasParserLockForClose || _parserLock.ThreadMayHaveLock(), + "Thread claims to have parser lock, but lock is not taken"); + + if (!ThreadHasParserLockForClose) + { + _parserLock.Wait(canReleaseFromAnyThread: false); + + // In case of error, let the connection know that we already own the parser lock + ThreadHasParserLockForClose = true; + releaseConnectionLock = true; + } + try + { + switch (transactionRequest) + { + case TransactionRequest.Begin: + requestType = TdsEnums.TransactionManagerRequestType.Begin; + break; + case TransactionRequest.Promote: + requestType = TdsEnums.TransactionManagerRequestType.Promote; + break; + case TransactionRequest.Commit: + requestType = TdsEnums.TransactionManagerRequestType.Commit; + break; + case TransactionRequest.IfRollback: + // Map IfRollback to Rollback since with 2005 and beyond we should never + // need the "if" since the server will inform us when transactions have + // completed as a result of an error on the server. + case TransactionRequest.Rollback: + requestType = TdsEnums.TransactionManagerRequestType.Rollback; + break; + case TransactionRequest.Save: + requestType = TdsEnums.TransactionManagerRequestType.Save; + break; + default: + Debug.Fail("Unknown transaction type"); + break; + } + + // only restore if connection lock has been taken within the function + if (internalTransaction != null && internalTransaction.RestoreBrokenConnection && releaseConnectionLock) + { + Task reconnectTask = internalTransaction.Parent.Connection.ValidateAndReconnect( + () => + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + releaseConnectionLock = false; + }, + ADP.InfiniteConnectionTimeout); + + if (reconnectTask != null) + { + // There is no specific timeout for BeginTransaction, uses ConnectTimeout + AsyncHelper.WaitForCompletion(reconnectTask, ADP.InfiniteConnectionTimeout); + internalTransaction.ConnectionHasBeenRestored = true; + return; + } + } + + // Promote, Commit and Rollback requests for delegated transactions often happen + // while there is an open result set, so we need to handle them by using a + // different MARS session. Otherwise, we'll write on the physical state objects + // while someone else is using it. When we don't have MARS enabled, we need to lock + // the physical state object to synchronize its use, at least until we increment + // the open results count. Once it's been incremented the delegated transaction + // requests will fail, so they won't stomp on anything. + + // We need to keep this lock through the duration of the TM request so that we + // won't hijack a different request's data stream and a different request won't + // hijack ours, so we have a lock here on an object that the ExecTMReq will also + // lock, but since we're on the same thread, the lock is a no-op. + + if (internalTransaction != null && internalTransaction.IsDelegated) + { + if (_parser.MARSOn) + { + stateObj = _parser.GetSession(this); + mustPutSession = true; + } + + if (internalTransaction.OpenResultsCount != 0) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Connection is marked to be doomed when closed. Transaction ended with OpenResultsCount {1} > 0, MARSOn {2}", + ObjectID, + internalTransaction.OpenResultsCount, + _parser.MARSOn); + throw SQL.CannotCompleteDelegatedTransactionWithOpenResults(this, _parser.MARSOn); + } + } + + // _parser may be nulled out during TdsExecuteTransactionManagerRequest. Only use + // local variable after this call. + _parser.TdsExecuteTransactionManagerRequest( + buffer: null, + requestType, + transactionName, + isoLevel, + ConnectionOptions.ConnectTimeout, + internalTransaction, + stateObj, + isDelegateControlRequest); + } + finally + { + if (mustPutSession) + { + parser.PutSession(stateObj); + } + + if (releaseConnectionLock) + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + } + } + } + /// /// Returns true if the SQL error is transient, as per . /// From 5a73c922045e9e292e54e6f8545ecb2454327f59 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:49:20 -0500 Subject: [PATCH 23/56] Merge GetDTCAddress, PropagateTransactionCookie; Remove DelegateTransactionEnded (redundant override) (split long lines) --- .../SqlClient/SqlInternalConnectionTds.cs | 22 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 22 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 17 ++++++++++++++ 3 files changed, 17 insertions(+), 44 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 789e7ad6f5..5a94e35e5d 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -342,28 +342,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // DISTRIBUTED TRANSACTION METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal override void DelegatedTransactionEnded() - { - // TODO: I don't like the way that this works, but I don't want to rototill the entire pooler to avoid this call. - base.DelegatedTransactionEnded(); - } - - protected override byte[] GetDTCAddress() - { - byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); - Debug.Assert(dtcAddress != null, "null dtcAddress?"); - return dtcAddress; - } - - protected override void PropagateTransactionCookie(byte[] cookie) - { - _parser.PropagateDistributedTransaction(cookie, ConnectionOptions.ConnectTimeout, _parser._physicalStateObj); - } - //////////////////////////////////////////////////////////////////////////////////////// // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 1f482a5fa3..c80962aa48 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,28 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // DISTRIBUTED TRANSACTION METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - internal override void DelegatedTransactionEnded() - { - // TODO: I don't like the way that this works, but I don't want to rototill the entire pooler to avoid this call. - base.DelegatedTransactionEnded(); - } - - protected override byte[] GetDTCAddress() - { - byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); - Debug.Assert(dtcAddress != null, "null dtcAddress?"); - return dtcAddress; - } - - protected override void PropagateTransactionCookie(byte[] cookie) - { - _parser.PropagateDistributedTransaction(cookie, ConnectionOptions.ConnectTimeout, _parser._physicalStateObj); - } - //////////////////////////////////////////////////////////////////////////////////////// // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// 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 68a995ea44..660200396f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -755,6 +755,15 @@ protected override void ChangeDatabaseInternal(string database) _parser._physicalStateObj); } + // @TODO: Rename to match guidelines + protected override byte[] GetDTCAddress() + { + byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); + + Debug.Assert(dtcAddress != null, "null dtcAddress?"); + return dtcAddress; + } + protected override void InternalDeactivate() { // When we're deactivated, the user must have called End on all the async commands, or @@ -784,6 +793,14 @@ protected override void InternalDeactivate() } } + protected override void PropagateTransactionCookie(byte[] cookie) + { + _parser.PropagateDistributedTransaction( + cookie, + ConnectionOptions.ConnectTimeout, + _parser._physicalStateObj); + } + #endregion #region Private Methods From 05baf5c1d3205bc4ba9bec7c52f74e7d671782bb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 17:53:34 -0500 Subject: [PATCH 24/56] Merge CompleteLogin (labels for constant parameters, split long lines, adopt netcore style trace event strings) --- .../SqlClient/SqlInternalConnectionTds.cs | 66 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 66 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 86 +++++++++++++++++++ 3 files changed, 86 insertions(+), 132 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 5a94e35e5d..37382a71e8 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,72 +346,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void CompleteLogin(bool enlistOK) - { - _parser.Run(RunBehavior.UntilDone, null, null, null, _parser._physicalStateObj); - - if (RoutingInfo == null) - { - // ROR should not affect state of connection recovery - if (_federatedAuthenticationRequested && !_federatedAuthenticationAcknowledged) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Server did not acknowledge the federated authentication request", ObjectID); - throw SQL.ParsingError(ParsingErrorState.FedAuthNotAcknowledged); - } - if (_federatedAuthenticationInfoRequested && !_federatedAuthenticationInfoReceived) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Server never sent the requested federated authentication info", ObjectID); - throw SQL.ParsingError(ParsingErrorState.FedAuthInfoNotReceived); - } - - if (!_sessionRecoveryAcknowledged) - { - _currentSessionData = null; - if (_recoverySessionData != null) - { - throw SQL.CR_NoCRAckAtReconnection(this); - } - } - if (_currentSessionData != null && _recoverySessionData == null) - { - _currentSessionData._initialDatabase = CurrentDatabase; - _currentSessionData._initialCollation = _currentSessionData._collation; - _currentSessionData._initialLanguage = _currentLanguage; - } - bool isEncrypted = (_parser.EncryptionOptions & EncryptionOptions.OPTIONS_MASK) == EncryptionOptions.ON; - if (_recoverySessionData != null) - { - if (_recoverySessionData._encrypted != isEncrypted) - { - throw SQL.CR_EncryptionChanged(this); - } - } - if (_currentSessionData != null) - { - _currentSessionData._encrypted = isEncrypted; - } - _recoverySessionData = null; - } - - Debug.Assert(SniContext.Snix_Login == Parser._physicalStateObj.SniContext, $"SniContext should be Snix_Login; actual Value: {Parser._physicalStateObj.SniContext}"); - _parser._physicalStateObj.SniContext = SniContext.Snix_EnableMars; - _parser.EnableMars(); - - _fConnectionOpen = true; // mark connection as open - SqlClientEventSource.Log.TryAdvancedTraceEvent(" Post-Login Phase: Server connection obtained."); - - // for non-pooled connections, enlist in a distributed transaction - // if present - and user specified to enlist - if (enlistOK && ConnectionOptions.Enlist && RoutingInfo == null) - { - _parser._physicalStateObj.SniContext = SniContext.Snix_AutoEnlist; - Transaction tx = ADP.GetCurrentTransaction(); - Enlist(tx); - } - - _parser._physicalStateObj.SniContext = SniContext.Snix_Login; - } - private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, SecureString newSecurePassword, SqlConnectionEncryptOption encrypt) { // create a new login record diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c80962aa48..563eced255 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,72 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void CompleteLogin(bool enlistOK) - { - _parser.Run(RunBehavior.UntilDone, null, null, null, _parser._physicalStateObj); - - if (RoutingInfo == null) - { - // ROR should not affect state of connection recovery - if (_federatedAuthenticationRequested && !_federatedAuthenticationAcknowledged) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Server did not acknowledge the federated authentication request", ObjectID); - throw SQL.ParsingError(ParsingErrorState.FedAuthNotAcknowledged); - } - if (_federatedAuthenticationInfoRequested && !_federatedAuthenticationInfoReceived) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Server never sent the requested federated authentication info", ObjectID); - throw SQL.ParsingError(ParsingErrorState.FedAuthInfoNotReceived); - } - - if (!_sessionRecoveryAcknowledged) - { - _currentSessionData = null; - if (_recoverySessionData != null) - { - throw SQL.CR_NoCRAckAtReconnection(this); - } - } - if (_currentSessionData != null && _recoverySessionData == null) - { - _currentSessionData._initialDatabase = CurrentDatabase; - _currentSessionData._initialCollation = _currentSessionData._collation; - _currentSessionData._initialLanguage = _currentLanguage; - } - bool isEncrypted = (_parser.EncryptionOptions & EncryptionOptions.OPTIONS_MASK) == EncryptionOptions.ON; - if (_recoverySessionData != null) - { - if (_recoverySessionData._encrypted != isEncrypted) - { - throw SQL.CR_EncryptionChanged(this); - } - } - if (_currentSessionData != null) - { - _currentSessionData._encrypted = isEncrypted; - } - _recoverySessionData = null; - } - - Debug.Assert(SniContext.Snix_Login == Parser._physicalStateObj.SniContext, $"SniContext should be Snix_Login; actual Value: {Parser._physicalStateObj.SniContext}"); - _parser._physicalStateObj.SniContext = SniContext.Snix_EnableMars; - _parser.EnableMars(); - - _fConnectionOpen = true; // mark connection as open - SqlClientEventSource.Log.TryAdvancedTraceEvent(" Post-Login Phase: Server connection obtained."); - - // for non-pooled connections, enlist in a distributed transaction - // if present - and user specified to enlist - if (enlistOK && ConnectionOptions.Enlist && RoutingInfo == null) - { - _parser._physicalStateObj.SniContext = SniContext.Snix_AutoEnlist; - Transaction tx = ADP.GetCurrentTransaction(); - Enlist(tx); - } - - _parser._physicalStateObj.SniContext = SniContext.Snix_Login; - } - private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, SecureString newSecurePassword, SqlConnectionEncryptOption encrypt) { // create a new login record 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 660200396f..960ebf1025 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -805,6 +805,92 @@ protected override void PropagateTransactionCookie(byte[] cookie) #region Private Methods + private void CompleteLogin(bool enlistOK) // @TODO: Rename as per guidelines + { + _parser.Run( + RunBehavior.UntilDone, + cmdHandler: null, + dataStream: null, + bulkCopyHandler: null, + _parser._physicalStateObj); + + if (RoutingInfo == null) + { + // ROR should not affect state of connection recovery + if (_federatedAuthenticationRequested && !_federatedAuthenticationAcknowledged) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.CompleteLogin | ERR | " + + $"Object ID {ObjectID}, " + + $"Server did not acknowledge the federated authentication request"); + throw SQL.ParsingError(ParsingErrorState.FedAuthNotAcknowledged); + } + + if (_federatedAuthenticationInfoRequested && !_federatedAuthenticationInfoReceived) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.CompleteLogin | ERR | " + + $"Object ID {ObjectID}, " + + $"Server never sent the requested federated authentication info"); + throw SQL.ParsingError(ParsingErrorState.FedAuthInfoNotReceived); + } + + if (!_sessionRecoveryAcknowledged) + { + _currentSessionData = null; + if (_recoverySessionData != null) + { + throw SQL.CR_NoCRAckAtReconnection(this); + } + } + + if (_currentSessionData != null && _recoverySessionData == null) + { + _currentSessionData._initialDatabase = CurrentDatabase; + _currentSessionData._initialCollation = _currentSessionData._collation; + _currentSessionData._initialLanguage = _currentLanguage; + } + + bool isEncrypted = (_parser.EncryptionOptions & EncryptionOptions.OPTIONS_MASK) == EncryptionOptions.ON; + if (_recoverySessionData != null) + { + if (_recoverySessionData._encrypted != isEncrypted) + { + throw SQL.CR_EncryptionChanged(this); + } + } + + if (_currentSessionData != null) + { + _currentSessionData._encrypted = isEncrypted; + } + + _recoverySessionData = null; + } + + Debug.Assert(SniContext.Snix_Login == Parser._physicalStateObj.SniContext, + $"SniContext should be Snix_Login; actual Value: {Parser._physicalStateObj.SniContext}"); + + _parser._physicalStateObj.SniContext = SniContext.Snix_EnableMars; + _parser.EnableMars(); + + // Mark connection as open + _fConnectionOpen = true; + SqlClientEventSource.Log.TryAdvancedTraceEvent( + "SqlInternalConnectionTds.CompleteLogin | ADV | Post-Login Phase: Server connection obtained."); + + // For non-pooled connections, enlist in a distributed transaction if present - and + // user specified to enlist + if (enlistOK && ConnectionOptions.Enlist && RoutingInfo == null) + { + _parser._physicalStateObj.SniContext = SniContext.Snix_AutoEnlist; + Transaction tx = ADP.GetCurrentTransaction(); + Enlist(tx); + } + + _parser._physicalStateObj.SniContext = SniContext.Snix_Login; + } + // @TODO: Rename to ExecuteTransactionInternal ... we don't have multiple server version implementations of this private void ExecuteTransaction2005( TransactionRequest transactionRequest, From cea5a8b424d2917e93bef692dd44aa6d34cae057 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 18:08:02 -0500 Subject: [PATCH 25/56] Merge Login (split long lines, rebalanced comments) --- .../SqlClient/SqlInternalConnectionTds.cs | 131 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 132 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 150 ++++++++++++++++++ 3 files changed, 150 insertions(+), 263 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 37382a71e8..2381ee9ea3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,137 +345,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - - private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, SecureString newSecurePassword, SqlConnectionEncryptOption encrypt) - { - // create a new login record - SqlLogin login = new SqlLogin(); - - // gather all the settings the user set in the connection string or - // properties and do the login - CurrentDatabase = server.ResolvedDatabaseName; - _currentPacketSize = ConnectionOptions.PacketSize; - _currentLanguage = ConnectionOptions.CurrentLanguage; - - int timeoutInSeconds = 0; - - // If a timeout tick value is specified, compute the timeout based - // upon the amount of time left in seconds. - if (!timeout.IsInfinite) - { - long t = timeout.MillisecondsRemaining / 1000; - if (t == 0 && LocalAppContextSwitches.UseMinimumLoginTimeout) - { - // Take 1 as the minimum value, since 0 is treated as an infinite timeout - // to allow 1 second more for login to complete, since it should take only a few milliseconds. - t = 1; - } - - if (int.MaxValue > t) - { - timeoutInSeconds = (int)t; - } - } - - login.authentication = ConnectionOptions.Authentication; - login.timeout = timeoutInSeconds; - login.userInstance = ConnectionOptions.UserInstance; - login.hostName = ConnectionOptions.ObtainWorkstationId(); - login.userName = ConnectionOptions.UserID; - login.password = ConnectionOptions.Password; - login.applicationName = ConnectionOptions.ApplicationName; - - login.language = _currentLanguage; - if (!login.userInstance) - { - // Do not send attachdbfilename or database to SSE primary instance - login.database = CurrentDatabase; - login.attachDBFilename = ConnectionOptions.AttachDBFilename; - } - - // VSTS#795621 - Ensure ServerName is Sent During TdsLogin To Enable Sql Azure Connectivity. - // Using server.UserServerName (versus ConnectionOptions.DataSource) since TdsLogin requires - // serverName to always be non-null. - login.serverName = server.UserServerName; - - login.useReplication = ConnectionOptions.Replication; - login.useSSPI = ConnectionOptions.IntegratedSecurity // Treat AD Integrated like Windows integrated when against a non-FedAuth endpoint - || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && !_fedAuthRequired); - login.packetSize = _currentPacketSize; - login.newPassword = newPassword; - login.readOnlyIntent = ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly; - login.credential = _credential; - if (newSecurePassword != null) - { - login.newSecurePassword = newSecurePassword; - } - - TdsEnums.FeatureExtension requestedFeatures = TdsEnums.FeatureExtension.None; - if (ConnectionOptions.ConnectRetryCount > 0) - { - requestedFeatures |= TdsEnums.FeatureExtension.SessionRecovery; - _sessionRecoveryRequested = true; - } - - // If the workflow being used is Active Directory Authentication and server's prelogin response - // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature Extension - // in Login7, indicating the intent to use Active Directory Authentication for SQL Server. - #pragma warning disable 0618 // Type or member is obsolete - if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword - #pragma warning restore 0618 // Type or member is obsolete - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity - // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired - || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired) - || _accessTokenCallback != null) - { - requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; - _federatedAuthenticationInfoRequested = true; - _fedAuthFeatureExtensionData = - new FederatedAuthenticationFeatureExtensionData - { - libraryType = TdsEnums.FedAuthLibrary.MSAL, - authentication = ConnectionOptions.Authentication, - fedAuthRequiredPreLoginResponse = _fedAuthRequired - }; - } - - if (_accessTokenInBytes != null) - { - requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; - _fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData - { - libraryType = TdsEnums.FedAuthLibrary.SecurityToken, - fedAuthRequiredPreLoginResponse = _fedAuthRequired, - accessToken = _accessTokenInBytes - }; - // No need any further info from the server for token based authentication. So set _federatedAuthenticationRequested to true - _federatedAuthenticationRequested = true; - } - - // The GLOBALTRANSACTIONS, DATACLASSIFICATION, TCE, and UTF8 support features are implicitly requested - requestedFeatures |= TdsEnums.FeatureExtension.GlobalTransactions | TdsEnums.FeatureExtension.DataClassification | TdsEnums.FeatureExtension.Tce | TdsEnums.FeatureExtension.UTF8Support; - - // The AzureSQLSupport feature is implicitly set for ReadOnly login - if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) - { - requestedFeatures |= TdsEnums.FeatureExtension.AzureSQLSupport; - } - - // The following features are implicitly set - requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; - requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; - requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; - requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - - _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); - } - private void LoginFailure() { SqlClientEventSource.Log.TryTraceEvent(" {0}", ObjectID); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 563eced255..ecc2b82900 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,138 +344,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - - private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, SecureString newSecurePassword, SqlConnectionEncryptOption encrypt) - { - // create a new login record - SqlLogin login = new SqlLogin(); - - // gather all the settings the user set in the connection string or - // properties and do the login - CurrentDatabase = server.ResolvedDatabaseName; - _currentPacketSize = ConnectionOptions.PacketSize; - _currentLanguage = ConnectionOptions.CurrentLanguage; - - int timeoutInSeconds = 0; - - // If a timeout tick value is specified, compute the timeout based - // upon the amount of time left in seconds. - if (!timeout.IsInfinite) - { - long t = timeout.MillisecondsRemaining / 1000; - if (t == 0 && LocalAppContextSwitches.UseMinimumLoginTimeout) - { - // Take 1 as the minimum value, since 0 is treated as an infinite timeout - // to allow 1 second more for login to complete, since it should take only a few milliseconds. - t = 1; - } - - if (int.MaxValue > t) - { - timeoutInSeconds = (int)t; - } - } - - login.authentication = ConnectionOptions.Authentication; - login.timeout = timeoutInSeconds; - login.userInstance = ConnectionOptions.UserInstance; - login.hostName = ConnectionOptions.ObtainWorkstationId(); - login.userName = ConnectionOptions.UserID; - login.password = ConnectionOptions.Password; - login.applicationName = ConnectionOptions.ApplicationName; - - login.language = _currentLanguage; - if (!login.userInstance) - { - // Do not send attachdbfilename or database to SSE primary instance - login.database = CurrentDatabase; - login.attachDBFilename = ConnectionOptions.AttachDBFilename; - } - - // VSTS#795621 - Ensure ServerName is Sent During TdsLogin To Enable Sql Azure Connectivity. - // Using server.UserServerName (versus ConnectionOptions.DataSource) since TdsLogin requires - // serverName to always be non-null. - login.serverName = server.UserServerName; - - login.useReplication = ConnectionOptions.Replication; - login.useSSPI = ConnectionOptions.IntegratedSecurity // Treat AD Integrated like Windows integrated when against a non-FedAuth endpoint - || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && !_fedAuthRequired); - login.packetSize = _currentPacketSize; - login.newPassword = newPassword; - login.readOnlyIntent = ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly; - login.credential = _credential; - if (newSecurePassword != null) - { - login.newSecurePassword = newSecurePassword; - } - - TdsEnums.FeatureExtension requestedFeatures = TdsEnums.FeatureExtension.None; - if (ConnectionOptions.ConnectRetryCount > 0) - { - requestedFeatures |= TdsEnums.FeatureExtension.SessionRecovery; - _sessionRecoveryRequested = true; - } - - // If the workflow being used is Active Directory Authentication and server's prelogin response - // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature Extension - // in Login7, indicating the intent to use Active Directory Authentication for SQL Server. - #pragma warning disable 0618 // Type or member is obsolete - if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword - #pragma warning restore 0618 // Type or member is obsolete - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity - // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired - || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired) - || _accessTokenCallback != null) - { - requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; - _federatedAuthenticationInfoRequested = true; - _fedAuthFeatureExtensionData = - new FederatedAuthenticationFeatureExtensionData - { - libraryType = TdsEnums.FedAuthLibrary.MSAL, - authentication = ConnectionOptions.Authentication, - fedAuthRequiredPreLoginResponse = _fedAuthRequired - }; - } - - if (_accessTokenInBytes != null) - { - requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; - _fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData - { - libraryType = TdsEnums.FedAuthLibrary.SecurityToken, - fedAuthRequiredPreLoginResponse = _fedAuthRequired, - accessToken = _accessTokenInBytes - }; - // No need any further info from the server for token based authentication. So set _federatedAuthenticationRequested to true - _federatedAuthenticationRequested = true; - } - - // The GLOBALTRANSACTIONS, DATACLASSIFICATION, TCE, and UTF8 support features are implicitly requested - requestedFeatures |= TdsEnums.FeatureExtension.GlobalTransactions | TdsEnums.FeatureExtension.DataClassification | TdsEnums.FeatureExtension.Tce | TdsEnums.FeatureExtension.UTF8Support; - - // The AzureSQLSupport feature is implicitly set for ReadOnly login - if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) - { - requestedFeatures |= TdsEnums.FeatureExtension.AzureSQLSupport; - } - - // The SQLDNSCaching and JSON features are implicitly set - requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; - requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; - requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; - - requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; - - _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); - } - private void LoginFailure() { SqlClientEventSource.Log.TryTraceEvent(" {0}", ObjectID); 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 960ebf1025..ec513d76c4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Security; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -1051,6 +1052,155 @@ private void ExecuteTransaction2005( } } + private void Login( + ServerInfo server, + TimeoutTimer timeout, + string newPassword, + SecureString newSecurePassword, + SqlConnectionEncryptOption encrypt) + { + // create a new login record + SqlLogin login = new SqlLogin(); + + // Gather all the settings the user set in the connection string or properties and do + // the login + CurrentDatabase = server.ResolvedDatabaseName; + _currentPacketSize = ConnectionOptions.PacketSize; + _currentLanguage = ConnectionOptions.CurrentLanguage; + + int timeoutInSeconds = 0; + + // If a timeout tick value is specified, compute the timeout based upon the amount of + // time left in seconds. + if (!timeout.IsInfinite) + { + long t = timeout.MillisecondsRemaining / 1000; + if (t == 0 && LocalAppContextSwitches.UseMinimumLoginTimeout) + { + // Take 1 as the minimum value, since 0 is treated as an infinite timeout to + // allow 1 second more for login to complete, since it should take only a few + // milliseconds. + t = 1; + } + + if (int.MaxValue > t) + { + timeoutInSeconds = (int)t; + } + } + + // @TODO: How about we define all the easy ones in one block, then have the conditional ones below that + login.authentication = ConnectionOptions.Authentication; + login.timeout = timeoutInSeconds; + login.userInstance = ConnectionOptions.UserInstance; + login.hostName = ConnectionOptions.ObtainWorkstationId(); + login.userName = ConnectionOptions.UserID; + login.password = ConnectionOptions.Password; + login.applicationName = ConnectionOptions.ApplicationName; + login.language = _currentLanguage; + + if (!login.userInstance) + { + // Do not send attachdbfilename or database to SSE primary instance + login.database = CurrentDatabase; + login.attachDBFilename = ConnectionOptions.AttachDBFilename; + } + + // Ensure ServerName is sent during TdsLogin to enable SQL Azure connectivity. + // Using server.UserServerName (versus ConnectionOptions.DataSource) since TdsLogin + // requires serverName to always be non-null. + login.serverName = server.UserServerName; + + login.useReplication = ConnectionOptions.Replication; + + // Treat AD Integrated like Windows integrated when against a non-FedAuth endpoint + login.useSSPI = ConnectionOptions.IntegratedSecurity || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && !_fedAuthRequired); + + login.packetSize = _currentPacketSize; + login.newPassword = newPassword; + login.readOnlyIntent = ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly; + login.credential = _credential; + + if (newSecurePassword != null) + { + // @TODO: Isn't this already null? + login.newSecurePassword = newSecurePassword; + } + + TdsEnums.FeatureExtension requestedFeatures = TdsEnums.FeatureExtension.None; + if (ConnectionOptions.ConnectRetryCount > 0) + { + requestedFeatures |= TdsEnums.FeatureExtension.SessionRecovery; + _sessionRecoveryRequested = true; + } + + // If the workflow being used is Active Directory Authentication and server's prelogin + // response for FEDAUTHREQUIRED option indicates Federated Authentication is required, + // we have to insert FedAuth Feature Extension in Login7, indicating the intent to use + // Active Directory Authentication for SQL Server. + // @TODO: Rewrite using a static readonly hash set of federated auth types + #pragma warning disable 0618 // Type or member is obsolete + if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword + #pragma warning restore 0618 // Type or member is obsolete + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity + // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired + || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired) + || _accessTokenCallback != null) + { + requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; + _federatedAuthenticationInfoRequested = true; + _fedAuthFeatureExtensionData = + new FederatedAuthenticationFeatureExtensionData + { + libraryType = TdsEnums.FedAuthLibrary.MSAL, + authentication = ConnectionOptions.Authentication, + fedAuthRequiredPreLoginResponse = _fedAuthRequired + }; + } + + if (_accessTokenInBytes != null) + { + requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; + _fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData + { + libraryType = TdsEnums.FedAuthLibrary.SecurityToken, + fedAuthRequiredPreLoginResponse = _fedAuthRequired, + accessToken = _accessTokenInBytes + }; + + // No need any further info from the server for token based authentication. So set + // _federatedAuthenticationRequested to true + _federatedAuthenticationRequested = true; + } + + // The GLOBALTRANSACTIONS, DATACLASSIFICATION, TCE, and UTF8 support features are implicitly requested + requestedFeatures |= TdsEnums.FeatureExtension.GlobalTransactions | + TdsEnums.FeatureExtension.DataClassification | + TdsEnums.FeatureExtension.Tce | + TdsEnums.FeatureExtension.UTF8Support; + + // The AzureSQLSupport feature is implicitly set for ReadOnly login + if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) + { + requestedFeatures |= TdsEnums.FeatureExtension.AzureSQLSupport; + } + + // The following features are implicitly set + // @TODO: Request all the implicit features in one place (probably at the very top) + requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; + requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; + requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; + requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; + + _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); + } + /// /// Returns true if the SQL error is transient, as per . /// From d94c9f2b1a6f2d52c6b3a7539e2c75480522cb72 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 18:10:32 -0500 Subject: [PATCH 26/56] Merge LoginFailure --- .../Data/SqlClient/SqlInternalConnectionTds.cs | 13 ------------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 13 ------------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 13 +++++++++++++ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 2381ee9ea3..18d6912124 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,19 +345,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void LoginFailure() - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", ObjectID); - - // If the parser was allocated and we failed, then we must have failed on - // either the Connect or Login, either way we should call Disconnect. - // Disconnect can be called if the connection is already closed - becomes - // no-op, so no issues there. - if (_parser != null) - { - _parser.Disconnect(); - } - } private void OpenLoginEnlist(TimeoutTimer timeout, SqlConnectionString connectionOptions, diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index ecc2b82900..353b7728ce 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,19 +344,6 @@ internal SqlInternalConnectionTds( //////////////////////////////////////////////////////////////////////////////////////// // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void LoginFailure() - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", ObjectID); - - // If the parser was allocated and we failed, then we must have failed on - // either the Connect or Login, either way we should call Disconnect. - // Disconnect can be called if the connection is already closed - becomes - // no-op, so no issues there. - if (_parser != null) - { - _parser.Disconnect(); - } - } private void OpenLoginEnlist(TimeoutTimer timeout, SqlConnectionString connectionOptions, 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 ec513d76c4..d9eac5721a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1201,6 +1201,19 @@ private void Login( _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } + + private void LoginFailure() + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.LoginFailure | RES | CPOOL | " + + $"Object ID {ObjectID}"); + + // If the parser was allocated, and we failed, then we must have failed on either the + // Connect or Login, either way we should call Disconnect. Disconnect can be called if + // the connection is already closed - becomes no-op, so no issues there. + _parser?.Disconnect(); + } + /// /// Returns true if the SQL error is transient, as per . /// From 842d13e9c7e82a037a750a0c074c176273690767 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 29 Oct 2025 18:16:04 -0500 Subject: [PATCH 27/56] Merge OpenLoginEnlist (split long lines, introduce temp variable for source type b/c the ternary was was too long to use in-line, reorder temp variable declaration order to group similar variables) --- .../SqlClient/SqlInternalConnectionTds.cs | 91 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 91 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 103 +++++++++++++++++- 3 files changed, 102 insertions(+), 183 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 18d6912124..6ce79783c7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,97 +346,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void OpenLoginEnlist(TimeoutTimer timeout, - SqlConnectionString connectionOptions, - SqlCredential credential, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance) - { - bool useFailoverPartner; // should we use primary or secondary first - ServerInfo dataSource = new ServerInfo(connectionOptions); - string failoverPartner; - - if (PoolGroupProviderInfo != null) - { - useFailoverPartner = PoolGroupProviderInfo.UseFailoverPartner; - failoverPartner = PoolGroupProviderInfo.FailoverPartner; - } - else - { - // Only ChangePassword or SSE User Instance comes through this code path. - useFailoverPartner = false; - failoverPartner = ConnectionOptions.FailoverPartner; - } - - _timeoutErrorInternal.SetInternalSourceType(useFailoverPartner ? SqlConnectionInternalSourceType.Failover : SqlConnectionInternalSourceType.Principle); - - bool hasFailoverPartner = !string.IsNullOrEmpty(failoverPartner); - - // Open the connection and Login - try - { - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PreLoginBegin); - if (hasFailoverPartner) - { - _timeoutErrorInternal.SetFailoverScenario(true); // this is a failover scenario - LoginWithFailover( - useFailoverPartner, - dataSource, - failoverPartner, - newPassword, - newSecurePassword, - redirectedUserInstance, - connectionOptions, - credential, - timeout); - } - else - { - _timeoutErrorInternal.SetFailoverScenario(false); // not a failover scenario - LoginNoFailover( - dataSource, - newPassword, - newSecurePassword, - redirectedUserInstance, - connectionOptions, - credential, - timeout); - } - - if (!IsAzureSqlConnection) - { - // If not a connection to Azure SQL, Readonly with FailoverPartner is not supported - if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) - { - if (!string.IsNullOrEmpty(ConnectionOptions.FailoverPartner)) - { - throw SQL.ROR_FailoverNotSupportedConnString(); - } - - if (ServerProvidedFailoverPartner != null) - { - throw SQL.ROR_FailoverNotSupportedServer(this); - } - } - } - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin); - } - catch (Exception e) - { - if (ADP.IsCatchableExceptionType(e)) - { - LoginFailure(); - } - throw; - } - _timeoutErrorInternal.SetAllCompleteMarker(); - -#if DEBUG - _parser._physicalStateObj.InvalidateDebugOnlyCopyOfSniContext(); -#endif - } - // Is the given Sql error one that should prevent retrying // to connect. private bool IsDoNotRetryConnectError(SqlException exc) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 353b7728ce..1e1ef2b0bd 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,97 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void OpenLoginEnlist(TimeoutTimer timeout, - SqlConnectionString connectionOptions, - SqlCredential credential, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance) - { - bool useFailoverPartner; // should we use primary or secondary first - ServerInfo dataSource = new ServerInfo(connectionOptions); - string failoverPartner; - - if (PoolGroupProviderInfo != null) - { - useFailoverPartner = PoolGroupProviderInfo.UseFailoverPartner; - failoverPartner = PoolGroupProviderInfo.FailoverPartner; - } - else - { - // Only ChangePassword or SSE User Instance comes through this code path. - useFailoverPartner = false; - failoverPartner = ConnectionOptions.FailoverPartner; - } - - _timeoutErrorInternal.SetInternalSourceType(useFailoverPartner ? SqlConnectionInternalSourceType.Failover : SqlConnectionInternalSourceType.Principle); - - bool hasFailoverPartner = !string.IsNullOrEmpty(failoverPartner); - - // Open the connection and Login - try - { - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PreLoginBegin); - if (hasFailoverPartner) - { - _timeoutErrorInternal.SetFailoverScenario(true); // this is a failover scenario - LoginWithFailover( - useFailoverPartner, - dataSource, - failoverPartner, - newPassword, - newSecurePassword, - redirectedUserInstance, - connectionOptions, - credential, - timeout); - } - else - { - _timeoutErrorInternal.SetFailoverScenario(false); // not a failover scenario - LoginNoFailover( - dataSource, - newPassword, - newSecurePassword, - redirectedUserInstance, - connectionOptions, - credential, - timeout); - } - - if (!IsAzureSqlConnection) - { - // If not a connection to Azure SQL, Readonly with FailoverPartner is not supported - if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) - { - if (!string.IsNullOrEmpty(ConnectionOptions.FailoverPartner)) - { - throw SQL.ROR_FailoverNotSupportedConnString(); - } - - if (ServerProvidedFailoverPartner != null) - { - throw SQL.ROR_FailoverNotSupportedServer(this); - } - } - } - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin); - } - catch (Exception e) - { - if (ADP.IsCatchableExceptionType(e)) - { - LoginFailure(); - } - throw; - } - _timeoutErrorInternal.SetAllCompleteMarker(); - -#if DEBUG - _parser._physicalStateObj.InvalidateDebugOnlyCopyOfSniContext(); -#endif - } - // Is the given Sql error one that should prevent retrying // to connect. private bool IsDoNotRetryConnectError(SqlException exc) 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 d9eac5721a..39973e8d11 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1201,7 +1201,6 @@ private void Login( _parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData, encrypt); } - private void LoginFailure() { SqlClientEventSource.Log.TryTraceEvent( @@ -1237,6 +1236,108 @@ private bool IsTransientError(SqlException exc) return false; } + private void OpenLoginEnlist( + TimeoutTimer timeout, + SqlConnectionString connectionOptions, + SqlCredential credential, + string newPassword, + SecureString newSecurePassword, + bool redirectedUserInstance) + { + // Indicates whether we should use primary or secondary first + bool useFailoverPartner; + string failoverPartner; + + ServerInfo dataSource = new ServerInfo(connectionOptions); + + if (PoolGroupProviderInfo != null) + { + useFailoverPartner = PoolGroupProviderInfo.UseFailoverPartner; + failoverPartner = PoolGroupProviderInfo.FailoverPartner; + } + else + { + // Only ChangePassword or SSE User Instance comes through this code path. + useFailoverPartner = false; + failoverPartner = ConnectionOptions.FailoverPartner; + } + + SqlConnectionInternalSourceType sourceType = useFailoverPartner + ? SqlConnectionInternalSourceType.Failover + : SqlConnectionInternalSourceType.Principle; + _timeoutErrorInternal.SetInternalSourceType(sourceType); + + bool hasFailoverPartner = !string.IsNullOrEmpty(failoverPartner); + + try + { + // Open the connection and Login + _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PreLoginBegin); + if (hasFailoverPartner) + { + // This is a failover scenario + _timeoutErrorInternal.SetFailoverScenario(true); + LoginWithFailover( + useFailoverPartner, + dataSource, + failoverPartner, + newPassword, + newSecurePassword, + redirectedUserInstance, + connectionOptions, + credential, + timeout); + } + else + { + // This is *not* a failover scenario + _timeoutErrorInternal.SetFailoverScenario(false); + LoginNoFailover( + dataSource, + newPassword, + newSecurePassword, + redirectedUserInstance, + connectionOptions, + credential, + timeout); + } + + if (!IsAzureSqlConnection) + { + // If not a connection to Azure SQL, Readonly with FailoverPartner is not supported + if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) + { + if (!string.IsNullOrEmpty(ConnectionOptions.FailoverPartner)) + { + throw SQL.ROR_FailoverNotSupportedConnString(); + } + + if (ServerProvidedFailoverPartner != null) + { + throw SQL.ROR_FailoverNotSupportedServer(this); + } + } + } + + _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin); + } + catch (Exception e) + { + if (ADP.IsCatchableExceptionType(e)) + { + LoginFailure(); + } + + throw; + } + + _timeoutErrorInternal.SetAllCompleteMarker(); + + #if DEBUG + _parser._physicalStateObj.InvalidateDebugOnlyCopyOfSniContext(); + #endif + } + // @TODO: Is this suppression still required [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs private void ResetConnection() From 3aeebc3da9d5defa9afc0eecfdf0b125ff4a37db Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 12:32:56 -0600 Subject: [PATCH 28/56] Merge IsDoNotRetryConnectError (extract exception number checks to variable, comment to xmldocs) --- .../Data/SqlClient/SqlInternalConnectionTds.cs | 11 ----------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 10 ---------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 12 ++++++++++++ 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6ce79783c7..5c61e6ca61 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,16 +346,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // Is the given Sql error one that should prevent retrying - // to connect. - private bool IsDoNotRetryConnectError(SqlException exc) - { - return (TdsEnums.LOGON_FAILED == exc.Number) // actual logon failed, i.e. bad password - || (TdsEnums.PASSWORD_EXPIRED == exc.Number) // actual logon failed, i.e. password isExpired - || (TdsEnums.IMPERSONATION_FAILED == exc.Number) // Insufficient privilege for named pipe, among others - || exc._doNotReconnect; // Exception explicitly suppressed reconnection attempts - } - // Attempt to login to a host that does not have a failover partner // // Will repeatedly attempt to connect, but back off between each attempt so as not to clog the network. @@ -377,7 +367,6 @@ private void LoginNoFailover(ServerInfo serverInfo, int routingAttempts = 0; ServerInfo originalServerInfo = serverInfo; // serverInfo may end up pointing to new object due to routing, original object is used to set CurrentDatasource - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, host={1}", ObjectID, serverInfo.UserServerName); int sleepInterval = 100; //milliseconds to sleep (back off) between attempts. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 1e1ef2b0bd..8162e7caae 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,16 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // Is the given Sql error one that should prevent retrying - // to connect. - private bool IsDoNotRetryConnectError(SqlException exc) - { - return (TdsEnums.LOGON_FAILED == exc.Number) // actual logon failed, i.e. bad password - || (TdsEnums.PASSWORD_EXPIRED == exc.Number) // actual logon failed, i.e. password isExpired - || (TdsEnums.IMPERSONATION_FAILED == exc.Number) // Insufficient privilege for named pipe, among others - || exc._doNotReconnect; // Exception explicitly suppressed reconnection attempts - } - // Attempt to login to a host that does not have a failover partner // // Will repeatedly attempt to connect, but back off between each attempt so as not to clog the network. 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 39973e8d11..660d9552dc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1213,6 +1213,18 @@ private void LoginFailure() _parser?.Disconnect(); } + /// + /// Is the given Sql error one that should prevent retrying to connect. + /// + // @TODO: Make static + private bool IsDoNotRetryConnectError(SqlException exc) + { + bool errorNumberMatch = exc.Number is TdsEnums.LOGON_FAILED // Actual login failed, ie bad password + or TdsEnums.PASSWORD_EXPIRED // Actual login failed, ie expired password + or TdsEnums.IMPERSONATION_FAILED; // Insufficient privilege for named pipe, etc + return errorNumberMatch || exc._doNotReconnect; + } + /// /// Returns true if the SQL error is transient, as per . /// From 26d4599ac339fa0f83d6ea7d426dc47d9ca19b6a Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 13:46:42 -0600 Subject: [PATCH 29/56] Merge LoginNoFailure (resolve conflicts with #if, split long lines, rebalance comments, introduce some ternaries, nothing too crazy) --- .../SqlClient/SqlInternalConnectionTds.cs | 212 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 238 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 305 ++++++++++++++++++ 3 files changed, 305 insertions(+), 450 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 5c61e6ca61..6c3010fabb 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,218 +346,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // Attempt to login to a host that does not have a failover partner - // - // Will repeatedly attempt to connect, but back off between each attempt so as not to clog the network. - // Back off period increases for first few failures: 100ms, 200ms, 400ms, 800ms, then 1000ms for subsequent attempts - // - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // DEVNOTE: The logic in this method is paralleled by the logic in LoginWithFailover. - // Changes to either one should be examined to see if they need to be reflected in the other - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - private void LoginNoFailover(ServerInfo serverInfo, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance, - SqlConnectionString connectionOptions, - SqlCredential credential, - TimeoutTimer timeout) - { - Debug.Assert(object.ReferenceEquals(connectionOptions, this.ConnectionOptions), "ConnectionOptions argument and property must be the same"); // consider removing the argument - int routingAttempts = 0; - ServerInfo originalServerInfo = serverInfo; // serverInfo may end up pointing to new object due to routing, original object is used to set CurrentDatasource - - - int sleepInterval = 100; //milliseconds to sleep (back off) between attempts. - - ResolveExtendedServerName(serverInfo, !redirectedUserInstance, connectionOptions); - - long timeoutUnitInterval = 0; - - bool isParallel = connectionOptions.MultiSubnetFailover; - - if (isParallel) - { - float failoverTimeoutStep = ADP.FailoverTimeoutStep; - - // Determine unit interval - if (timeout.IsInfinite) - { - timeoutUnitInterval = checked((long)(failoverTimeoutStep * (1000L * ADP.DefaultConnectionTimeout))); - } - else - { - timeoutUnitInterval = checked((long)(failoverTimeoutStep * timeout.MillisecondsRemaining)); - } - } - // Only three ways out of this loop: - // 1) Successfully connected - // 2) Parser threw exception while main timer was expired - // 3) Parser threw logon failure-related exception - // 4) Parser threw exception in post-initial connect code, - // such as pre-login handshake or during actual logon. (parser state != Closed) - // - // Of these methods, only #1 exits normally. This preserves the call stack on the exception - // back into the parser for the error cases. - int attemptNumber = 0; - TimeoutTimer intervalTimer = null; - TimeoutTimer attemptOneLoginTimeout = timeout; - - while (true) - { - if (isParallel) - { - int multiplier = ++attemptNumber; - - // Set timeout for this attempt, but don't exceed original timer - long nextTimeoutInterval = checked(timeoutUnitInterval * multiplier); - long milliseconds = timeout.MillisecondsRemaining; - - if (nextTimeoutInterval > milliseconds) - { - nextTimeoutInterval = milliseconds; - } - intervalTimer = TimeoutTimer.StartMillisecondsTimeout(nextTimeoutInterval); - } - - // Re-allocate parser each time to make sure state is known - // RFC 50002652 - if parser was created by previous attempt, dispose it to properly close the socket, if created - if (_parser != null) - { - _parser.Disconnect(); - } - - _parser = new TdsParser(ConnectionOptions.MARS, ConnectionOptions.Asynchronous); - Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); - - try - { - if (isParallel) - { - attemptOneLoginTimeout = intervalTimer; - } - - AttemptOneLogin(serverInfo, - newPassword, - newSecurePassword, - attemptOneLoginTimeout); - - if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) - { - // connection succeeded: trigger exception if server sends failover partner and MultiSubnetFailover is used - throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); - } - - if (RoutingInfo != null) - { - SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", serverInfo.ExtendedServerName); - if (routingAttempts > MaxNumberOfRedirectRoute) - { - throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); - } - - if (timeout.IsExpired) - { - throw SQL.ROR_TimeoutAfterRoutingInfo(this); - } - - serverInfo = new ServerInfo(ConnectionOptions, RoutingInfo, serverInfo.ResolvedServerName, serverInfo.ServerSPN); - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = serverInfo.UserServerName; - - // restore properties that could be changed by the environment tokens - _currentPacketSize = ConnectionOptions.PacketSize; - _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; - CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - ServerProvidedFailoverPartner = null; - _instanceName = string.Empty; - - routingAttempts++; - - continue; // repeat the loop, but skip code reserved for failed connections (after the catch) - } - else - { - break; // leave the while loop -- we've successfully connected - } - } - catch (SqlException sqlex) - { - if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) - { - continue; - } - - if (_parser == null - || TdsParserState.Closed != _parser.State - || IsDoNotRetryConnectError(sqlex) - || timeout.IsExpired) - { - // no more time to try again - throw; // Caller will call LoginFailure() - } - - // Check sleep interval to make sure we won't exceed the timeout - // Do this in the catch block so we can re-throw the current exception - if (timeout.MillisecondsRemaining <= sleepInterval) - { - throw; - } - } - - // We only get here when we failed to connect, but are going to re-try - - // Switch to failover logic if the server provided a partner - if (ServerProvidedFailoverPartner != null) - { - if (connectionOptions.MultiSubnetFailover) - { - // connection failed: do not allow failover to server-provided failover partner if MultiSubnetFailover is set - throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); - } - Debug.Assert(ConnectionOptions.ApplicationIntent != ApplicationIntent.ReadOnly, "FAILOVER+AppIntent=RO: Should already fail (at LOGSHIPNODE in OnEnvChange)"); - - _timeoutErrorInternal.ResetAndRestartPhase(); - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PreLoginBegin); - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); - _timeoutErrorInternal.SetFailoverScenario(true); // this is a failover scenario - LoginWithFailover( - true, // start by using failover partner, since we already failed to connect to the primary - serverInfo, - ServerProvidedFailoverPartner, - newPassword, - newSecurePassword, - redirectedUserInstance, - connectionOptions, - credential, - timeout); - return; // LoginWithFailover successfully connected and handled entire connection setup - } - - // Sleep for a bit to prevent clogging the network with requests, - // then update sleep interval for next iteration (max 1 second interval) - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[milisec]", ObjectID, sleepInterval); - Thread.Sleep(sleepInterval); - sleepInterval = (sleepInterval < 500) ? sleepInterval * 2 : 1000; - } - _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; - - if (PoolGroupProviderInfo != null) - { - // We must wait for CompleteLogin to finish for to have the - // env change from the server to know its designated failover - // partner; save this information in ServerProvidedFailoverPartner. - - // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. - // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. - string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? "" : ServerProvidedFailoverPartner; - - PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); - } - CurrentDataSource = originalServerInfo.UserServerName; - } - // With possible MFA support in all AD auth providers, the duration for acquiring a token can be unpredictable. // If a timeout error (client or server) happened, we silently retry if a cached token exists from a previous auth attempt (see GetFedAuthToken) private bool AttemptRetryADAuthWithTimeoutError(SqlException sqlex, SqlConnectionString connectionOptions, TimeoutTimer timeout) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8162e7caae..9899419cab 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,244 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // Attempt to login to a host that does not have a failover partner - // - // Will repeatedly attempt to connect, but back off between each attempt so as not to clog the network. - // Back off period increases for first few failures: 100ms, 200ms, 400ms, 800ms, then 1000ms for subsequent attempts - // - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // DEVNOTE: The logic in this method is paralleled by the logic in LoginWithFailover. - // Changes to either one should be examined to see if they need to be reflected in the other - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - private void LoginNoFailover(ServerInfo serverInfo, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance, - SqlConnectionString connectionOptions, - SqlCredential credential, - TimeoutTimer timeout) - { - Debug.Assert(object.ReferenceEquals(connectionOptions, this.ConnectionOptions), "ConnectionOptions argument and property must be the same"); // consider removing the argument - int routingAttempts = 0; - ServerInfo originalServerInfo = serverInfo; // serverInfo may end up pointing to new object due to routing, original object is used to set CurrentDatasource - - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, host={1}", ObjectID, serverInfo.UserServerName); - - int sleepInterval = 100; //milliseconds to sleep (back off) between attempts. - - ResolveExtendedServerName(serverInfo, !redirectedUserInstance, connectionOptions); - - bool disableTnir = ShouldDisableTnir(connectionOptions); - - long timeoutUnitInterval = 0; - - bool isParallel = connectionOptions.MultiSubnetFailover || (connectionOptions.TransparentNetworkIPResolution && !disableTnir); - - if (isParallel) - { - float failoverTimeoutStep = connectionOptions.MultiSubnetFailover ? ADP.FailoverTimeoutStep : ADP.FailoverTimeoutStepForTnir; - - // Determine unit interval - if (timeout.IsInfinite) - { - timeoutUnitInterval = checked((long)(failoverTimeoutStep * (1000L * ADP.DefaultConnectionTimeout))); - } - else - { - timeoutUnitInterval = checked((long)(failoverTimeoutStep * timeout.MillisecondsRemaining)); - } - } - // Only three ways out of this loop: - // 1) Successfully connected - // 2) Parser threw exception while main timer was expired - // 3) Parser threw logon failure-related exception - // 4) Parser threw exception in post-initial connect code, - // such as pre-login handshake or during actual logon. (parser state != Closed) - // - // Of these methods, only #1 exits normally. This preserves the call stack on the exception - // back into the parser for the error cases. - int attemptNumber = 0; - TimeoutTimer intervalTimer = null; - TimeoutTimer attemptOneLoginTimeout = timeout; - - while (true) - { - Boolean isFirstTransparentAttempt = connectionOptions.TransparentNetworkIPResolution && !disableTnir && attemptNumber == 1; - - if (isParallel) - { - int multiplier = ++attemptNumber; - - if (connectionOptions.TransparentNetworkIPResolution) - { - // While connecting using TNIR the timeout multiplier should be increased to allow steps of 1,2,4 instead of 1,2,3. - // This will allow half the time out for the last connection attempt in case of Tnir. - multiplier = 1 << (attemptNumber - 1); - } - // Set timeout for this attempt, but don't exceed original timer - long nextTimeoutInterval = checked(timeoutUnitInterval * multiplier); - long milliseconds = timeout.MillisecondsRemaining; - - // If it is the first attempt at TNIR connection, then allow at least 500 ms for timeout. With the current failover step of 0.125 - // and Connection Time of < 4000 ms, the first attempt can be lower than 500 ms. - if (isFirstTransparentAttempt) - { - nextTimeoutInterval = Math.Max(ADP.MinimumTimeoutForTnirMs, nextTimeoutInterval); - } - if (nextTimeoutInterval > milliseconds) - { - nextTimeoutInterval = milliseconds; - } - intervalTimer = TimeoutTimer.StartMillisecondsTimeout(nextTimeoutInterval); - } - - // Re-allocate parser each time to make sure state is known - // RFC 50002652 - if parser was created by previous attempt, dispose it to properly close the socket, if created - if (_parser != null) - { - _parser.Disconnect(); - } - - _parser = new TdsParser(ConnectionOptions.MARS, ConnectionOptions.Asynchronous); - Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); - - try - { - // UNDONE: ITEM12001110 (DB Mirroring Reconnect) Old behavior of not truly honoring timeout preserved - // for non-failover, non-MSF scenarios to avoid breaking changes as part of a QFE. Consider fixing timeout - // handling in next full release and removing ignoreSniOpenTimeout parameter. - - if (isParallel) - { - attemptOneLoginTimeout = intervalTimer; - } - - AttemptOneLogin(serverInfo, - newPassword, - newSecurePassword, - attemptOneLoginTimeout, - isFirstTransparentAttempt: isFirstTransparentAttempt, - disableTnir: disableTnir); - - if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) - { - // connection succeeded: trigger exception if server sends failover partner and MultiSubnetFailover is used - throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); - } - - if (RoutingInfo != null) - { - SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", serverInfo.ExtendedServerName); - if (routingAttempts > MaxNumberOfRedirectRoute) - { - throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); - } - - if (timeout.IsExpired) - { - throw SQL.ROR_TimeoutAfterRoutingInfo(this); - } - - serverInfo = new ServerInfo(ConnectionOptions, RoutingInfo, serverInfo.ResolvedServerName, serverInfo.ServerSPN); - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = serverInfo.UserServerName; - - // restore properties that could be changed by the environment tokens - _currentPacketSize = ConnectionOptions.PacketSize; - _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; - CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; - ServerProvidedFailoverPartner = null; - _instanceName = string.Empty; - - routingAttempts++; - - continue; // repeat the loop, but skip code reserved for failed connections (after the catch) - } - else - { - break; // leave the while loop -- we've successfully connected - } - } - catch (SqlException sqlex) - { - if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) - { - continue; - } - - // If state != closed, indicates that the parser encountered an error while processing the - // login response (e.g. an explicit error token). Transient network errors that impact - // connectivity will result in parser state being closed. - if (_parser == null - || _parser.State != TdsParserState.Closed - || IsDoNotRetryConnectError(sqlex) - || timeout.IsExpired) - { - // no more time to try again - throw; // Caller will call LoginFailure() - } - - // Check sleep interval to make sure we won't exceed the timeout - // Do this in the catch block so we can re-throw the current exception - if (timeout.MillisecondsRemaining <= sleepInterval) - { - throw; - } - } - - // We only get here when we failed to connect, but are going to re-try - - // Switch to failover logic if the server provided a partner - if (ServerProvidedFailoverPartner != null) - { - if (connectionOptions.MultiSubnetFailover) - { - // connection failed: do not allow failover to server-provided failover partner if MultiSubnetFailover is set - throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); - } - Debug.Assert(ConnectionOptions.ApplicationIntent != ApplicationIntent.ReadOnly, "FAILOVER+AppIntent=RO: Should already fail (at LOGSHIPNODE in OnEnvChange)"); - - _timeoutErrorInternal.ResetAndRestartPhase(); - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PreLoginBegin); - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); - _timeoutErrorInternal.SetFailoverScenario(true); // this is a failover scenario - LoginWithFailover( - true, // start by using failover partner, since we already failed to connect to the primary - serverInfo, - ServerProvidedFailoverPartner, - newPassword, - newSecurePassword, - redirectedUserInstance, - connectionOptions, - credential, - timeout); - return; // LoginWithFailover successfully connected and handled entire connection setup - } - - // Sleep for a bit to prevent clogging the network with requests, - // then update sleep interval for next iteration (max 1 second interval) - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[milisec]", ObjectID, sleepInterval); - Thread.Sleep(sleepInterval); - sleepInterval = (sleepInterval < 500) ? sleepInterval * 2 : 1000; - } - _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; - - if (PoolGroupProviderInfo != null) - { - // We must wait for CompleteLogin to finish for to have the - // env change from the server to know its designated failover - // partner; save this information in ServerProvidedFailoverPartner. - - // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. - // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. - string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? "" : ServerProvidedFailoverPartner; - - PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); - } - CurrentDataSource = originalServerInfo.UserServerName; - } - private bool ShouldDisableTnir(SqlConnectionString connectionOptions) { Boolean isAzureEndPoint = ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource); 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 660d9552dc..4278465143 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1213,6 +1213,311 @@ private void LoginFailure() _parser?.Disconnect(); } + /// + /// Attempt to log in to a host that does not have a failover partner. + /// + /// + /// Will repeatedly attempt to connect, but back off between each attempt so as not to clog + /// the network. Back off period increases for first few failures: 100ms, 200ms, 400ms, + /// 800ms, then 1000ms for subsequent attempts. + /// + /// The logic in this method is paralleled by the logic in LoginWithFailover. Changes to + /// either one should be examined to see if they need to be reflected in the other. + /// + // @TODO: If the code is parallel with LoginWithFailover ... surely there's a way to factor it out a bit + private void LoginNoFailover( + ServerInfo serverInfo, + string newPassword, + SecureString newSecurePassword, + bool redirectedUserInstance, + SqlConnectionString connectionOptions, + SqlCredential credential, + TimeoutTimer timeout) + { + Debug.Assert(ReferenceEquals(connectionOptions, ConnectionOptions), + "ConnectionOptions argument and property must be the same"); + + int routingAttempts = 0; + + // ServerInfo may end up pointing to new object due to routing, original object is used + // to set CurrentDatasource + ServerInfo originalServerInfo = serverInfo; + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.LoginNoFailover | ADV | " + + $"Object ID {ObjectID}, " + + $"Host={serverInfo.UserServerName}"); + + // Milliseconds to sleep (back off) between attempts. + int sleepInterval = 100; + + ResolveExtendedServerName(serverInfo, !redirectedUserInstance, connectionOptions); + + #if NET + bool isParallel = connectionOptions.MultiSubnetFailover; + #else + bool disableTnir = ShouldDisableTnir(connectionOptions); + bool isParallel = connectionOptions.MultiSubnetFailover || + (connectionOptions.TransparentNetworkIPResolution && !disableTnir); + #endif + + long timeoutUnitInterval = 0; + if (isParallel) + { + // @TODO: Can we just use an int or a timespan or something? This math is annoying. + #if NET + float failoverTimeoutStep = ADP.FailoverTimeoutStep; + #else + float failoverTimeoutStep = connectionOptions.MultiSubnetFailover + ? ADP.FailoverTimeoutStep + : ADP.FailoverTimeoutStepForTnir; + #endif + + // Determine unit interval + timeoutUnitInterval = timeout.IsInfinite + ? checked((long)(failoverTimeoutStep * (1000L * ADP.DefaultConnectionTimeout))) + : checked((long)(failoverTimeoutStep * timeout.MillisecondsRemaining)); + } + + // Only three ways out of this loop: + // 1) Successfully connected + // 2) Parser threw exception while main timer was expired + // 3) Parser threw logon failure-related exception + // 4) Parser threw exception in post-initial connect code, + // such as pre-login handshake or during actual logon. (parser state != Closed) + // + // Of these methods, only #1 exits normally. This preserves the call stack on the exception + // back into the parser for the error cases. + int attemptNumber = 0; + TimeoutTimer intervalTimer = null; + TimeoutTimer attemptOneLoginTimeout = timeout; + + // @TODO: Break down this while true loop, consider replacing with for, since it is a fixed number of tries + while (true) + { + #if NETFRAMEWORK + bool isFirstTransparentAttempt = connectionOptions.TransparentNetworkIPResolution && + !disableTnir && + attemptNumber == 1; + #endif + + if (isParallel) + { + int multiplier = ++attemptNumber; + + #if NETFRAMEWORK + if (connectionOptions.TransparentNetworkIPResolution) + { + // While connecting using TNIR the timeout multiplier should be increased + // to allow steps of 1,2,4 instead of 1,2,3. This will allow half the + // timeout for the last connection attempt in case of TNIR. + multiplier = 1 << (attemptNumber - 1); + } + #endif + + // Set timeout for this attempt, but don't exceed original timer + long nextTimeoutInterval = checked(timeoutUnitInterval * multiplier); + long milliseconds = timeout.MillisecondsRemaining; + + #if NETFRAMEWORK + // If it is the first attempt at TNIR connection, then allow at least 500ms for + // timeout. With the current failover step of 0.125 and Connection Time of + // <4000ms, the first attempt can be lower than 500 ms. + if (isFirstTransparentAttempt) + { + nextTimeoutInterval = Math.Max(ADP.MinimumTimeoutForTnirMs, nextTimeoutInterval); + } + #endif + + if (nextTimeoutInterval > milliseconds) + { + nextTimeoutInterval = milliseconds; + } + intervalTimer = TimeoutTimer.StartMillisecondsTimeout(nextTimeoutInterval); + } + + // Re-allocate parser each time to make sure state is known. + // If parser was created by previous attempt, dispose it to properly close the + // socket, if created. + _parser?.Disconnect(); + + _parser = new TdsParser(ConnectionOptions.MARS, ConnectionOptions.Asynchronous); + Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, + $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); + + try + { + if (isParallel) + { + attemptOneLoginTimeout = intervalTimer; + } + + #if NET + AttemptOneLogin( + serverInfo, + newPassword, + newSecurePassword, + attemptOneLoginTimeout); + #else + AttemptOneLogin( + serverInfo, + newPassword, + newSecurePassword, + attemptOneLoginTimeout, + isFirstTransparentAttempt: isFirstTransparentAttempt, + disableTnir: disableTnir); + #endif + + if (connectionOptions.MultiSubnetFailover && ServerProvidedFailoverPartner != null) + { + // connection succeeded: trigger exception if server sends failover partner + // and MultiSubnetFailover is used + throw SQL.MultiSubnetFailoverWithFailoverPartner( + serverProvidedFailoverPartner: true, + internalConnection: this); + } + + if (RoutingInfo != null) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.LoginNoFailover | " + + $"Routed to {serverInfo.ExtendedServerName}"); + + if (routingAttempts > MaxNumberOfRedirectRoute) + { + throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); + } + + if (timeout.IsExpired) + { + throw SQL.ROR_TimeoutAfterRoutingInfo(this); + } + + serverInfo = new ServerInfo( + ConnectionOptions, + RoutingInfo, + serverInfo.ResolvedServerName, + serverInfo.ServerSPN); + _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); + _originalClientConnectionId = _clientConnectionId; + _routingDestination = serverInfo.UserServerName; + + // Restore properties that could be changed by the environment tokens + _currentPacketSize = ConnectionOptions.PacketSize; + _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; + CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; + ServerProvidedFailoverPartner = null; + _instanceName = string.Empty; + + routingAttempts++; + + // Repeat the loop, but skip code reserved for failed connections (after the catch) + continue; + } + else + { + // Leave the while loop -- we've successfully connected + break; + } + } + catch (SqlException sqlex) + { + if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) + { + continue; + } + + // If state != closed, indicates that the parser encountered an error while + // processing the login response (e.g. an explicit error token). Transient + // network errors that impact connectivity will result in parser state being + // closed. + if (_parser?.State is not TdsParserState.Closed || + IsDoNotRetryConnectError(sqlex) || + timeout.IsExpired) + { + // No more time to try again + // Caller will call LoginFailure() + throw; + } + + // Check sleep interval to make sure we won't exceed the timeout. Do this in + // the catch block so we can re-throw the current exception. + if (timeout.MillisecondsRemaining <= sleepInterval) + { + throw; + } + } + + // We only get here when we failed to connect, but are going to re-try + + // Switch to failover logic if the server provided a partner + if (ServerProvidedFailoverPartner != null) + { + if (connectionOptions.MultiSubnetFailover) + { + // connection failed: do not allow failover to server-provided failover partner if MultiSubnetFailover is set + throw SQL.MultiSubnetFailoverWithFailoverPartner(serverProvidedFailoverPartner: true, internalConnection: this); + } + Debug.Assert(ConnectionOptions.ApplicationIntent != ApplicationIntent.ReadOnly, "FAILOVER+AppIntent=RO: Should already fail (at LOGSHIPNODE in OnEnvChange)"); + + _timeoutErrorInternal.ResetAndRestartPhase(); + _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PreLoginBegin); + _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); + + // This is a failover scenario + _timeoutErrorInternal.SetFailoverScenario(true); + + // Start by using failover partner, since we already failed to connect to the primary + LoginWithFailover( + useFailoverHost: true, + serverInfo, + ServerProvidedFailoverPartner, + newPassword, + newSecurePassword, + redirectedUserInstance, + connectionOptions, + credential, + timeout); + + // LoginWithFailover successfully connected and handled entire connection setup + return; + } + + // Sleep for a bit to prevent clogging the network with requests, then update sleep + // interval for next iteration (max 1 second interval) + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.LoginNoFailover | ADV " + + $"Object ID {ObjectID}, " + + $"Sleeping {sleepInterval}ms"); + + Thread.Sleep(sleepInterval); + + sleepInterval = sleepInterval < 500 + ? sleepInterval * 2 + : 1000; + } + + _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; + + if (PoolGroupProviderInfo != null) + { + // We must wait for CompleteLogin to finish for to have the env change from the + // server to know its designated failover partner; save this information in + // ServerProvidedFailoverPartner. + + // When ignoring server provided failover partner, we must pass in the original + // failover partner from the connection string. Otherwise, the pool group's + // failover partner designation will be updated to point to the server provided + // value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner + ? string.Empty + : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(false, connectionOptions, actualFailoverPartner); + } + CurrentDataSource = originalServerInfo.UserServerName; + } + /// /// Is the given Sql error one that should prevent retrying to connect. /// From 8e8f76093d742f84304e3e6ed14d967f7d824055 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 13:57:17 -0600 Subject: [PATCH 30/56] Merge ShouldDisableTnir from netframework --- .../SqlClient/SqlInternalConnectionTds.cs | 24 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 33 +++++++++++++++++++ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 9899419cab..353fd5f6c8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,30 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private bool ShouldDisableTnir(SqlConnectionString connectionOptions) - { - Boolean isAzureEndPoint = ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource); - - Boolean isFedAuthEnabled = this._accessTokenInBytes != null || - #pragma warning disable 0618 // Type or member is obsolete - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || - #pragma warning restore 0618 // Type or member is obsolete - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; - - // Check if the user had explicitly specified the TNIR option in the connection string or the connection string builder. - // If the user has specified the option in the connection string explicitly, then we shouldn't disable TNIR. - bool isTnirExplicitlySpecifiedInConnectionOptions = connectionOptions.Parsetable.ContainsKey(DbConnectionStringKeywords.TransparentNetworkIpResolution); - - return isTnirExplicitlySpecifiedInConnectionOptions ? false : (isAzureEndPoint || isFedAuthEnabled); - } - // With possible MFA support in all AD auth providers, the duration for acquiring a token can be unpredictable. // If a timeout error (client or server) happened, we silently retry if a cached token exists from a previous auth attempt (see GetFedAuthToken) private bool AttemptRetryADAuthWithTimeoutError(SqlException sqlex, SqlConnectionString connectionOptions, TimeoutTimer timeout) 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 4278465143..fac2f73227 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -14,6 +14,10 @@ using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; +#if NETFRAMEWORK +using Microsoft.Data.Common.ConnectionString; +#endif + namespace Microsoft.Data.SqlClient { internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable @@ -1682,6 +1686,35 @@ private void ResetConnection() } } + #if NETFRAMEWORK + private bool ShouldDisableTnir(SqlConnectionString connectionOptions) + { + bool isAzureEndPoint = ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource); + + // @TODO: Turn into a HashSet and just check the list instead of this MESS. + bool isFedAuthEnabled = _accessTokenInBytes != null || + #pragma warning disable 0618 // Type or member is obsolete + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || + #pragma warning restore 0618 // Type or member is obsolete + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + + // Check if the user had explicitly specified the TNIR option in the connection string + // or the connection string builder. If the user has specified the option in the + // connection string explicitly, then we shouldn't disable TNIR. + bool isTnirExplicitlySpecifiedInConnectionOptions = connectionOptions.Parsetable.ContainsKey( + DbConnectionStringKeywords.TransparentNetworkIpResolution); + + return isTnirExplicitlySpecifiedInConnectionOptions ? false : (isAzureEndPoint || isFedAuthEnabled); + } + #endif + #endregion } } From 9bb5b821691fd720edac22d4b87b17e65170f7b0 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 14:14:21 -0600 Subject: [PATCH 31/56] Merge AttemptRetryADAuthWithTimeoutError (xmldocs, more spacing) --- .../SqlClient/SqlInternalConnectionTds.cs | 19 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 19 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 30 +++++++++++++++++++ 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6c3010fabb..985411f291 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,25 +346,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // With possible MFA support in all AD auth providers, the duration for acquiring a token can be unpredictable. - // If a timeout error (client or server) happened, we silently retry if a cached token exists from a previous auth attempt (see GetFedAuthToken) - private bool AttemptRetryADAuthWithTimeoutError(SqlException sqlex, SqlConnectionString connectionOptions, TimeoutTimer timeout) - { - if (!_activeDirectoryAuthTimeoutRetryHelper.CanRetryWithSqlException(sqlex)) - { - return false; - } - // Reset client-side timeout. - timeout.Reset(); - // When server timeout, the auth context key was already created. Clean it up here. - _dbConnectionPoolAuthenticationContextKey = null; - // When server timeouts, connection is doomed. Reset here to allow reconnect. - UnDoomThisConnection(); - // Change retry state so it only retries once for timeout error. - _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.Retrying; - return true; - } - // Attempt to login to a host that has a failover partner // // Connection & timeout sequence is diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 353fd5f6c8..3e7b811804 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,25 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // With possible MFA support in all AD auth providers, the duration for acquiring a token can be unpredictable. - // If a timeout error (client or server) happened, we silently retry if a cached token exists from a previous auth attempt (see GetFedAuthToken) - private bool AttemptRetryADAuthWithTimeoutError(SqlException sqlex, SqlConnectionString connectionOptions, TimeoutTimer timeout) - { - if (!_activeDirectoryAuthTimeoutRetryHelper.CanRetryWithSqlException(sqlex)) - { - return false; - } - // Reset client-side timeout. - timeout.Reset(); - // When server timeout, the auth context key was already created. Clean it up here. - _dbConnectionPoolAuthenticationContextKey = null; - // When server timeouts, connection is doomed. Reset here to allow reconnect. - UnDoomThisConnection(); - // Change retry state so it only retries once for timeout error. - _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.Retrying; - return true; - } - // Attempt to login to a host that has a failover partner // // Connection & timeout sequence is 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 fac2f73227..cc152f845d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -810,6 +810,36 @@ protected override void PropagateTransactionCookie(byte[] cookie) #region Private Methods + // + /// + /// With possible MFA support in all AD auth providers, the duration for acquiring a token + /// can be unpredictable. If a timeout error (client or server) happened, we silently retry + /// if a cached token exists from a previous auth attempt (see GetFedAuthToken). + /// + // @TODO: Rename to meet naming conventions + private bool AttemptRetryADAuthWithTimeoutError( + SqlException sqlex, + SqlConnectionString connectionOptions, // @TODO: this is not used + TimeoutTimer timeout) + { + if (!_activeDirectoryAuthTimeoutRetryHelper.CanRetryWithSqlException(sqlex)) + { + return false; + } + // Reset client-side timeout. + timeout.Reset(); + + // When server timeout, the auth context key was already created. Clean it up here. + _dbConnectionPoolAuthenticationContextKey = null; + + // When server timeouts, connection is doomed. Reset here to allow reconnection. + UnDoomThisConnection(); + + // Change retry state so it only retries once for timeout error. + _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.Retrying; + return true; + } + private void CompleteLogin(bool enlistOK) // @TODO: Rename as per guidelines { _parser.Run( From 73b41ce534d2cc7c530e076ff9cc444133d9c826 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 17:13:23 -0600 Subject: [PATCH 32/56] Merge LoginWithFailover (rebalance comments, split long lines, introduce a couple ternaries, etc) --- .../SqlClient/SqlInternalConnectionTds.cs | 242 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 244 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 292 ++++++++++++++++++ 3 files changed, 292 insertions(+), 486 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 985411f291..aa04578dfa 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,248 +346,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // Attempt to login to a host that has a failover partner - // - // Connection & timeout sequence is - // First target, timeout = interval * 1 - // second target, timeout = interval * 1 - // sleep for 100ms - // First target, timeout = interval * 2 - // Second target, timeout = interval * 2 - // sleep for 200ms - // First Target, timeout = interval * 3 - // etc. - // - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // DEVNOTE: The logic in this method is paralleled by the logic in LoginNoFailover. - // Changes to either one should be examined to see if they need to be reflected in the other - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - private void LoginWithFailover( - bool useFailoverHost, - ServerInfo primaryServerInfo, - string failoverHost, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance, - SqlConnectionString connectionOptions, - SqlCredential credential, - TimeoutTimer timeout - ) - { - Debug.Assert(!connectionOptions.MultiSubnetFailover, "MultiSubnetFailover should not be set if failover partner is used"); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, useFailover={1}[bool], primary={2}, failover={3}", ObjectID, useFailoverHost, primaryServerInfo.UserServerName, failoverHost); - - int sleepInterval = 100; //milliseconds to sleep (back off) between attempts. - long timeoutUnitInterval; - - ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); - - ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (ServerProvidedFailoverPartner == null) - { - ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); - } - - // Determine unit interval - if (timeout.IsInfinite) - { - timeoutUnitInterval = checked((long)(ADP.FailoverTimeoutStep * ADP.TimerFromSeconds(ADP.DefaultConnectionTimeout))); - } - else - { - timeoutUnitInterval = checked((long)(ADP.FailoverTimeoutStep * timeout.MillisecondsRemaining)); - } - - // Initialize loop variables - bool failoverDemandDone = false; // have we demanded for partner information yet (as necessary)? - int attemptNumber = 0; - - // Only three ways out of this loop: - // 1) Successfully connected - // 2) Parser threw exception while main timer was expired - // 3) Parser threw logon failure-related exception (LOGON_FAILED, PASSWORD_EXPIRED, etc) - // - // Of these methods, only #1 exits normally. This preserves the call stack on the exception - // back into the parser for the error cases. - while (true) - { - // Set timeout for this attempt, but don't exceed original timer - long nextTimeoutInterval = checked(timeoutUnitInterval * ((attemptNumber / 2) + 1)); - long milliseconds = timeout.MillisecondsRemaining; - if (nextTimeoutInterval > milliseconds) - { - nextTimeoutInterval = milliseconds; - } - - TimeoutTimer intervalTimer = TimeoutTimer.StartMillisecondsTimeout(nextTimeoutInterval); - - // Re-allocate parser each time to make sure state is known - // RFC 50002652 - if parser was created by previous attempt, dispose it to properly close the socket, if created - if (_parser != null) - { - _parser.Disconnect(); - } - - _parser = new TdsParser(ConnectionOptions.MARS, ConnectionOptions.Asynchronous); - Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); - - ServerInfo currentServerInfo; - if (useFailoverHost) - { - if (!failoverDemandDone) - { -#if NETFRAMEWORK - FailoverPermissionDemand(); -#endif - failoverDemandDone = true; - } - - // Primary server may give us a different failover partner than the connection string indicates. - // Update it only if we are respecting server-provided failover partner values. - if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) - { - if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Ignoring server provided failover partner '{1}' due to IgnoreServerProvidedFailoverPartner AppContext switch.", ObjectID, ServerProvidedFailoverPartner); - } - else - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailoverPartner); - failoverServerInfo.SetDerivedNames(string.Empty, ServerProvidedFailoverPartner); - } - } - - currentServerInfo = failoverServerInfo; - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); - } - else - { - currentServerInfo = primaryServerInfo; - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Principle); - } - - try - { - // Attempt login. Use timerInterval for attempt timeout unless infinite timeout was requested. - AttemptOneLogin( - currentServerInfo, - newPassword, - newSecurePassword, - intervalTimer, - withFailover: true - ); - - int routingAttempts = 0; - while (RoutingInfo != null) - { - if (routingAttempts > MaxNumberOfRedirectRoute) - { - throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); - } - routingAttempts++; - - SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", RoutingInfo.ServerName); - - if (_parser != null) - { - _parser.Disconnect(); - } - - _parser = new TdsParser(ConnectionOptions.MARS, connectionOptions.Asynchronous); - - Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); - - currentServerInfo = new ServerInfo(ConnectionOptions, RoutingInfo, currentServerInfo.ResolvedServerName, currentServerInfo.ServerSPN); - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = currentServerInfo.UserServerName; - - // restore properties that could be changed by the environment tokens - _currentPacketSize = connectionOptions.PacketSize; - _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; - CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - ServerProvidedFailoverPartner = null; - _instanceName = string.Empty; - - AttemptOneLogin( - currentServerInfo, - newPassword, - newSecurePassword, - intervalTimer, - withFailover: true); - } - - break; // leave the while loop -- we've successfully connected - } - catch (SqlException sqlex) - { - if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) - { - continue; - } - - if (IsDoNotRetryConnectError(sqlex) - || timeout.IsExpired) - { // no more time to try again - throw; // Caller will call LoginFailure() - } - - if (!ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource) && IsConnectionDoomed) - { - throw; - } - - if (1 == attemptNumber % 2) - { - // Check sleep interval to make sure we won't exceed the original timeout - // Do this in the catch block so we can re-throw the current exception - if (timeout.MillisecondsRemaining <= sleepInterval) - { - throw; - } - } - } - - // We only get here when we failed to connect, but are going to re-try - - // After trying to connect to both servers fails, sleep for a bit to prevent clogging - // the network with requests, then update sleep interval for next iteration (max 1 second interval) - if (1 == attemptNumber % 2) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[milisec]", ObjectID, sleepInterval); - Thread.Sleep(sleepInterval); - sleepInterval = (sleepInterval < 500) ? sleepInterval * 2 : 1000; - } - - // Update attempt number and target host - attemptNumber++; - useFailoverHost = !useFailoverHost; - } - - // If we get here, connection/login succeeded! Just a few more checks & record-keeping - _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; - - // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error - if (useFailoverHost && ServerProvidedFailoverPartner == null) - { - throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); - } - - if (PoolGroupProviderInfo != null) - { - // We must wait for CompleteLogin to finish for to have the - // env change from the server to know its designated failover - // partner. - - // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. - // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. - string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? failoverHost : ServerProvidedFailoverPartner; - - PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); - } - CurrentDataSource = (useFailoverHost ? failoverHost : primaryServerInfo.UserServerName); - } - private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup, SqlConnectionString options) { if (serverInfo.ExtendedServerName == null) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 3e7b811804..7ea2794549 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,250 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - // Attempt to login to a host that has a failover partner - // - // Connection & timeout sequence is - // First target, timeout = interval * 1 - // second target, timeout = interval * 1 - // sleep for 100ms - // First target, timeout = interval * 2 - // Second target, timeout = interval * 2 - // sleep for 200ms - // First Target, timeout = interval * 3 - // etc. - // - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // DEVNOTE: The logic in this method is paralleled by the logic in LoginNoFailover. - // Changes to either one should be examined to see if they need to be reflected in the other - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - private void LoginWithFailover( - bool useFailoverHost, - ServerInfo primaryServerInfo, - string failoverHost, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance, - SqlConnectionString connectionOptions, - SqlCredential credential, - TimeoutTimer timeout - ) - { - Debug.Assert(!connectionOptions.MultiSubnetFailover, "MultiSubnetFailover should not be set if failover partner is used"); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, useFailover={1}[bool], primary={2}, failover={3}", ObjectID, useFailoverHost, primaryServerInfo.UserServerName, failoverHost); - - int sleepInterval = 100; //milliseconds to sleep (back off) between attempts. - long timeoutUnitInterval; - - string protocol = ConnectionOptions.NetworkLibrary; - ServerInfo failoverServerInfo = new ServerInfo(connectionOptions, failoverHost, connectionOptions.FailoverPartnerSPN); - - ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); - if (ServerProvidedFailoverPartner == null) - { - ResolveExtendedServerName(failoverServerInfo, !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, connectionOptions); - } - - // Determine unit interval - if (timeout.IsInfinite) - { - timeoutUnitInterval = checked((long)(ADP.FailoverTimeoutStep * ADP.TimerFromSeconds(ADP.DefaultConnectionTimeout))); - } - else - { - timeoutUnitInterval = checked((long)(ADP.FailoverTimeoutStep * timeout.MillisecondsRemaining)); - } - - // Initialize loop variables - bool failoverDemandDone = false; // have we demanded for partner information yet (as necessary)? - int attemptNumber = 0; - - // Only three ways out of this loop: - // 1) Successfully connected - // 2) Parser threw exception while main timer was expired - // 3) Parser threw logon failure-related exception (LOGON_FAILED, PASSWORD_EXPIRED, etc) - // - // Of these methods, only #1 exits normally. This preserves the call stack on the exception - // back into the parser for the error cases. - while (true) - { - // Set timeout for this attempt, but don't exceed original timer - long nextTimeoutInterval = checked(timeoutUnitInterval * ((attemptNumber / 2) + 1)); - long milliseconds = timeout.MillisecondsRemaining; - if (nextTimeoutInterval > milliseconds) - { - nextTimeoutInterval = milliseconds; - } - - TimeoutTimer intervalTimer = TimeoutTimer.StartMillisecondsTimeout(nextTimeoutInterval); - - // Re-allocate parser each time to make sure state is known - // RFC 50002652 - if parser was created by previous attempt, dispose it to properly close the socket, if created - if (_parser != null) - { - _parser.Disconnect(); - } - - _parser = new TdsParser(ConnectionOptions.MARS, ConnectionOptions.Asynchronous); - Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); - - ServerInfo currentServerInfo; - if (useFailoverHost) - { - if (!failoverDemandDone) - { - FailoverPermissionDemand(); - failoverDemandDone = true; - } - - // Primary server may give us a different failover partner than the connection string indicates. - // Update it only if we are respecting server-provided failover partner values. - if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) - { - if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Ignoring server provided failover partner '{1}' due to IgnoreServerProvidedFailoverPartner AppContext switch.", ObjectID, ServerProvidedFailoverPartner); - } - else - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, new failover partner={1}", ObjectID, ServerProvidedFailoverPartner); - failoverServerInfo.SetDerivedNames(protocol, ServerProvidedFailoverPartner); - } - } - - currentServerInfo = failoverServerInfo; - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); - } - else - { - currentServerInfo = primaryServerInfo; - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Principle); - } - - try - { - // Attempt login. Use timerInterval for attempt timeout unless infinite timeout was requested. - AttemptOneLogin( - currentServerInfo, - newPassword, - newSecurePassword, - intervalTimer, - withFailover: true - ); - - int routingAttempts = 0; - while (RoutingInfo != null) - { - if (routingAttempts > MaxNumberOfRedirectRoute) - { - throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); - } - routingAttempts++; - - SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", RoutingInfo.ServerName); - - if (_parser != null) - { - _parser.Disconnect(); - } - - _parser = new TdsParser(ConnectionOptions.MARS, connectionOptions.Asynchronous); - - Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); - - currentServerInfo = new ServerInfo(ConnectionOptions, RoutingInfo, currentServerInfo.ResolvedServerName, currentServerInfo.ServerSPN); - _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = currentServerInfo.UserServerName; - - // restore properties that could be changed by the environment tokens - _currentPacketSize = connectionOptions.PacketSize; - _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; - CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; - ServerProvidedFailoverPartner = null; - _instanceName = string.Empty; - - AttemptOneLogin( - currentServerInfo, - newPassword, - newSecurePassword, - intervalTimer, - withFailover: true); - } - - break; // leave the while loop -- we've successfully connected - } - catch (SqlException sqlex) - { - if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) - { - continue; - } - - if (IsDoNotRetryConnectError(sqlex) - || timeout.IsExpired) - { // no more time to try again - throw; // Caller will call LoginFailure() - } - - // TODO: It doesn't make sense to connect to an azure sql server instance with a failover partner - // specified. Azure SQL Server does not support failover partners. Other availability technologies - // like Failover Groups should be used instead. - if (!ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource) && IsConnectionDoomed) - { - throw; - } - - if (1 == attemptNumber % 2) - { - // Check sleep interval to make sure we won't exceed the original timeout - // Do this in the catch block so we can re-throw the current exception - if (timeout.MillisecondsRemaining <= sleepInterval) - { - throw; - } - } - } - - // We only get here when we failed to connect, but are going to re-try - - // After trying to connect to both servers fails, sleep for a bit to prevent clogging - // the network with requests, then update sleep interval for next iteration (max 1 second interval) - if (1 == attemptNumber % 2) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[milisec]", ObjectID, sleepInterval); - Thread.Sleep(sleepInterval); - sleepInterval = (sleepInterval < 500) ? sleepInterval * 2 : 1000; - } - - // Update attempt number and target host - attemptNumber++; - useFailoverHost = !useFailoverHost; - } - - // If we get here, connection/login succeeded! Just a few more checks & record-keeping - _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; - - // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error - if (useFailoverHost && ServerProvidedFailoverPartner == null) - { - throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); - } - - if (PoolGroupProviderInfo != null) - { - // We must wait for CompleteLogin to finish for to have the - // env change from the server to know its designated failover - // partner. - - // When ignoring server provided failover partner, we must pass in the original failover partner from the connection string. - // Otherwise the pool group's failover partner designation will be updated to point to the server provided value. - string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner ? failoverHost : ServerProvidedFailoverPartner; - - PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); - } - CurrentDataSource = (useFailoverHost ? failoverHost : primaryServerInfo.UserServerName); - } - private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup, SqlConnectionString options) { if (serverInfo.ExtendedServerName == 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 cc152f845d..3179cef8a7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1552,6 +1552,298 @@ private void LoginNoFailover( CurrentDataSource = originalServerInfo.UserServerName; } + /// + /// Attempt to log in to a host that has a failover partner. + /// + /// + /// Connection and timeout sequence is: + /// - First target, timeout = interval * 1 + /// - second target, timeout = interval * 1 + /// - sleep for 100ms + /// - First target, timeout = interval * 2 + /// - Second target, timeout = interval * 2 + /// - sleep for 200ms + /// - First Target, timeout = interval * 3 + /// - etc. + /// + /// The logic in this method is paralleled by the logic in LoginNoFailover. Changes to + /// either one should be examined to see if they need to be reflected in the other. + /// + // @TODO: If it's so similar, then why don't we factor out some common code from it? + private void LoginWithFailover( + bool useFailoverHost, + ServerInfo primaryServerInfo, + string failoverHost, + string newPassword, + SecureString newSecurePassword, + bool redirectedUserInstance, + SqlConnectionString connectionOptions, + SqlCredential credential, // @TODO: This isn't used anywhere + TimeoutTimer timeout) + { + Debug.Assert(!connectionOptions.MultiSubnetFailover, + "MultiSubnetFailover should not be set if failover partner is used"); + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.LoginWithFailover | ADV | " + + $"Object ID {ObjectID}, " + + $"useFailover={useFailoverHost}, " + + $"primary={primaryServerInfo.UserServerName}, " + + $"failover={failoverHost}"); + + #if NETFRAMEWORK + string protocol = ConnectionOptions.NetworkLibrary; + #endif + + ServerInfo failoverServerInfo = new ServerInfo( + connectionOptions, + failoverHost, + connectionOptions.FailoverPartnerSPN); + + ResolveExtendedServerName(primaryServerInfo, !redirectedUserInstance, connectionOptions); + if (ServerProvidedFailoverPartner == null) + { + ResolveExtendedServerName( + failoverServerInfo, + aliasLookup: !redirectedUserInstance && failoverHost != primaryServerInfo.UserServerName, + connectionOptions); + } + + // Milliseconds to sleep (back off) between attempts. + // @TODO: Rename to include units (or use TimeSpan!) + int sleepInterval = 100; + + // Determine unit interval + // @TODO: Use ints or timespans or something that doesn't requires floating point math + long timeoutUnitInterval = timeout.IsInfinite + ? checked((long)(ADP.FailoverTimeoutStep * ADP.TimerFromSeconds(ADP.DefaultConnectionTimeout))) + : checked((long)(ADP.FailoverTimeoutStep * timeout.MillisecondsRemaining)); + + // Initialize loop variables + // Have we demanded for partner information yet (as necessary)? + bool failoverDemandDone = false; + int attemptNumber = 0; + + // Only three ways out of this loop: + // 1) Successfully connected + // 2) Parser threw exception while main timer was expired + // 3) Parser threw logon failure-related exception (LOGON_FAILED, PASSWORD_EXPIRED, etc) + // + // Of these methods, only #1 exits normally. This preserves the call stack on the exception + // back into the parser for the error cases. + while (true) + { + // Set timeout for this attempt, but don't exceed original timer + long nextTimeoutInterval = checked(timeoutUnitInterval * ((attemptNumber / 2) + 1)); + long milliseconds = timeout.MillisecondsRemaining; + if (nextTimeoutInterval > milliseconds) + { + nextTimeoutInterval = milliseconds; + } + + TimeoutTimer intervalTimer = TimeoutTimer.StartMillisecondsTimeout(nextTimeoutInterval); + + // Re-allocate parser each time to make sure state is known. If parser was created + // by previous attempt, dispose it to properly close the socket, if created. + _parser?.Disconnect(); + _parser = new TdsParser(ConnectionOptions.MARS, ConnectionOptions.Asynchronous); + + Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, + $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); + + ServerInfo currentServerInfo; + if (useFailoverHost) + { + if (!failoverDemandDone) + { + #if NETFRAMEWORK + FailoverPermissionDemand(); + #endif + + failoverDemandDone = true; + } + + // Primary server may give us a different failover partner than the connection + // string indicates. Update it only if we are respecting server-provided + // failover partner values. + if (ServerProvidedFailoverPartner != null && failoverServerInfo.ResolvedServerName != ServerProvidedFailoverPartner) + { + if (LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.LoginWithFailover | ADV | " + + $"Object ID {ObjectID}, " + + $"Ignoring server provided failover partner '{ServerProvidedFailoverPartner}' " + + $"due to IgnoreServerProvidedFailoverPartner AppContext switch."); + } + else + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.LoginWithFailover | ADV | " + + $"Object ID {ObjectID}, " + + $"new failover partner={ServerProvidedFailoverPartner}"); + + #if NET + failoverServerInfo.SetDerivedNames(string.Empty, ServerProvidedFailoverPartner); + #else + failoverServerInfo.SetDerivedNames(protocol, ServerProvidedFailoverPartner); + #endif + } + } + + currentServerInfo = failoverServerInfo; + _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Failover); + } + else + { + currentServerInfo = primaryServerInfo; + _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.Principle); + } + + try + { + // Attempt login. Use timerInterval for attempt timeout unless infinite timeout + // was requested. + AttemptOneLogin( + currentServerInfo, + newPassword, + newSecurePassword, + intervalTimer, + withFailover: true); + + int routingAttempts = 0; + while (RoutingInfo != null) + { + if (routingAttempts > MaxNumberOfRedirectRoute) + { + throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); + } + routingAttempts++; + + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.LoginWithFailover | " + + $"Routed to {RoutingInfo.ServerName}", RoutingInfo.ServerName); + + _parser?.Disconnect(); + _parser = new TdsParser(ConnectionOptions.MARS, connectionOptions.Asynchronous); + + Debug.Assert(SniContext.Undefined == Parser._physicalStateObj.SniContext, + $"SniContext should be Undefined; actual Value: {Parser._physicalStateObj.SniContext}"); + + currentServerInfo = new ServerInfo( + ConnectionOptions, + RoutingInfo, + currentServerInfo.ResolvedServerName, + currentServerInfo.ServerSPN); + _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); + _originalClientConnectionId = _clientConnectionId; + _routingDestination = currentServerInfo.UserServerName; + + // Restore properties that could be changed by the environment tokens + _currentPacketSize = connectionOptions.PacketSize; + _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; + CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; + ServerProvidedFailoverPartner = null; + _instanceName = string.Empty; + + AttemptOneLogin( + currentServerInfo, + newPassword, + newSecurePassword, + intervalTimer, + withFailover: true); + } + + // Leave the while loop -- we've successfully connected + break; + } + catch (SqlException sqlex) + { + if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) + { + continue; + } + + if (IsDoNotRetryConnectError(sqlex) || timeout.IsExpired) + { + // No more time to try again. + // Caller will call LoginFailure() + throw; + } + + // TODO: It doesn't make sense to connect to an azure sql server instance with a failover partner + // specified. Azure SQL Server does not support failover partners. Other availability technologies + // like Failover Groups should be used instead. + if (!ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource) && IsConnectionDoomed) + { + throw; + } + + if (attemptNumber % 2 == 1) + { + // Check sleep interval to make sure we won't exceed the original timeout. + // Do this in the catch block so we can re-throw the current exception + if (timeout.MillisecondsRemaining <= sleepInterval) + { + throw; + } + } + } + + // We only get here when we failed to connect, but are going to re-try + + // After trying to connect to both servers fails, sleep for a bit to prevent + // clogging the network with requests, then update sleep interval for next + // iteration (max 1 second interval). + if (attemptNumber % 2 == 1) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.LoginWithFailover | ADV | " + + $"Object ID {ObjectID}, " + + $"sleeping {sleepInterval}ms"); + + Thread.Sleep(sleepInterval); + + sleepInterval = sleepInterval < 500 + ? sleepInterval * 2 + : 1000; + } + + // Update attempt number and target host + attemptNumber++; + useFailoverHost = !useFailoverHost; + } + + // If we get here, connection/login succeeded! Just a few more checks & record-keeping + _activeDirectoryAuthTimeoutRetryHelper.State = ActiveDirectoryAuthenticationTimeoutRetryState.HasLoggedIn; + + // if connected to failover host, but said host doesn't have DbMirroring set up, throw an error + if (useFailoverHost && ServerProvidedFailoverPartner == null) + { + throw SQL.InvalidPartnerConfiguration(failoverHost, CurrentDatabase); + } + + if (PoolGroupProviderInfo != null) + { + // We must wait for CompleteLogin to finish for to have the env change from the + // server to know its designated failover partner. + + // When ignoring server provided failover partner, we must pass in the original + // failover partner from the connection string. Otherwise, the pool group's + // failover partner designation will be updated to point to the server provided + // value. + string actualFailoverPartner = LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner + ? failoverHost + : ServerProvidedFailoverPartner; + + PoolGroupProviderInfo.FailoverCheck(useFailoverHost, connectionOptions, actualFailoverPartner); + } + + CurrentDataSource = useFailoverHost + ? failoverHost + : primaryServerInfo.UserServerName; + } + /// /// Is the given Sql error one that should prevent retrying to connect. /// From a01200aac1f480303fde2593e28d30e4e33465c3 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 17:32:56 -0600 Subject: [PATCH 33/56] Merge ResolveExtendedServerName (rebalance comments, split long lines, add argument parameters) --- .../SqlClient/SqlInternalConnectionTds.cs | 45 +--------------- .../SqlClient/SqlInternalConnectionTds.cs | 43 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 52 +++++++++++++++++++ 3 files changed, 54 insertions(+), 86 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index aa04578dfa..89a9665cf0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -45,6 +45,8 @@ internal sealed class SessionData internal string _initialLanguage; internal byte _unrecoverableStatesCount = 0; + + // @TODO: Introduce record/struct type to replace the tuple. internal Dictionary> _resolvedAliases; #if DEBUG @@ -346,49 +348,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup, SqlConnectionString options) - { - if (serverInfo.ExtendedServerName == null) - { - string host = serverInfo.UserServerName; - string protocol = serverInfo.UserProtocol; - - if (aliasLookup) - { // We skip this for UserInstances... - // Perform registry lookup to see if host is an alias. It will appropriately set host and protocol, if an Alias. - // Check if it was already resolved, during CR reconnection _currentSessionData values will be copied from - // _reconnectSessonData of the previous connection - if (_currentSessionData != null && !string.IsNullOrEmpty(host)) - { - Tuple hostPortPair; - if (_currentSessionData._resolvedAliases.TryGetValue(host, out hostPortPair)) - { - host = hostPortPair.Item1; - protocol = hostPortPair.Item2; - } - else - { - TdsParserStaticMethods.AliasRegistryLookup(ref host, ref protocol); - _currentSessionData._resolvedAliases.Add(serverInfo.UserServerName, new Tuple(host, protocol)); - } - } - else - { - TdsParserStaticMethods.AliasRegistryLookup(ref host, ref protocol); - } - - //TODO: fix local host enforcement with datadirectory and failover - if (options.EnforceLocalHost) - { - // verify LocalHost for |DataDirectory| usage - SqlConnectionString.VerifyLocalHostAndFixup(ref host, true, true /*fix-up to "."*/); - } - } - - serverInfo.SetDerivedNames(protocol, host); - } - } - // Common code path for making one attempt to establish a connection and log in to server. private void AttemptOneLogin(ServerInfo serverInfo, string newPassword, diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 7ea2794549..72288e6d3c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,49 +345,6 @@ internal SqlInternalConnectionTds( // LOGIN-RELATED METHODS //////////////////////////////////////////////////////////////////////////////////////// - private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup, SqlConnectionString options) - { - if (serverInfo.ExtendedServerName == null) - { - string host = serverInfo.UserServerName; - string protocol = serverInfo.UserProtocol; - - if (aliasLookup) - { // We skip this for UserInstances... - // Perform registry lookup to see if host is an alias. It will appropriately set host and protocol, if an Alias. - // Check if it was already resolved, during CR reconnection _currentSessionData values will be copied from - // _reconnectSessonData of the previous connection - if (_currentSessionData != null && !string.IsNullOrEmpty(host)) - { - Tuple hostPortPair; - if (_currentSessionData._resolvedAliases.TryGetValue(host, out hostPortPair)) - { - host = hostPortPair.Item1; - protocol = hostPortPair.Item2; - } - else - { - TdsParserStaticMethods.AliasRegistryLookup(ref host, ref protocol); - _currentSessionData._resolvedAliases.Add(serverInfo.UserServerName, new Tuple(host, protocol)); - } - } - else - { - TdsParserStaticMethods.AliasRegistryLookup(ref host, ref protocol); - } - - //TODO: fix local host enforcement with datadirectory and failover - if (options.EnforceLocalHost) - { - // verify LocalHost for |DataDirectory| usage - SqlConnectionString.VerifyLocalHostAndFixup(ref host, true, true /*fix-up to "."*/); - } - } - - serverInfo.SetDerivedNames(protocol, host); - } - } - // Common code path for making one attempt to establish a connection and log in to server. private void AttemptOneLogin(ServerInfo serverInfo, string newPassword, 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 3179cef8a7..b5c98da05e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2008,6 +2008,58 @@ private void ResetConnection() } } + private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup, SqlConnectionString options) + { + // @TODO: Invert to save on indentation + if (serverInfo.ExtendedServerName == null) + { + string host = serverInfo.UserServerName; + string protocol = serverInfo.UserProtocol; + + if (aliasLookup) + { + // We skip this for UserInstances... + // Perform registry lookup to see if host is an alias. It will appropriately + // set host and protocol, if an Alias. Check if it was already resolved, during + // CR reconnection _currentSessionData values will be copied from + // _reconnectSessionData of the previous connection. + if (_currentSessionData != null && !string.IsNullOrEmpty(host)) + { + Tuple hostPortPair; + if (_currentSessionData._resolvedAliases.TryGetValue(host, out hostPortPair)) + { + host = hostPortPair.Item1; + protocol = hostPortPair.Item2; + } + else + { + // @TODO: What are these refs doing here?? Just return the values! + TdsParserStaticMethods.AliasRegistryLookup(ref host, ref protocol); + _currentSessionData._resolvedAliases.Add( + serverInfo.UserServerName, + new Tuple(host, protocol)); + } + } + else + { + TdsParserStaticMethods.AliasRegistryLookup(ref host, ref protocol); + } + + // TODO: fix local host enforcement with datadirectory and failover + if (options.EnforceLocalHost) + { + // Verify LocalHost for |DataDirectory| usage + SqlConnectionString.VerifyLocalHostAndFixup( + ref host, + enforceLocalHost: true, + fixup: true); + } + } + + serverInfo.SetDerivedNames(protocol, host); + } + } + #if NETFRAMEWORK private bool ShouldDisableTnir(SqlConnectionString connectionOptions) { From bfb58e5a007912f47be720392a58f7ec82cf3c0d Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 17:45:32 -0600 Subject: [PATCH 34/56] Merge AttemptOneLogin (resolve conflicts), FailoverPermissionDemand (conditional access, expression body) --- .../SqlClient/SqlInternalConnectionTds.cs | 46 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 48 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 61 +++++++++++++++++++ 3 files changed, 61 insertions(+), 94 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 89a9665cf0..6f4a6ed9da 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,52 +344,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // LOGIN-RELATED METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - // Common code path for making one attempt to establish a connection and log in to server. - private void AttemptOneLogin(ServerInfo serverInfo, - string newPassword, - SecureString newSecurePassword, - TimeoutTimer timeout, - bool withFailover = false) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, timeout={1}[msec], server={2}", ObjectID, timeout.MillisecondsRemaining, serverInfo.ExtendedServerName); - RoutingInfo = null; // forget routing information - - _parser._physicalStateObj.SniContext = SniContext.Snix_Connect; - - _parser.Connect(serverInfo, - this, - timeout, - ConnectionOptions, - withFailover); - - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ConsumePreLoginHandshake); - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); - - _parser._physicalStateObj.SniContext = SniContext.Snix_Login; - this.Login(serverInfo, timeout, newPassword, newSecurePassword, ConnectionOptions.Encrypt); - - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth); - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PostLogin); - - CompleteLogin(!ConnectionOptions.Pooling); - - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin); - } - -#if NETFRAMEWORK - internal void FailoverPermissionDemand() - { - if (PoolGroupProviderInfo != null) - { - PoolGroupProviderInfo.FailoverPermissionDemand(); - } - } -#endif - //////////////////////////////////////////////////////////////////////////////////////// // PREPARED COMMAND METHODS //////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 72288e6d3c..8ae72455f1 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,54 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // LOGIN-RELATED METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - // Common code path for making one attempt to establish a connection and log in to server. - private void AttemptOneLogin(ServerInfo serverInfo, - string newPassword, - SecureString newSecurePassword, - TimeoutTimer timeout, - bool withFailover = false, - bool isFirstTransparentAttempt = true, - bool disableTnir = false) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, timeout={1}[msec], server={2}", ObjectID, timeout.MillisecondsRemaining, serverInfo.ExtendedServerName); - RoutingInfo = null; // forget routing information - - _parser._physicalStateObj.SniContext = SniContext.Snix_Connect; - - _parser.Connect(serverInfo, - this, - timeout, - ConnectionOptions, - withFailover, - isFirstTransparentAttempt, - disableTnir); - - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ConsumePreLoginHandshake); - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); - - _parser._physicalStateObj.SniContext = SniContext.Snix_Login; - this.Login(serverInfo, timeout, newPassword, newSecurePassword, ConnectionOptions.Encrypt); - - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth); - _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PostLogin); - - CompleteLogin(!ConnectionOptions.Pooling); - - _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin); - } - - internal void FailoverPermissionDemand() - { - if (PoolGroupProviderInfo != null) - { - PoolGroupProviderInfo.FailoverPermissionDemand(); - } - } - //////////////////////////////////////////////////////////////////////////////////////// // PREPARED COMMAND METHODS //////////////////////////////////////////////////////////////////////////////////////// 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 b5c98da05e..584da6c597 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -810,7 +810,63 @@ protected override void PropagateTransactionCookie(byte[] cookie) #region Private Methods + // + /// + /// Common code path for making one attempt to establish a connection and log in to server. + /// + // @TODO: This is gross - there is no good way to #if a multi-line method signature. Introduce a record/struct type the different values. + private void AttemptOneLogin( + ServerInfo serverInfo, + string newPassword, + SecureString newSecurePassword, + TimeoutTimer timeout, + bool withFailover = false + + #if NETFRAMEWORK + , + bool isFirstTransparentAttempt = true, + bool disableTnir = false + #endif + ) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, timeout={1}[msec], server={2}", ObjectID, timeout.MillisecondsRemaining, serverInfo.ExtendedServerName); + RoutingInfo = null; // forget routing information + + _parser._physicalStateObj.SniContext = SniContext.Snix_Connect; + + #if NETFRAMEWORK + _parser.Connect( + serverInfo, + this, + timeout, + ConnectionOptions, + withFailover, + isFirstTransparentAttempt, + disableTnir); + #else + _parser.Connect( + serverInfo, + this, + timeout, + ConnectionOptions, + withFailover); + #endif + + _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ConsumePreLoginHandshake); + _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); + + _parser._physicalStateObj.SniContext = SniContext.Snix_Login; + Login(serverInfo, timeout, newPassword, newSecurePassword, ConnectionOptions.Encrypt); + + _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth); + _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PostLogin); + + CompleteLogin(!ConnectionOptions.Pooling); + + _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin); + } + /// /// With possible MFA support in all AD auth providers, the duration for acquiring a token /// can be unpredictable. If a timeout error (client or server) happened, we silently retry @@ -1086,6 +1142,11 @@ private void ExecuteTransaction2005( } } + #if NETFRAMEWORK + private void FailoverPermissionDemand() => + PoolGroupProviderInfo?.FailoverPermissionDemand(); + #endif + private void Login( ServerInfo server, TimeoutTimer timeout, From d5987c3897b9d201eaced475a72aa84c9cc0bf68 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 17:50:35 -0600 Subject: [PATCH 35/56] Merge ObtainAdditionalLocksForClose, ReleaseAdditionalLocksForClose --- .../SqlClient/SqlInternalConnectionTds.cs | 21 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 21 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 27 +++++++++++++++++-- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6f4a6ed9da..8383985ff3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,27 +348,6 @@ internal SqlInternalConnectionTds( // PREPARED COMMAND METHODS //////////////////////////////////////////////////////////////////////////////////////// - protected override bool ObtainAdditionalLocksForClose() - { - bool obtainParserLock = !ThreadHasParserLockForClose; - Debug.Assert(obtainParserLock || _parserLock.ThreadMayHaveLock(), "Thread claims to have lock, but lock is not taken"); - if (obtainParserLock) - { - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; - } - return obtainParserLock; - } - - protected override void ReleaseAdditionalLocksForClose(bool lockToken) - { - if (lockToken) - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - } - // called by SqlConnection.RepairConnection which is a relatively expensive way of repair inner connection // prior to execution of request, used from EnlistTransaction, EnlistDistributedTransaction and ChangeDatabase internal bool GetSessionAndReconnectIfNeeded(SqlConnection parent, int timeout = 0) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8ae72455f1..7cf8599ae8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -345,27 +345,6 @@ internal SqlInternalConnectionTds( // PREPARED COMMAND METHODS //////////////////////////////////////////////////////////////////////////////////////// - protected override bool ObtainAdditionalLocksForClose() - { - bool obtainParserLock = !ThreadHasParserLockForClose; - Debug.Assert(obtainParserLock || _parserLock.ThreadMayHaveLock(), "Thread claims to have lock, but lock is not taken"); - if (obtainParserLock) - { - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; - } - return obtainParserLock; - } - - protected override void ReleaseAdditionalLocksForClose(bool lockToken) - { - if (lockToken) - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - } - // called by SqlConnection.RepairConnection which is a relatively expensive way of repair inner connection // prior to execution of request, used from EnlistTransaction, EnlistDistributedTransaction and ChangeDatabase internal bool GetSessionAndReconnectIfNeeded(SqlConnection parent, int timeout = 0) 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 584da6c597..6f6223381e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -798,6 +798,22 @@ protected override void InternalDeactivate() } } + protected override bool ObtainAdditionalLocksForClose() + { + bool obtainParserLock = !ThreadHasParserLockForClose; + + Debug.Assert(obtainParserLock || _parserLock.ThreadMayHaveLock(), + "Thread claims to have lock, but lock is not taken"); + + if (obtainParserLock) + { + _parserLock.Wait(canReleaseFromAnyThread: false); + ThreadHasParserLockForClose = true; + } + + return obtainParserLock; + } + protected override void PropagateTransactionCookie(byte[] cookie) { _parser.PropagateDistributedTransaction( @@ -806,12 +822,19 @@ protected override void PropagateTransactionCookie(byte[] cookie) _parser._physicalStateObj); } + protected override void ReleaseAdditionalLocksForClose(bool lockToken) + { + if (lockToken) + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + } + } + #endregion #region Private Methods - - // /// /// Common code path for making one attempt to establish a connection and log in to server. /// From ed706b87ece4da0dd24fa672e86efb108c216c2a Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 17:55:04 -0600 Subject: [PATCH 36/56] Merge GetSessionAndReconnectIfNeeded (rebalance comments, add some newlines) --- .../SqlClient/SqlInternalConnectionTds.cs | 44 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 43 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 51 +++++++++++++++++++ 3 files changed, 51 insertions(+), 87 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8383985ff3..a4e77ca1b9 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,50 +344,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // PREPARED COMMAND METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - // called by SqlConnection.RepairConnection which is a relatively expensive way of repair inner connection - // prior to execution of request, used from EnlistTransaction, EnlistDistributedTransaction and ChangeDatabase - internal bool GetSessionAndReconnectIfNeeded(SqlConnection parent, int timeout = 0) - { - Debug.Assert(!ThreadHasParserLockForClose, "Cannot call this method if caller has parser lock"); - if (ThreadHasParserLockForClose) - { - return false; // we cannot restore if we cannot release lock - } - - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; // In case of error, let the connection know that we already own the parser lock - bool releaseConnectionLock = true; - - try - { - Task reconnectTask = parent.ValidateAndReconnect(() => - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - releaseConnectionLock = false; - }, timeout); - if (reconnectTask != null) - { - AsyncHelper.WaitForCompletion(reconnectTask, timeout); - return true; - } - return false; - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - finally - { - if (releaseConnectionLock) - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - } - } - //////////////////////////////////////////////////////////////////////////////////////// // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 7cf8599ae8..78ccf2f3f6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,49 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // PREPARED COMMAND METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - // called by SqlConnection.RepairConnection which is a relatively expensive way of repair inner connection - // prior to execution of request, used from EnlistTransaction, EnlistDistributedTransaction and ChangeDatabase - internal bool GetSessionAndReconnectIfNeeded(SqlConnection parent, int timeout = 0) - { - Debug.Assert(!ThreadHasParserLockForClose, "Cannot call this method if caller has parser lock"); - if (ThreadHasParserLockForClose) - { - return false; // we cannot restore if we cannot release lock - } - - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; // In case of error, let the connection know that we already own the parser lock - bool releaseConnectionLock = true; - - try - { - Task reconnectTask = parent.ValidateAndReconnect(() => - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - releaseConnectionLock = false; - }, timeout); - if (reconnectTask != null) - { - AsyncHelper.WaitForCompletion(reconnectTask, timeout); - return true; - } - return false; - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - finally - { - if (releaseConnectionLock) - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - } - } //////////////////////////////////////////////////////////////////////////////////////// // PARSER CALLBACKS 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 6f6223381e..35d3b5261b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -652,6 +652,57 @@ TransactionRequest.Rollback or ExecuteTransaction2005(transactionRequest, transactionName, iso, internalTransaction, isDelegateControlRequest); } + /// + /// Called by SqlConnection.RepairConnection which is a relatively expensive way of repair + /// inner connection prior to execution of request, used from EnlistTransaction, + /// EnlistDistributedTransaction and ChangeDatabase. + /// + internal bool GetSessionAndReconnectIfNeeded(SqlConnection parent, int timeout = 0) // @TODO: Return value is never used + { + Debug.Assert(!ThreadHasParserLockForClose, "Cannot call this method if caller has parser lock"); + + if (ThreadHasParserLockForClose) + { + // We cannot restore if we cannot release lock + return false; + } + + _parserLock.Wait(canReleaseFromAnyThread: false); + + // In case of error, let the connection know that we already own the parser lock + ThreadHasParserLockForClose = true; + bool releaseConnectionLock = true; + + try + { + Task reconnectTask = parent.ValidateAndReconnect( + () => + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + releaseConnectionLock = false; + }, + timeout); + + if (reconnectTask != null) + { + AsyncHelper.WaitForCompletion(reconnectTask, timeout); + return true; + } + + return false; + // @TODO: CER Exception Handling was removed here (see GH#3581) + } + finally + { + if (releaseConnectionLock) + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + } + } + } + internal void IncrementAsyncCount() { Interlocked.Increment(ref _asyncCommandCount); From 901b0f61efe2941989b4f70587f9982151750b87 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 17:57:09 -0600 Subject: [PATCH 37/56] Merge BreakConnection (split lines, conditional access, remove temp variable) --- .../Data/SqlClient/SqlInternalConnectionTds.cs | 11 ----------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 11 ----------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 11 +++++++++++ 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index a4e77ca1b9..9756086e81 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,17 +348,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - internal void BreakConnection() - { - SqlConnection connection = Connection; - SqlClientEventSource.Log.TryTraceEvent(" {0}, Breaking connection.", ObjectID); - DoomThisConnection(); // Mark connection as unusable, so it will be destroyed - if (connection != null) - { - connection.Close(); - } - } - internal void OnEnvChange(SqlEnvChange rec) { Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 78ccf2f3f6..bacd9adc56 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,17 +346,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - internal void BreakConnection() - { - SqlConnection connection = Connection; - SqlClientEventSource.Log.TryTraceEvent(" {0}, Breaking connection.", ObjectID); - DoomThisConnection(); // Mark connection as unusable, so it will be destroyed - if (connection != null) - { - connection.Close(); - } - } - internal void OnEnvChange(SqlEnvChange rec) { Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); 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 35d3b5261b..856bd9ca95 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -522,6 +522,17 @@ private int accessTokenExpirationBufferTime #region Public and Internal Methods + internal void BreakConnection() + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.BreakConnection | RES | CPOOL " + + $"Object ID {ObjectID}, " + + $"Breaking connection."); + + DoomThisConnection(); // Mark connection as unusable, so it will be destroyed + Connection?.Close(); + } + /// /// Validate the enlisted transaction state, taking into consideration the ambient /// transaction and transaction unbinding mode. If there is no enlisted transaction, this From faaf38407aec9a487fa971acf7df9bbcdd0c5e0e Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 18:04:59 -0600 Subject: [PATCH 38/56] Merge OnEnvChange (rebalance comments, conditional access, split long lines) --- .../SqlClient/SqlInternalConnectionTds.cs | 105 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 100 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 117 ++++++++++++++++++ 3 files changed, 117 insertions(+), 205 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 9756086e81..3d2e068700 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,111 +348,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - internal void OnEnvChange(SqlEnvChange rec) - { - Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); - switch (rec._type) - { - case TdsEnums.ENV_DATABASE: - // If connection is not open and recovery is not in progress, store the server value as the original. - if (!_fConnectionOpen && _recoverySessionData == null) - { - _originalDatabase = rec._newValue; - } - - CurrentDatabase = rec._newValue; - break; - - case TdsEnums.ENV_LANG: - // If connection is not open and recovery is not in progress, store the server value as the original. - if (!_fConnectionOpen && _recoverySessionData == null) - { - _originalLanguage = rec._newValue; - } - - _currentLanguage = rec._newValue; - break; - - case TdsEnums.ENV_PACKETSIZE: - _currentPacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); - break; - - case TdsEnums.ENV_COLLATION: - if (_currentSessionData != null) - { - _currentSessionData._collation = rec._newCollation; - } - break; - - case TdsEnums.ENV_CHARSET: - case TdsEnums.ENV_LOCALEID: - case TdsEnums.ENV_COMPFLAGS: - case TdsEnums.ENV_BEGINTRAN: - case TdsEnums.ENV_COMMITTRAN: - case TdsEnums.ENV_ROLLBACKTRAN: - case TdsEnums.ENV_ENLISTDTC: - case TdsEnums.ENV_DEFECTDTC: - // only used on parser - break; - - case TdsEnums.ENV_LOGSHIPNODE: - if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) - { - throw SQL.ROR_FailoverNotSupportedServer(this); - } - - ServerProvidedFailoverPartner = rec._newValue; - break; - - case TdsEnums.ENV_PROMOTETRANSACTION: - byte[] dtcToken; - if (rec._newBinRented) - { - dtcToken = new byte[rec._newLength]; - Buffer.BlockCopy(rec._newBinValue, 0, dtcToken, 0, dtcToken.Length); - } - else - { - dtcToken = rec._newBinValue; - rec._newBinValue = null; - } - PromotedDtcToken = dtcToken; - break; - - case TdsEnums.ENV_TRANSACTIONENDED: - break; - - case TdsEnums.ENV_TRANSACTIONMANAGERADDRESS: - // For now we skip these 2005 only env change notifications - break; - - case TdsEnums.ENV_SPRESETCONNECTIONACK: - // connection is being reset - if (_currentSessionData != null) - { - _currentSessionData.Reset(); - } - break; - - case TdsEnums.ENV_USERINSTANCE: - _instanceName = rec._newValue; - break; - - case TdsEnums.ENV_ROUTING: - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received routing info", ObjectID); - if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || rec._newRoutingInfo.Protocol != 0 || rec._newRoutingInfo.Port == 0) - { - throw SQL.ROR_InvalidRoutingInfo(this); - } - RoutingInfo = rec._newRoutingInfo; - break; - - default: - Debug.Fail("Missed token in EnvChange!"); - break; - } - } - internal void OnLoginAck(SqlLoginAck rec) { _loginAck = rec; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index bacd9adc56..ca8447fc16 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,106 +346,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - internal void OnEnvChange(SqlEnvChange rec) - { - Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); - switch (rec._type) - { - case TdsEnums.ENV_DATABASE: - // If connection is not open and recovery is not in progress, store the server value as the original. - if (!_fConnectionOpen && _recoverySessionData == null) - { - _originalDatabase = rec._newValue; - } - - CurrentDatabase = rec._newValue; - break; - - case TdsEnums.ENV_LANG: - // If connection is not open and recovery is not in progress, store the server value as the original. - if (!_fConnectionOpen && _recoverySessionData == null) - { - _originalLanguage = rec._newValue; - } - - _currentLanguage = rec._newValue; - break; - - case TdsEnums.ENV_PACKETSIZE: - _currentPacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); - break; - - case TdsEnums.ENV_COLLATION: - if (_currentSessionData != null) - { - _currentSessionData._collation = rec._newCollation; - } - break; - - case TdsEnums.ENV_CHARSET: - case TdsEnums.ENV_LOCALEID: - case TdsEnums.ENV_COMPFLAGS: - case TdsEnums.ENV_BEGINTRAN: - case TdsEnums.ENV_COMMITTRAN: - case TdsEnums.ENV_ROLLBACKTRAN: - case TdsEnums.ENV_ENLISTDTC: - case TdsEnums.ENV_DEFECTDTC: - // only used on parser - break; - - case TdsEnums.ENV_LOGSHIPNODE: - ServerProvidedFailoverPartner = rec._newValue; - break; - - case TdsEnums.ENV_PROMOTETRANSACTION: - byte[] dtcToken; - if (rec._newBinRented) - { - dtcToken = new byte[rec._newLength]; - Buffer.BlockCopy(rec._newBinValue, 0, dtcToken, 0, dtcToken.Length); - } - else - { - dtcToken = rec._newBinValue; - rec._newBinValue = null; - } - PromotedDtcToken = dtcToken; - break; - - case TdsEnums.ENV_TRANSACTIONENDED: - break; - - case TdsEnums.ENV_TRANSACTIONMANAGERADDRESS: - // For now we skip these 2005 only env change notifications - break; - - case TdsEnums.ENV_SPRESETCONNECTIONACK: - // connection is being reset - if (_currentSessionData != null) - { - _currentSessionData.Reset(); - } - break; - - case TdsEnums.ENV_USERINSTANCE: - _instanceName = rec._newValue; - break; - - case TdsEnums.ENV_ROUTING: - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received routing info", ObjectID); - if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || rec._newRoutingInfo.Protocol != 0 || rec._newRoutingInfo.Port == 0) - { - throw SQL.ROR_InvalidRoutingInfo(this); - } - RoutingInfo = rec._newRoutingInfo; - break; - - default: - Debug.Fail("Missed token in EnvChange!"); - break; - } - } - internal void OnLoginAck(SqlLoginAck rec) { _loginAck = rec; 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 856bd9ca95..e432a5bcfc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Security; using System.Threading; using System.Threading.Tasks; @@ -722,6 +723,122 @@ internal void IncrementAsyncCount() internal override bool IsConnectionAlive(bool throwOnException) => _parser._physicalStateObj.IsConnectionAlive(throwOnException); + // @TODO: It seems fishy to have an entire environment change processor in here. Maybe either have specialized callbacks for specific scenarios, or maybe have the connection register with the parser what env changes it can handle? Idk, just seems a bit weird to do low-level stuff up here. + internal void OnEnvChange(SqlEnvChange rec) + { + Debug.Assert(!IgnoreEnvChange, "This function should not be called if IgnoreEnvChange is set!"); + + switch (rec._type) + { + case TdsEnums.ENV_DATABASE: + // If connection is not open and recovery is not in progress, store the server + // value as the original. + if (!_fConnectionOpen && _recoverySessionData == null) + { + _originalDatabase = rec._newValue; + } + + CurrentDatabase = rec._newValue; + break; + + case TdsEnums.ENV_LANG: + // If connection is not open and recovery is not in progress, store the server + // value as the original. + if (!_fConnectionOpen && _recoverySessionData == null) + { + _originalLanguage = rec._newValue; + } + + _currentLanguage = rec._newValue; + break; + + case TdsEnums.ENV_PACKETSIZE: + _currentPacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); + break; + + case TdsEnums.ENV_COLLATION: + if (_currentSessionData != null) + { + _currentSessionData._collation = rec._newCollation; + } + break; + + case TdsEnums.ENV_CHARSET: + case TdsEnums.ENV_LOCALEID: + case TdsEnums.ENV_COMPFLAGS: + case TdsEnums.ENV_BEGINTRAN: + case TdsEnums.ENV_COMMITTRAN: + case TdsEnums.ENV_ROLLBACKTRAN: + case TdsEnums.ENV_ENLISTDTC: + case TdsEnums.ENV_DEFECTDTC: + // Only used on parser + // @TODO: Well ... why do they have cases here? Why are they in the middle of everything? Why aren't other skipped cases handled here?? + break; + + case TdsEnums.ENV_LOGSHIPNODE: + #if NET + if (ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly) + { + throw SQL.ROR_FailoverNotSupportedServer(this); + } + #endif + + ServerProvidedFailoverPartner = rec._newValue; + break; + + case TdsEnums.ENV_PROMOTETRANSACTION: + byte[] dtcToken; + if (rec._newBinRented) + { + dtcToken = new byte[rec._newLength]; + Buffer.BlockCopy(rec._newBinValue, 0, dtcToken, 0, dtcToken.Length); + } + else + { + dtcToken = rec._newBinValue; + rec._newBinValue = null; + } + PromotedDtcToken = dtcToken; + break; + + case TdsEnums.ENV_TRANSACTIONENDED: + break; + + case TdsEnums.ENV_TRANSACTIONMANAGERADDRESS: + // For now, we skip these 2005 only env change notifications + break; + + case TdsEnums.ENV_SPRESETCONNECTIONACK: + // Connection is being reset + _currentSessionData?.Reset(); + break; + + case TdsEnums.ENV_USERINSTANCE: + _instanceName = rec._newValue; + break; + + case TdsEnums.ENV_ROUTING: + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnEnvChange | ADV | " + + $"Object ID {ObjectID}, " + + $"Received routing info"); + + if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || + rec._newRoutingInfo.Protocol != 0 || + rec._newRoutingInfo.Port == 0) + { + throw SQL.ROR_InvalidRoutingInfo(this); + } + + RoutingInfo = rec._newRoutingInfo; + break; + + default: + Debug.Fail("Missed token in EnvChange!"); + break; + } + } + internal override void ValidateConnectionForExecute(SqlCommand command) { TdsParser parser = _parser; From a948fb3b89a9bb915417aead9880201ed70aaffd Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 18:06:51 -0600 Subject: [PATCH 39/56] Merge OnLoginAck --- .../Data/SqlClient/SqlInternalConnectionTds.cs | 16 ---------------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 16 ---------------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 17 +++++++++++++++++ 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 3d2e068700..3219639c4e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,22 +348,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - internal void OnLoginAck(SqlLoginAck rec) - { - _loginAck = rec; - if (_recoverySessionData != null) - { - if (_recoverySessionData._tdsVersion != rec.tdsVersion) - { - throw SQL.CR_TDSVersionNotPreserved(this); - } - } - if (_currentSessionData != null) - { - _currentSessionData._tdsVersion = rec.tdsVersion; - } - } - /// /// Generates (if appropriate) and sends a Federated Authentication Access token to the server, using the Federated Authentication Info. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index ca8447fc16..93a1214656 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,22 +346,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - internal void OnLoginAck(SqlLoginAck rec) - { - _loginAck = rec; - if (_recoverySessionData != null) - { - if (_recoverySessionData._tdsVersion != rec.tdsVersion) - { - throw SQL.CR_TDSVersionNotPreserved(this); - } - } - if (_currentSessionData != null) - { - _currentSessionData._tdsVersion = rec.tdsVersion; - } - } - /// /// Generates (if appropriate) and sends a Federated Authentication Access token to the server, using the Federated Authentication Info. /// 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 e432a5bcfc..8b34b70bb7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -839,6 +839,23 @@ internal void OnEnvChange(SqlEnvChange rec) } } + internal void OnLoginAck(SqlLoginAck rec) + { + _loginAck = rec; + if (_recoverySessionData != null) + { + if (_recoverySessionData._tdsVersion != rec.tdsVersion) + { + throw SQL.CR_TDSVersionNotPreserved(this); + } + } + + if (_currentSessionData != null) + { + _currentSessionData._tdsVersion = rec.tdsVersion; + } + } + internal override void ValidateConnectionForExecute(SqlCommand command) { TdsParser parser = _parser; From 3d3744e454e838edb3b424da2577fcd09143d62b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 18:22:18 -0600 Subject: [PATCH 40/56] Merge OnFedAuthInfo (split long lines, rebalance comments, remove if around trace and replace with Try* version) --- .../SqlClient/SqlInternalConnectionTds.cs | 134 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 134 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 175 ++++++++++++++++++ 3 files changed, 175 insertions(+), 268 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 3219639c4e..0634d62305 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,140 +348,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Generates (if appropriate) and sends a Federated Authentication Access token to the server, using the Federated Authentication Info. - /// - /// Federated Authentication Info. - internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) - { - Debug.Assert((ConnectionOptions._hasUserIdKeyword && ConnectionOptions._hasPasswordKeyword) - || _credential != null - || _accessTokenCallback != null - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity - || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), - "Credentials aren't provided for calling MSAL"); - Debug.Assert(fedAuthInfo != null, "info should not be null."); - Debug.Assert(_dbConnectionPoolAuthenticationContextKey == null, "_dbConnectionPoolAuthenticationContextKey should be null."); - SqlClientEventSource.Log.TryTraceEvent(" {0}, Generating federated authentication token", ObjectID); - DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext = null; - - // We want to refresh the token without taking a lock on the context, allowed when the access token is expiring within the next 10 mins. - bool attemptRefreshTokenUnLocked = false; - - // We want to refresh the token, if taking the lock on the authentication context is successful. - bool attemptRefreshTokenLocked = false; - - if (_dbConnectionPool != null) - { - Debug.Assert(_dbConnectionPool.AuthenticationContexts != null); - - // Construct the dbAuthenticationContextKey with information from FedAuthInfo and store for later use, when inserting in to the token cache. - _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.stsurl, fedAuthInfo.spn); - - // Try to retrieve the authentication context from the pool, if one does exist for this key. - if (_dbConnectionPool.AuthenticationContexts.TryGetValue(_dbConnectionPoolAuthenticationContextKey, out dbConnectionPoolAuthenticationContext)) - { - Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null."); - - // The timespan between UTCNow and the token expiry. - TimeSpan contextValidity = dbConnectionPoolAuthenticationContext.ExpirationTime.Subtract(DateTime.UtcNow); - - // If the authentication context is expiring within next 10 minutes, lets just re-create a token for this connection attempt. - // And on successful login, try to update the cache with the new token. - if (contextValidity <= _dbAuthenticationContextUnLockedRefreshTimeSpan) - { - if (SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TraceEvent(" {0}, " + - "The expiration time is less than 10 mins, so trying to get new access token regardless of if an other thread is also trying to update it." + - "The expiration time is {1}. Current Time is {2}.", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString()); - } - attemptRefreshTokenUnLocked = true; - } - -#if DEBUG - // Checking if any failpoints are enabled. - else if (_forceExpiryUnLocked) - { - attemptRefreshTokenUnLocked = true; - } - else if (_forceExpiryLocked) - { - attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out _fedAuthToken); - } -#endif - - // If the token is expiring within the next 45 mins, try to fetch a new token, if there is no thread already doing it. - // If a thread is already doing the refresh, just use the existing token in the cache and proceed. - else if (contextValidity <= _dbAuthenticationContextLockedRefreshTimeSpan) - { - if (SqlClientEventSource.Log.IsAdvancedTraceOn()) - { - SqlClientEventSource.Log.AdvancedTraceEvent(" {0}, " + - "The authentication context needs a refresh.The expiration time is {1}. " + - "Current Time is {2}.", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString()); - } - - // Call the function which tries to acquire a lock over the authentication context before trying to update. - // If the lock could not be obtained, it will return false, without attempting to fetch a new token. - attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out _fedAuthToken); - - // If TryGetFedAuthTokenLocked returns true, it means lock was obtained and _fedAuthToken should not be null. - // If there was an exception in retrieving the new token, TryGetFedAuthTokenLocked should have thrown, so we won't be here. - Debug.Assert(!attemptRefreshTokenLocked || _fedAuthToken != null, "Either Lock should not have been obtained or _fedAuthToken should not be null."); - Debug.Assert(!attemptRefreshTokenLocked || _newDbConnectionPoolAuthenticationContext != null, "Either Lock should not have been obtained or _newDbConnectionPoolAuthenticationContext should not be null."); - - // Indicate in EventSource Trace that we are successful with the update. - if (attemptRefreshTokenLocked) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, The attempt to get a new access token succeeded under the locked mode.", ObjectID); - } - } - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Found an authentication context in the cache that does not need a refresh at this time. Re-using the cached token.", ObjectID); - } - } - - // dbConnectionPoolAuthenticationContext will be null if either this is the first connection attempt in the pool or pooling is disabled. - if (dbConnectionPoolAuthenticationContext == null || attemptRefreshTokenUnLocked) - { - // Get the Federated Authentication Token. - _fedAuthToken = GetFedAuthToken(fedAuthInfo); - Debug.Assert(_fedAuthToken != null, "_fedAuthToken should not be null."); - - if (_dbConnectionPool != null) - { - // GetFedAuthToken should have updated _newDbConnectionPoolAuthenticationContext. - Debug.Assert(_newDbConnectionPoolAuthenticationContext != null, "_newDbConnectionPoolAuthenticationContext should not be null."); - - if (_newDbConnectionPoolAuthenticationContext != null) - { - _dbConnectionPool.AuthenticationContexts.TryAdd(_dbConnectionPoolAuthenticationContextKey, _newDbConnectionPoolAuthenticationContext); - } - } - } - else if (!attemptRefreshTokenLocked) - { - Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null."); - Debug.Assert(_fedAuthToken == null, "_fedAuthToken should be null in this case."); - Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, "_newDbConnectionPoolAuthenticationContext should be null."); - - _fedAuthToken = new SqlFedAuthToken(); - - // If the code flow is here, then we are re-using the context from the cache for this connection attempt and not - // generating a new access token on this thread. - _fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken; - _fedAuthToken.expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime(); - } - - Debug.Assert(_fedAuthToken != null && _fedAuthToken.accessToken != null, "_fedAuthToken and _fedAuthToken.accessToken cannot be null."); - _parser.SendFedAuthToken(_fedAuthToken); - } - /// /// Tries to acquire a lock on the authentication context. If successful in acquiring the lock, gets a new token and assigns it in the out parameter. Else returns false. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 93a1214656..1a1742eb81 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,140 +346,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Generates (if appropriate) and sends a Federated Authentication Access token to the server, using the Federated Authentication Info. - /// - /// Federated Authentication Info. - internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) - { - Debug.Assert((ConnectionOptions._hasUserIdKeyword && ConnectionOptions._hasPasswordKeyword) - || _credential != null - || _accessTokenCallback != null - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault - || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity - || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), - "Credentials aren't provided for calling MSAL"); - Debug.Assert(fedAuthInfo != null, "info should not be null."); - Debug.Assert(_dbConnectionPoolAuthenticationContextKey == null, "_dbConnectionPoolAuthenticationContextKey should be null."); - SqlClientEventSource.Log.TryTraceEvent(" {0}, Generating federated authentication token", ObjectID); - DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext = null; - - // We want to refresh the token without taking a lock on the context, allowed when the access token is expiring within the next 10 mins. - bool attemptRefreshTokenUnLocked = false; - - // We want to refresh the token, if taking the lock on the authentication context is successful. - bool attemptRefreshTokenLocked = false; - - if (_dbConnectionPool != null) - { - Debug.Assert(_dbConnectionPool.AuthenticationContexts != null); - - // Construct the dbAuthenticationContextKey with information from FedAuthInfo and store for later use, when inserting in to the token cache. - _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.stsurl, fedAuthInfo.spn); - - // Try to retrieve the authentication context from the pool, if one does exist for this key. - if (_dbConnectionPool.AuthenticationContexts.TryGetValue(_dbConnectionPoolAuthenticationContextKey, out dbConnectionPoolAuthenticationContext)) - { - Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null."); - - // The timespan between UTCNow and the token expiry. - TimeSpan contextValidity = dbConnectionPoolAuthenticationContext.ExpirationTime.Subtract(DateTime.UtcNow); - - // If the authentication context is expiring within next 10 minutes, lets just re-create a token for this connection attempt. - // And on successful login, try to update the cache with the new token. - if (contextValidity <= _dbAuthenticationContextUnLockedRefreshTimeSpan) - { - if (SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TraceEvent(" {0}, " + - "The expiration time is less than 10 mins, so trying to get new access token regardless of if an other thread is also trying to update it." + - "The expiration time is {1}. Current Time is {2}.", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString()); - } - attemptRefreshTokenUnLocked = true; - } - -#if DEBUG - // Checking if any failpoints are enabled. - else if (_forceExpiryUnLocked) - { - attemptRefreshTokenUnLocked = true; - } - else if (_forceExpiryLocked) - { - attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out _fedAuthToken); - } -#endif - - // If the token is expiring within the next 45 mins, try to fetch a new token, if there is no thread already doing it. - // If a thread is already doing the refresh, just use the existing token in the cache and proceed. - else if (contextValidity <= _dbAuthenticationContextLockedRefreshTimeSpan) - { - if (SqlClientEventSource.Log.IsAdvancedTraceOn()) - { - SqlClientEventSource.Log.AdvancedTraceEvent(" {0}, " + - "The authentication context needs a refresh.The expiration time is {1}. " + - "Current Time is {2}.", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString()); - } - - // Call the function which tries to acquire a lock over the authentication context before trying to update. - // If the lock could not be obtained, it will return false, without attempting to fetch a new token. - attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out _fedAuthToken); - - // If TryGetFedAuthTokenLocked returns true, it means lock was obtained and _fedAuthToken should not be null. - // If there was an exception in retrieving the new token, TryGetFedAuthTokenLocked should have thrown, so we won't be here. - Debug.Assert(!attemptRefreshTokenLocked || _fedAuthToken != null, "Either Lock should not have been obtained or _fedAuthToken should not be null."); - Debug.Assert(!attemptRefreshTokenLocked || _newDbConnectionPoolAuthenticationContext != null, "Either Lock should not have been obtained or _newDbConnectionPoolAuthenticationContext should not be null."); - - // Indicate in EventSource Trace that we are successful with the update. - if (attemptRefreshTokenLocked) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, The attempt to get a new access token succeeded under the locked mode.", ObjectID); - } - } - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Found an authentication context in the cache that does not need a refresh at this time. Re-using the cached token.", ObjectID); - } - } - - // dbConnectionPoolAuthenticationContext will be null if either this is the first connection attempt in the pool or pooling is disabled. - if (dbConnectionPoolAuthenticationContext == null || attemptRefreshTokenUnLocked) - { - // Get the Federated Authentication Token. - _fedAuthToken = GetFedAuthToken(fedAuthInfo); - Debug.Assert(_fedAuthToken != null, "_fedAuthToken should not be null."); - - if (_dbConnectionPool != null) - { - // GetFedAuthToken should have updated _newDbConnectionPoolAuthenticationContext. - Debug.Assert(_newDbConnectionPoolAuthenticationContext != null, "_newDbConnectionPoolAuthenticationContext should not be null."); - - if (_newDbConnectionPoolAuthenticationContext != null) - { - _dbConnectionPool.AuthenticationContexts.TryAdd(_dbConnectionPoolAuthenticationContextKey, _newDbConnectionPoolAuthenticationContext); - } - } - } - else if (!attemptRefreshTokenLocked) - { - Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null."); - Debug.Assert(_fedAuthToken == null, "_fedAuthToken should be null in this case."); - Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, "_newDbConnectionPoolAuthenticationContext should be null."); - - _fedAuthToken = new SqlFedAuthToken(); - - // If the code flow is here, then we are re-using the context from the cache for this connection attempt and not - // generating a new access token on this thread. - _fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken; - _fedAuthToken.expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime(); - } - - Debug.Assert(_fedAuthToken != null && _fedAuthToken.accessToken != null, "_fedAuthToken and _fedAuthToken.accessToken cannot be null."); - _parser.SendFedAuthToken(_fedAuthToken); - } - /// /// Tries to acquire a lock on the authentication context. If successful in acquiring the lock, gets a new token and assigns it in the out parameter. Else returns false. /// 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 8b34b70bb7..3482aed5d1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -839,6 +839,181 @@ internal void OnEnvChange(SqlEnvChange rec) } } + /// + /// Generates (if appropriate) and sends a Federated Authentication Access token to the + /// server, using the Federated Authentication Info. + /// + /// Federated Authentication Info. + internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) + { + // @TODO: Seriously, put this into a hash set or give it a helper or something! We're gonna forget one in *one* spot and cause a big ol bug someday. + Debug.Assert((ConnectionOptions._hasUserIdKeyword && ConnectionOptions._hasPasswordKeyword) + || _credential != null + || _accessTokenCallback != null + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity + || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), + "Credentials aren't provided for calling MSAL"); + Debug.Assert(fedAuthInfo != null, "info should not be null."); + Debug.Assert(_dbConnectionPoolAuthenticationContextKey == null, + "_dbConnectionPoolAuthenticationContextKey should be null."); + SqlClientEventSource.Log.TryTraceEvent(" {0}, Generating federated authentication token", ObjectID); + DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext = null; + + // We want to refresh the token without taking a lock on the context, allowed when the + // access token is expiring within the next 10 mins. + bool attemptRefreshTokenUnLocked = false; + + // We want to refresh the token, if taking the lock on the authentication context is + // successful. + bool attemptRefreshTokenLocked = false; + + if (_dbConnectionPool != null) + { + Debug.Assert(_dbConnectionPool.AuthenticationContexts != null); + + // Construct the dbAuthenticationContextKey with information from FedAuthInfo and + // store for later use, when inserting in to the token cache. + _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey( + fedAuthInfo.stsurl, + fedAuthInfo.spn); + + // Try to retrieve the authentication context from the pool, if one does exist for + // this key. + if (_dbConnectionPool.AuthenticationContexts.TryGetValue(_dbConnectionPoolAuthenticationContextKey, out dbConnectionPoolAuthenticationContext)) + { + Debug.Assert(dbConnectionPoolAuthenticationContext != null, + "dbConnectionPoolAuthenticationContext should not be null."); + + // The timespan between UTCNow and the token expiry. + TimeSpan contextValidity = + dbConnectionPoolAuthenticationContext.ExpirationTime.Subtract(DateTime.UtcNow); + + + if (contextValidity <= _dbAuthenticationContextUnLockedRefreshTimeSpan) + { + // If the authentication context is expiring within next 10 minutes, lets + // just re-create a token for this connection attempt. And on successful + // login, try to update the cache with the new token. + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFedAuthInfo | " + + $"Object ID {ObjectID}, " + + $"The expiration time is less than 10 mins, trying to get new access " + + $"token regardless of if an other thread is also trying to update it. " + + $"The expiration time is {dbConnectionPoolAuthenticationContext.ExpirationTime:T}. " + + $"Current Time is {DateTime.UtcNow:T}."); + attemptRefreshTokenUnLocked = true; + } + #if DEBUG + // Checking if any failpoints are enabled. + else if (_forceExpiryUnLocked) + { + attemptRefreshTokenUnLocked = true; + } + else if (_forceExpiryLocked) + { + attemptRefreshTokenLocked = TryGetFedAuthTokenLocked( + fedAuthInfo, + dbConnectionPoolAuthenticationContext, + out _fedAuthToken); + } + #endif + else if (contextValidity <= _dbAuthenticationContextLockedRefreshTimeSpan) + { + // If the token is expiring within the next 45 mins, try to fetch a new + // token, if there is no thread already doing it. If a thread is already + // doing the refresh, just use the existing token in the cache and proceed. + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFedAuthInfo | ADV | " + + $"Object ID {ObjectID}, " + + $"The authentication context needs a refresh. " + + $"The expiration time is {dbConnectionPoolAuthenticationContext.ExpirationTime:T}. " + + $"Current Time is {DateTime.UtcNow:T}."); + + // Call the function which tries to acquire a lock over the authentication + // context before trying to update. If the lock could not be obtained, it + // will return false, without attempting to fetch a new token. + attemptRefreshTokenLocked = TryGetFedAuthTokenLocked( + fedAuthInfo, + dbConnectionPoolAuthenticationContext, + out _fedAuthToken); + + // If TryGetFedAuthTokenLocked returns true, it means lock was obtained and + // _fedAuthToken should not be null. If there was an exception in + // retrieving the new token, TryGetFedAuthTokenLocked should have thrown, + // so we won't be here. + Debug.Assert(!attemptRefreshTokenLocked || _fedAuthToken != null, + "Either Lock should not have been obtained or _fedAuthToken should not be null."); + Debug.Assert(!attemptRefreshTokenLocked || _newDbConnectionPoolAuthenticationContext != null, + "Either Lock should not have been obtained or _newDbConnectionPoolAuthenticationContext should not be null."); + + // Indicate in EventSource Trace that we are successful with the update. + if (attemptRefreshTokenLocked) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFedAuthInfo | " + + $"Object ID {ObjectID}, " + + $"The attempt to get a new access token succeeded under the locked mode."); + } + } + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFedAuthInfo | " + + $"Object ID {ObjectID}, " + + $"Found an authentication context in the cache that does not need a refresh at this time. " + + $"Re-using the cached token."); + } + } + + // dbConnectionPoolAuthenticationContext will be null if either this is the first + // connection attempt in the pool or pooling is disabled. + if (dbConnectionPoolAuthenticationContext == null || attemptRefreshTokenUnLocked) + { + // Get the Federated Authentication Token. + _fedAuthToken = GetFedAuthToken(fedAuthInfo); + Debug.Assert(_fedAuthToken != null, "_fedAuthToken should not be null."); + + if (_dbConnectionPool != null) + { + // GetFedAuthToken should have updated _newDbConnectionPoolAuthenticationContext. + Debug.Assert(_newDbConnectionPoolAuthenticationContext != null, + "_newDbConnectionPoolAuthenticationContext should not be null."); + + if (_newDbConnectionPoolAuthenticationContext != null) + { + _dbConnectionPool.AuthenticationContexts.TryAdd( + _dbConnectionPoolAuthenticationContextKey, + _newDbConnectionPoolAuthenticationContext); + } + } + } + else if (!attemptRefreshTokenLocked) + { + Debug.Assert(dbConnectionPoolAuthenticationContext != null, + "dbConnectionPoolAuthenticationContext should not be null."); + Debug.Assert(_fedAuthToken == null, "_fedAuthToken should be null in this case."); + Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, + "_newDbConnectionPoolAuthenticationContext should be null."); + + // If the code flow is here, then we are re-using the context from the cache for + // this connection attempt and not generating a new access token on this thread. + _fedAuthToken = new SqlFedAuthToken + { + accessToken = dbConnectionPoolAuthenticationContext.AccessToken, + expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime() + }; + } + + Debug.Assert(_fedAuthToken?.accessToken != null, + "_fedAuthToken and _fedAuthToken.accessToken cannot be null."); + + _parser.SendFedAuthToken(_fedAuthToken); + } + internal void OnLoginAck(SqlLoginAck rec) { _loginAck = rec; From a2e5e426bb254812d2d81db34fc6c218ac9052eb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 18:28:52 -0600 Subject: [PATCH 41/56] Merge TryGetFedAuthTokenLocked (made private, remove IfTraceEnabled and use Try* version, rebalance comments, split long lines) --- .../SqlClient/SqlInternalConnectionTds.cs | 56 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 56 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 75 +++++++++++++++++++ 3 files changed, 75 insertions(+), 112 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0634d62305..edba7a7572 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,62 +348,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Tries to acquire a lock on the authentication context. If successful in acquiring the lock, gets a new token and assigns it in the out parameter. Else returns false. - /// - /// Federated Authentication Info - /// Authentication Context cached in the connection pool. - /// Out parameter, carrying the token if we acquired a lock and got the token. - /// - internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext, out SqlFedAuthToken fedAuthToken) - { - - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null."); - - fedAuthToken = null; - - // Variable which indicates if we did indeed manage to acquire the lock on the authentication context, to try update it. - bool authenticationContextLocked = false; - - try - { - // Try to obtain a lock on the context. If acquired, this thread got the opportunity to update. - // Else some other thread is already updating it, so just proceed forward with the existing token in the cache. - if (dbConnectionPoolAuthenticationContext.LockToUpdate()) - { - if (SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TraceEvent(" {0}, " + - "Acquired the lock to update the authentication context.The expiration time is {1}. " + - "Current Time is {2}.", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString()); - } - authenticationContextLocked = true; - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Refreshing the context is already in progress by another thread.", ObjectID); - } - - if (authenticationContextLocked) - { - // Get the Federated Authentication Token. - fedAuthToken = GetFedAuthToken(fedAuthInfo); - Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null."); - } - } - finally - { - if (authenticationContextLocked) - { - // Release the lock we took on the authentication context, even if we have not yet updated the cache with the new context. Login process can fail at several places after this step and so there is no guarantee that the new context will make it to the cache. So we shouldn't miss resetting the flag. With the reset, at-least another thread may have a chance to update it. - dbConnectionPoolAuthenticationContext.ReleaseLockToUpdate(); - } - } - - return authenticationContextLocked; - } - /// /// Get the Federated Authentication Token. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 1a1742eb81..2c94508698 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,62 +346,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Tries to acquire a lock on the authentication context. If successful in acquiring the lock, gets a new token and assigns it in the out parameter. Else returns false. - /// - /// Federated Authentication Info - /// Authentication Context cached in the connection pool. - /// Out parameter, carrying the token if we acquired a lock and got the token. - /// - internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext, out SqlFedAuthToken fedAuthToken) - { - - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null."); - - fedAuthToken = null; - - // Variable which indicates if we did indeed manage to acquire the lock on the authentication context, to try update it. - bool authenticationContextLocked = false; - - try - { - // Try to obtain a lock on the context. If acquired, this thread got the opportunity to update. - // Else some other thread is already updating it, so just proceed forward with the existing token in the cache. - if (dbConnectionPoolAuthenticationContext.LockToUpdate()) - { - if (SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TraceEvent(" {0}, " + - "Acquired the lock to update the authentication context.The expiration time is {1}. " + - "Current Time is {2}.", ObjectID, dbConnectionPoolAuthenticationContext.ExpirationTime.ToLongTimeString(), DateTime.UtcNow.ToLongTimeString()); - } - authenticationContextLocked = true; - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Refreshing the context is already in progress by another thread.", ObjectID); - } - - if (authenticationContextLocked) - { - // Get the Federated Authentication Token. - fedAuthToken = GetFedAuthToken(fedAuthInfo); - Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null."); - } - } - finally - { - if (authenticationContextLocked) - { - // Release the lock we took on the authentication context, even if we have not yet updated the cache with the new context. Login process can fail at several places after this step and so there is no guarantee that the new context will make it to the cache. So we shouldn't miss resetting the flag. With the reset, at-least another thread may have a chance to update it. - dbConnectionPoolAuthenticationContext.ReleaseLockToUpdate(); - } - } - - return authenticationContextLocked; - } - /// /// Get the Federated Authentication Token. /// 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 3482aed5d1..140a408688 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2544,6 +2544,81 @@ private bool ShouldDisableTnir(SqlConnectionString connectionOptions) } #endif + /// + /// Tries to acquire a lock on the authentication context. If successful in acquiring the + /// lock, gets a new token and assigns it in the out parameter. Else returns false. + /// + /// Federated Authentication Info + /// + /// Authentication Context cached in the connection pool. + /// + /// + /// Out parameter, carrying the token if we acquired a lock and got the token. + /// + private bool TryGetFedAuthTokenLocked( + SqlFedAuthInfo fedAuthInfo, + DbConnectionPoolAuthenticationContext dbConnectionPoolAuthenticationContext, + out SqlFedAuthToken fedAuthToken) + { + + Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); + Debug.Assert(dbConnectionPoolAuthenticationContext != null, + "dbConnectionPoolAuthenticationContext should not be null."); + + fedAuthToken = null; + + // Variable which indicates if we did indeed manage to acquire the lock on the + // authentication context, to try update it. + bool authenticationContextLocked = false; + + try + { + // Try to obtain a lock on the context. If acquired, this thread got the + // opportunity to update. Else some other thread is already updating it, so just + // proceed forward with the existing token in the cache. + if (dbConnectionPoolAuthenticationContext.LockToUpdate()) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.TryGetFedAuthTokenLocked | " + + $"Object ID {ObjectID}, " + + $"Acquired the lock to update the authentication context. " + + $"The expiration time is {dbConnectionPoolAuthenticationContext.ExpirationTime:T}. " + + $"Current Time is {DateTime.UtcNow:T}."); + + authenticationContextLocked = true; + } + else + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.TryGetFedAuthTokenLocked | " + + $"Object ID {ObjectID}, " + + $"Refreshing the context is already in progress by another thread."); + } + + if (authenticationContextLocked) + { + // Get the Federated Authentication Token. + fedAuthToken = GetFedAuthToken(fedAuthInfo); + + Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null."); + } + } + finally + { + if (authenticationContextLocked) + { + // Release the lock we took on the authentication context, even if we have not + // yet updated the cache with the new context. Login process can fail at + // several places after this step and so there is no guarantee that the new + // context will make it to the cache. So we shouldn't miss resetting the flag. + // With the reset, at-least another thread may have a chance to update it. + dbConnectionPoolAuthenticationContext.ReleaseLockToUpdate(); + } + } + + return authenticationContextLocked; + } + #endregion } } From 40edcfb3efeb9bf558939b4f3c4943036f680da6 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 18:56:12 -0600 Subject: [PATCH 42/56] Introduce SqlException overload for single error scenarios (ie, the majority of them...) (and split long lines) --- .../Microsoft/Data/SqlClient/SqlException.cs | 70 ++++++++++++++++--- 1 file changed, 59 insertions(+), 11 deletions(-) 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 80c810e748..e070bb7d2e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs @@ -166,20 +166,57 @@ public override string ToString() // NOTE: do not combine the overloads below using an optional parameter - // they must remain ditinct because external projects use private reflection + // they must remain distinct because external projects use private reflection // to find and invoke the functions, changing the signatures will break many // things elsewhere + // @TODO: Don't be ridiculous, no external project should take a dependency on internal/private methods. Otherwise they would've been made public APIs. - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion) - => CreateException(errorCollection, serverVersion, Guid.Empty, innerException: null, batchCommand: null); + internal static SqlException CreateException( + SqlError error, + string serverVersion, + SqlInternalConnectionTds internalConnection, + Exception innerException = null) + { + SqlErrorCollection errorCollection = new() { error }; + return CreateException(errorCollection, serverVersion, internalConnection, innerException); + } - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, SqlBatchCommand batchCommand) - => CreateException(errorCollection, serverVersion, Guid.Empty, innerException: null, batchCommand: batchCommand); + internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion) => + CreateException(errorCollection, serverVersion, Guid.Empty, innerException: null, batchCommand: null); - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, SqlInternalConnectionTds internalConnection, Exception innerException = null) - => CreateException(errorCollection, serverVersion, internalConnection, innerException: innerException, batchCommand: null); + internal static SqlException CreateException( + SqlErrorCollection errorCollection, + string serverVersion, + SqlBatchCommand batchCommand) + { + return CreateException( + errorCollection, + serverVersion, + Guid.Empty, + innerException: null, + batchCommand: batchCommand); + } - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, SqlInternalConnectionTds internalConnection, Exception innerException = null, SqlBatchCommand batchCommand = null) + internal static SqlException CreateException( + SqlErrorCollection errorCollection, + string serverVersion, + SqlInternalConnectionTds internalConnection, + Exception innerException = null) + { + return CreateException( + errorCollection, + serverVersion, + internalConnection, + innerException: innerException, + batchCommand: null); + } + + internal static SqlException CreateException( + SqlErrorCollection errorCollection, + string serverVersion, + SqlInternalConnectionTds internalConnection, + Exception innerException = null, + SqlBatchCommand batchCommand = null) { Guid connectionId = (internalConnection == null) ? Guid.Empty : internalConnection._clientConnectionId; SqlException exception = CreateException(errorCollection, serverVersion, connectionId, innerException, batchCommand); @@ -200,10 +237,21 @@ internal static SqlException CreateException(SqlErrorCollection errorCollection, return exception; } - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, Guid conId, Exception innerException = null) - => CreateException(errorCollection, serverVersion, conId, innerException, batchCommand: null); + internal static SqlException CreateException( + SqlErrorCollection errorCollection, + string serverVersion, + Guid conId, + Exception innerException = null) + { + return CreateException(errorCollection, serverVersion, conId, innerException, batchCommand: null); + } - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, Guid conId, Exception innerException = null, SqlBatchCommand batchCommand = null) + internal static SqlException CreateException( + SqlErrorCollection errorCollection, + string serverVersion, + Guid conId, + Exception innerException = null, + SqlBatchCommand batchCommand = null) { Debug.Assert(errorCollection != null && errorCollection.Count > 0, "no errorCollection?"); From 80053e1a12114c926ede0e2036e4c981d323a6dd Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 18:57:46 -0600 Subject: [PATCH 43/56] Merge GetFedAuthToken (rebalance comments, split long lines, etc) --- .../SqlClient/SqlInternalConnectionTds.cs | 239 ------------- .../SqlClient/SqlInternalConnectionTds.cs | 228 ------------- .../SqlClient/SqlInternalConnectionTds.cs | 313 ++++++++++++++++++ 3 files changed, 313 insertions(+), 467 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index edba7a7572..895ad2a679 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -348,245 +348,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Get the Federated Authentication Token. - /// - /// Information obtained from server as Federated Authentication Info. - /// SqlFedAuthToken - internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) - { - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - - // No:of milliseconds to sleep for the inital back off. - int sleepInterval = 100; - - // No:of attempts, for tracing purposes, if we underwent retries. - int numberOfAttempts = 0; - - // Object that will be returned to the caller, containing all required data about the token. - _fedAuthToken = new SqlFedAuthToken(); - - // Username to use in error messages. - string username = null; - - SqlAuthenticationProvider authProvider = SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); - if (authProvider == null && _accessTokenCallback == null) - { - throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication.ToString()); - } - - // retry getting access token once if MsalException.error_code is unknown_error. - // extra logic to deal with HTTP 429 (Retry after). - while (numberOfAttempts <= 1) - { - numberOfAttempts++; - try - { - var authParamsBuilder = new SqlAuthenticationParameters.Builder( - authenticationMethod: ConnectionOptions.Authentication, - resource: fedAuthInfo.spn, - authority: fedAuthInfo.stsurl, - serverName: ConnectionOptions.DataSource, - databaseName: ConnectionOptions.InitialCatalog) - .WithConnectionId(_clientConnectionId) - .WithConnectionTimeout(ConnectionOptions.ConnectTimeout); - switch (ConnectionOptions.Authentication) - { - case SqlAuthenticationMethod.ActiveDirectoryIntegrated: - // In some scenarios for .NET Core, MSAL cannot detect the current user and needs it passed in - // for Integrated auth. Allow the user/application to pass it in to work around those scenarios. - if (!string.IsNullOrEmpty(ConnectionOptions.UserID)) - { - username = ConnectionOptions.UserID; - authParamsBuilder.WithUserId(username); - } - else - { - username = TdsEnums.NTAUTHORITYANONYMOUSLOGON; - } - - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - // We use Task.Run here in all places to execute task synchronously in the same context. - // Fixes block-over-async deadlock possibilities https://github.com/dotnet/SqlClient/issues/1209 - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - case SqlAuthenticationMethod.ActiveDirectoryInteractive: - case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: - case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: - case SqlAuthenticationMethod.ActiveDirectoryMSI: - case SqlAuthenticationMethod.ActiveDirectoryDefault: - case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - authParamsBuilder.WithUserId(ConnectionOptions.UserID); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - #pragma warning disable 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryPassword: - #pragma warning restore 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - if (_credential != null) - { - username = _credential.UserId; - authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - } - else - { - username = ConnectionOptions.UserID; - authParamsBuilder.WithUserId(username).WithPassword(ConnectionOptions.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - } - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - default: - if (_accessTokenCallback == null) - { - throw SQL.UnsupportedAuthenticationSpecified(ConnectionOptions.Authentication); - } - - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - if (_credential != null) - { - username = _credential.UserId; - authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - } - else - { - authParamsBuilder.WithUserId(ConnectionOptions.UserID); - authParamsBuilder.WithPassword(ConnectionOptions.Password); - } - SqlAuthenticationParameters parameters = authParamsBuilder; - CancellationTokenSource cts = new(); - // Use Connection timeout value to cancel token acquire request after certain period of time.(int) - if (_timeout.MillisecondsRemaining < Int32.MaxValue) - { - cts.CancelAfter((int)_timeout.MillisecondsRemaining); - } - _fedAuthToken = Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - } - - Debug.Assert(_fedAuthToken.accessToken != null, "AccessToken should not be null."); -#if DEBUG - if (_forceMsalRetry) - { - // 3399614468 is 0xCAA20004L just for testing. - throw new MsalServiceException(MsalError.UnknownError, "Force retry in GetFedAuthToken"); - } -#endif - // Break out of the retry loop in successful case. - break; - } - // Deal with Msal service exceptions first, retry if 429 received. - catch (MsalServiceException serviceException) - { - if (serviceException.StatusCode == MsalHttpRetryStatusCode) - { - RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter; - if (retryAfter.Delta.HasValue) - { - sleepInterval = retryAfter.Delta.Value.Milliseconds; - } - else if (retryAfter.Date.HasValue) - { - sleepInterval = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); - } - - // if there's enough time to retry before timeout, then retry, otherwise break out the retry loop. - if (sleepInterval < _timeout.MillisecondsRemaining) - { - Thread.Sleep(sleepInterval); - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" Timeout: {0}", serviceException.ErrorCode); - throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), serviceException.ErrorCode, serviceException); - } - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", serviceException.ErrorCode); - throw ADP.CreateSqlException(serviceException, ConnectionOptions, this, username); - } - } - // Deal with normal MsalExceptions. - catch (MsalException msalException) - { - if (MsalError.UnknownError != msalException.ErrorCode || _timeout.IsExpired || _timeout.MillisecondsRemaining <= sleepInterval) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", msalException.ErrorCode); - - throw ADP.CreateSqlException(msalException, ConnectionOptions, this, username); - } - - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[Milliseconds]", ObjectID, sleepInterval); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, remaining {1}[Milliseconds]", ObjectID, _timeout.MillisecondsRemaining); - - Thread.Sleep(sleepInterval); - sleepInterval *= 2; - } - // All other exceptions from MSAL/Azure Identity APIs - catch (Exception e) - { - throw SqlException.CreateException( - new() - { - new( - 0, - (byte)0x00, - (byte)TdsEnums.FATAL_ERROR_CLASS, - ConnectionOptions.DataSource, - e.Message, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, - 0) - }, - "", - this, - e); - } - } - - Debug.Assert(_fedAuthToken != null, "fedAuthToken should not be null."); - Debug.Assert(_fedAuthToken.accessToken != null && _fedAuthToken.accessToken.Length > 0, "fedAuthToken.accessToken should not be null or empty."); - - // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling. - if (_dbConnectionPool != null) - { - DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime); - _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.accessToken, expirationTime); - } - SqlClientEventSource.Log.TryTraceEvent(" {0}, Finished generating federated authentication token.", ObjectID); - return _fedAuthToken; - } - internal void OnFeatureExtAck(int featureId, byte[] data) { if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 2c94508698..bf9f8b0e57 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -346,234 +346,6 @@ internal SqlInternalConnectionTds( // PARSER CALLBACKS //////////////////////////////////////////////////////////////////////////////////////// - /// - /// Get the Federated Authentication Token. - /// - /// Information obtained from server as Federated Authentication Info. - /// SqlFedAuthToken - internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) - { - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - - // No:of milliseconds to sleep for the inital back off. - int sleepInterval = 100; - - // No:of attempts, for tracing purposes, if we underwent retries. - int numberOfAttempts = 0; - - // Object that will be returned to the caller, containing all required data about the token. - _fedAuthToken = new SqlFedAuthToken(); - - // Username to use in error messages. - string username = null; - - SqlAuthenticationProvider authProvider = SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); - if (authProvider == null && _accessTokenCallback == null) - { - throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication.ToString()); - } - - // retry getting access token once if MsalException.error_code is unknown_error. - // extra logic to deal with HTTP 429 (Retry after). - while (numberOfAttempts <= 1 && sleepInterval <= _timeout.MillisecondsRemaining) - { - numberOfAttempts++; - try - { - var authParamsBuilder = new SqlAuthenticationParameters.Builder( - authenticationMethod: ConnectionOptions.Authentication, - resource: fedAuthInfo.spn, - authority: fedAuthInfo.stsurl, - serverName: ConnectionOptions.DataSource, - databaseName: ConnectionOptions.InitialCatalog) - .WithConnectionId(_clientConnectionId) - .WithConnectionTimeout(ConnectionOptions.ConnectTimeout); - switch (ConnectionOptions.Authentication) - { - case SqlAuthenticationMethod.ActiveDirectoryIntegrated: - username = TdsEnums.NTAUTHORITYANONYMOUSLOGON; - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - // We use Task.Run here in all places to execute task synchronously in the same context. - // Fixes block-over-async deadlock possibilities https://github.com/dotnet/SqlClient/issues/1209 - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - case SqlAuthenticationMethod.ActiveDirectoryInteractive: - case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: - case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: - case SqlAuthenticationMethod.ActiveDirectoryMSI: - case SqlAuthenticationMethod.ActiveDirectoryDefault: - case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - authParamsBuilder.WithUserId(ConnectionOptions.UserID); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - #pragma warning disable 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryPassword: - #pragma warning restore 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - if (_credential != null) - { - username = _credential.UserId; - authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - } - else - { - username = ConnectionOptions.UserID; - authParamsBuilder.WithUserId(username).WithPassword(ConnectionOptions.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - } - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - default: - if (_accessTokenCallback == null) - { - throw SQL.UnsupportedAuthenticationSpecified(ConnectionOptions.Authentication); - } - - if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) - { - _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; - } - else - { - if (_credential != null) - { - username = _credential.UserId; - authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - } - else - { - authParamsBuilder.WithUserId(ConnectionOptions.UserID); - authParamsBuilder.WithPassword(ConnectionOptions.Password); - } - SqlAuthenticationParameters parameters = authParamsBuilder; - CancellationTokenSource cts = new(); - // Use Connection timeout value to cancel token acquire request after certain period of time.(int) - if (_timeout.MillisecondsRemaining < Int32.MaxValue) - { - cts.CancelAfter((int)_timeout.MillisecondsRemaining); - } - _fedAuthToken = Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult().ToSqlFedAuthToken(); - _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; - } - break; - } - - Debug.Assert(_fedAuthToken.accessToken != null, "AccessToken should not be null."); -#if DEBUG - if (_forceMsalRetry) - { - // 3399614468 is 0xCAA20004L just for testing. - throw new MsalServiceException(MsalError.UnknownError, "Force retry in GetFedAuthToken"); - } -#endif - // Break out of the retry loop in successful case. - break; - } - // Deal with Msal service exceptions first, retry if 429 received. - catch (MsalServiceException serviceException) - { - if (serviceException.StatusCode == MsalHttpRetryStatusCode) - { - RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter; - if (retryAfter.Delta.HasValue) - { - sleepInterval = retryAfter.Delta.Value.Milliseconds; - } - else if (retryAfter.Date.HasValue) - { - sleepInterval = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); - } - - // if there's enough time to retry before timeout, then retry, otherwise break out the retry loop. - if (sleepInterval < _timeout.MillisecondsRemaining) - { - Thread.Sleep(sleepInterval); - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" Timeout: {0}", serviceException.ErrorCode); - throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), serviceException.ErrorCode, serviceException); - } - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", serviceException.ErrorCode); - throw ADP.CreateSqlException(serviceException, ConnectionOptions, this, username); - } - } - // Deal with normal MsalExceptions. - catch (MsalException msalException) - { - if (MsalError.UnknownError != msalException.ErrorCode || _timeout.IsExpired || _timeout.MillisecondsRemaining <= sleepInterval) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", msalException.ErrorCode); - - throw ADP.CreateSqlException(msalException, ConnectionOptions, this, username); - } - - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[Milliseconds]", ObjectID, sleepInterval); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, remaining {1}[Milliseconds]", ObjectID, _timeout.MillisecondsRemaining); - - Thread.Sleep(sleepInterval); - sleepInterval *= 2; - } - // All other exceptions from MSAL/Azure Identity APIs - catch (Exception e) - { - throw SqlException.CreateException( - new() - { - new( - 0, - (byte)0x00, - (byte)TdsEnums.FATAL_ERROR_CLASS, - ConnectionOptions.DataSource, - e.Message, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, - 0) - }, - "", - this, - e); - } - } - - Debug.Assert(_fedAuthToken != null, "fedAuthToken should not be null."); - Debug.Assert(_fedAuthToken.accessToken != null && _fedAuthToken.accessToken.Length > 0, "fedAuthToken.accessToken should not be null or empty."); - - // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling. - if (_dbConnectionPool != null) - { - DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime); - _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.accessToken, expirationTime); - } - SqlClientEventSource.Log.TryTraceEvent(" {0}, Finished generating federated authentication token.", ObjectID); - return _fedAuthToken; - } - internal void OnFeatureExtAck(int featureId, byte[] data) { if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) 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 140a408688..d41579b9f5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Net.Http.Headers; using System.Security; using System.Threading; using System.Threading.Tasks; @@ -14,6 +15,7 @@ using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; +using Microsoft.Identity.Client; #if NETFRAMEWORK using Microsoft.Data.Common.ConnectionString; @@ -1541,6 +1543,317 @@ private void FailoverPermissionDemand() => PoolGroupProviderInfo?.FailoverPermissionDemand(); #endif + /// + /// Get the Federated Authentication Token. + /// + /// Information obtained from server as Federated Authentication Info. + private SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) + { + Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); + + // Number of milliseconds to sleep for the initial back off. + int sleepInterval = 100; + + // Number of attempts, for tracing purposes, if we underwent retries. + int numberOfAttempts = 0; + + // Object that will be returned to the caller, containing all required data about the token. + _fedAuthToken = new SqlFedAuthToken(); + + // Username to use in error messages. + string username = null; + + SqlAuthenticationProvider authProvider = + SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); + + if (authProvider == null && _accessTokenCallback == null) + { + throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication.ToString()); + } + + // Retry getting access token once if MsalException.error_code is unknown_error. + // extra logic to deal with HTTP 429 (Retry after). + // @TODO: Can we pick one or the other? + // @TODO: Wait ... are we counting up but only looping while the number of attempts is <=1 ? Huh? + // @TODO: Can we consider using a for loop here since there's a fixed number of times to loop + + #if NET + while (numberOfAttempts <= 1) + #else + while (numberOfAttempts <= 1 && sleepInterval <= _timeout.MillisecondsRemaining) + #endif + { + numberOfAttempts++; + try + { + var authParamsBuilder = new SqlAuthenticationParameters.Builder( + authenticationMethod: ConnectionOptions.Authentication, + resource: fedAuthInfo.spn, + authority: fedAuthInfo.stsurl, + serverName: ConnectionOptions.DataSource, + databaseName: ConnectionOptions.InitialCatalog) + .WithConnectionId(_clientConnectionId) + .WithConnectionTimeout(ConnectionOptions.ConnectTimeout); + switch (ConnectionOptions.Authentication) + { + case SqlAuthenticationMethod.ActiveDirectoryIntegrated: + #if NET + // In some scenarios for .NET Core, MSAL cannot detect the current user and needs it passed in + // for Integrated auth. Allow the user/application to pass it in to work around those scenarios. + if (!string.IsNullOrEmpty(ConnectionOptions.UserID)) + { + username = ConnectionOptions.UserID; + authParamsBuilder.WithUserId(username); + } + else + { + username = TdsEnums.NTAUTHORITYANONYMOUSLOGON; + } + #else + username = TdsEnums.NTAUTHORITYANONYMOUSLOGON; + #endif + + if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) + { + _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; + } + else + { + // We use Task.Run here in all places to execute task synchronously + // in the same context. Fixes block-over-async deadlock possibilities + // https://github.com/dotnet/SqlClient/issues/1209 + // @TODO: Verify that the wrapping/unwrapping is necessary. + _fedAuthToken = Task.Run(async () => + await authProvider.AcquireTokenAsync(authParamsBuilder)) + .GetAwaiter() + .GetResult() + .ToSqlFedAuthToken(); + _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; + } + + break; + + case SqlAuthenticationMethod.ActiveDirectoryInteractive: + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: + case SqlAuthenticationMethod.ActiveDirectoryDefault: + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: + if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) + { + _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; + } + else + { + authParamsBuilder.WithUserId(ConnectionOptions.UserID); + _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; + } + + break; + + #pragma warning disable 0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryPassword: + #pragma warning restore 0618 // Type or member is obsolete + + case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: + if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) + { + _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; + } + else + { + if (_credential != null) + { + username = _credential.UserId; + authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); + _fedAuthToken = Task.Run(async () => + await authProvider.AcquireTokenAsync(authParamsBuilder)) + .GetAwaiter() + .GetResult() + .ToSqlFedAuthToken(); + } + else + { + username = ConnectionOptions.UserID; + authParamsBuilder.WithUserId(username).WithPassword(ConnectionOptions.Password); + _fedAuthToken = Task.Run(async () => + await authProvider.AcquireTokenAsync(authParamsBuilder)) + .GetAwaiter() + .GetResult() + .ToSqlFedAuthToken(); + } + _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; + } + + break; + + default: + if (_accessTokenCallback == null) + { + throw SQL.UnsupportedAuthenticationSpecified(ConnectionOptions.Authentication); + } + + if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) + { + _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; + } + else + { + if (_credential != null) + { + username = _credential.UserId; + authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); + } + else + { + authParamsBuilder.WithUserId(ConnectionOptions.UserID); + authParamsBuilder.WithPassword(ConnectionOptions.Password); + } + + SqlAuthenticationParameters parameters = authParamsBuilder; + CancellationTokenSource cts = new(); + + // Use Connection timeout value to cancel token acquire request + // after certain period of time.(int) + if (_timeout.MillisecondsRemaining < int.MaxValue) + { + cts.CancelAfter((int)_timeout.MillisecondsRemaining); + } + + _fedAuthToken = Task.Run(async () => + await _accessTokenCallback(parameters, cts.Token)) + .GetAwaiter() + .GetResult() + .ToSqlFedAuthToken(); + _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; + } + break; + } + + Debug.Assert(_fedAuthToken.accessToken != null, "AccessToken should not be null."); + + #if DEBUG + if (_forceMsalRetry) + { + // 3399614468 is 0xCAA20004L just for testing. + throw new MsalServiceException(MsalError.UnknownError, "Force retry in GetFedAuthToken"); + } + #endif + + // Break out of the retry loop in successful case. + break; + } + catch (MsalServiceException serviceException) + { + // Deal with Msal service exceptions first, retry if 429 received. + if (serviceException.StatusCode == MsalHttpRetryStatusCode) + { + RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter; + if (retryAfter.Delta.HasValue) + { + sleepInterval = retryAfter.Delta.Value.Milliseconds; + } + else if (retryAfter.Date.HasValue) + { + sleepInterval = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); + } + + // if there's enough time to retry before timeout, then retry, otherwise + // break out the retry loop. + if (sleepInterval < _timeout.MillisecondsRemaining) + { + Thread.Sleep(sleepInterval); + } + else + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.GetFedAuthToken.MsalServiceException | ERR | " + + $"Timeout: {serviceException.ErrorCode}"); + + throw SQL.ActiveDirectoryTokenRetrievingTimeout( + Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), + serviceException.ErrorCode, + serviceException); + } + } + else + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.GetFedAuthToken.MsalServiceException | ERR | " + + $"{serviceException.ErrorCode}"); + + throw ADP.CreateSqlException(serviceException, ConnectionOptions, this, username); + } + } + catch (MsalException msalException) + { + // Deal with normal MsalExceptions. + if (MsalError.UnknownError != msalException.ErrorCode || + _timeout.IsExpired || + _timeout.MillisecondsRemaining <= sleepInterval) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.GetFedAuthToken.MSALException | ERR | " + + $"{msalException.ErrorCode}"); + + throw ADP.CreateSqlException(msalException, ConnectionOptions, this, username); + } + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.GetFedAuthToken | ADV | " + + $"Object ID: {ObjectID}, " + + $"sleeping {sleepInterval}ms"); + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.GetFedAuthToken | ADV | " + + $"Object ID: {ObjectID}, " + + $"remaining {_timeout.MillisecondsRemaining}ms"); + + Thread.Sleep(sleepInterval); + + sleepInterval *= 2; + } + // All other exceptions from MSAL/Azure Identity APIs + catch (Exception e) + { + SqlError error = new( + infoNumber: 0, + errorState: 0x00, + errorClass: TdsEnums.FATAL_ERROR_CLASS, + server: ConnectionOptions.DataSource, + errorMessage: e.Message, + procedure: ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, + lineNumber: 0); + + throw SqlException.CreateException( + error, + serverVersion: string.Empty, + internalConnection: this, + innerException: e); + } + } + + Debug.Assert(_fedAuthToken != null, "fedAuthToken should not be null."); + Debug.Assert(_fedAuthToken.accessToken?.Length > 0, + "fedAuthToken.accessToken should not be null or empty."); + + // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling. + if (_dbConnectionPool != null) + { + DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime); + _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext( + _fedAuthToken.accessToken, + expirationTime); + } + + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.GetFedAuthToken | " + + $"Object ID {ObjectID}, " + + $"Finished generating federated authentication token."); + + return _fedAuthToken; + } + private void Login( ServerInfo server, TimeoutTimer timeout, From ee8f95e89a182dd859c4fed7ec7311ece710b8bd Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 12:17:13 -0600 Subject: [PATCH 44/56] Merge OnFeatureExtAck (Split long lines, flip constant comparisons, standardize the indenting) --- .../SqlClient/SqlInternalConnectionTds.cs | 308 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 314 ------------ .../SqlClient/SqlInternalConnectionTds.cs | 452 ++++++++++++++++++ 3 files changed, 452 insertions(+), 622 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 895ad2a679..d8a481a249 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -344,314 +344,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - //////////////////////////////////////////////////////////////////////////////////////// - // PARSER CALLBACKS - //////////////////////////////////////////////////////////////////////////////////////// - - internal void OnFeatureExtAck(int featureId, byte[] data) - { - if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) - { - return; - } - - switch (featureId) - { - case TdsEnums.FEATUREEXT_SRECOVERY: - { - // Session recovery not requested - if (!_sessionRecoveryRequested) - { - throw SQL.ParsingError(); - } - _sessionRecoveryAcknowledged = true; - -#if DEBUG - foreach (var s in _currentSessionData._delta) - { - Debug.Assert(s == null, "Delta should be null at this point"); - } -#endif - Debug.Assert(_currentSessionData._unrecoverableStatesCount == 0, "Unrecoverable states count should be 0"); - - int i = 0; - while (i < data.Length) - { - byte stateId = data[i]; - i++; - int len; - byte bLen = data[i]; - i++; - if (bLen == 0xFF) - { - len = BitConverter.ToInt32(data, i); - i += 4; - } - else - { - len = bLen; - } - byte[] stateData = new byte[len]; - Buffer.BlockCopy(data, i, stateData, 0, len); - i += len; - if (_recoverySessionData == null) - { - _currentSessionData._initialState[stateId] = stateData; - } - else - { - _currentSessionData._delta[stateId] = new SessionStateRecord { _data = stateData, _dataLength = len, _recoverable = true, _version = 0 }; - _currentSessionData._deltaDirty = true; - } - } - break; - } - - case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for GlobalTransactions", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown version number for GlobalTransactions", ObjectID); - throw SQL.ParsingError(); - } - - IsGlobalTransaction = true; - if (1 == data[0]) - { - IsGlobalTransactionsEnabledForServer = true; - } - break; - } - - case TdsEnums.FEATUREEXT_FEDAUTH: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for federated authentication", ObjectID); - - if (!_federatedAuthenticationRequested) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Did not request federated authentication", ObjectID); - throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnrequestedFeatureAckReceived, featureId); - } - - Debug.Assert(_fedAuthFeatureExtensionData != null, "_fedAuthFeatureExtensionData must not be null when _federatedAuthenticationRequested == true"); - - switch (_fedAuthFeatureExtensionData.libraryType) - { - case TdsEnums.FedAuthLibrary.MSAL: - case TdsEnums.FedAuthLibrary.SecurityToken: - // The server shouldn't have sent any additional data with the ack (like a nonce) - if (data.Length != 0) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Federated authentication feature extension ack for MSAL and Security Token includes extra data", ObjectID); - throw SQL.ParsingError(ParsingErrorState.FedAuthFeatureAckContainsExtraData); - } - break; - - default: - Debug.Fail("Unknown _fedAuthLibrary type"); - - SqlClientEventSource.Log.TryTraceEvent(" {0}, Attempting to use unknown federated authentication library", ObjectID); - throw SQL.ParsingErrorLibraryType(ParsingErrorState.FedAuthFeatureAckUnknownLibraryType, (int)_fedAuthFeatureExtensionData.libraryType); - } - _federatedAuthenticationAcknowledged = true; - - // If a new authentication context was used as part of this login attempt, try to update the new context in the cache, i.e.dbConnectionPool.AuthenticationContexts. - // ChooseAuthenticationContextToUpdate will take care that only the context which has more validity will remain in the cache, based on the Update logic. - if (_newDbConnectionPoolAuthenticationContext != null) - { - Debug.Assert(_dbConnectionPool != null, "_dbConnectionPool should not be null when _newDbConnectionPoolAuthenticationContext != null."); - - DbConnectionPoolAuthenticationContext newAuthenticationContextInCacheAfterAddOrUpdate = _dbConnectionPool.AuthenticationContexts.AddOrUpdate(_dbConnectionPoolAuthenticationContextKey, _newDbConnectionPoolAuthenticationContext, - (key, oldValue) => DbConnectionPoolAuthenticationContext.ChooseAuthenticationContextToUpdate(oldValue, _newDbConnectionPoolAuthenticationContext)); - - Debug.Assert(newAuthenticationContextInCacheAfterAddOrUpdate != null, "newAuthenticationContextInCacheAfterAddOrUpdate should not be null."); -#if DEBUG - // For debug purposes, assert and trace if we ended up updating the cache with the new one or some other thread's context won the expiration race. - if (newAuthenticationContextInCacheAfterAddOrUpdate == _newDbConnectionPoolAuthenticationContext) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Updated the new dbAuthenticationContext in the _dbConnectionPool.AuthenticationContexts.", ObjectID); - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, but it did not update the new value.", ObjectID); - } -#endif - } - break; - } - case TdsEnums.FEATUREEXT_TCE: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for TCE", ObjectID); - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown version number for TCE", ObjectID); - throw SQL.ParsingError(ParsingErrorState.TceUnknownVersion); - } - - byte supportedTceVersion = data[0]; - if (0 == supportedTceVersion || supportedTceVersion > TdsEnums.MAX_SUPPORTED_TCE_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number for TCE", ObjectID); - throw SQL.ParsingErrorValue(ParsingErrorState.TceInvalidVersion, supportedTceVersion); - } - - _tceVersionSupported = supportedTceVersion; - Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, "Client support TCE version 2"); - _parser.IsColumnEncryptionSupported = true; - _parser.TceVersionSupported = _tceVersionSupported; - _parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3; - - if (data.Length > 1) - { - // Extract the type of enclave being used by the server. - _parser.EnclaveType = Encoding.Unicode.GetString(data, 2, (data.Length - 2)); - } - break; - } - - case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for AzureSQLSupport", ObjectID); - - if (data.Length < 1) - { - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - IsAzureSqlConnection = true; - - // Bit 0 for RO/FP support - if ((data[0] & 1) == 1 && SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, FailoverPartner enabled with Readonly intent for AzureSQL DB", ObjectID); - } - break; - } - - case TdsEnums.FEATUREEXT_DATACLASSIFICATION: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for DATACLASSIFICATION", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for DATACLASSIFICATION", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte supportedDataClassificationVersion = data[0]; - if ((0 == supportedDataClassificationVersion) || (supportedDataClassificationVersion > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED)) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number for DATACLASSIFICATION", ObjectID); - throw SQL.ParsingErrorValue(ParsingErrorState.DataClassificationInvalidVersion, supportedDataClassificationVersion); - } - - if (data.Length != 2) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for DATACLASSIFICATION", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte enabled = data[1]; - _parser.DataClassificationVersion = (enabled == 0) ? TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED : supportedDataClassificationVersion; - break; - } - - case TdsEnums.FEATUREEXT_UTF8SUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for UTF8 support", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown value for UTF8 support", ObjectID); - throw SQL.ParsingError(); - } - break; - } - - case TdsEnums.FEATUREEXT_SQLDNSCACHING: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for SQLDNSCACHING", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for SQLDNSCACHING", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - if (1 == data[0]) - { - IsSQLDNSCachingSupported = true; - _cleanSQLDNSCaching = false; - - if (RoutingInfo != null) - { - IsDNSCachingBeforeRedirectSupported = true; - } - } - else - { - // we receive the IsSupported whose value is 0 - IsSQLDNSCachingSupported = false; - _cleanSQLDNSCaching = true; - } - - // need to add more steps for phase 2 - // get IPv4 + IPv6 + Port number - // not put them in the DNS cache at this point but need to store them somewhere - // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag - break; - } - - case TdsEnums.FEATUREEXT_JSONSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for JSONSUPPORT", ObjectID); - if (data.Length != 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for JSONSUPPORT", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte jsonSupportVersion = data[0]; - if (jsonSupportVersion == 0 || jsonSupportVersion > TdsEnums.MAX_SUPPORTED_JSON_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number for JSONSUPPORT", ObjectID); - throw SQL.ParsingError(); - } - IsJsonSupportEnabled = true; - break; - } - - case TdsEnums.FEATUREEXT_VECTORSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for VECTORSUPPORT", ObjectID); - if (data.Length != 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for VECTORSUPPORT", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte vectorSupportVersion = data[0]; - if (vectorSupportVersion == 0 || vectorSupportVersion > TdsEnums.MAX_SUPPORTED_VECTOR_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number {1} for VECTORSUPPORT, Max supported version is {2}", ObjectID, vectorSupportVersion, TdsEnums.MAX_SUPPORTED_VECTOR_VERSION); - throw SQL.ParsingError(); - } - IsVectorSupportEnabled = true; - break; - } - case TdsEnums.FEATUREEXT_USERAGENT: - { - // Unexpected ack from server but we ignore it entirely - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); - break; - } - - default: - { - // Unknown feature ack - throw SQL.ParsingError(); - } - } - } - //////////////////////////////////////////////////////////////////////////////////////// // Helper methods for Locks //////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index bf9f8b0e57..446944e5e6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -341,320 +341,6 @@ internal SqlInternalConnectionTds( SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); } - - //////////////////////////////////////////////////////////////////////////////////////// - // PARSER CALLBACKS - //////////////////////////////////////////////////////////////////////////////////////// - - internal void OnFeatureExtAck(int featureId, byte[] data) - { - if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) - { - return; - } - - switch (featureId) - { - case TdsEnums.FEATUREEXT_SRECOVERY: - { - // Session recovery not requested - if (!_sessionRecoveryRequested) - { - throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnrequestedFeatureAckReceived, featureId); - } - _sessionRecoveryAcknowledged = true; - -#if DEBUG - foreach (var s in _currentSessionData._delta) - { - Debug.Assert(s == null, "Delta should be null at this point"); - } -#endif - Debug.Assert(_currentSessionData._unrecoverableStatesCount == 0, "Unrecoverable states count should be 0"); - - int i = 0; - while (i < data.Length) - { - byte stateId = data[i]; - i++; - int len; - byte bLen = data[i]; - i++; - if (bLen == 0xFF) - { - len = BitConverter.ToInt32(data, i); - i += 4; - } - else - { - len = bLen; - } - byte[] stateData = new byte[len]; - Buffer.BlockCopy(data, i, stateData, 0, len); - i += len; - if (_recoverySessionData == null) - { - _currentSessionData._initialState[stateId] = stateData; - } - else - { - _currentSessionData._delta[stateId] = new SessionStateRecord { _data = stateData, _dataLength = len, _recoverable = true, _version = 0 }; - _currentSessionData._deltaDirty = true; - } - } - break; - } - - case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for GlobalTransactions", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown version number for GlobalTransactions", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - IsGlobalTransaction = true; - if (1 == data[0]) - { - IsGlobalTransactionsEnabledForServer = true; - } - break; - } - - case TdsEnums.FEATUREEXT_FEDAUTH: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for federated authentication", ObjectID); - - if (!_federatedAuthenticationRequested) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Did not request federated authentication", ObjectID); - throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnrequestedFeatureAckReceived, featureId); - } - - Debug.Assert(_fedAuthFeatureExtensionData != null, "_fedAuthFeatureExtensionData must not be null when _federatedAuthenticationRequested == true"); - - switch (_fedAuthFeatureExtensionData.libraryType) - { - case TdsEnums.FedAuthLibrary.MSAL: - case TdsEnums.FedAuthLibrary.SecurityToken: - // The server shouldn't have sent any additional data with the ack (like a nonce) - if (data.Length != 0) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Federated authentication feature extension ack for MSAL and Security Token includes extra data", ObjectID); - throw SQL.ParsingError(ParsingErrorState.FedAuthFeatureAckContainsExtraData); - } - break; - - default: - Debug.Fail("Unknown _fedAuthLibrary type"); - - SqlClientEventSource.Log.TryTraceEvent(" {0}, Attempting to use unknown federated authentication library", ObjectID); - throw SQL.ParsingErrorLibraryType(ParsingErrorState.FedAuthFeatureAckUnknownLibraryType, (int)_fedAuthFeatureExtensionData.libraryType); - } - _federatedAuthenticationAcknowledged = true; - - // If a new authentication context was used as part of this login attempt, try to update the new context in the cache, i.e.dbConnectionPool.AuthenticationContexts. - // ChooseAuthenticationContextToUpdate will take care that only the context which has more validity will remain in the cache, based on the Update logic. - if (_newDbConnectionPoolAuthenticationContext != null) - { - Debug.Assert(_dbConnectionPool != null, "_dbConnectionPool should not be null when _newDbConnectionPoolAuthenticationContext != null."); - - DbConnectionPoolAuthenticationContext newAuthenticationContextInCacheAfterAddOrUpdate = _dbConnectionPool.AuthenticationContexts.AddOrUpdate(_dbConnectionPoolAuthenticationContextKey, _newDbConnectionPoolAuthenticationContext, - (key, oldValue) => DbConnectionPoolAuthenticationContext.ChooseAuthenticationContextToUpdate(oldValue, _newDbConnectionPoolAuthenticationContext)); - - Debug.Assert(newAuthenticationContextInCacheAfterAddOrUpdate != null, "newAuthenticationContextInCacheAfterAddOrUpdate should not be null."); -#if DEBUG - // For debug purposes, assert and trace if we ended up updating the cache with the new one or some other thread's context won the expiration race. - if (newAuthenticationContextInCacheAfterAddOrUpdate == _newDbConnectionPoolAuthenticationContext) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Updated the new dbAuthenticationContext in the _dbConnectionPool.AuthenticationContexts.", ObjectID); - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, but it did not update the new value.", ObjectID); - } -#endif - } - break; - } - case TdsEnums.FEATUREEXT_TCE: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for TCE", ObjectID); - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown version number for TCE", ObjectID); - throw SQL.ParsingError(ParsingErrorState.TceUnknownVersion); - } - - byte supportedTceVersion = data[0]; - if (0 == supportedTceVersion || supportedTceVersion > TdsEnums.MAX_SUPPORTED_TCE_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number for TCE", ObjectID); - throw SQL.ParsingErrorValue(ParsingErrorState.TceInvalidVersion, supportedTceVersion); - } - - _tceVersionSupported = supportedTceVersion; - Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, "Client support TCE version 2"); - _parser.IsColumnEncryptionSupported = true; - _parser.TceVersionSupported = _tceVersionSupported; - _parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3; - - if (data.Length > 1) - { - // Extract the type of enclave being used by the server. - _parser.EnclaveType = Encoding.Unicode.GetString(data, 2, (data.Length - 2)); - } - break; - } - - case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for AzureSQLSupport", ObjectID); - - if (data.Length < 1) - { - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - IsAzureSqlConnection = true; - - // Bit 0 for RO/FP support - if ((data[0] & 1) == 1 && SqlClientEventSource.Log.IsTraceEnabled()) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, FailoverPartner enabled with Readonly intent for AzureSQL DB", ObjectID); - } - break; - } - - case TdsEnums.FEATUREEXT_DATACLASSIFICATION: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for DATACLASSIFICATION", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for DATACLASSIFICATION", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte supportedDataClassificationVersion = data[0]; - if ((0 == supportedDataClassificationVersion) || (supportedDataClassificationVersion > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED)) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number for DATACLASSIFICATION", ObjectID); - throw SQL.ParsingErrorValue(ParsingErrorState.DataClassificationInvalidVersion, supportedDataClassificationVersion); - } - - if (data.Length != 2) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for DATACLASSIFICATION", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte enabled = data[1]; - _parser.DataClassificationVersion = (enabled == 0) ? TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED : supportedDataClassificationVersion; - break; - } - - case TdsEnums.FEATUREEXT_UTF8SUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for UTF8 support", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown value for UTF8 support", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - break; - } - - case TdsEnums.FEATUREEXT_SQLDNSCACHING: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for SQLDNSCACHING", ObjectID); - - if (data.Length < 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for SQLDNSCACHING", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - - if (1 == data[0]) - { - IsSQLDNSCachingSupported = true; - _cleanSQLDNSCaching = false; - - if (RoutingInfo != null) - { - IsDNSCachingBeforeRedirectSupported = true; - } - } - else - { - // we receive the IsSupported whose value is 0 - IsSQLDNSCachingSupported = false; - _cleanSQLDNSCaching = true; - } - - // need to add more steps for phase 2 - // get IPv4 + IPv6 + Port number - // not put them in the DNS cache at this point but need to store them somewhere - // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag - break; - } - - case TdsEnums.FEATUREEXT_JSONSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for JSONSUPPORT", ObjectID); - if (data.Length != 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for JSONSUPPORT", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte jsonSupportVersion = data[0]; - if (jsonSupportVersion == 0 || jsonSupportVersion > TdsEnums.MAX_SUPPORTED_JSON_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number for JSONSUPPORT", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - IsJsonSupportEnabled = true; - break; - } - - case TdsEnums.FEATUREEXT_VECTORSUPPORT: - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for VECTORSUPPORT", ObjectID); - if (data.Length != 1) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for VECTORSUPPORT", ObjectID); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - byte vectorSupportVersion = data[0]; - if (vectorSupportVersion == 0 || vectorSupportVersion > TdsEnums.MAX_SUPPORTED_VECTOR_VERSION) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Invalid version number {1} for VECTORSUPPORT, Max supported version is {2}", ObjectID, vectorSupportVersion, TdsEnums.MAX_SUPPORTED_VECTOR_VERSION); - throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); - } - IsVectorSupportEnabled = true; - break; - } - case TdsEnums.FEATUREEXT_USERAGENT: - { - // TODO: Verify that the server sends an acknowledgment (Ack) - // using this log message in the future. - - // This Ack from the server is unexpected and is ignored completely. - // According to the TDS specification, an Ack is not defined/expected - // for this scenario. We handle it only for completeness - // and to support testing. - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)", ObjectID); - break; - } - default: - { - // Unknown feature ack - throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnknownFeatureAck, featureId); - } - } - } - //////////////////////////////////////////////////////////////////////////////////////// // Helper methods for Locks //////////////////////////////////////////////////////////////////////////////////////// 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 d41579b9f5..7a7a10f321 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.Net.Http.Headers; using System.Security; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -841,6 +842,457 @@ internal void OnEnvChange(SqlEnvChange rec) } } + // @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. + internal void OnFeatureExtAck(int featureId, byte[] data) + { + if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) + { + return; + } + + switch (featureId) + { + case TdsEnums.FEATUREEXT_SRECOVERY: + { + // Session recovery not requested + if (!_sessionRecoveryRequested) + { + throw SQL.ParsingError(); + } + + _sessionRecoveryAcknowledged = true; + + #if DEBUG + foreach (var s in _currentSessionData._delta) + { + Debug.Assert(s == null, "Delta should be null at this point"); + } + #endif + + Debug.Assert(_currentSessionData._unrecoverableStatesCount == 0, + "Unrecoverable states count should be 0"); + + int i = 0; + while (i < data.Length) + { + byte stateId = data[i]; + i++; + int len; + byte bLen = data[i]; + i++; + if (bLen == 0xFF) + { + len = BitConverter.ToInt32(data, i); + i += 4; + } + else + { + len = bLen; + } + + byte[] stateData = new byte[len]; + Buffer.BlockCopy(data, i, stateData, 0, len); + i += len; + if (_recoverySessionData == null) + { + _currentSessionData._initialState[stateId] = stateData; + } + else + { + _currentSessionData._delta[stateId] = new SessionStateRecord + { + _data = stateData, _dataLength = len, _recoverable = true, _version = 0 + }; + _currentSessionData._deltaDirty = true; + } + } + + break; + } + + case TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for GlobalTransactions"); + + if (data.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown version number for GlobalTransactions"); + + throw SQL.ParsingError(); + } + + IsGlobalTransaction = true; + if (data[0] == 0x01) + { + IsGlobalTransactionsEnabledForServer = true; + } + + break; + } + + case TdsEnums.FEATUREEXT_FEDAUTH: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {0}, " + + $"Received feature extension acknowledgement for federated authentication"); + + if (!_federatedAuthenticationRequested) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Did not request federated authentication"); + + throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnrequestedFeatureAckReceived, featureId); + } + + Debug.Assert(_fedAuthFeatureExtensionData != null, + "_fedAuthFeatureExtensionData must not be null when _federatedAuthenticationRequested == true"); + + switch (_fedAuthFeatureExtensionData.libraryType) + { + case TdsEnums.FedAuthLibrary.MSAL: + case TdsEnums.FedAuthLibrary.SecurityToken: + // The server shouldn't have sent any additional data with the ack (like a nonce) + if (data.Length != 0) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Federated authentication feature extension ack for MSAL and Security Token includes extra data"); + + throw SQL.ParsingError(ParsingErrorState.FedAuthFeatureAckContainsExtraData); + } + + break; + + default: + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Attempting to use unknown federated authentication library"); + + Debug.Fail("Unknown _fedAuthLibrary type"); + throw SQL.ParsingErrorLibraryType( + ParsingErrorState.FedAuthFeatureAckUnknownLibraryType, + (int)_fedAuthFeatureExtensionData.libraryType); + } + + _federatedAuthenticationAcknowledged = true; + + // If a new authentication context was used as part of this login attempt, try + // to update the new context in the cache, i.e. + // dbConnectionPool.AuthenticationContexts. ChooseAuthenticationContextToUpdate + // will take care that only the context which has more validity will remain in + // the cache, based on the Update logic. + if (_newDbConnectionPoolAuthenticationContext != null) + { + Debug.Assert(_dbConnectionPool != null, + "_dbConnectionPool should not be null when _newDbConnectionPoolAuthenticationContext != null."); + + DbConnectionPoolAuthenticationContext newAuthenticationContextInCacheAfterAddOrUpdate = + _dbConnectionPool.AuthenticationContexts.AddOrUpdate( + _dbConnectionPoolAuthenticationContextKey, + _newDbConnectionPoolAuthenticationContext, + (_, oldValue) => + DbConnectionPoolAuthenticationContext.ChooseAuthenticationContextToUpdate( + oldValue, + _newDbConnectionPoolAuthenticationContext)); + + Debug.Assert(newAuthenticationContextInCacheAfterAddOrUpdate != null, + "newAuthenticationContextInCacheAfterAddOrUpdate should not be null."); + + #if DEBUG + // For debug purposes, assert and trace if we ended up updating the cache + // with the new one or some other thread's context won the expiration race. + if (newAuthenticationContextInCacheAfterAddOrUpdate == _newDbConnectionPoolAuthenticationContext) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Updated the new dbAuthenticationContext in the _dbConnectionPool.AuthenticationContexts."); + } + else + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID }, " + + $"AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, but it did not update the new value.", + ObjectID); + } + #endif + } + + break; + } + case TdsEnums.FEATUREEXT_TCE: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for TCE"); + + if (data.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown version number for TCE"); + + throw SQL.ParsingError(ParsingErrorState.TceUnknownVersion); + } + + byte supportedTceVersion = data[0]; + if (supportedTceVersion == 0 || supportedTceVersion > TdsEnums.MAX_SUPPORTED_TCE_VERSION) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Invalid version number for TCE"); + + throw SQL.ParsingErrorValue(ParsingErrorState.TceInvalidVersion, supportedTceVersion); + } + + _tceVersionSupported = supportedTceVersion; + + Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, + "Client support TCE version 2"); + + _parser.IsColumnEncryptionSupported = true; + _parser.TceVersionSupported = _tceVersionSupported; + _parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3; + + if (data.Length > 1) + { + // Extract the type of enclave being used by the server. + _parser.EnclaveType = Encoding.Unicode.GetString(data, 2, data.Length - 2); + } + break; + } + case TdsEnums.FEATUREEXT_AZURESQLSUPPORT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for AzureSQLSupport"); + + if (data.Length < 1) + { + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + IsAzureSqlConnection = true; + + // Bit 0 for RO/FP support + // @TODO: Add a constant somewhere for that + if ((data[0] & 1) == 1 && SqlClientEventSource.Log.IsTraceEnabled()) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"FailoverPartner enabled with Readonly intent for AzureSQL DB"); + } + + break; + } + case TdsEnums.FEATUREEXT_DATACLASSIFICATION: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for DATACLASSIFICATION"); + + if (data.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown token for DATACLASSIFICATION"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + byte supportedDataClassificationVersion = data[0]; + if (supportedDataClassificationVersion == 0 || + supportedDataClassificationVersion > TdsEnums.DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Invalid version number for DATACLASSIFICATION"); + + throw SQL.ParsingErrorValue( + ParsingErrorState.DataClassificationInvalidVersion, + supportedDataClassificationVersion); + } + + if (data.Length != 2) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown token for DATACLASSIFICATION"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + byte enabled = data[1]; + _parser.DataClassificationVersion = enabled == 0 + ? TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED + : supportedDataClassificationVersion; + + break; + } + + case TdsEnums.FEATUREEXT_UTF8SUPPORT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for UTF8 support"); + + if (data.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown value for UTF8 support", ObjectID); + + throw SQL.ParsingError(); + } + break; + } + case TdsEnums.FEATUREEXT_SQLDNSCACHING: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for SQLDNSCACHING"); + + if (data.Length < 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown token for SQLDNSCACHING"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + if (data[0] == 1) + { + IsSQLDNSCachingSupported = true; + _cleanSQLDNSCaching = false; + + if (RoutingInfo != null) + { + IsDNSCachingBeforeRedirectSupported = true; + } + } + else + { + // we receive the IsSupported whose value is 0 + IsSQLDNSCachingSupported = false; + _cleanSQLDNSCaching = true; + } + + // TODO: need to add more steps for phase 2 + // get IPv4 + IPv6 + Port number + // not put them in the DNS cache at this point but need to store them somewhere + // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag + break; + } + case TdsEnums.FEATUREEXT_JSONSUPPORT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for JSONSUPPORT"); + + if (data.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown token for JSONSUPPORT"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + byte jsonSupportVersion = data[0]; + if (jsonSupportVersion == 0 || jsonSupportVersion > TdsEnums.MAX_SUPPORTED_JSON_VERSION) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Invalid version number for JSONSUPPORT"); + + throw SQL.ParsingError(); + } + + IsJsonSupportEnabled = true; + break; + } + case TdsEnums.FEATUREEXT_VECTORSUPPORT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for VECTORSUPPORT"); + + if (data.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Unknown token for VECTORSUPPORT"); + + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + byte vectorSupportVersion = data[0]; + if (vectorSupportVersion == 0 || vectorSupportVersion > TdsEnums.MAX_SUPPORTED_VECTOR_VERSION) + { + SqlClientEventSource.Log.TryTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + + $"Object ID {ObjectID}, " + + $"Invalid version number {vectorSupportVersion} for VECTORSUPPORT, " + + $"Max supported version is {TdsEnums.MAX_SUPPORTED_VECTOR_VERSION}"); + + throw SQL.ParsingError(); + } + + IsVectorSupportEnabled = true; + + break; + } + case TdsEnums.FEATUREEXT_USERAGENT: + { + // Unexpected ack from server but we ignore it entirely + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.OnFeatureExtAck | ADV | " + + $"Object ID {ObjectID}, " + + $"Received feature extension acknowledgement for USERAGENTSUPPORT (ignored)"); + + break; + } + default: + { + // Unknown feature ack + throw SQL.ParsingError(); + } + } + } + /// /// Generates (if appropriate) and sends a Federated Authentication Access token to the /// server, using the Federated Authentication Info. From 484e96f14ef9ccd6ff9682656096e51901b4abc8 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 13:26:19 -0600 Subject: [PATCH 45/56] Merge constructor and TryReplaceConnection (rebalanced comments, use typed provider info) --- .../SqlClient/SqlInternalConnectionTds.cs | 147 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 145 -------------- .../SqlClient/SqlInternalConnectionTds.cs | 186 +++++++++++++++++- 3 files changed, 182 insertions(+), 296 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index d8a481a249..8f50a8926e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -209,153 +209,6 @@ internal bool ThreadMayHaveLock() } internal SyncAsyncLock _parserLock = new SyncAsyncLock(); - - // although the new password is generally not used it must be passed to the ctor - // the new Login7 packet will always write out the new password (or a length of zero and no bytes if not present) - // - internal SqlInternalConnectionTds( - DbConnectionPoolIdentity identity, - SqlConnectionString connectionOptions, - SqlCredential credential, - object providerInfo, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance, - SqlConnectionString userConnectionOptions = null, // NOTE: userConnectionOptions may be different to connectionOptions if the connection string has been expanded (see SqlConnectionString.Expand) - SessionData reconnectSessionData = null, - bool applyTransientFaultHandling = false, - string accessToken = null, - IDbConnectionPool pool = null, - Func> accessTokenCallback = null, - SspiContextProvider sspiContextProvider = null) : base(connectionOptions) - { -#if DEBUG - if (reconnectSessionData != null) - { - reconnectSessionData._debugReconnectDataApplied = true; - } -#if NETFRAMEWORK - try - { - // use this to help validate this object is only created after the following permission has been previously demanded in the current codepath - if (userConnectionOptions != null) - { - // As mentioned above, userConnectionOptions may be different to connectionOptions, so we need to demand on the correct connection string - userConnectionOptions.DemandPermission(); - } - else - { - connectionOptions.DemandPermission(); - } - } - catch (System.Security.SecurityException) - { - System.Diagnostics.Debug.Assert(false, "unexpected SecurityException for current codepath"); - throw; - } -#endif -#endif - Debug.Assert(reconnectSessionData == null || connectionOptions.ConnectRetryCount > 0, "Reconnect data supplied with CR turned off"); - - _dbConnectionPool = pool; - - if (connectionOptions.ConnectRetryCount > 0) - { - _recoverySessionData = reconnectSessionData; - if (reconnectSessionData == null) - { - _currentSessionData = new SessionData(); - } - else - { - _currentSessionData = new SessionData(_recoverySessionData); - _originalDatabase = _recoverySessionData._initialDatabase; - _originalLanguage = _recoverySessionData._initialLanguage; - } - } - - if (accessToken != null) - { - _accessTokenInBytes = System.Text.Encoding.Unicode.GetBytes(accessToken); - } - - _accessTokenCallback = accessTokenCallback; - _sspiContextProvider = sspiContextProvider; - - _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); - - _identity = identity; - Debug.Assert(newSecurePassword != null || newPassword != null, "cannot have both new secure change password and string based change password to be null"); - Debug.Assert(credential == null || (string.IsNullOrEmpty(connectionOptions.UserID) && string.IsNullOrEmpty(connectionOptions.Password)), "cannot mix the new secure password system and the connection string based password"); - - Debug.Assert(credential == null || !connectionOptions.IntegratedSecurity, "Cannot use SqlCredential and Integrated Security"); - - _poolGroupProviderInfo = (SqlConnectionPoolGroupProviderInfo)providerInfo; - _fResetConnection = connectionOptions.ConnectionReset; - if (_fResetConnection && _recoverySessionData == null) - { - _originalDatabase = connectionOptions.InitialCatalog; - _originalLanguage = connectionOptions.CurrentLanguage; - } - - _timeoutErrorInternal = new SqlConnectionTimeoutErrorInternal(); - _credential = credential; - - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; // In case of error, let ourselves know that we already own the parser lock - - try - { - _timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout); - - // If transient fault handling is enabled then we can retry the login up to the ConnectRetryCount. - int connectionEstablishCount = applyTransientFaultHandling ? connectionOptions.ConnectRetryCount + 1 : 1; - int transientRetryIntervalInMilliSeconds = connectionOptions.ConnectRetryInterval * 1000; // Max value of transientRetryInterval is 60*1000 ms. The max value allowed for ConnectRetryInterval is 60 - for (int i = 0; i < connectionEstablishCount; i++) - { - try - { - OpenLoginEnlist(_timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance); - break; - } - catch (SqlException sqlex) - { - if (i + 1 == connectionEstablishCount - || !applyTransientFaultHandling - || _timeout.IsExpired - || _timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds - || !IsTransientError(sqlex)) - { - throw; - } - else - { - Thread.Sleep(transientRetryIntervalInMilliSeconds); - } - } - } - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - finally - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); - } - - //////////////////////////////////////////////////////////////////////////////////////// - // Helper methods for Locks - //////////////////////////////////////////////////////////////////////////////////////// - - internal override bool TryReplaceConnection( - DbConnection outerConnection, - SqlConnectionFactory connectionFactory, - TaskCompletionSource retry, - DbConnectionOptions userOptions) - { - return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); - } } internal sealed class ServerInfo diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 446944e5e6..6a4eb63608 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -208,151 +208,6 @@ internal bool ThreadMayHaveLock() } internal SyncAsyncLock _parserLock = new SyncAsyncLock(); - - // although the new password is generally not used it must be passed to the ctor - // the new Login7 packet will always write out the new password (or a length of zero and no bytes if not present) - // - internal SqlInternalConnectionTds( - DbConnectionPoolIdentity identity, - SqlConnectionString connectionOptions, - SqlCredential credential, - DbConnectionPoolGroupProviderInfo providerInfo, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInstance, - SqlConnectionString userConnectionOptions = null, // NOTE: userConnectionOptions may be different to connectionOptions if the connection string has been expanded (see SqlConnectionString.Expand) - SessionData reconnectSessionData = null, - bool applyTransientFaultHandling = false, - string accessToken = null, - IDbConnectionPool pool = null, - Func> accessTokenCallback = null, - SspiContextProvider sspiContextProvider = null) - : base(connectionOptions) - { -#if DEBUG - if (reconnectSessionData != null) - { - reconnectSessionData._debugReconnectDataApplied = true; - } - try - { - // use this to help validate this object is only created after the following permission has been previously demanded in the current codepath - if (userConnectionOptions != null) - { - // As mentioned above, userConnectionOptions may be different to connectionOptions, so we need to demand on the correct connection string - userConnectionOptions.DemandPermission(); - } - else - { - connectionOptions.DemandPermission(); - } - } - catch (System.Security.SecurityException) - { - System.Diagnostics.Debug.Assert(false, "unexpected SecurityException for current codepath"); - throw; - } -#endif - Debug.Assert(reconnectSessionData == null || connectionOptions.ConnectRetryCount > 0, "Reconnect data supplied with CR turned off"); - - _dbConnectionPool = pool; - - if (connectionOptions.ConnectRetryCount > 0) - { - _recoverySessionData = reconnectSessionData; - if (reconnectSessionData == null) - { - _currentSessionData = new SessionData(); - } - else - { - _currentSessionData = new SessionData(_recoverySessionData); - _originalDatabase = _recoverySessionData._initialDatabase; - _originalLanguage = _recoverySessionData._initialLanguage; - } - } - - if (accessToken != null) - { - _accessTokenInBytes = System.Text.Encoding.Unicode.GetBytes(accessToken); - } - - _accessTokenCallback = accessTokenCallback; - _sspiContextProvider = sspiContextProvider; - - _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); - - _identity = identity; - Debug.Assert(newSecurePassword != null || newPassword != null, "cannot have both new secure change password and string based change password to be null"); - Debug.Assert(credential == null || (string.IsNullOrEmpty(connectionOptions.UserID) && string.IsNullOrEmpty(connectionOptions.Password)), "cannot mix the new secure password system and the connection string based password"); - Debug.Assert(credential == null || !connectionOptions.IntegratedSecurity, "Cannot use SqlCredential and Integrated Security"); - - _poolGroupProviderInfo = (SqlConnectionPoolGroupProviderInfo)providerInfo; - _fResetConnection = connectionOptions.ConnectionReset; - if (_fResetConnection && _recoverySessionData == null) - { - _originalDatabase = connectionOptions.InitialCatalog; - _originalLanguage = connectionOptions.CurrentLanguage; - } - - _timeoutErrorInternal = new SqlConnectionTimeoutErrorInternal(); - _credential = credential; - - _parserLock.Wait(canReleaseFromAnyThread: false); - ThreadHasParserLockForClose = true; // In case of error, let ourselves know that we already own the parser lock - - try - { - _timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout); - - // If transient fault handling is enabled then we can retry the login up to the ConnectRetryCount. - int connectionEstablishCount = applyTransientFaultHandling ? connectionOptions.ConnectRetryCount + 1 : 1; - int transientRetryIntervalInMilliSeconds = connectionOptions.ConnectRetryInterval * 1000; // Max value of transientRetryInterval is 60*1000 ms. The max value allowed for ConnectRetryInterval is 60 - for (int i = 0; i < connectionEstablishCount; i++) - { - try - { - OpenLoginEnlist(_timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance); - break; - } - catch (SqlException sqlex) - { - if (i + 1 == connectionEstablishCount - || !applyTransientFaultHandling - || _timeout.IsExpired - || _timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds - || !IsTransientError(sqlex)) - { - throw; - } - else - { - Thread.Sleep(transientRetryIntervalInMilliSeconds); - } - } - } - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - finally - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - } - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, constructed new TDS internal connection", ObjectID); - } - - //////////////////////////////////////////////////////////////////////////////////////// - // Helper methods for Locks - //////////////////////////////////////////////////////////////////////////////////////// - - internal override bool TryReplaceConnection( - DbConnection outerConnection, - SqlConnectionFactory connectionFactory, - TaskCompletionSource retry, - DbConnectionOptions userOptions) - { - return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); - } } internal sealed class ServerInfo 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 7a7a10f321..1a084942f9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -14,14 +15,11 @@ using System.Threading.Tasks; using System.Transactions; using Microsoft.Data.Common; +using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Identity.Client; -#if NETFRAMEWORK -using Microsoft.Data.Common.ConnectionString; -#endif - namespace Microsoft.Data.SqlClient { internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable @@ -306,6 +304,177 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos #endregion + #region Constructors + + /// + /// - Although the new password is generally not used it must be passed to the ctor. The + /// new Login7 packet will always write out the new password (or a length of zero and no + /// bytes if not present). + /// - userConnectionOptions may be different to connectionOptions if the connection string + /// 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( + DbConnectionPoolIdentity identity, + SqlConnectionString connectionOptions, + SqlCredential credential, + DbConnectionPoolGroupProviderInfo providerInfo, + string newPassword, + SecureString newSecurePassword, + bool redirectedUserInstance, + SqlConnectionString userConnectionOptions = null, + SessionData reconnectSessionData = null, + bool applyTransientFaultHandling = false, + string accessToken = null, + IDbConnectionPool pool = null, + Func> accessTokenCallback = null, + SspiContextProvider sspiContextProvider = null) : base(connectionOptions) + { + #if DEBUG + if (reconnectSessionData != null) + { + reconnectSessionData._debugReconnectDataApplied = true; + } + + #if NETFRAMEWORK + try + { + // use this to help validate this object is only created after the following + // permission has been previously demanded in the current codepath + if (userConnectionOptions != null) + { + // As mentioned above, userConnectionOptions may be different to + // connectionOptions, so we need to demand on the correct connection string + userConnectionOptions.DemandPermission(); + } + else + { + connectionOptions.DemandPermission(); + } + } + catch (SecurityException) + { + Debug.Assert(false, "unexpected SecurityException for current codepath"); + throw; + } + #endif + #endif + + Debug.Assert(reconnectSessionData == null || connectionOptions.ConnectRetryCount > 0, + "Reconnect data supplied with CR turned off"); + + _dbConnectionPool = pool; + + if (connectionOptions.ConnectRetryCount > 0) + { + _recoverySessionData = reconnectSessionData; + if (reconnectSessionData == null) + { + _currentSessionData = new SessionData(); + } + else + { + _currentSessionData = new SessionData(_recoverySessionData); + _originalDatabase = _recoverySessionData._initialDatabase; + _originalLanguage = _recoverySessionData._initialLanguage; + } + } + + if (accessToken != null) + { + _accessTokenInBytes = Encoding.Unicode.GetBytes(accessToken); + } + + _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; + + _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); + + _identity = identity; + + Debug.Assert(newSecurePassword != null || newPassword != null, + "cannot have both new secure change password and string based change password to be null"); + Debug.Assert(credential == null || (string.IsNullOrEmpty(connectionOptions.UserID) && + string.IsNullOrEmpty(connectionOptions.Password)), + "cannot mix the new secure password system and the connection string based password"); + + Debug.Assert(credential == null || !connectionOptions.IntegratedSecurity, + "Cannot use SqlCredential and Integrated Security"); + + _poolGroupProviderInfo = (SqlConnectionPoolGroupProviderInfo)providerInfo; + _fResetConnection = connectionOptions.ConnectionReset; + if (_fResetConnection && _recoverySessionData == null) + { + _originalDatabase = connectionOptions.InitialCatalog; + _originalLanguage = connectionOptions.CurrentLanguage; + } + + _timeoutErrorInternal = new SqlConnectionTimeoutErrorInternal(); + _credential = credential; + + _parserLock.Wait(canReleaseFromAnyThread: false); + + // In case of error, let ourselves know that we already own the parser lock + ThreadHasParserLockForClose = true; + + try + { + _timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout); + + // If transient fault handling is enabled then we can retry the login up to the + // ConnectRetryCount. + int connectionEstablishCount = applyTransientFaultHandling + ? connectionOptions.ConnectRetryCount + 1 + : 1; + + // Max value of transientRetryInterval is 60*1000 ms. The max value allowed for + // ConnectRetryInterval is 60 + int transientRetryIntervalInMilliSeconds = connectionOptions.ConnectRetryInterval * 1000; + for (int i = 0; i < connectionEstablishCount; i++) + { + try + { + OpenLoginEnlist( + _timeout, + connectionOptions, + credential, + newPassword, + newSecurePassword, + redirectedUserInstance); + break; + } + catch (SqlException sqlex) + { + if (connectionEstablishCount == i + 1 + || !applyTransientFaultHandling + || _timeout.IsExpired + || _timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds + || !IsTransientError(sqlex)) + { + throw; + } + else + { + Thread.Sleep(transientRetryIntervalInMilliSeconds); + } + } + } + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + finally + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + } + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnectionTds.ctor | ADV | " + + $"Object ID {ObjectID}, " + + $"constructed new TDS internal connection"); + } + + #endregion + #region Properties // @TODO: Make internal @@ -1485,6 +1654,15 @@ internal void OnLoginAck(SqlLoginAck rec) } } + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) + { + return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + } + internal override void ValidateConnectionForExecute(SqlCommand command) { TdsParser parser = _parser; From 07e4eced9c80353519040e111d64b4dd936b73c3 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 13:35:35 -0600 Subject: [PATCH 46/56] Start merging the SyncAsyncLock --- .../SqlClient/SqlInternalConnectionTds.cs | 13 +-------- .../SqlClient/SqlInternalConnectionTds.cs | 2 +- .../SqlClient/SqlInternalConnectionTds.cs | 29 +++++++++++++++++++ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8f50a8926e..0b78a54edb 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -108,18 +108,7 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // FOR SYNCHRONIZATION IN TdsParser - // How to use these locks: - // 1. Whenever writing to the connection (with the exception of Cancellation) the _parserLock MUST be taken - // 2. _parserLock will also be taken during close (to prevent closing in the middle of a write) - // 3. Whenever you have the _parserLock and are calling a method that would cause the connection to close if it failed (with the exception of any writing method), you MUST set ThreadHasParserLockForClose to true - // * This is to prevent the connection deadlocking with itself (since you already have the _parserLock, and Closing the connection will attempt to re-take that lock) - // * It is safe to set ThreadHasParserLockForClose to true when writing as well, but it is unnecessary - // * If you have a method that takes _parserLock, it is a good idea check ThreadHasParserLockForClose first (if you don't expect _parserLock to be taken by something higher on the stack, then you should at least assert that it is false) - // 4. ThreadHasParserLockForClose is thread-specific - this means that you must set it to false before returning a Task, and set it back to true in the continuation - // 5. ThreadHasParserLockForClose should only be modified if you currently own the _parserLock - // 6. Reading ThreadHasParserLockForClose is thread-safe - internal class SyncAsyncLock + internal partial class SyncAsyncLock { private SemaphoreSlim _semaphore = new SemaphoreSlim(1); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 6a4eb63608..0884768850 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -118,7 +118,7 @@ internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, // 4. ThreadHasParserLockForClose is thread-specific - this means that you must set it to false before returning a Task, and set it back to true in the continuation // 5. ThreadHasParserLockForClose should only be modified if you currently own the _parserLock // 6. Reading ThreadHasParserLockForClose is thread-safe - internal class SyncAsyncLock + internal partial class SyncAsyncLock { private SemaphoreSlim _semaphore = new SemaphoreSlim(1); 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 1a084942f9..c5328ba48e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -3563,5 +3563,34 @@ private bool TryGetFedAuthTokenLocked( } #endregion + + /// + /// How to use these locks: + /// 1. Whenever writing to the connection (except Cancellation) the _parserLock MUST be + /// taken. + /// 2. _parserLock will also be taken during close (to prevent closing in the middle of a + /// write operation) + /// 3. Whenever you have the _parserLock and are calling a method that would cause the + /// connection to close if it failed (except for any writing method), you MUST set + /// ThreadHasParserLockForClose to true. + /// a. This is to prevent the connection deadlocking with itself (since you already have + /// the _parserLock, and Closing the connection will attempt to re-take that lock). + /// b. It is safe to set ThreadHasParserLockForClose to true when writing as well, but + /// it is unnecessary. + /// c. If you have a method that takes _parserLock, it is a good idea check + /// ThreadHasParserLockForClose first (if you don't expect _parserLock to be taken by + /// something higher on the stack, then you should at least assert that it is false). + /// 4. ThreadHasParserLockForClose is thread-specific - this means that you must set it to + /// false before returning a Task, and set it back to true in the continuation. + /// 5. ThreadHasParserLockForClose should only be modified if you currently own the _parserLock + /// 6. Reading ThreadHasParserLockForClose is thread-safe + /// + // @TODO: This really should be private + // @TODO: This is a ridiculous number of rules to use this class - it is guaranteed someone will fail these rules. + internal partial class SyncAsyncLock + { + + } + } } From 55aeb403ade4553d7d2d4af15779f231d8347b26 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 13:45:42 -0600 Subject: [PATCH 47/56] Merge SyncAsyncLock._semaphor, SyncAsyncLock.CanBeReleasedFromAnyThread, SyncAsyncLock.ThreadMayHaveLock (moved to property) --- .../SqlClient/SqlInternalConnectionTds.cs | 17 ------------- .../SqlClient/SqlInternalConnectionTds.cs | 17 ------------- .../SqlClient/SqlInternalConnectionTds.cs | 25 ++++++++++++++++--- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 12 ++++----- .../Data/SqlClient/TdsParserStateObject.cs | 2 +- 5 files changed, 28 insertions(+), 45 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0b78a54edb..06e0b877b7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -110,8 +110,6 @@ internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, { internal partial class SyncAsyncLock { - private SemaphoreSlim _semaphore = new SemaphoreSlim(1); - internal void Wait(bool canReleaseFromAnyThread) { Monitor.Enter(_semaphore); // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods @@ -180,21 +178,6 @@ internal void Release() Monitor.Exit(_semaphore); } } - - - internal bool CanBeReleasedFromAnyThread - { - get - { - return _semaphore.CurrentCount == 0; - } - } - - // Necessary but not sufficient condition for thread to have lock (since semaphore may be obtained by any thread) - internal bool ThreadMayHaveLock() - { - return Monitor.IsEntered(_semaphore) || _semaphore.CurrentCount == 0; - } } internal SyncAsyncLock _parserLock = new SyncAsyncLock(); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0884768850..15403a2b1b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -120,8 +120,6 @@ internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, // 6. Reading ThreadHasParserLockForClose is thread-safe internal partial class SyncAsyncLock { - private SemaphoreSlim _semaphore = new SemaphoreSlim(1); - internal void Wait(bool canReleaseFromAnyThread) { Monitor.Enter(_semaphore); // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods @@ -190,21 +188,6 @@ internal void Release() Monitor.Exit(_semaphore); } } - - - internal bool CanBeReleasedFromAnyThread - { - get - { - return _semaphore.CurrentCount == 0; - } - } - - // Necessary but not sufficient condition for thread to have lock (since semaphore may be obtained by any thread) - internal bool ThreadMayHaveLock() - { - return Monitor.IsEntered(_semaphore) || _semaphore.CurrentCount == 0; - } } internal SyncAsyncLock _parserLock = new SyncAsyncLock(); 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 c5328ba48e..7e9a4a971e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -642,8 +642,10 @@ internal bool ThreadHasParserLockForClose get => _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId; set { - Debug.Assert(_parserLock.ThreadMayHaveLock(), "Should not modify ThreadHasParserLockForClose without taking the lock first"); - Debug.Assert(_threadIdOwningParserLock == -1 || _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId, "Another thread already claims to own the parser lock"); + Debug.Assert(_parserLock.ThreadMayHaveLock, + "Should not modify ThreadHasParserLockForClose without taking the lock first"); + Debug.Assert(_threadIdOwningParserLock == -1 || _threadIdOwningParserLock == Thread.CurrentThread.ManagedThreadId, + "Another thread already claims to own the parser lock"); if (value) { @@ -1805,7 +1807,7 @@ protected override bool ObtainAdditionalLocksForClose() { bool obtainParserLock = !ThreadHasParserLockForClose; - Debug.Assert(obtainParserLock || _parserLock.ThreadMayHaveLock(), + Debug.Assert(obtainParserLock || _parserLock.ThreadMayHaveLock, "Thread claims to have lock, but lock is not taken"); if (obtainParserLock) @@ -2050,7 +2052,7 @@ private void ExecuteTransaction2005( bool mustPutSession = false; bool releaseConnectionLock = false; - Debug.Assert(!ThreadHasParserLockForClose || _parserLock.ThreadMayHaveLock(), + Debug.Assert(!ThreadHasParserLockForClose || _parserLock.ThreadMayHaveLock, "Thread claims to have parser lock, but lock is not taken"); if (!ThreadHasParserLockForClose) @@ -3589,7 +3591,22 @@ private bool TryGetFedAuthTokenLocked( // @TODO: This is a ridiculous number of rules to use this class - it is guaranteed someone will fail these rules. internal partial class SyncAsyncLock { + private SemaphoreSlim _semaphore = new SemaphoreSlim(1); + internal bool CanBeReleasedFromAnyThread + { + get => _semaphore.CurrentCount == 0; + } + + /// + /// Necessary but not sufficient condition for thread to have lock (since semaphore may + /// be obtained by any thread). + /// + // @TODO: This is only used for debug checks. It's really dicey, too, calling it "May Have" - it should either have it or not! + internal bool ThreadMayHaveLock + { + get => Monitor.IsEntered(_semaphore) || CanBeReleasedFromAnyThread; + } } } 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 eb72b8d6b1..44da411e4d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -1608,7 +1608,7 @@ internal void RollbackOrphanedAPITransactions() internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command = null, bool callerHasConnectionLock = false, bool asyncClose = false) { - Debug.Assert(!callerHasConnectionLock || _connHandler._parserLock.ThreadMayHaveLock(), "Caller claims to have lock, but connection lock is not taken"); + Debug.Assert(!callerHasConnectionLock || _connHandler._parserLock.ThreadMayHaveLock, "Caller claims to have lock, but connection lock is not taken"); SqlException exception = null; bool breakConnection; @@ -1908,7 +1908,7 @@ internal void CheckResetConnection(TdsParserStateObject stateObj) stateObj.ResetBuffer(); Debug.Assert(_connHandler != null, "SqlConnectionInternalTds handler can not be null at this point."); stateObj.AddError(new SqlError(TdsEnums.TIMEOUT_EXPIRED, (byte)0x00, TdsEnums.MIN_ERROR_CLASS, _server, _connHandler.TimeoutErrorInternal.GetErrorMessage(), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); - Debug.Assert(_connHandler._parserLock.ThreadMayHaveLock(), "Thread is writing without taking the connection lock"); + Debug.Assert(_connHandler._parserLock.ThreadMayHaveLock, "Thread is writing without taking the connection lock"); ThrowExceptionAndWarning(stateObj, callerHasConnectionLock: true); } } @@ -9671,7 +9671,7 @@ internal SqlDataReader TdsExecuteTransactionManagerRequest( // won't stomp on anything. - Debug.Assert(!_connHandler.ThreadHasParserLockForClose || _connHandler._parserLock.ThreadMayHaveLock(), "Thread claims to have parser lock, but lock is not taken"); + Debug.Assert(!_connHandler.ThreadHasParserLockForClose || _connHandler._parserLock.ThreadMayHaveLock, "Thread claims to have parser lock, but lock is not taken"); bool callerHasConnectionLock = _connHandler.ThreadHasParserLockForClose; // If the thread already claims to have the parser lock, then we will let the caller handle releasing it if (!callerHasConnectionLock) { @@ -9869,7 +9869,7 @@ internal void FailureCleanup(TdsParserStateObject stateObj, Exception e) if (old_outputPacketNumber != 1 && _state == TdsParserState.OpenLoggedIn) { - Debug.Assert(_connHandler._parserLock.ThreadMayHaveLock(), "Should not be calling into FailureCleanup without first taking the parser lock"); + Debug.Assert(_connHandler._parserLock.ThreadMayHaveLock, "Should not be calling into FailureCleanup without first taking the parser lock"); bool originalThreadHasParserLock = _connHandler.ThreadHasParserLockForClose; try @@ -9917,7 +9917,7 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques // Only need to take the lock if neither the thread nor the caller claims to already have it bool needToTakeParserLock = (!callerHasConnectionLock) && (!_connHandler.ThreadHasParserLockForClose); Debug.Assert(!_connHandler.ThreadHasParserLockForClose || sync, "Thread shouldn't claim to have the parser lock if we are doing async writes"); // Since we have the possibility of pending with async writes, make sure the thread doesn't claim to already have the lock - Debug.Assert(needToTakeParserLock || _connHandler._parserLock.ThreadMayHaveLock(), "Thread or caller claims to have connection lock, but lock is not taken"); + Debug.Assert(needToTakeParserLock || _connHandler._parserLock.ThreadMayHaveLock, "Thread or caller claims to have connection lock, but lock is not taken"); bool releaseConnectionLock = false; if (needToTakeParserLock) @@ -10034,7 +10034,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout Debug.Assert(!firstCall || startRpc == 0, "startRpc is not 0 on first call"); Debug.Assert(!firstCall || startParam == 0, "startParam is not 0 on first call"); Debug.Assert(!firstCall || !_connHandler.ThreadHasParserLockForClose, "Thread should not already have connection lock"); - Debug.Assert(firstCall || _connHandler._parserLock.ThreadMayHaveLock(), "Connection lock not taken after the first call"); + Debug.Assert(firstCall || _connHandler._parserLock.ThreadMayHaveLock, "Connection lock not taken after the first call"); try { _SqlRPC rpcext = null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 6a9b2d7369..7d29ebc23f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -3196,7 +3196,7 @@ private Task WriteSni(bool canAccumulate) _outBuff.AsSpan(0, _outBytesUsed).Clear(); } - Debug.Assert(Parser.Connection._parserLock.ThreadMayHaveLock(), "Thread is writing without taking the connection lock"); + Debug.Assert(Parser.Connection._parserLock.ThreadMayHaveLock, "Thread is writing without taking the connection lock"); Task task = SNIWritePacket(packet, out _, canAccumulate, callerHasConnectionLock: true); // Check to see if the timeout has occurred. This time out code is special case code to allow BCP writes to timeout. Eventually we should make all writes timeout. From ff352f02cb0b9eced9df49f9a9766667ddf8957f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 13:50:07 -0600 Subject: [PATCH 48/56] Merge SyncAsyncLock.Release, SyncAsyncLock.Wait --- .../SqlClient/SqlInternalConnectionTds.cs | 71 ---------------- .../SqlClient/SqlInternalConnectionTds.cs | 83 ------------------- .../SqlClient/SqlInternalConnectionTds.cs | 75 ++++++++++++++++- 3 files changed, 73 insertions(+), 156 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 06e0b877b7..949705b017 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -108,77 +108,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - internal partial class SyncAsyncLock - { - internal void Wait(bool canReleaseFromAnyThread) - { - Monitor.Enter(_semaphore); // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods - if (canReleaseFromAnyThread || _semaphore.CurrentCount == 0) - { - _semaphore.Wait(); - if (canReleaseFromAnyThread) - { - Monitor.Exit(_semaphore); - } - else - { - _semaphore.Release(); - } - } - } - - internal void Wait(bool canReleaseFromAnyThread, int timeout, ref bool lockTaken) - { - lockTaken = false; - bool hasMonitor = false; - try - { - Monitor.TryEnter(_semaphore, timeout, ref hasMonitor); // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods - if (hasMonitor) - { - if ((canReleaseFromAnyThread) || (_semaphore.CurrentCount == 0)) - { - if (_semaphore.Wait(timeout)) - { - if (canReleaseFromAnyThread) - { - Monitor.Exit(_semaphore); - hasMonitor = false; - } - else - { - _semaphore.Release(); - } - lockTaken = true; - } - } - else - { - lockTaken = true; - } - } - } - finally - { - if ((!lockTaken) && (hasMonitor)) - { - Monitor.Exit(_semaphore); - } - } - } - - internal void Release() - { - if (_semaphore.CurrentCount == 0) - { // semaphore methods were used for locking - _semaphore.Release(); - } - else - { - Monitor.Exit(_semaphore); - } - } - } internal SyncAsyncLock _parserLock = new SyncAsyncLock(); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 15403a2b1b..c254813f02 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -107,89 +107,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // FOR SYNCHRONIZATION IN TdsParser - // How to use these locks: - // 1. Whenever writing to the connection (with the exception of Cancellation) the _parserLock MUST be taken - // 2. _parserLock will also be taken during close (to prevent closing in the middle of a write) - // 3. Whenever you have the _parserLock and are calling a method that would cause the connection to close if it failed (with the exception of any writing method), you MUST set ThreadHasParserLockForClose to true - // * This is to prevent the connection deadlocking with itself (since you already have the _parserLock, and Closing the connection will attempt to re-take that lock) - // * It is safe to set ThreadHasParserLockForClose to true when writing as well, but it is unnecessary - // * If you have a method that takes _parserLock, it is a good idea check ThreadHasParserLockForClose first (if you don't expect _parserLock to be taken by something higher on the stack, then you should at least assert that it is false) - // 4. ThreadHasParserLockForClose is thread-specific - this means that you must set it to false before returning a Task, and set it back to true in the continuation - // 5. ThreadHasParserLockForClose should only be modified if you currently own the _parserLock - // 6. Reading ThreadHasParserLockForClose is thread-safe - internal partial class SyncAsyncLock - { - internal void Wait(bool canReleaseFromAnyThread) - { - Monitor.Enter(_semaphore); // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods - if (canReleaseFromAnyThread || _semaphore.CurrentCount == 0) - { - _semaphore.Wait(); - if (canReleaseFromAnyThread) - { - Monitor.Exit(_semaphore); - } - else - { - _semaphore.Release(); - } - } - } - - internal void Wait(bool canReleaseFromAnyThread, int timeout, ref bool lockTaken) - { - lockTaken = false; - bool hasMonitor = false; - try - { - Monitor.TryEnter(_semaphore, timeout, ref hasMonitor); // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods - if (hasMonitor) - { - if ((canReleaseFromAnyThread) || (_semaphore.CurrentCount == 0)) - { - if (_semaphore.Wait(timeout)) - { - if (canReleaseFromAnyThread) - { - Monitor.Exit(_semaphore); - hasMonitor = false; - } - else - { - _semaphore.Release(); - } - lockTaken = true; - } - } - else - { - lockTaken = true; - } - } - } - finally - { - if ((!lockTaken) && (hasMonitor)) - { - Monitor.Exit(_semaphore); - } - } - } - - internal void Release() - { - if (_semaphore.CurrentCount == 0) - { // semaphore methods were used for locking - _semaphore.Release(); - } - else - { - Monitor.Exit(_semaphore); - } - } - } - internal SyncAsyncLock _parserLock = new SyncAsyncLock(); } 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 7e9a4a971e..14924ccdf0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -3589,7 +3589,7 @@ private bool TryGetFedAuthTokenLocked( /// // @TODO: This really should be private // @TODO: This is a ridiculous number of rules to use this class - it is guaranteed someone will fail these rules. - internal partial class SyncAsyncLock + internal class SyncAsyncLock { private SemaphoreSlim _semaphore = new SemaphoreSlim(1); @@ -3607,7 +3607,78 @@ internal bool ThreadMayHaveLock { get => Monitor.IsEntered(_semaphore) || CanBeReleasedFromAnyThread; } - } + internal void Release() + { + if (_semaphore.CurrentCount == 0) + { + // Semaphore methods were used for locking + _semaphore.Release(); + } + else + { + Monitor.Exit(_semaphore); + } + } + + internal void Wait(bool canReleaseFromAnyThread) + { + // Semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods + Monitor.Enter(_semaphore); + if (canReleaseFromAnyThread || CanBeReleasedFromAnyThread) + { + _semaphore.Wait(); + if (canReleaseFromAnyThread) + { + Monitor.Exit(_semaphore); + } + else + { + _semaphore.Release(); + } + } + } + + internal void Wait(bool canReleaseFromAnyThread, int timeout, ref bool lockTaken) + { + lockTaken = false; + bool hasMonitor = false; + try + { + // semaphore is used as lock object, no relation to SemaphoreSlim.Wait/Release methods + Monitor.TryEnter(_semaphore, timeout, ref hasMonitor); + if (hasMonitor) + { + if (canReleaseFromAnyThread || CanBeReleasedFromAnyThread) + { + if (_semaphore.Wait(timeout)) + { + if (canReleaseFromAnyThread) + { + Monitor.Exit(_semaphore); + hasMonitor = false; + } + else + { + _semaphore.Release(); + } + lockTaken = true; + } + } + else + { + lockTaken = true; + } + } + } + finally + { + if (!lockTaken && hasMonitor) + { + Monitor.Exit(_semaphore); + } + } + } + } } } From 7d887d6d4329c4850c3cf037f2d4fa6a4e3e7c86 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 13:53:20 -0600 Subject: [PATCH 49/56] Finishing merge of SqlInternalConnectionTds.cs, removing stub --- .../SqlClient/SqlInternalConnectionTds.cs | 6 --- .../SqlClient/SqlInternalConnectionTds.cs | 5 -- .../SqlClient/SqlInternalConnectionTds.cs | 5 +- .../SqlInternalConnectionTds.stub.cs | 49 ------------------- 4 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 949705b017..81abe803c8 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -106,12 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() } } - internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable - { - - internal SyncAsyncLock _parserLock = new SyncAsyncLock(); - } - internal sealed class ServerInfo { internal string ExtendedServerName { get; private set; } // the resolved servername with protocol diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c254813f02..a38757ecd5 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -105,11 +105,6 @@ public void AssertUnrecoverableStateCountIsCorrect() } } - internal sealed partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable - { - internal SyncAsyncLock _parserLock = new SyncAsyncLock(); - } - internal sealed class ServerInfo { internal string ExtendedServerName { get; private set; } // the resolved servername with protocol 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 14924ccdf0..e91b784a77 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -22,7 +22,7 @@ namespace Microsoft.Data.SqlClient { - internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDisposable + internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { #region Constants @@ -202,6 +202,9 @@ internal partial class SqlInternalConnectionTds : SqlInternalConnection, IDispos // @TODO: Should be private and accessed via internal property internal bool IsVectorSupportEnabled = false; + // @TODO: This should be private + internal SyncAsyncLock _parserLock = new SyncAsyncLock(); + // @TODO: Should be private and accessed via internal property internal SQLDNSInfo pendingSQLDNSObject = null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs deleted file mode 100644 index b2a450d90d..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs +++ /dev/null @@ -1,49 +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. - -// @TODO: This is only a stub class for clearing errors while merging other files. - -using System; -using System.Security; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient.ConnectionPool; - -namespace Microsoft.Data.SqlClient -{ - internal partial class SqlInternalConnectionTds - { - internal SyncAsyncLock _parserLock = null; - - internal SqlInternalConnectionTds( - DbConnectionPoolIdentity identity, - SqlConnectionString connectionOptions, - SqlCredential credential, - object providerInfo, - string newPassword, - SecureString newSecurePassword, - bool redirectedUserInterface, - SqlConnectionString userConnectionOptions = null, - SessionData reconnectSessionData = null, - bool applyTransientFaultHandling = false, - string accessToken = null, - IDbConnectionPool pool = null, - Func> accessTokenCallback = null) - { - } - - internal void Dispose() { } - - internal void DoomThisConnection() { } - - internal class SyncAsyncLock - { - internal bool CanBeReleasedFromAnyThread { get; set; } - - internal void Release() { } - - internal void Wait(bool canReleaseFromAnyThread) {} - } - } -} From 11f9839222060731543fc2a66143b941fd2445c2 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 14:03:50 -0600 Subject: [PATCH 50/56] Move SessionStateRecord into new Connection namespace --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Data/SqlClient/SqlInternalConnectionTds.cs | 9 +-------- .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Data/SqlClient/SqlInternalConnectionTds.cs | 9 +-------- .../SqlClient/Connection/SessionStateRecord.cs | 17 +++++++++++++++++ .../Data/SqlClient/SqlInternalConnectionTds.cs | 1 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 12 ++++-------- 7 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionStateRecord.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 bdddf73f37..ce992a6605 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\SessionStateRecord.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 81abe803c8..409222e46e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -17,19 +17,12 @@ using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Identity.Client; namespace Microsoft.Data.SqlClient { - internal sealed class SessionStateRecord - { - internal bool _recoverable; - internal uint _version; - internal int _dataLength; - internal byte[] _data; - } - internal sealed class SessionData { internal const int _maxNumberOfSessionStates = 256; 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 e759baa926..cbfeb1c921 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\SessionStateRecord.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index a38757ecd5..728375fc0c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -18,19 +18,12 @@ using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Identity.Client; namespace Microsoft.Data.SqlClient { - internal sealed class SessionStateRecord - { - internal bool _recoverable; - internal uint _version; - internal int _dataLength; - internal byte[] _data; - } - internal sealed class SessionData { internal const int _maxNumberOfSessionStates = 256; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionStateRecord.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionStateRecord.cs new file mode 100644 index 0000000000..2b9fd08656 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionStateRecord.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.Data.SqlClient.Connection +{ + // @TODO: if this is a "record", why not make it a record? + // @TODO: All names should be renamed to be follow guidelines. + // @TODO: All the values in this "record" are assigned but allegedly never used (might be used in parser but since it's not analyzed it's not showing up) + internal sealed class SessionStateRecord + { + internal bool _recoverable; + internal uint _version; + internal int _dataLength; + internal byte[] _data; + } +} 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 e91b784a77..a2f880e57d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -17,6 +17,7 @@ using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Identity.Client; 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 44da411e4d..29a0708756 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -12,31 +12,27 @@ using System.Globalization; using System.IO; using System.Security.Authentication; -#if NETFRAMEWORK -using System.Runtime.CompilerServices; -#endif using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using Interop.Common.Sni; -#if NETFRAMEWORK -using Interop.Windows.Sni; -#endif using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; using Microsoft.Data.Sql; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.LocalDb; using Microsoft.Data.SqlClient.Server; using Microsoft.Data.SqlClient.UserAgent; using Microsoft.Data.SqlClient.Utilities; - +using Microsoft.SqlServer.Server; #if NETFRAMEWORK +using System.Runtime.CompilerServices; +using Interop.Windows.Sni; using Microsoft.Data.SqlTypes; #endif -using Microsoft.SqlServer.Server; namespace Microsoft.Data.SqlClient { From 8ac4724253664a6e0d87fc19cd1f93d12f49d5a0 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 14:08:36 -0600 Subject: [PATCH 51/56] Introduce partial for SessionData in Connection folder --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs | 2 +- .../src/Microsoft/Data/SqlClient/Connection/SessionData.cs | 7 +++++++ 5 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.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 ce992a6605..1d308fe1f6 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\SessionData.cs + Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 409222e46e..00ff1a6afa 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -23,7 +23,7 @@ namespace Microsoft.Data.SqlClient { - internal sealed class SessionData + internal sealed partial class SessionData { internal const int _maxNumberOfSessionStates = 256; internal uint _tdsVersion; 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 cbfeb1c921..ca67e7b2e6 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\SessionData.cs + Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 728375fc0c..517d39d920 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -24,7 +24,7 @@ namespace Microsoft.Data.SqlClient { - internal sealed class SessionData + internal sealed partial class SessionData { internal const int _maxNumberOfSessionStates = 256; internal uint _tdsVersion; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs new file mode 100644 index 0000000000..44aa88ee28 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Data.SqlClient +{ + internal sealed partial class SessionData + { + + } +} From 11e1b71e0e5078f8070cf7b1de465e76ede7170d Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 12:00:20 -0600 Subject: [PATCH 52/56] Merge the SessionData class --- .../SqlClient/SqlInternalConnectionTds.cs | 76 --------------- .../SqlClient/SqlInternalConnectionTds.cs | 74 -------------- .../Data/SqlClient/Connection/SessionData.cs | 97 ++++++++++++++++++- .../Data/SqlClient/SessionData.stub.cs | 12 --- 4 files changed, 96 insertions(+), 163 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SessionData.stub.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 00ff1a6afa..ed8e39ef04 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -23,82 +23,6 @@ namespace Microsoft.Data.SqlClient { - internal sealed partial class SessionData - { - internal const int _maxNumberOfSessionStates = 256; - internal uint _tdsVersion; - internal bool _encrypted; - - internal string _database; - internal SqlCollation _collation; - internal string _language; - - internal string _initialDatabase; - internal SqlCollation _initialCollation; - internal string _initialLanguage; - - internal byte _unrecoverableStatesCount = 0; - - // @TODO: Introduce record/struct type to replace the tuple. - internal Dictionary> _resolvedAliases; - -#if DEBUG - internal bool _debugReconnectDataApplied; -#endif - - internal SessionStateRecord[] _delta = new SessionStateRecord[_maxNumberOfSessionStates]; - internal bool _deltaDirty = false; - internal byte[][] _initialState = new byte[_maxNumberOfSessionStates][]; - - public SessionData(SessionData recoveryData) - { - _initialDatabase = recoveryData._initialDatabase; - _initialCollation = recoveryData._initialCollation; - _initialLanguage = recoveryData._initialLanguage; - _resolvedAliases = recoveryData._resolvedAliases; - - for (int i = 0; i < _maxNumberOfSessionStates; i++) - { - if (recoveryData._initialState[i] != null) - { - _initialState[i] = (byte[])recoveryData._initialState[i].Clone(); - } - } - } - - public SessionData() - { - _resolvedAliases = new Dictionary>(2); - } - - public void Reset() - { - _database = null; - _collation = null; - _language = null; - if (_deltaDirty) - { - Array.Clear(_delta, 0, _delta.Length); - _deltaDirty = false; - } - _unrecoverableStatesCount = 0; - } - - [Conditional("DEBUG")] - public void AssertUnrecoverableStateCountIsCorrect() - { - byte unrecoverableCount = 0; - foreach (var state in _delta) - { - if (state != null && !state._recoverable) - { - unrecoverableCount++; - } - } - Debug.Assert(unrecoverableCount == _unrecoverableStatesCount, "Unrecoverable count does not match"); - } - } - internal sealed class ServerInfo { internal string ExtendedServerName { get; private set; } // the resolved servername with protocol diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 517d39d920..090f1f53b8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -24,80 +24,6 @@ namespace Microsoft.Data.SqlClient { - internal sealed partial class SessionData - { - internal const int _maxNumberOfSessionStates = 256; - internal uint _tdsVersion; - internal bool _encrypted; - - internal string _database; - internal SqlCollation _collation; - internal string _language; - - internal string _initialDatabase; - internal SqlCollation _initialCollation; - internal string _initialLanguage; - - internal byte _unrecoverableStatesCount = 0; - internal Dictionary> _resolvedAliases; - -#if DEBUG - internal bool _debugReconnectDataApplied; -#endif - - internal SessionStateRecord[] _delta = new SessionStateRecord[_maxNumberOfSessionStates]; - internal bool _deltaDirty = false; - internal byte[][] _initialState = new byte[_maxNumberOfSessionStates][]; - - public SessionData(SessionData recoveryData) - { - _initialDatabase = recoveryData._initialDatabase; - _initialCollation = recoveryData._initialCollation; - _initialLanguage = recoveryData._initialLanguage; - _resolvedAliases = recoveryData._resolvedAliases; - - for (int i = 0; i < _maxNumberOfSessionStates; i++) - { - if (recoveryData._initialState[i] != null) - { - _initialState[i] = (byte[])recoveryData._initialState[i].Clone(); - } - } - } - - public SessionData() - { - _resolvedAliases = new Dictionary>(2); - } - - public void Reset() - { - _database = null; - _collation = null; - _language = null; - if (_deltaDirty) - { - _delta = new SessionStateRecord[_maxNumberOfSessionStates]; - _deltaDirty = false; - } - _unrecoverableStatesCount = 0; - } - - [Conditional("DEBUG")] - public void AssertUnrecoverableStateCountIsCorrect() - { - byte unrecoverableCount = 0; - foreach (var state in _delta) - { - if (state != null && !state._recoverable) - { - unrecoverableCount++; - } - } - Debug.Assert(unrecoverableCount == _unrecoverableStatesCount, "Unrecoverable count does not match"); - } - } - internal sealed class ServerInfo { internal string ExtendedServerName { get; private set; } // the resolved servername with protocol diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs index 44aa88ee28..c66d708466 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs @@ -1,7 +1,102 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using Microsoft.Data.SqlClient.Connection; + namespace Microsoft.Data.SqlClient { - internal sealed partial class SessionData + internal sealed class SessionData { + // @TODO: Rename to match guidelines + internal const int _maxNumberOfSessionStates = 256; + + #region Fields + // @TODO: Use properties! + + #if DEBUG + internal bool _debugReconnectDataApplied; + #endif + + internal SqlCollation _collation; + internal string _database; + internal SessionStateRecord[] _delta = new SessionStateRecord[_maxNumberOfSessionStates]; + internal bool _deltaDirty = false; + internal bool _encrypted; + internal SqlCollation _initialCollation; + internal string _initialLanguage; + internal string _initialDatabase; + internal byte[][] _initialState = new byte[_maxNumberOfSessionStates][]; + internal string _language; + + // @TODO: Introduce record/struct type to replace the tuple. + internal Dictionary> _resolvedAliases; + + internal uint _tdsVersion; + internal byte _unrecoverableStatesCount = 0; + + #endregion + + #region Constructors + + public SessionData() + { + _resolvedAliases = new Dictionary>(2); + } + + public SessionData(SessionData recoveryData) + { + _initialDatabase = recoveryData._initialDatabase; + _initialCollation = recoveryData._initialCollation; + _initialLanguage = recoveryData._initialLanguage; + _resolvedAliases = recoveryData._resolvedAliases; + + for (int i = 0; i < _maxNumberOfSessionStates; i++) + { + if (recoveryData._initialState[i] != null) + { + _initialState[i] = (byte[])recoveryData._initialState[i].Clone(); + } + } + } + + #endregion + + #region Methods + + [Conditional("DEBUG")] + public void AssertUnrecoverableStateCountIsCorrect() + { + byte unrecoverableCount = 0; + foreach (var state in _delta) + { + if (state != null && !state._recoverable) + { + unrecoverableCount++; + } + } + + Debug.Assert(unrecoverableCount == _unrecoverableStatesCount, + "Unrecoverable count does not match"); + } + + public void Reset() + { + _database = null; + _collation = null; + _language = null; + if (_deltaDirty) + { + Array.Clear(_delta, 0, _delta.Length); + _deltaDirty = false; + } + + _unrecoverableStatesCount = 0; + } + #endregion } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SessionData.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SessionData.stub.cs deleted file mode 100644 index 8911f38626..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SessionData.stub.cs +++ /dev/null @@ -1,12 +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. - -// @TODO: This is only a stub class for removing clearing errors while merging other files. - -namespace Microsoft.Data.SqlClient -{ - internal class SessionData - { - } -} From ef412fc05fe09d737b969997290fc569df473258 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 12:21:45 -0600 Subject: [PATCH 53/56] Merge ServerInfo class --- .../src/Microsoft.Data.SqlClient.csproj | 3 + .../SqlClient/SqlInternalConnectionTds.cs | 96 ------------- .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 + .../SqlClient/SqlInternalConnectionTds.cs | 97 ------------- .../Data/SqlClient/Connection/ServerInfo.cs | 131 ++++++++++++++++++ 5 files changed, 137 insertions(+), 193 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.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 1d308fe1f6..9b7ce33069 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\ServerInfo.cs + Microsoft\Data\SqlClient\Connection\SessionData.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index ed8e39ef04..d7a3707311 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -23,101 +23,5 @@ namespace Microsoft.Data.SqlClient { - internal sealed class ServerInfo - { - internal string ExtendedServerName { get; private set; } // the resolved servername with protocol - internal string ResolvedServerName { get; private set; } // the resolved servername only - internal string ResolvedDatabaseName { get; private set; } // name of target database after resolution - internal string UserProtocol { get; private set; } // the user specified protocol - internal string ServerSPN { get; private set; } // the server SPN - // The original user-supplied server name from the connection string. - // If connection string has no Data Source, the value is set to string.Empty. - // In case of routing, will be changed to routing destination - internal string UserServerName - { - get - { - return _userServerName; - } - private set - { - _userServerName = value; - } - } - private string _userServerName; - - internal readonly string PreRoutingServerName; - - // Initialize server info from connection options, - internal ServerInfo(SqlConnectionString userOptions) : this(userOptions, userOptions.DataSource, userOptions.ServerSPN) { } - - // Initialize server info from connection options, but override DataSource and ServerSPN with given server name and server SPN - internal ServerInfo(SqlConnectionString userOptions, string serverName, string serverSPN) : this(userOptions, serverName) - { - ServerSPN = serverSPN; - } - - // Initialize server info from connection options, but override DataSource with given server name - private ServerInfo(SqlConnectionString userOptions, string serverName) - { - //----------------- - // Preconditions - Debug.Assert(userOptions != null); - - //----------------- - //Method body - - Debug.Assert(serverName != null, "server name should never be null"); - UserServerName = (serverName ?? string.Empty); // ensure user server name is not null - - UserProtocol = string.Empty; - ResolvedDatabaseName = userOptions.InitialCatalog; - PreRoutingServerName = null; - } - - - // Initialize server info from connection options, but override DataSource with given server name - internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string preRoutingServerName, string serverSPN) - { - //----------------- - // Preconditions - Debug.Assert(userOptions != null && routing != null); - - //----------------- - //Method body - Debug.Assert(routing.ServerName != null, "server name should never be null"); - if (routing == null || routing.ServerName == null) - { - UserServerName = string.Empty; // ensure user server name is not null - } - else - { - UserServerName = string.Format(CultureInfo.InvariantCulture, "{0},{1}", routing.ServerName, routing.Port); - } - PreRoutingServerName = preRoutingServerName; - UserProtocol = TdsEnums.TCP; - SetDerivedNames(UserProtocol, UserServerName); - ResolvedDatabaseName = userOptions.InitialCatalog; - ServerSPN = serverSPN; - } - - internal void SetDerivedNames(string protocol, string serverName) - { - // The following concatenates the specified netlib network protocol to the host string, if netlib is not null - // and the flag is on. This allows the user to specify the network protocol for the connection - but only - // when using the Dbnetlib dll. If the protocol is not specified, the netlib will - // try all protocols in the order listed in the Client Network Utility. Connect will - // then fail if all protocols fail. - if (!string.IsNullOrEmpty(protocol)) - { - ExtendedServerName = protocol + ":" + serverName; - } - else - { - ExtendedServerName = serverName; - } - ResolvedServerName = serverName; - } - } } 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 ca67e7b2e6..763ddedec7 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\ServerInfo.cs + Microsoft\Data\SqlClient\Connection\SessionData.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 090f1f53b8..358303dc81 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -24,101 +24,4 @@ namespace Microsoft.Data.SqlClient { - internal sealed class ServerInfo - { - internal string ExtendedServerName { get; private set; } // the resolved servername with protocol - internal string ResolvedServerName { get; private set; } // the resolved servername only - internal string ResolvedDatabaseName { get; private set; } // name of target database after resolution - internal string UserProtocol { get; private set; } // the user specified protocol - internal string ServerSPN { get; private set; } // the server SPN - - // The original user-supplied server name from the connection string. - // If connection string has no Data Source, the value is set to string.Empty. - // In case of routing, will be changed to routing destination - internal string UserServerName - { - get - { - return _userServerName; - } - private set - { - _userServerName = value; - } - } - private string _userServerName; - - internal readonly string PreRoutingServerName; - - // Initialize server info from connection options, - internal ServerInfo(SqlConnectionString userOptions) : this(userOptions, userOptions.DataSource, userOptions.ServerSPN) { } - - // Initialize server info from connection options, but override DataSource and ServerSPN with given server name and server SPN - internal ServerInfo(SqlConnectionString userOptions, string serverName, string serverSPN) : this(userOptions, serverName) - { - ServerSPN = serverSPN; - } - - // Initialize server info from connection options, but override DataSource with given server name - private ServerInfo(SqlConnectionString userOptions, string serverName) - { - //----------------- - // Preconditions - Debug.Assert(userOptions != null); - - //----------------- - //Method body - - Debug.Assert(serverName != null, "server name should never be null"); - UserServerName = (serverName ?? string.Empty); // ensure user server name is not null - - UserProtocol = userOptions.NetworkLibrary; - ResolvedDatabaseName = userOptions.InitialCatalog; - PreRoutingServerName = null; - } - - - // Initialize server info from connection options, but override DataSource with given server name - internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string preRoutingServerName, string serverSPN) - { - //----------------- - // Preconditions - Debug.Assert(userOptions != null && routing != null); - - //----------------- - //Method body - Debug.Assert(routing.ServerName != null, "server name should never be null"); - if (routing == null || routing.ServerName == null) - { - UserServerName = string.Empty; // ensure user server name is not null - } - else - { - UserServerName = string.Format(CultureInfo.InvariantCulture, "{0},{1}", routing.ServerName, routing.Port); - } - PreRoutingServerName = preRoutingServerName; - UserProtocol = TdsEnums.TCP; - SetDerivedNames(UserProtocol, UserServerName); - ResolvedDatabaseName = userOptions.InitialCatalog; - ServerSPN = serverSPN; - } - - internal void SetDerivedNames(string protocol, string serverName) - { - // The following concatenates the specified netlib network protocol to the host string, if netlib is not null - // and the flag is on. This allows the user to specify the network protocol for the connection - but only - // when using the Dbnetlib dll. If the protocol is not specified, the netlib will - // try all protocols in the order listed in the Client Network Utility. Connect will - // then fail if all protocols fail. - if (!string.IsNullOrEmpty(protocol)) - { - ExtendedServerName = protocol + ":" + serverName; - } - else - { - ExtendedServerName = serverName; - } - ResolvedServerName = serverName; - } - } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs new file mode 100644 index 0000000000..f92736246f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs @@ -0,0 +1,131 @@ +// 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.Diagnostics; + +namespace Microsoft.Data.SqlClient +{ + internal sealed class ServerInfo + { + #region Constructors + + /// + /// Initialize server info from connection options alone. + /// + internal ServerInfo(SqlConnectionString userOptions) + : this(userOptions, userOptions.DataSource, userOptions.ServerSPN) + { } + + /// + /// Initialize server info from connection options, but override DataSource and ServerSPN + /// with given server name and server SPN. + /// + internal ServerInfo(SqlConnectionString userOptions, string serverName, string serverSpn) + : this(userOptions, serverName) + { + ServerSPN = serverSpn; + } + + // + /// + /// Initialize server info from connection options, but override DataSource with given + /// server name. + /// + private ServerInfo(SqlConnectionString userOptions, string serverName) + { + Debug.Assert(userOptions != null); + Debug.Assert(serverName != null, "server name should never be null"); + + // Ensure user server name is not null + UserServerName = serverName ?? string.Empty; + + #if NET + UserProtocol = string.Empty; + #else + UserProtocol = userOptions.NetworkLibrary; + #endif + + ResolvedDatabaseName = userOptions.InitialCatalog; + PreRoutingServerName = null; + } + + /// + /// Initialize server info from connection options, but override DataSource with given + /// server name. + /// + internal ServerInfo( + SqlConnectionString userOptions, + RoutingInfo routing, + string preRoutingServerName, + string serverSpn) + { + Debug.Assert(userOptions != null && routing != null); + Debug.Assert(routing.ServerName != null, "server name should never be null"); + + // Ensure user server name is not null + UserServerName = routing == null || routing.ServerName == null + ? string.Empty + : $"{routing.ServerName}.{routing.Port}"; + + PreRoutingServerName = preRoutingServerName; + UserProtocol = TdsEnums.TCP; + SetDerivedNames(UserProtocol, UserServerName); + ResolvedDatabaseName = userOptions.InitialCatalog; + ServerSPN = serverSpn; + } + + #endregion + + #region Properties + + /// + /// Resolved servername with protocol + /// + internal string ExtendedServerName { get; private set; } + + internal string PreRoutingServerName { get; private set; } + + /// + /// Name of target database after resolution + /// + internal string ResolvedDatabaseName { get; private set; } + + /// + /// Resolved servername only + /// + internal string ResolvedServerName { get; private set; } + + /// + /// Server SPN + /// + internal string ServerSPN { get; private set; } + + /// + /// Original user-supplied server name from the connection string. If connection string + /// has no Data Source, the value is set to string.Empty. In case of routing, will be + /// changed to routing destination. + /// + internal string UserServerName { get; private set; } + + /// + /// User specified protocol + /// + internal string UserProtocol { get; private set; } + + #endregion + + internal void SetDerivedNames(string protocol, string serverName) + { + // The following concatenates the specified netlib network protocol to the host string, + // if netlib is not null and the flag is on. This allows the user to specify the + // network protocol for the connection - but only when using the Dbnetlib dll. If the + // protocol is not specified, the netlib will try all protocols in the order listed in + // the Client Network Utility. Connect will then fail if all protocols fail. + ExtendedServerName = !string.IsNullOrEmpty(protocol) + ? $"{protocol}:{serverName}" + : serverName; + ResolvedServerName = serverName; + } + } +} From 3f49af6ec9fb3fb9143f224ca20dabecbf773aeb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 12:23:34 -0600 Subject: [PATCH 54/56] Remove (now empty) SqlInternalConnectionTds files --- .../src/Microsoft.Data.SqlClient.csproj | 2 -- .../SqlClient/SqlInternalConnectionTds.cs | 27 ------------------- .../netfx/src/Microsoft.Data.SqlClient.csproj | 2 -- .../SqlClient/SqlInternalConnectionTds.cs | 27 ------------------- 4 files changed, 58 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs delete mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.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 9b7ce33069..950d7f83b1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -861,8 +861,6 @@ System\Diagnostics\CodeAnalysis.cs - - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs deleted file mode 100644 index d7a3707311..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.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.Collections.Generic; -using System.Data.Common; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Net.Http.Headers; -using System.Security; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Transactions; -using Microsoft.Data.Common; -using Microsoft.Data.Common.ConnectionString; -using Microsoft.Data.ProviderBase; -using Microsoft.Data.SqlClient.Connection; -using Microsoft.Data.SqlClient.ConnectionPool; -using Microsoft.Identity.Client; - -namespace Microsoft.Data.SqlClient -{ - -} 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 763ddedec7..104b9261e7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -1031,8 +1031,6 @@ System\Runtime\CompilerServices\IsExternalInit.netfx.cs - - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs deleted file mode 100644 index 358303dc81..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.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.Collections.Generic; -using System.Data.Common; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using System.Security; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Transactions; -using Microsoft.Data.Common; -using Microsoft.Data.Common.ConnectionString; -using Microsoft.Data.ProviderBase; -using Microsoft.Data.SqlClient.Connection; -using Microsoft.Data.SqlClient.ConnectionPool; -using Microsoft.Identity.Client; - -namespace Microsoft.Data.SqlClient -{ -} From bef6043767b454e28708401e3441fe2460ea6cf3 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 12 Nov 2025 12:31:42 -0600 Subject: [PATCH 55/56] Put the little classes in the connection namespace --- .../src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs | 2 +- .../src/Microsoft/Data/SqlClient/Connection/SessionData.cs | 2 +- .../src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs | 1 + .../src/Microsoft/Data/SqlClient/SqlConnection.cs | 1 + .../src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs index f92736246f..139eb52ab6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs @@ -4,7 +4,7 @@ using System.Diagnostics; -namespace Microsoft.Data.SqlClient +namespace Microsoft.Data.SqlClient.Connection { internal sealed class ServerInfo { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs index c66d708466..69bc2e4c59 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs @@ -7,7 +7,7 @@ using System.Diagnostics; using Microsoft.Data.SqlClient.Connection; -namespace Microsoft.Data.SqlClient +namespace Microsoft.Data.SqlClient.Connection { internal sealed class SessionData { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs index d23200410c..f70e2e8a12 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Diagnostics; +using Microsoft.Data.SqlClient.Connection; #nullable enable 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 962173b41a..4f9d9ca14f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -20,6 +20,7 @@ using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Data.SqlClient.Diagnostics; using Microsoft.SqlServer.Server; 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 80c3d57343..6f697d37e8 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -14,6 +14,7 @@ using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; #if NET From d40e98ee94044817307cd72b95b22a1f38a34eb0 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 13 Nov 2025 17:54:35 -0600 Subject: [PATCH 56/56] Address comments: * Use string.Format again for UserServerName assignment in ServerInfo.cs * Make fields readonly that were called out by copilot * Fixed typo in comment for _dbConnectionPoolAuthenticationContextKey * Restored TODO comment from @mdaigle * Added `using` for cancellation token source in the middle of active directory junk --- .../Data/SqlClient/Connection/ServerInfo.cs | 4 +++- .../Data/SqlClient/Connection/SessionData.cs | 6 +++--- .../SqlClient/SqlInternalConnectionTds.cs | 20 ++++++++++--------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs index 139eb52ab6..c9f69f165f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Globalization; namespace Microsoft.Data.SqlClient.Connection { @@ -64,9 +65,10 @@ internal ServerInfo( Debug.Assert(routing.ServerName != null, "server name should never be null"); // Ensure user server name is not null + // NOTE: string.Format should be used here to ensure invariant culture is used for port number. UserServerName = routing == null || routing.ServerName == null ? string.Empty - : $"{routing.ServerName}.{routing.Port}"; + : string.Format(CultureInfo.InvariantCulture, "{0},{1}", routing.ServerName, routing.Port); PreRoutingServerName = preRoutingServerName; UserProtocol = TdsEnums.TCP; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs index 69bc2e4c59..b3d629ea1b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs @@ -23,17 +23,17 @@ internal sealed class SessionData internal SqlCollation _collation; internal string _database; - internal SessionStateRecord[] _delta = new SessionStateRecord[_maxNumberOfSessionStates]; + internal readonly SessionStateRecord[] _delta = new SessionStateRecord[_maxNumberOfSessionStates]; internal bool _deltaDirty = false; internal bool _encrypted; internal SqlCollation _initialCollation; internal string _initialLanguage; internal string _initialDatabase; - internal byte[][] _initialState = new byte[_maxNumberOfSessionStates][]; + internal readonly byte[][] _initialState = new byte[_maxNumberOfSessionStates][]; internal string _language; // @TODO: Introduce record/struct type to replace the tuple. - internal Dictionary> _resolvedAliases; + internal readonly Dictionary> _resolvedAliases; internal uint _tdsVersion; internal byte _unrecoverableStatesCount = 0; 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 a2f880e57d..9f7858f2c9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -151,7 +151,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable #endregion // @TODO: Should be private and accessed via internal property - internal byte[] _accessTokenInBytes; + internal readonly byte[] _accessTokenInBytes; // @TODO: Should be private and accessed via internal property // @TODO: Probably a good idea to introduce a delegate type @@ -204,7 +204,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable internal bool IsVectorSupportEnabled = false; // @TODO: This should be private - internal SyncAsyncLock _parserLock = new SyncAsyncLock(); + internal readonly SyncAsyncLock _parserLock = new SyncAsyncLock(); // @TODO: Should be private and accessed via internal property internal SQLDNSInfo pendingSQLDNSObject = null; @@ -228,7 +228,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable // @TODO: Rename for naming conventions (remove f prefix) private bool _fConnectionOpen = false; - private SqlCredential _credential; + private readonly SqlCredential _credential; private string _currentLanguage; @@ -240,7 +240,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable private IDbConnectionPool _dbConnectionPool; /// - /// Ley of the authentication context, built from information found in the FedAuthInfoToken. + /// Key of the authentication context, built from information found in the FedAuthInfoToken. /// private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey; @@ -254,7 +254,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable /// /// Used to lookup info for notification matching Start(). /// - private DbConnectionPoolIdentity _identity; + private readonly DbConnectionPoolIdentity _identity; private string _instanceName = string.Empty; @@ -287,7 +287,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable private SessionData _recoverySessionData; // @TODO: Rename to match naming conventions (remove f prefix) - private bool _fResetConnection; + private readonly bool _fResetConnection; private string _routingDestination = null; @@ -304,7 +304,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable // @TODO: Rename to indicate this has to do with routing private readonly TimeoutTimer _timeout; - private SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; + private readonly SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; #endregion @@ -1792,6 +1792,7 @@ protected override void InternalDeactivate() // 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?"); @@ -2299,6 +2300,7 @@ await authProvider.AcquireTokenAsync(authParamsBuilder)) } else { + // @TODO: _fedAuthToken assignment is identical in both cases - move outside if (_credential != null) { username = _credential.UserId; @@ -2348,7 +2350,7 @@ await authProvider.AcquireTokenAsync(authParamsBuilder)) } SqlAuthenticationParameters parameters = authParamsBuilder; - CancellationTokenSource cts = new(); + using CancellationTokenSource cts = new(); // Use Connection timeout value to cancel token acquire request // after certain period of time.(int) @@ -3595,7 +3597,7 @@ private bool TryGetFedAuthTokenLocked( // @TODO: This is a ridiculous number of rules to use this class - it is guaranteed someone will fail these rules. internal class SyncAsyncLock { - private SemaphoreSlim _semaphore = new SemaphoreSlim(1); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); internal bool CanBeReleasedFromAnyThread {