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..950d7f83b1 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,15 @@
Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs
+
+ Microsoft\Data\SqlClient\Connection\ServerInfo.cs
+
+
+ Microsoft\Data\SqlClient\Connection\SessionData.cs
+
+
+ Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs
+
Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs
@@ -693,6 +702,9 @@
Microsoft\Data\SqlClient\SqlInternalConnection.cs
+
+ Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs
+
Microsoft\Data\SqlClient\SqlInternalTransaction.cs
@@ -849,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 482551fe79..0000000000
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs
+++ /dev/null
@@ -1,3166 +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.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;
- 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)
- {
- 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 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.
- ///
- 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.
- ///
- 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;
-
- // 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.
- 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
- {
- 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
- // 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
- {
- 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
- 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 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();
- 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)
- //
- 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);
- }
-
- // 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
- {
- 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.
- ///
- ///
- /// 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
- ////////////////////////////////////////////////////////////////////////////////////////
- [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);
- 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)
- {
- 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.
- ///
- ///
- ///
- /// 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);
-
- ////////////////////////////////////////////////////////////////////////////////////////
- // 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
- // 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()
- {
- // 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);
- 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);
- }
-
- 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,
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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
- 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);
-
- // 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,
- 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)
- {
- 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.
- // 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);
-
- 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)
- {
- 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
- // 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)
- {
- 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,
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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)
- {
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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 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!");
- 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;
- 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.
- ///
- /// 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.
- ///
- /// 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.
- ///
- /// 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)
- {
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- // 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,
- TaskCompletionSource retry,
- DbConnectionOptions userOptions)
- {
- return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions);
- }
- }
-
- 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 be475493fb..104b9261e7 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,15 @@
Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs
+
+ Microsoft\Data\SqlClient\Connection\ServerInfo.cs
+
+
+ Microsoft\Data\SqlClient\Connection\SessionData.cs
+
+
+ Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs
+
Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs
@@ -857,6 +866,9 @@
Microsoft\Data\SqlClient\SqlInternalConnection.cs
+
+ Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs
+
Microsoft\Data\SqlClient\SqlInternalTransaction.cs
@@ -1019,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 523f1288e5..0000000000
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs
+++ /dev/null
@@ -1,3217 +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.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;
- 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 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.
- ///
- 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.
- ///
- 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;
-
- // 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.
- 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
- {
- 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
- // 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
- {
- 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
- 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 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();
- 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)
- //
- 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);
- }
-
- // 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
- {
- 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.
- ///
- ///
- /// 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
- ////////////////////////////////////////////////////////////////////////////////////////
- [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);
- 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)
- {
- 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.
- ///
- ///
- ///
- /// 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);
-
- ////////////////////////////////////////////////////////////////////////////////////////
- // 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
- // 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()
- {
- // 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);
- 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);
- }
-
- 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,
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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
- 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);
-
- // 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,
- 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)
- {
- 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.
- // 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);
-
- 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)
- {
- 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
- // 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)
- {
- 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,
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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)
- {
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- 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 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!");
- 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;
- 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.
- ///
- /// 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.
- ///
- /// 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.
- ///
- /// 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)
- {
- 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
- ////////////////////////////////////////////////////////////////////////////////////////
-
- // 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,
- TaskCompletionSource retry,
- DbConnectionOptions userOptions)
- {
- return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions);
- }
- }
-
- 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..c9f69f165f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ServerInfo.cs
@@ -0,0 +1,133 @@
+// 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;
+using System.Globalization;
+
+namespace Microsoft.Data.SqlClient.Connection
+{
+ 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
+ // NOTE: string.Format should be used here to ensure invariant culture is used for port number.
+ UserServerName = routing == null || routing.ServerName == null
+ ? string.Empty
+ : string.Format(CultureInfo.InvariantCulture, "{0},{1}", 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;
+ }
+ }
+}
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..b3d629ea1b
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SessionData.cs
@@ -0,0 +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.Connection
+{
+ 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 readonly SessionStateRecord[] _delta = new SessionStateRecord[_maxNumberOfSessionStates];
+ internal bool _deltaDirty = false;
+ internal bool _encrypted;
+ internal SqlCollation _initialCollation;
+ internal string _initialLanguage;
+ internal string _initialDatabase;
+ internal readonly byte[][] _initialState = new byte[_maxNumberOfSessionStates][];
+ internal string _language;
+
+ // @TODO: Introduce record/struct type to replace the tuple.
+ internal readonly 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/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/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/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
- {
- }
-}
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
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?");
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..9f7858f2c9
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs
@@ -0,0 +1,3690 @@
+// 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
+{
+ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable
+ {
+ #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;
+
+ ///
+ /// 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
+
+ #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 readonly 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 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.
+ ///
+ 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;
+
+ ///
+ /// 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: This should be private
+ internal readonly SyncAsyncLock _parserLock = new SyncAsyncLock();
+
+ // @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;
+
+ ///
+ /// 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 readonly SqlCredential _credential;
+
+ private string _currentLanguage;
+
+ private int _currentPacketSize;
+
+ ///
+ /// Pool this connection is associated with, if any.
+ ///
+ private IDbConnectionPool _dbConnectionPool;
+
+ ///
+ /// Key of the authentication context, built from information found in the FedAuthInfoToken.
+ ///
+ private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey;
+
+ // @TODO: Rename to match naming conventions
+ private bool _dnsCachingBeforeRedirect = false;
+
+ private FederatedAuthenticationFeatureExtensionData _fedAuthFeatureExtensionData;
+
+ private SqlFedAuthToken _fedAuthToken = null;
+
+ ///
+ /// Used to lookup info for notification matching Start().
+ ///
+ private readonly DbConnectionPoolIdentity _identity;
+
+ private string _instanceName = string.Empty;
+
+ 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 Guid _originalClientConnectionId = Guid.Empty;
+
+ private string _originalDatabase;
+
+ private string _originalLanguage;
+
+ 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 (remove f prefix)
+ private readonly bool _fResetConnection;
+
+ private string _routingDestination = null;
+
+ // @TODO: Rename to match naming conventions
+ private bool _SQLDNSRetryEnabled = false;
+
+ // @TODO: Rename to match naming conventions
+ private bool _serverSupportsDNSCaching = false;
+
+ private bool _sessionRecoveryRequested;
+
+ private int _threadIdOwningParserLock = -1;
+
+ // @TODO: Rename to indicate this has to do with routing
+ private readonly TimeoutTimer _timeout;
+
+ private readonly SqlConnectionTimeoutErrorInternal _timeoutErrorInternal;
+
+ #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
+ public override string ServerVersion
+ {
+ get => $"{_loginAck.majorVersion:00}.{(short)_loginAck.minorVersion:00}.{_loginAck.buildNum:0000}";
+ }
+
+ internal override SqlInternalTransaction AvailableInternalTransaction
+ {
+ get => _parser._fResetConnection ? null : CurrentTransaction;
+ }
+
+ // @TODO: Make auto-property
+ internal Guid ClientConnectionId
+ {
+ get => _clientConnectionId;
+ }
+
+ internal SessionData CurrentSessionData
+ {
+ get
+ {
+ if (_currentSessionData != null)
+ {
+ _currentSessionData._database = CurrentDatabase;
+ _currentSessionData._language = _currentLanguage;
+ }
+ return _currentSessionData;
+ }
+ }
+
+ internal override SqlInternalTransaction CurrentTransaction
+ {
+ get => _parser.CurrentTransaction;
+ }
+
+ // @TODO: Make auto-property
+ 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
+ {
+ get => _instanceName;
+ }
+
+ internal override bool Is2008OrNewer
+ {
+ get => _parser.Is2008OrNewer;
+ }
+
+ ///
+ /// Validates if federated authentication is used, Access Token used by this connection is
+ /// active for the value of 'accessTokenExpirationBufferTime'.
+ ///
+ internal override bool IsAccessTokenExpired
+ {
+ get => _federatedAuthenticationInfoRequested &&
+ DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime);
+ }
+
+ ///
+ /// 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;
+ }
+
+ 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.
+ ///
+ // @TODO: Make auto-property
+ internal bool IsSQLDNSRetryEnabled
+ {
+ get => _SQLDNSRetryEnabled;
+ set => _SQLDNSRetryEnabled = value;
+ }
+
+ // @TODO: Make auto-property
+ 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
+ {
+ get => _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;
+ }
+
+ ///
+ /// 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
+ {
+ // 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...
+ }
+
+ ///
+ /// 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.
+ ///
+ // @TODO: Rename to match naming convention
+ private int accessTokenExpirationBufferTime
+ {
+ get => ConnectionOptions.ConnectTimeout == ADP.InfiniteConnectionTimeout ||
+ ConnectionOptions.ConnectTimeout >= ADP.MaxBufferAccessTokenExpiry
+ ? ADP.MaxBufferAccessTokenExpiry
+ : ConnectionOptions.ConnectTimeout;
+ }
+
+ #endregion
+
+ #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
+ /// 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();
+ }
+ }
+ }
+ }
+ }
+
+ 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()
+ {
+ 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();
+ }
+
+ 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ 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;
+ }
+ }
+
+ // @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.
+ ///
+ /// 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;
+ if (_recoverySessionData != null)
+ {
+ if (_recoverySessionData._tdsVersion != rec.tdsVersion)
+ {
+ throw SQL.CR_TDSVersionNotPreserved(this);
+ }
+ }
+
+ if (_currentSessionData != null)
+ {
+ _currentSessionData._tdsVersion = rec.tdsVersion;
+ }
+ }
+
+ 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;
+ 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
+
+ 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)
+ {
+ // MDAC 73598 - add brackets around database
+ database = SqlConnection.FixupDatabaseTransactionName(database); // @TODO: Should go to a utility method
+ Task executeTask = _parser.TdsExecuteSQLBatch(
+ $@"USE {database}",
+ ConnectionOptions.ConnectTimeout,
+ notificationRequest: null,
+ _parser._physicalStateObj,
+ sync: true);
+
+ Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes");
+
+ _parser.Run(
+ RunBehavior.UntilDone,
+ cmdHandler: null,
+ dataStream: null,
+ bulkCopyHandler: null,
+ _parser._physicalStateObj);
+ }
+
+ // @TODO: Rename to match guidelines
+ protected override byte[] GetDTCAddress()
+ {
+ 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
+ // 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.
+ // TODO: I think this logic cares about pooling because the pool will handle deactivation of pool-associated trasaction roots?
+ if (!(IsTransactionRoot && Pool == null))
+ {
+ Debug.Assert(_parser != null || IsConnectionDoomed, "Deactivating a disposed connection?");
+ if (_parser != null)
+ {
+ _parser.Deactivate(IsConnectionDoomed);
+
+ if (!IsConnectionDoomed)
+ {
+ ResetConnection();
+ }
+ }
+ }
+ }
+
+ 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(
+ cookie,
+ ConnectionOptions.ConnectTimeout,
+ _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.
+ ///
+ // @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
+ /// 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(
+ 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,
+ 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();
+ }
+ }
+ }
+
+ #if NETFRAMEWORK
+ 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
+ {
+ // @TODO: _fedAuthToken assignment is identical in both cases - move outside
+ 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;
+ using 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,
+ 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);
+ }
+
+ 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();
+ }
+
+ ///
+ /// 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ // @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 .
+ ///
+ 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;
+ }
+
+ 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()
+ {
+ // 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;
+ }
+ }
+
+ 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)
+ {
+ 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
+
+ ///
+ /// 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
+
+ ///
+ /// 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 class SyncAsyncLock
+ {
+ private readonly 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;
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+ }
+}
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 1cc6d14ec6..0000000000
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.stub.cs
+++ /dev/null
@@ -1,53 +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 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 string InstanceName => null;
-
- internal bool ThreadHasParserLockForClose { get; set; }
-
- internal void Dispose() { }
-
- internal void DoomThisConnection() { }
-
- internal class SyncAsyncLock
- {
- internal bool CanBeReleasedFromAnyThread { get; set; }
-
- internal void Release() { }
-
- internal void Wait(bool canReleaseFromAnyThread) {}
- }
- }
-}
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..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
{
@@ -1608,7 +1604,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 +1904,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 +9667,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 +9865,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 +9913,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 +10030,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.