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 32b9e76c98..36100440ce 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -210,6 +210,9 @@ Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs + + Microsoft\Data\SqlClient\Connection\CachedContexts.cs + Microsoft\Data\SqlClient\Connection\ServerInfo.cs @@ -219,6 +222,9 @@ Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs + + Microsoft\Data\SqlClient\Connection\SqlConnectionInternal.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs @@ -699,12 +705,6 @@ Microsoft\Data\SqlClient\SqlInfoMessageEventHandler.cs - - Microsoft\Data\SqlClient\SqlInternalConnection.cs - - - Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs - Microsoft\Data\SqlClient\SqlInternalTransaction.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 4bfe45ee72..6b6d5eb35a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -470,6 +470,9 @@ Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs + + Microsoft\Data\SqlClient\Connection\CachedContexts.cs + Microsoft\Data\SqlClient\Connection\ServerInfo.cs @@ -479,6 +482,9 @@ Microsoft\Data\SqlClient\Connection\SessionStateRecord.cs + + Microsoft\Data\SqlClient\Connection\SqlConnectionInternal.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs @@ -854,12 +860,6 @@ Microsoft\Data\SqlClient\SqlInfoMessageEventHandler.cs - - Microsoft\Data\SqlClient\SqlInternalConnection.cs - - - Microsoft\Data\SqlClient\SqlInternalConnectionTds.cs - Microsoft\Data\SqlClient\SqlInternalTransaction.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 46375fbf20..321befedff 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -25,6 +25,7 @@ using Microsoft.SqlServer.Server; using System.Security.Authentication; using System.Collections.Generic; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Reflection; @@ -455,7 +456,11 @@ internal static ArgumentException InvalidArgumentLength(string argumentName, int internal static ArgumentException MustBeReadOnly(string argumentName) => Argument(StringsHelper.GetString(Strings.ADP_MustBeReadOnly, argumentName)); - internal static Exception CreateSqlException(MsalException msalException, SqlConnectionString connectionOptions, SqlInternalConnectionTds sender, string username) + internal static Exception CreateSqlException( + MsalException msalException, + SqlConnectionString connectionOptions, + SqlConnectionInternal sender, + string username) { // Error[0] SqlErrorCollection sqlErs = new(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs new file mode 100644 index 0000000000..d7a6073d13 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/CachedContexts.cs @@ -0,0 +1,180 @@ +// 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.Threading; + +#nullable enable + +namespace Microsoft.Data.SqlClient.Connection +{ + /// + /// Provides thread-safe caching and sharing of asynchronous call contexts between objects + /// within a single SQL connection context. + /// + /// + /// This internal class manages reusable context objects for various asynchronous operations + /// such as ExecuteNonQueryAsync, ExecuteReaderAsync, etc, performed on a connection, enabling + /// efficient reuse and reducing allocations. + /// + /// Thread safety is ensured via interlocked operations, allowing concurrent access and + /// updates without explicit locking. All accessors and mutators are designed to be safe for + /// use by multiple threads. + /// + /// Intended for internal use by connection management infrastructure. + /// + internal class CachedContexts + { + #region Fields + + /// + /// Stores reusable context for ExecuteNonQueryAsync invocations. + /// + private SqlCommand.ExecuteNonQueryAsyncCallContext? _commandExecuteNonQueryAsyncContext; + + /// + /// Stores reusable context for ExecuteReaderAsync invocations. + /// + private SqlCommand.ExecuteReaderAsyncCallContext? _commandExecuteReaderAsyncContext; + + /// + /// Stores reusable context for ExecuteXmlReaderAsync invocations. + /// + private SqlCommand.ExecuteXmlReaderAsyncCallContext? _commandExecuteXmlReaderAsyncContext; + + /// + /// Stores reusable context for IsDBNullAsync invocations. + /// + private SqlDataReader.IsDBNullAsyncCallContext? _dataReaderIsDbNullContext; + + /// + /// Stores reusable context for ReadAsync invocations. + /// + private SqlDataReader.ReadAsyncCallContext? _dataReaderReadAsyncContext; + + /// + /// Stores a data reader snapshot. + /// + private SqlDataReader.Snapshot? _dataReaderSnapshot; + + #endregion + + #region Access Methods + + /// + /// Removes and returns the cached ExecuteNonQueryAsync context. + /// + /// The previously cached context or null when empty. + internal SqlCommand.ExecuteNonQueryAsyncCallContext? TakeCommandExecuteNonQueryAsyncContext() => + Interlocked.Exchange(ref _commandExecuteNonQueryAsyncContext, null); + + /// + /// Removes and returns the cached ExecuteReaderAsync context. + /// + /// The previously cached context or null when empty. + internal SqlCommand.ExecuteReaderAsyncCallContext? TakeCommandExecuteReaderAsyncContext() => + Interlocked.Exchange(ref _commandExecuteReaderAsyncContext, null); + + /// + /// Removes and returns the cached ExecuteXmlReaderAsync context. + /// + /// The previously cached context or null when empty. + internal SqlCommand.ExecuteXmlReaderAsyncCallContext? TakeCommandExecuteXmlReaderAsyncContext() => + Interlocked.Exchange(ref _commandExecuteXmlReaderAsyncContext, null); + + /// + /// Removes and returns the cached ReadAsync context. + /// + /// The previously cached context or null when empty. + internal SqlDataReader.ReadAsyncCallContext? TakeDataReaderReadAsyncContext() => + Interlocked.Exchange(ref _dataReaderReadAsyncContext, null); + + /// + /// Removes and returns the cached IsDBNullAsync context. + /// + /// The previously cached context or null when empty. + internal SqlDataReader.IsDBNullAsyncCallContext? TakeDataReaderIsDbNullContext() => + Interlocked.Exchange(ref _dataReaderIsDbNullContext, null); + + /// + /// Removes and returns the cached data reader snapshot. + /// + /// The previously cached snapshot or null when empty. + internal SqlDataReader.Snapshot? TakeDataReaderSnapshot() => + Interlocked.Exchange(ref _dataReaderSnapshot, null); + + /// + /// Attempts to cache the provided ExecuteNonQueryAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// + internal bool TrySetCommandExecuteNonQueryAsyncContext(SqlCommand.ExecuteNonQueryAsyncCallContext value) => + TrySetContext(value, ref _commandExecuteNonQueryAsyncContext); + + /// + /// Attempts to cache the provided ExecuteReaderAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// + internal bool TrySetCommandExecuteReaderAsyncContext(SqlCommand.ExecuteReaderAsyncCallContext value) => + TrySetContext(value, ref _commandExecuteReaderAsyncContext); + + /// + /// Attempts to cache the provided ExecuteXmlReaderAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// + internal bool TrySetCommandExecuteXmlReaderAsyncContext(SqlCommand.ExecuteXmlReaderAsyncCallContext value) => + TrySetContext(value, ref _commandExecuteXmlReaderAsyncContext); + + /// + /// Attempts to cache the provided ReadAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// + internal bool TrySetDataReaderReadAsyncContext(SqlDataReader.ReadAsyncCallContext value) => + TrySetContext(value, ref _dataReaderReadAsyncContext); + + /// + /// Attempts to cache the provided IsDBNullAsync context. + /// + /// Context instance to store. + /// + /// True when the context is cached; false if an existing value is preserved. + /// + internal bool TrySetDataReaderIsDbNullContext(SqlDataReader.IsDBNullAsyncCallContext value) => + TrySetContext(value, ref _dataReaderIsDbNullContext); + + /// + /// Attempts to cache the provided data reader snapshot context. + /// + /// Context instance to store. + /// + /// True when the snapshot is cached; false if an existing snapshot is preserved. + /// + internal bool TrySetDataReaderSnapshot(SqlDataReader.Snapshot value) => + TrySetContext(value, ref _dataReaderSnapshot); + + #endregion + + private static bool TrySetContext(TContext value, ref TContext? location) + where TContext : class + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + return Interlocked.CompareExchange(ref location, value, null) is null; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs similarity index 86% rename from src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 80864716a3..5567dac6d5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -20,10 +20,11 @@ using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; using Microsoft.Identity.Client; +using IsolationLevel = System.Data.IsolationLevel; -namespace Microsoft.Data.SqlClient +namespace Microsoft.Data.SqlClient.Connection { - internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable + internal class SqlConnectionInternal : DbConnectionInternal, IDisposable { #region Constants @@ -57,7 +58,14 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable /// 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 TimeSpan _dbAuthenticationContextUnLockedRefreshTimeSpan = + new TimeSpan(hours: 0, minutes: 10, seconds: 00); + + /// + /// ID of the Azure SQL DB Transaction Manager (Non-MSDTC) + /// + // @TODO: Rename to a match naming conventions (s_camelCase or PascalCase) + private static readonly Guid s_globalTransactionTMID = new("1C742CAF-6680-40EA-9C26-6B6846079764"); private static readonly HashSet s_transientErrors = [ @@ -306,6 +314,12 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable private readonly SqlConnectionTimeoutErrorInternal _timeoutErrorInternal; + /// + /// Cache the whereabouts (DTC Address) for exporting. + /// + // @TODO: This name ... doesn't make a whole lot of sense. + private byte[] _whereAbouts; + #endregion #region Constructors @@ -318,7 +332,7 @@ internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposable /// 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( + internal SqlConnectionInternal( DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential, @@ -332,8 +346,12 @@ internal SqlInternalConnectionTds( string accessToken = null, IDbConnectionPool pool = null, Func> accessTokenCallback = null, - SspiContextProvider sspiContextProvider = null) : base(connectionOptions) + SspiContextProvider sspiContextProvider = null) { + Debug.Assert(connectionOptions is not null, "null connectionOptions"); + + ConnectionOptions = connectionOptions; + #if DEBUG if (reconnectSessionData != null) { @@ -487,10 +505,10 @@ public override string ServerVersion get => $"{_loginAck.majorVersion:00}.{(short)_loginAck.minorVersion:00}.{_loginAck.buildNum:0000}"; } - internal override SqlInternalTransaction AvailableInternalTransaction - { - get => _parser._fResetConnection ? null : CurrentTransaction; - } + /// + /// Gets the collection of async call contexts that belong to this connection. + /// + internal CachedContexts CachedContexts { get; private set; } = new CachedContexts(); // @TODO: Make auto-property internal Guid ClientConnectionId @@ -498,6 +516,32 @@ internal Guid ClientConnectionId get => _clientConnectionId; } + /// + /// A reference to the SqlConnection that owns this internal connection. + /// + internal SqlConnection Connection => (SqlConnection)Owner; + + /// + /// The connection options to be used for this connection. + /// + internal SqlConnectionString ConnectionOptions { get; } + + /// + /// The current database for this connection. Null if the connection is not open yet. + /// + internal string CurrentDatabase { get; private set; } + + /// + /// The current data source for this connection. + /// + /// + /// If connection is not open yet, CurrentDataSource is null + /// If connection is open: + /// * for regular connections, it is set to the Data Source value from connection string + /// * for failover connections, it is set to the FailoverPartner value from the connection string + /// + internal string CurrentDataSource { get; set; } + internal SessionData CurrentSessionData { get @@ -511,11 +555,37 @@ internal SessionData CurrentSessionData } } - internal override SqlInternalTransaction CurrentTransaction + /// + /// The Transaction currently associated with this connection. + /// + internal SqlInternalTransaction CurrentTransaction { get => _parser.CurrentTransaction; } + /// + /// The delegated (or promoted) transaction this connection is responsible for. + /// + internal SqlDelegatedTransaction DelegatedTransaction { get; set; } + + /// + /// Whether this connection has a local (non-delegated) transaction. + /// + internal bool HasLocalTransaction + { + get => CurrentTransaction?.IsLocal == true; + } + + /// + /// Whether this connection has a local transaction started from the API (i.e., + /// SqlConnection.BeginTransaction) or had a TSQL transaction and later got wrapped by an + /// API transaction. + /// + internal bool HasLocalTransactionFromAPI + { + get => CurrentTransaction?.HasParentTransaction == true; + } + // @TODO: Make auto-property internal DbConnectionPoolIdentity Identity { @@ -537,7 +607,7 @@ internal string InstanceName get => _instanceName; } - internal override bool Is2008OrNewer + internal bool Is2008OrNewer { get => _parser.Is2008OrNewer; } @@ -553,7 +623,8 @@ internal override bool IsAccessTokenExpired } /// - /// Get or set if the control ring send redirect token and feature ext ack with true for DNSCaching + /// 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 @@ -562,7 +633,27 @@ internal bool IsDNSCachingBeforeRedirectSupported set => _dnsCachingBeforeRedirect = value; } - internal override bool IsLockedForBulkCopy + /// + /// Indicates whether the connection is currently enlisted in a transaction. + /// + internal bool IsEnlistedInTransaction { get; private set; } + + /// + /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) + /// TODO: overlaps with IsGlobalTransactionsEnabledForServer, need to consolidate to avoid bugs + /// + internal bool IsGlobalTransaction { get; private set; } + + /// + /// Whether Global Transactions are enabled. Only supported by Azure SQL. False if disabled + /// or connected to on-prem SQL Server. + /// + internal bool IsGlobalTransactionsEnabledForServer { get; private set; } + + /// + /// Whether this connection is locked for bulk copy operations. + /// + internal bool IsLockedForBulkCopy { get => !_parser.MARSOn && _parser._physicalStateObj.BcpLock; } @@ -587,6 +678,14 @@ internal bool IsSQLDNSRetryEnabled set => _SQLDNSRetryEnabled = value; } + /// + /// Whether this connection is the root of a delegated or promoted transaction. + /// + internal override bool IsTransactionRoot + { + get => DelegatedTransaction?.IsActive == true; + } + // @TODO: Make auto-property internal Guid OriginalClientConnectionId { @@ -605,7 +704,10 @@ internal TdsParser Parser get => _parser; } - internal override SqlInternalTransaction PendingTransaction + /// + /// TODO: need to understand this property better + /// + internal SqlInternalTransaction PendingTransaction { get => _parser.PendingTransaction; } @@ -616,6 +718,11 @@ internal SqlConnectionPoolGroupProviderInfo PoolGroupProviderInfo get => _poolGroupProviderInfo; } + /// + /// A token returned by the server when we promote transaction. + /// + internal byte[] PromotedDtcToken { get; private set; } + // @TODO: Make auto-property internal string RoutingDestination { @@ -697,10 +804,130 @@ private int accessTokenExpirationBufferTime : ConnectionOptions.ConnectTimeout; } + // SQLBU 415870 + // Get the internal transaction that should be hooked to a new outer transaction + // during a BeginTransaction API call. In some cases (i.e. connection is going to + // be reset), CurrentTransaction should not be hooked up this way. + // TODO: (mdaigle) need to understand this property better + private SqlInternalTransaction AvailableInternalTransaction + { + get => _parser._fResetConnection ? null : CurrentTransaction; + } + + /// + /// Whether this connection is to an Azure SQL Database. + /// + // @TODO: Make private field. + private bool IsAzureSqlConnection { get; set; } + #endregion #region Public and Internal Methods + public override DbTransaction BeginTransaction(IsolationLevel iso) => + BeginSqlTransaction(iso, transactionName: null, shouldReconnect: false); + + public override void ChangeDatabase(string database) + { + if (string.IsNullOrEmpty(database)) + { + throw ADP.EmptyDatabaseName(); + } + + ValidateConnectionForExecute(null); + + // 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); + } + + public override void EnlistTransaction(Transaction transaction) + { + #if NETFRAMEWORK + SqlConnection.VerifyExecutePermission(); + #endif + + ValidateConnectionForExecute(null); + + // If a connection has a local transaction outstanding, and you try to enlist in a DTC + // transaction, SQL Server will roll back the local transaction and then enlist (7.0 and + // 2000). So, if the user tries to do this, throw. + if (HasLocalTransaction) + { + throw ADP.LocalTransactionPresent(); + } + + if (transaction != null && transaction.Equals(EnlistedTransaction)) + { + // No-op if this is the current transaction + return; + } + + // If a connection is already enlisted in a DTC transaction, and you try to enlist in + // another one, in 7.0 the existing DTC transaction would roll back and then the + // connection would enlist in the new one. In SQL 2000 & 2005, when you enlist in a DTC + // transaction while the connection is already enlisted in a DTC transaction, the + // connection simply switches enlistments. Regardless, simply enlist in the user + // specified distributed transaction. This behavior matches OLEDB and ODBC. + + Enlist(transaction); + // @TODO: CER Exception Handling was removed here (see GH#3581) + } + + internal SqlTransaction BeginSqlTransaction( + IsolationLevel iso, + string transactionName, + bool shouldReconnect) + { + SqlStatistics statistics = null; + try + { + statistics = SqlStatistics.StartTimer(Connection.Statistics); + + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); // MDAC 81476 + #endif + + ValidateConnectionForExecute(null); + + if (HasLocalTransactionFromAPI) + { + throw ADP.ParallelTransactionsNotSupported(Connection); + } + + if (iso == IsolationLevel.Unspecified) + { + // Default to ReadCommitted if unspecified. + iso = IsolationLevel.ReadCommitted; + } + + SqlTransaction transaction = new(this, Connection, iso, AvailableInternalTransaction); + transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect; + ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false); + transaction.InternalTransaction.RestoreBrokenConnection = false; + return transaction; + } + // @TODO: CER Exception Handling was removed here (see GH#3581) + finally + { + SqlStatistics.StopTimer(statistics); + } + } + internal void BreakConnection() { SqlClientEventSource.Log.TryTraceEvent( @@ -774,10 +1001,10 @@ internal void DecrementAsyncCount() Interlocked.Decrement(ref _asyncCommandCount); } - internal override void DisconnectTransaction(SqlInternalTransaction internalTransaction) => + internal void DisconnectTransaction(SqlInternalTransaction internalTransaction) => _parser?.DisconnectTransaction(internalTransaction); - // @TODO: Make internal by making the SqlInternalConnection implementation internal + // @TODO: Make internal by making the DbConnectionInternal implementation internal public override void Dispose() { SqlClientEventSource.Log.TryAdvancedTraceEvent( @@ -805,10 +1032,45 @@ public override void Dispose() _fConnectionOpen = false; } + _whereAbouts = null; + base.Dispose(); } - internal override void ExecuteTransaction( + internal void EnlistNull() + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNull | ADV | " + + $"Object ID {ObjectID}, " + + $"unenlisting."); + + // We were in a transaction, but now we are not - so send message to server with empty + // transaction - confirmed proper behavior from Sameet Agarwal. + + // The connection pooler maintains separate pools for enlisted transactions. Only when + // that transaction is committed or rolled back will those connections be taken from + // that separate pool and returned to the general pool of connections that are not + // affiliated with any transactions. When this occurs, we will have a new transaction + // of null, and we are required to send an empty transaction payload to the server. + + PropagateTransactionCookie(null); + + // Tell the base class about our enlistment + IsEnlistedInTransaction = false; + EnlistedTransaction = null; + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNull | ADV | " + + $"Object ID {ObjectID}, " + + $"unenlisted."); + + // The EnlistTransaction above will return an TransactionEnded event, which causes the + // TdsParser to clear the current transaction. In either case, when we're working with + // a 2005 or newer server we better not have a current transaction at this point. + Debug.Assert(CurrentTransaction == null, "unenlisted transaction with non-null current transaction?"); + } + + internal void ExecuteTransaction( TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, @@ -842,6 +1104,17 @@ TransactionRequest.Rollback or ExecuteTransaction2005(transactionRequest, transactionName, iso, internalTransaction, isDelegateControlRequest); } + internal SqlDataReader FindLiveReader(SqlCommand command) + { + SqlDataReader reader = null; + SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; + if (referenceCollection != null) + { + reader = referenceCollection.FindLiveReader(command); + } + return reader; + } + /// /// Called by SqlConnection.RepairConnection which is a relatively expensive way of repair /// inner connection prior to execution of request, used from EnlistTransaction, @@ -1017,6 +1290,31 @@ internal void OnEnvChange(SqlEnvChange rec) } } + /// + /// If wrapCloseInAction is defined, then the action it defines will be run with the + /// connection close action passed in as a parameter. The close action also supports being + /// run asynchronously. + /// + internal void OnError(SqlException exception, bool breakConnection, Action wrapCloseInAction = null) + { + if (breakConnection) + { + DoomThisConnection(); + } + + SqlConnection connection = Connection; + if (connection != null) + { + connection.OnError(exception, breakConnection, wrapCloseInAction); + } + else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) + { + // It is an error, and should be thrown. Class of TdsEnums.MIN_ERROR_CLASS + // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message. + throw exception; + } + } + // @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. @@ -1201,8 +1499,8 @@ internal void OnFeatureExtAck(int featureId, byte[] data) SqlClientEventSource.Log.TryTraceEvent( $"SqlInternalConnectionTds.OnFeatureExtAck | ERR | " + $"Object ID {ObjectID }, " + - $"AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, but it did not update the new value.", - ObjectID); + $"AddOrUpdate attempted on _dbConnectionPool.AuthenticationContexts, " + + $"but it did not update the new value."); } #endif } @@ -1669,7 +1967,7 @@ internal override bool TryReplaceConnection( return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } - internal override void ValidateConnectionForExecute(SqlCommand command) + internal void ValidateConnectionForExecute(SqlCommand command) { TdsParser parser = _parser; if (parser == null || parser.State is TdsParserState.Broken or TdsParserState.Closed) @@ -1746,66 +2044,77 @@ protected override void Activate(Transaction transaction) } } - // @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"); + protected override void CleanupTransactionOnCompletion(Transaction transaction) => + DelegatedTransaction?.TransactionEnded(transaction); - _parser.Run( - RunBehavior.UntilDone, - cmdHandler: null, - dataStream: null, - bulkCopyHandler: null, - _parser._physicalStateObj); - } + protected override DbReferenceCollection CreateReferenceCollection() => + new SqlReferenceCollection(); - // @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() + /// + protected override void Deactivate() { - // 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) + try { - DoomThisConnection(); - } + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.Deactivate | ADV | " + + $"Object ID {ObjectID} deactivating, " + + $"Client Connection Id {Connection?.ClientConnectionId}"); - // 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) + SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; + referenceCollection?.Deactivate(); + + // 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) { - _parser.Deactivate(IsConnectionDoomed); + DoomThisConnection(); + } - if (!IsConnectionDoomed) + // 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) { - ResetConnection(); + _parser.Deactivate(IsConnectionDoomed); + + if (!IsConnectionDoomed) + { + ResetConnection(); + } } } } + // @TODO: CER Exception Handling was removed here (see GH#3581) + catch (Exception e) + { + if (!ADP.IsCatchableExceptionType(e)) + { + throw; + } + + // If an exception occurred, the inner connection will be marked as unusable and + // destroyed upon returning to the pool + DoomThisConnection(); + + #if NETFRAMEWORK + ADP.TraceExceptionWithoutRethrow(e); + #endif + } + } + + // @TODO: Rename to match guidelines + protected byte[] GetDTCAddress() + { + byte[] dtcAddress = _parser.GetDTCAddress(ConnectionOptions.ConnectTimeout, _parser.GetSession(this)); + + Debug.Assert(dtcAddress != null, "null dtcAddress?"); + return dtcAddress; } protected override bool ObtainAdditionalLocksForClose() @@ -1824,7 +2133,7 @@ protected override bool ObtainAdditionalLocksForClose() return obtainParserLock; } - protected override void PropagateTransactionCookie(byte[] cookie) + protected void PropagateTransactionCookie(byte[] cookie) { _parser.PropagateDistributedTransaction( cookie, @@ -1845,6 +2154,13 @@ protected override void ReleaseAdditionalLocksForClose(bool lockToken) #region Private Methods + private static byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) + { + return transaction is not null + ? TransactionInterop.GetExportCookie(transaction, whereAbouts) + : null; + } + /// /// Common code path for making one attempt to establish a connection and log in to server. /// @@ -2015,6 +2331,213 @@ private void CompleteLogin(bool enlistOK) // @TODO: Rename as per guidelines _parser._physicalStateObj.SniContext = SniContext.Snix_Login; } + private void Enlist(Transaction transaction) + { + // This method should not be called while the connection has a reference to an active + // delegated transaction. Manual enlistment via SqlConnection.EnlistTransaction should + // catch this case and throw an exception. + + // Automatic enlistment isn't possible because Sys.Tx keeps the connection alive until + // the transaction is completed. + // @TODO: What does the above mean? Is it still valid in a post-SDS world? + + // TODO: why do we assert pooling status? shouldn't we just be checking whether the connection is the root of the transaction? + // @TODO: potential race condition, but it's an assert + Debug.Assert(!(IsTransactionRoot && Pool == null), "cannot defect an active delegated transaction!"); + + if (transaction is null) + { + if (IsEnlistedInTransaction) + { + EnlistNull(); + } + else + { + // When IsEnlistedInTransaction is false, it means we are in one of two states: + // 1. EnlistTransaction is null, so the connection is truly not enlisted in a + // transaction + // 2. Connection is enlisted in a SqlDelegatedTransaction. + // + // For #2, we have to consider whether the delegated transaction is active. If + // it is not active, we allow the enlistment in the NULL transaction. If it is + // active, technically this is an error. + // + // However, no exception is thrown as this was the precedent (and this case is + // silently ignored, no error, but no enlistment either). There are two + // mitigations for this: + // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has + // completed before allowing a different enlistment. + // 2. For debug builds, the assertion at the beginning of this method checks + // for an enlistment in an active delegated transaction. + Transaction enlistedTransaction = EnlistedTransaction; + if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != TransactionStatus.Active) + { + EnlistNull(); + } + } + } + else if (!transaction.Equals(EnlistedTransaction)) + { + // Only enlist if it's different... + EnlistNonNull(transaction); + } + } + + private void EnlistNonNull(Transaction transaction) + { + Debug.Assert(transaction != null, "null transaction?"); + + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"Transaction Id {transaction?.TransactionInformation?.LocalIdentifier}, " + + $"attempting to delegate."); + + bool hasDelegatedTransaction = false; + SqlDelegatedTransaction delegatedTransaction = new(this, transaction); + + try + { + // NOTE: System.Transactions claims to resolve all potential race conditions + // between multiple delegate requests of the same transaction to different + // connections in their code, such that only one attempt to delegate will succeed. + + // NOTE: PromotableSinglePhaseEnlist will eventually make a round trip to the + // server; doing this inside a lock is not the best choice. We presume that you + // aren't trying to enlist concurrently on two threads and leave it at that. We + // don't claim any thread safety with regard to multiple concurrent requests to + // enlist the same connection in different transactions, which is good, because we + // don't have it anyway. + + // PromotableSinglePhaseEnlist may not actually promote the transaction when it is + // already delegated (this is the way they resolve the race condition when two + // threads attempt to delegate the same Lightweight Transaction). In that case, we + // can safely ignore our delegated transaction, and proceed to enlist in the + // promoted one. + + // NOTE: Global Transactions is an Azure SQL DB only feature where the Transaction + // Manager (TM) is not MS-DTC. Sys.Tx added APIs to support Non MS-DTC promoter + // types/TM in .NET 4.6.2. Following directions from .NETFX shiproom, to avoid a + // "hard-dependency" (compile time) on Sys.Tx, we use reflection to invoke the new + // APIs. Further, the IsGlobalTransaction flag indicates that this is an Azure SQL + // DB Transaction that could be promoted to a Global Transaction (it's always false + // for on-prem SQL Server). The Promote() call in SqlDelegatedTransaction makes + // sure that the right Sys.Tx.dll is loaded and that Global Transactions are + // actually allowed for this Azure SQL DB. + // @TODO: Revisit these comments and see if they are still necessary/desirable. + + if (IsGlobalTransaction) + { + if (SysTxForGlobalTransactions.EnlistPromotableSinglePhase == null) + { + // This could be a local Azure SQL DB transaction. + hasDelegatedTransaction = transaction.EnlistPromotableSinglePhase(delegatedTransaction); + } + else + { + hasDelegatedTransaction = (bool)SysTxForGlobalTransactions.EnlistPromotableSinglePhase.Invoke( + obj: transaction, + parameters: [delegatedTransaction, s_globalTransactionTMID]); + } + } + else + { + // This is an MS-DTC distributed transaction + hasDelegatedTransaction = transaction.EnlistPromotableSinglePhase(delegatedTransaction); + } + + if (hasDelegatedTransaction) + { + DelegatedTransaction = delegatedTransaction; + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"Client Connection Id {Connection?.ClientConnectionId} " + + $"delegated to transaction {delegatedTransaction?.ObjectID} " + + $"with transactionId {delegatedTransaction?.Transaction?.TransactionInformation?.LocalIdentifier}"); + } + } + catch (SqlException e) + { + // we do not want to eat the error if it is a fatal one + if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) + { + throw; + } + + if (Parser?.State is not TdsParserState.OpenLoggedIn) + { + // If the parser is null or its state is not openloggedin, the connection is no + // longer good. + throw; + } + + #if NETFRAMEWORK + ADP.TraceExceptionWithoutRethrow(e); + #endif + + // In this case, SqlDelegatedTransaction.Initialize failed, and we don't + // necessarily want to reject things - there may have been a legitimate reason for + // the failure. + } + + if (!hasDelegatedTransaction) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"delegation not possible, enlisting."); + + byte[] cookie = null; + + if (IsGlobalTransaction) + { + if (SysTxForGlobalTransactions.GetPromotedToken == null) + { + throw SQL.UnsupportedSysTxForGlobalTransactions(); + } + + cookie = (byte[])SysTxForGlobalTransactions.GetPromotedToken.Invoke(transaction, null); + } + else + { + if (_whereAbouts == null) + { + byte[] dtcAddress = GetDTCAddress(); + _whereAbouts = dtcAddress ?? throw SQL.CannotGetDTCAddress(); + } + + cookie = GetTransactionCookie(transaction, _whereAbouts); + } + + // send cookie to server to finish enlistment + PropagateTransactionCookie(cookie); + + IsEnlistedInTransaction = true; + SqlClientEventSource.Log.TryAdvancedTraceEvent( + $"SqlInternalConnection.EnlistNonNull | ADV | " + + $"Object ID {ObjectID}, " + + $"Client Connection Id {Connection?.ClientConnectionId}, " + + $"Enlisted in transaction with transactionId {transaction?.TransactionInformation?.LocalIdentifier}"); + } + + // Tell the base class about our enlistment + EnlistedTransaction = transaction; + + // If we're on a 2005 or newer server, and we delegate the transaction successfully, we + // will have begun a transaction, which produces a transaction ID that we should + // execute all requests on. The TdsParser will store this information as the current + // transaction. + + // Likewise, propagating a transaction to a 2005 or newer server will produce a + // transaction id that The TdsParser will store as the current transaction. + + // In either case, when we're working with a 2005 or newer server we better have a + // current transaction by now. + + Debug.Assert(CurrentTransaction != null, "delegated/enlisted transaction with null current transaction?"); + } + // @TODO: Rename to ExecuteTransactionInternal ... we don't have multiple server version implementations of this private void ExecuteTransaction2005( TransactionRequest transactionRequest, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs index 9784117a5e..39e4f570c7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -230,7 +231,7 @@ private int RowNumber private bool _hasMoreRowToCopy = false; private bool _isAsyncBulkCopy = false; private bool _isBulkCopyingInProgress = false; - private SqlInternalConnectionTds.SyncAsyncLock _parserLock = null; + private SqlConnectionInternal.SyncAsyncLock _parserLock = null; private SourceColumnMetadata[] _currentRowMetadata; @@ -1156,8 +1157,8 @@ private Task ReadFromRowSourceAsync(CancellationToken cts) } else { // This will call Read for DataRows, DataTable and IDataReader (this includes all IDataReader except DbDataReader) - // Release lock to prevent possible deadlocks - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + // Release lock to prevent possible deadlocks + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); bool semaphoreLock = internalConnection._parserLock.CanBeReleasedFromAnyThread; internalConnection._parserLock.Release(); @@ -1366,7 +1367,7 @@ private void CreateOrValidateConnection(string method) private void RunParser(BulkCopySimpleResultSet bulkCopyHandler = null) { // In case of error while reading, we should let the connection know that we already own the _parserLock - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; try @@ -1384,7 +1385,7 @@ private void RunParser(BulkCopySimpleResultSet bulkCopyHandler = null) private void RunParserReliably(BulkCopySimpleResultSet bulkCopyHandler = null) { // In case of error while reading, we should let the connection know that we already own the _parserLock - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; try { @@ -1401,7 +1402,7 @@ private void CommitTransaction() { if (_internalTransaction != null) { - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; // In case of error, let the connection know that we have the lock try { @@ -1422,7 +1423,7 @@ private void AbortTransaction() { if (!_internalTransaction.IsZombied) { - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); internalConnection.ThreadHasParserLockForClose = true; // In case of error, let the connection know that we have the lock try { @@ -2069,7 +2070,7 @@ private Task WriteRowSourceToServerAsync(int columnCount, CancellationToken ctok CreateOrValidateConnection(nameof(WriteToServer)); - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); Debug.Assert(_parserLock == null, "Previous parser lock not cleaned"); _parserLock = internalConnection._parserLock; @@ -2222,7 +2223,7 @@ internal void OnConnectionClosed() private bool FireRowsCopiedEvent(long rowsCopied) { // Release lock to prevent possible deadlocks - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); bool semaphoreLock = internalConnection._parserLock.CanBeReleasedFromAnyThread; internalConnection._parserLock.Release(); @@ -2578,7 +2579,7 @@ private Task CopyBatchesAsync(BulkCopySimpleResultSet internalResults, string up while (_hasMoreRowToCopy) { //pre->before every batch: Transaction, BulkCmd and metadata are done. - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); if (IsCopyOption(SqlBulkCopyOptions.UseInternalTransaction)) { //internal transaction is started prior to each batch if the Option is set. @@ -2965,7 +2966,7 @@ private void WriteToServerInternalRestAsync(CancellationToken cts, TaskCompletio _hasMoreRowToCopy = true; Task internalResultsTask = null; BulkCopySimpleResultSet internalResults = new BulkCopySimpleResultSet(); - SqlInternalConnectionTds internalConnection = _connection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); try { _parser = _connection.Parser; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index cf90d29632..ee2ee1aabd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -266,8 +267,7 @@ private SqlDataReader GetParameterEncryptionDataReader( // If it is async, then TryFetchInputParameterEncryptionInfo -> // RunExecuteReaderTds would have incremented the async count. Decrement it // when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = - command._activeConnection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnectionTds = command._activeConnection.GetOpenTdsConnection(); if (internalConnectionTds is not null) { internalConnectionTds.DecrementAsyncCount(); @@ -345,7 +345,7 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync( // If it is async, then TryFetchInputParameterEncryptionInfo -> // RunExecuteReaderTds would have incremented the async count. Decrement it // when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnectionTds = _activeConnection.GetOpenTdsConnection(); if (internalConnectionTds is not null) { internalConnectionTds.DecrementAsyncCount(); @@ -769,7 +769,7 @@ private void PrepareTransparentEncryptionFinallyBlock( if (decrementAsyncCount) { // Decrement the async count - SqlInternalConnectionTds internalConnection = _activeConnection.GetOpenTdsConnection(); + SqlConnectionInternal internalConnection = _activeConnection.GetOpenTdsConnection(); internalConnection?.DecrementAsyncCount(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs index 28bd1cf2ef..5d4cba4976 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs @@ -9,6 +9,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Security.Permissions; @@ -904,18 +906,6 @@ private void RunExecuteNonQueryTdsSetupReconnnectContinuation( }); } - private void SetCachedCommandExecuteNonQueryAsyncContext(ExecuteNonQueryAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - // @TODO: Add this to SqlInternalConnection - Interlocked.CompareExchange( - ref sqlInternalConnection.CachedCommandExecuteNonQueryAsyncContext, - instance, - comparand: null); - } - } - #endregion internal sealed class ExecuteNonQueryAsyncCallContext @@ -939,7 +929,11 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { - owner?.SetCachedCommandExecuteNonQueryAsyncContext(this); + DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; + if (internalConnection is SqlConnectionInternal sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetCommandExecuteNonQueryAsyncContext(this); + } } protected override void Clear() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs index 332195efb5..dda6534047 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -10,6 +10,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Security.Permissions; @@ -979,11 +981,9 @@ private Task InternalExecuteReaderAsync( { returnedTask = RegisterForConnectionCloseNotification(returnedTask); - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = Interlocked.Exchange( - ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, - null); + context = sqlInternalConnection.CachedContexts.TakeCommandExecuteReaderAsyncContext(); } context ??= new ExecuteReaderAsyncCallContext(); @@ -1577,7 +1577,7 @@ private SqlDataReader RunExecuteReaderTds( if (decrementAsyncCountOnFailure) { - if (_activeConnection.InnerConnection is SqlInternalConnectionTds innerConnectionTds) + if (_activeConnection.InnerConnection is SqlConnectionInternal innerConnectionTds) { // It may be closed innerConnectionTds.DecrementAsyncCount(); @@ -1785,18 +1785,6 @@ private SqlDataReader RunExecuteReaderWithRetry( this, () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); - private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - // @TODO: This should be part of the sql internal connection class. - Interlocked.CompareExchange( - ref sqlInternalConnection.CachedCommandExecuteReaderAsyncContext, - instance, - null); - } - } - #endregion internal sealed class ExecuteReaderAsyncCallContext @@ -1824,7 +1812,11 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { - owner?.SetCachedCommandExecuteReaderAsyncContext(this); + DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; + if (internalConnection is SqlConnectionInternal sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetCommandExecuteReaderAsyncContext(this); + } } protected override void Clear() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs index 6a18b6b1b6..add7f90407 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Server; #if NETFRAMEWORK @@ -486,11 +488,9 @@ private Task InternalExecuteXmlReaderAsync(CancellationToken cancella // @TODO: This can be cleaned up to lines if InnerConnection is always SqlInternalConnection ExecuteXmlReaderAsyncCallContext context = null; - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_activeConnection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = Interlocked.Exchange( - ref sqlInternalConnection.CachedCommandExecuteXmlReaderAsyncContext, - null); + context = sqlInternalConnection.CachedContexts.TakeCommandExecuteXmlReaderAsyncContext(); } context ??= new ExecuteXmlReaderAsyncCallContext(); @@ -546,18 +546,6 @@ private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken sender: this, () => InternalExecuteXmlReaderAsync(cancellationToken), cancellationToken); - - private void SetCachedCommandExecuteXmlReaderContext(ExecuteXmlReaderAsyncCallContext instance) - { - if (_activeConnection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - // @TODO: Move this compare exchange into the SqlInternalConnection class (or better yet, do away with this context) - Interlocked.CompareExchange( - ref sqlInternalConnection.CachedCommandExecuteXmlReaderAsyncContext, - instance, - comparand: null); - } - } #endregion @@ -582,7 +570,11 @@ public void Set( protected override void AfterCleared(SqlCommand owner) { - owner?.SetCachedCommandExecuteXmlReaderContext(this); + DbConnectionInternal internalConnection = owner?._activeConnection?.InnerConnection; + if (internalConnection is SqlConnectionInternal sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetCommandExecuteXmlReaderAsyncContext(this); + } } protected override void Clear() diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 784692cb2e..f3a85723c6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Sql; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Diagnostics; #if NETFRAMEWORK @@ -958,10 +959,10 @@ private int DefaultCommandTimeout } // @TODO: Should be used in more than one place to justify its existence - private SqlInternalConnectionTds InternalTdsConnection + private SqlConnectionInternal InternalTdsConnection { // @TODO: Should check for null? Should use Connection? - get => (SqlInternalConnectionTds)_activeConnection.InnerConnection; + get => (SqlConnectionInternal)_activeConnection.InnerConnection; } private bool IsColumnEncryptionEnabled @@ -1084,7 +1085,7 @@ public override void Cancel() // Note that this model is implementable because we only allow one active command // at any one time. This code will have to change we allow multiple outstanding // batches. - if (_activeConnection?.InnerConnection is not SqlInternalConnectionTds connection) + if (_activeConnection?.InnerConnection is not SqlConnectionInternal connection) { // @TODO: Really this case only applies if the connection is null. // Fail without locking @@ -1100,7 +1101,7 @@ public override void Cancel() { // Make sure the connection did not get changed getting the connection and // taking the lock. If it has, the connection has been closed. - if (connection != _activeConnection.InnerConnection as SqlInternalConnectionTds) + if (connection != _activeConnection.InnerConnection as SqlConnectionInternal) { return; } @@ -2380,9 +2381,8 @@ private void CheckNotificationStateAndAutoEnlist() // 3) database // Obtain identity from connection. - // @TODO: Remove cast when possible. - SqlInternalConnectionTds internalConnection = - (SqlInternalConnectionTds)_activeConnection.InnerConnection; + SqlConnectionInternal internalConnection = + (SqlConnectionInternal)_activeConnection.InnerConnection; SqlDependency.IdentityUserNamePair identityUserName = internalConnection.Identity is not null ? new SqlDependency.IdentityUserNamePair( @@ -2951,8 +2951,7 @@ private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "" } // Ensure that the connection is open and that the parser is in the correct state - // @TODO: Remove cast when possible. - SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; + SqlConnectionInternal tdsConnection = _activeConnection.InnerConnection as SqlConnectionInternal; // Ensure that if column encryption override was used then server supports it // @TODO: This is kinda clunky 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 4f9d9ca14f..32ba9d7148 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -816,10 +816,8 @@ public override string Database // just return what the connection string had. get { - SqlInternalConnection innerConnection = (InnerConnection as SqlInternalConnection); string result; - - if (innerConnection != null) + if (InnerConnection is SqlConnectionInternal innerConnection) { result = innerConnection.CurrentDatabase; } @@ -828,6 +826,7 @@ public override string Database SqlConnectionString constr = (SqlConnectionString)ConnectionOptions; result = constr != null ? constr.InitialCatalog : DbConnectionStringDefaults.InitialCatalog; } + return result; } } @@ -840,7 +839,7 @@ internal string SQLDNSCachingSupportedState { get { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; string result; if (innerConnection != null) @@ -864,7 +863,7 @@ internal string SQLDNSCachingSupportedStateBeforeRedirect { get { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; string result; if (innerConnection != null) @@ -889,10 +888,8 @@ public override string DataSource { get { - SqlInternalConnection innerConnection = (InnerConnection as SqlInternalConnection); string result; - - if (innerConnection != null) + if (InnerConnection is SqlConnectionInternal innerConnection) { result = innerConnection.CurrentDataSource; } @@ -901,6 +898,7 @@ public override string DataSource SqlConnectionString constr = (SqlConnectionString)ConnectionOptions; result = constr != null ? constr.DataSource : DbConnectionStringDefaults.DataSource; } + return result; } } @@ -918,7 +916,7 @@ public int PacketSize { int result; - if (InnerConnection is SqlInternalConnectionTds innerConnection) + if (InnerConnection is SqlConnectionInternal innerConnection) { result = innerConnection.PacketSize; } @@ -940,7 +938,7 @@ public Guid ClientConnectionId { get { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; if (innerConnection != null) { @@ -1504,7 +1502,7 @@ private void DisposeMe(bool disposing) // For non-pooled connections we need to make sure that if the SqlConnection was not closed, // then we release the GCHandle on the stateObject to allow it to be GCed // For pooled connections, we will rely on the pool reclaiming the connection - var innerConnection = (InnerConnection as SqlInternalConnectionTds); + var innerConnection = (InnerConnection as SqlConnectionInternal); if ((innerConnection != null) && (!innerConnection.ConnectionOptions.Pooling)) { var parser = innerConnection.Parser; @@ -1744,7 +1742,7 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) { if (_connectRetryCount > 0) { - SqlInternalConnectionTds tdsConn = GetOpenTdsConnection(); + SqlConnectionInternal tdsConn = GetOpenTdsConnection(); if (tdsConn._sessionRecoveryAcknowledged) { TdsParserStateObject stateObj = tdsConn.Parser._physicalStateObj; @@ -1845,7 +1843,7 @@ private void RepairInnerConnection() { return; } - SqlInternalConnectionTds tdsConn = InnerConnection as SqlInternalConnectionTds; + SqlConnectionInternal tdsConn = InnerConnection as SqlConnectionInternal; if (tdsConn != null) { tdsConn.ValidateConnectionForExecute(null); @@ -2216,7 +2214,7 @@ private bool TryOpenInner(TaskCompletionSource retry) } // does not require GC.KeepAlive(this) because of ReRegisterForFinalize below. - var tdsInnerConnection = (SqlInternalConnectionTds)InnerConnection; + var tdsInnerConnection = (SqlConnectionInternal)InnerConnection; Debug.Assert(tdsInnerConnection.Parser != null, "Where's the parser?"); @@ -2328,7 +2326,7 @@ internal TdsParser Parser { get { - SqlInternalConnectionTds tdsConnection = GetOpenTdsConnection(); + SqlConnectionInternal tdsConnection = GetOpenTdsConnection(); return tdsConnection.Parser; } } @@ -2429,7 +2427,7 @@ internal void ValidateConnectionForExecute(string method, SqlCommand command) return; // execution will wait for this task later } } - SqlInternalConnectionTds innerConnection = GetOpenTdsConnection(method); + SqlConnectionInternal innerConnection = GetOpenTdsConnection(method); innerConnection.ValidateConnectionForExecute(command); } @@ -2531,9 +2529,9 @@ private void ConnectionString_Set(DbConnectionPoolKey key) } } - internal SqlInternalConnectionTds GetOpenTdsConnection() + internal SqlConnectionInternal GetOpenTdsConnection() { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; if (innerConnection == null) { throw ADP.ClosedConnectionError(); @@ -2541,9 +2539,9 @@ internal SqlInternalConnectionTds GetOpenTdsConnection() return innerConnection; } - internal SqlInternalConnectionTds GetOpenTdsConnection(string method) + internal SqlConnectionInternal GetOpenTdsConnection(string method) { - SqlInternalConnectionTds innerConnection = (InnerConnection as SqlInternalConnectionTds); + SqlConnectionInternal innerConnection = InnerConnection as SqlConnectionInternal; if (innerConnection == null) { throw ADP.OpenConnectionRequired(method, InnerConnection.State); @@ -2697,10 +2695,17 @@ private static void ChangePassword(string connectionString, SqlConnectionString // note: This is the only case where we directly construct the internal connection, passing in the new password. // Normally we would simply create a regular connection and open it, but there is no other way to pass the // new password down to the constructor. This would have an unwanted impact on the connection pool. - SqlInternalConnectionTds con = null; + SqlConnectionInternal con = null; try { - con = new SqlInternalConnectionTds(null, connectionOptions, credential, null, newPassword, newSecurePassword, false); + con = new SqlConnectionInternal( + identity: null, + connectionOptions, + credential, + providerInfo: null, + newPassword, + newSecurePassword, + redirectedUserInstance: false); } finally { 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 6f697d37e8..5c7759d921 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -643,46 +643,58 @@ protected virtual DbConnectionInternal CreateConnection( redirectedUserInstance = true; string instanceName; - if (pool == null || (pool != null && pool.Count <= 0)) - { // Non-pooled or pooled and no connections in the pool. - SqlInternalConnectionTds sseConnection = null; - try + if (pool == null || pool.Count <= 0) + { + // Non-pooled or pooled and no connections in the pool. + + // NOTE: Cloning connection option opt to set 'UserInstance=True' and 'Enlist=False' + // This first connection is established to SqlExpress to get the instance name + // of the UserInstance. + SqlConnectionString sseopt = new SqlConnectionString( + opt, + opt.DataSource, + userInstance: true, + setEnlistValue: false); + + SqlConnectionInternal sseConnection = new SqlConnectionInternal( + identity, + sseopt, + key.Credential, + providerInfo: null, + newPassword: string.Empty, + newSecurePassword: null, + redirectedUserInstance: false, + applyTransientFaultHandling: applyTransientFaultHandling, + sspiContextProvider: key.SspiContextProvider); + using (sseConnection) { - // We throw an exception in case of a failure - // NOTE: Cloning connection option opt to set 'UserInstance=True' and 'Enlist=False' - // This first connection is established to SqlExpress to get the instance name - // of the UserInstance. - SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false); - sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling, sspiContextProvider: key.SspiContextProvider); - // NOTE: Retrieve here. This user instance name will be used below to connect to the Sql Express User Instance. + // NOTE: Retrieve here. This user instance name will be + // used below to connect to the SQL Express User Instance. instanceName = sseConnection.InstanceName; // Set future transient fault handling based on connection options sqlOwningConnection._applyTransientFaultHandling = opt != null && opt.ConnectRetryCount > 0; - if (!instanceName.StartsWith("\\\\.\\", StringComparison.Ordinal)) + if (!instanceName.StartsWith(@"\\.\", StringComparison.Ordinal)) { throw SQL.NonLocalSSEInstance(); } if (pool != null) - { // Pooled connection - cache result + { + // Pooled connection - cache result SqlConnectionPoolProviderInfo providerInfo = (SqlConnectionPoolProviderInfo)pool.ProviderInfo; + // No lock since we are already in creation mutex providerInfo.InstanceName = instanceName; } } - finally - { - if (sseConnection != null) - { - sseConnection.Dispose(); - } - } } else - { // Cached info from pool. + { + // Cached info from pool. SqlConnectionPoolProviderInfo providerInfo = (SqlConnectionPoolProviderInfo)pool.ProviderInfo; + // No lock since we are already in creation mutex instanceName = providerInfo.InstanceName; } @@ -694,7 +706,7 @@ protected virtual DbConnectionInternal CreateConnection( poolGroupProviderInfo = null; // null so we do not pass to constructor below... } - return new SqlInternalConnectionTds( + return new SqlConnectionInternal( identity, opt, key.Credential, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 22da8a07a3..adb06498c1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -24,6 +24,7 @@ using System.Xml; using Microsoft.Data.Common; using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.Server; using Microsoft.Data.SqlTypes; @@ -4101,10 +4102,11 @@ internal TdsOperationStatus TryReadColumnInternal(int i, bool readHeaderOnly = f { // reset snapshot to save memory use. We can safely do that here because all SqlDataReader values are stable. // The retry logic can use the current values to get back to the right state. - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } + _snapshot = null; PrepareAsyncInvocation(useSnapshot: true); } @@ -5038,9 +5040,9 @@ public override Task ReadAsync(CancellationToken cancellationToken) } ReadAsyncCallContext context = null; - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, null); + context = sqlInternalConnection.CachedContexts.TakeDataReaderReadAsyncContext(); } if (context is null) { @@ -5085,10 +5087,11 @@ private static Task ReadAsyncExecute(Task task, object state) if (!hasReadRowToken) { hasReadRowToken = true; - if (reader.Connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (reader.Connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = reader._snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(reader._snapshot); } + reader._snapshot = null; reader.PrepareAsyncInvocation(useSnapshot: true); } @@ -5106,14 +5109,6 @@ private static Task ReadAsyncExecute(Task task, object state) return reader.ExecuteAsyncCall(context); } - private void SetCachedReadAsyncCallContext(ReadAsyncCallContext instance) - { - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, instance, null); - } - } - /// override public Task IsDBNullAsync(int i, CancellationToken cancellationToken) { @@ -5214,9 +5209,9 @@ override public Task IsDBNullAsync(int i, CancellationToken cancellationTo } IsDBNullAsyncCallContext context = null; - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, null); + context = sqlInternalConnection.CachedContexts.TakeDataReaderIsDbNullContext(); } if (context is null) { @@ -5257,14 +5252,6 @@ private static Task IsDBNullAsyncExecute(Task task, object state) } } - private void SetCachedIDBNullAsyncCallContext(IsDBNullAsyncCallContext instance) - { - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) - { - Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, instance, null); - } - } - /// override public Task GetFieldValueAsync(int i, CancellationToken cancellationToken) { @@ -5503,7 +5490,11 @@ internal ReadAsyncCallContext() protected override void AfterCleared(SqlDataReader owner) { - owner.SetCachedReadAsyncCallContext(this); + DbConnectionInternal internalConnection = owner?._connection?.InnerConnection; + if (internalConnection is SqlConnectionInternal sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetDataReaderReadAsyncContext(this); + } } } @@ -5519,7 +5510,11 @@ internal IsDBNullAsyncCallContext() { } protected override void AfterCleared(SqlDataReader owner) { - owner.SetCachedIDBNullAsyncCallContext(this); + DbConnectionInternal internalConnection = owner?._connection?.InnerConnection; + if (internalConnection is SqlConnectionInternal sqlInternalConnection) + { + sqlInternalConnection.CachedContexts.TrySetDataReaderIsDbNullContext(this); + } } } @@ -5800,9 +5795,9 @@ private void PrepareAsyncInvocation(bool useSnapshot) if (_snapshot == null) { - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - _snapshot = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderSnapshot, null) ?? new Snapshot(); + _snapshot = sqlInternalConnection.CachedContexts.TakeDataReaderSnapshot() ?? new Snapshot(); } else { @@ -5880,10 +5875,11 @@ private void CleanupAfterAsyncInvocationInternal(TdsParserStateObject stateObj, stateObj._permitReplayStackTraceToDiffer = false; #endif - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } + // We are setting this to null inside the if-statement because stateObj==null means that the reader hasn't been initialized or has been closed (either way _snapshot should already be null) _snapshot = null; } @@ -5922,10 +5918,11 @@ private void SwitchToAsyncWithoutSnapshot() Debug.Assert(_snapshot != null, "Should currently have a snapshot"); Debug.Assert(_stateObj != null && !_stateObj._asyncReadWithoutSnapshot, "Already in async without snapshot"); - if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + if (_connection?.InnerConnection is SqlConnectionInternal sqlInternalConnection) { - sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + sqlInternalConnection.CachedContexts.TrySetDataReaderSnapshot(_snapshot); } + _snapshot = null; _stateObj.ResetSnapshot(); _stateObj._asyncReadWithoutSnapshot = true; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs index eb7b65a497..c463b2aeb4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDelegatedTransaction.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Transactions; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -24,7 +25,7 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati // or notifications of same. Updates to the connection's association with the transaction or to the connection pool // may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy. - private SqlInternalConnection _connection; // the internal connection that is the root of the transaction + private SqlConnectionInternal _connection; // the internal connection that is the root of the transaction private System.Data.IsolationLevel _isolationLevel; // the IsolationLevel of the transaction we delegated to the server private SqlInternalTransaction _internalTransaction; // the SQL Server transaction we're delegating to @@ -32,7 +33,7 @@ internal sealed class SqlDelegatedTransaction : IPromotableSinglePhaseNotificati private bool _active; // Is the transaction active? - internal SqlDelegatedTransaction(SqlInternalConnection connection, Transaction tx) + internal SqlDelegatedTransaction(SqlConnectionInternal connection, Transaction tx) { Debug.Assert(connection != null, "null connection?"); _connection = connection; @@ -78,7 +79,7 @@ public void Initialize() { // if we get here, then we know for certain that we're the delegated // transaction. - SqlInternalConnection connection = _connection; + SqlConnectionInternal connection = _connection; SqlConnection usersConnection = connection.Connection; SqlClientEventSource.Log.TryTraceEvent("SqlDelegatedTransaction.Initialize | RES | CPOOL | Object Id {0}, Client Connection Id {1}, delegating transaction.", ObjectID, usersConnection?.ClientConnectionId); @@ -119,7 +120,7 @@ public byte[] Promote() // Don't read values off of the connection outside the lock unless it doesn't really matter // from an operational standpoint (i.e. logging connection's ObjectID should be fine, // but the PromotedDTCToken can change over calls. so that must be protected). - SqlInternalConnection connection = GetValidConnection(); + SqlConnectionInternal connection = GetValidConnection(); Exception promoteException; byte[] returnValue = null; @@ -212,7 +213,7 @@ public byte[] Promote() public void Rollback(SinglePhaseEnlistment enlistment) { Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnection connection = GetValidConnection(); + SqlConnectionInternal connection = GetValidConnection(); if (connection != null) { @@ -280,7 +281,7 @@ public void Rollback(SinglePhaseEnlistment enlistment) public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) { Debug.Assert(enlistment != null, "null enlistment?"); - SqlInternalConnection connection = GetValidConnection(); + SqlConnectionInternal connection = GetValidConnection(); if (connection != null) { @@ -385,7 +386,7 @@ public void SinglePhaseCommit(SinglePhaseEnlistment enlistment) // the transaction). internal void TransactionEnded(Transaction transaction) { - SqlInternalConnection connection = _connection; + SqlConnectionInternal connection = _connection; if (connection != null) { @@ -406,9 +407,9 @@ internal void TransactionEnded(Transaction transaction) } // Check for connection validity - private SqlInternalConnection GetValidConnection() + private SqlConnectionInternal GetValidConnection() { - SqlInternalConnection connection = _connection; + SqlConnectionInternal connection = _connection; if (connection == null && Transaction.TransactionInformation.Status != TransactionStatus.Aborted) { throw ADP.ObjectDisposed(this); @@ -420,7 +421,7 @@ private SqlInternalConnection GetValidConnection() // Dooms connection and throws and error if not a valid, active, delegated transaction for the given // connection. Designed to be called AFTER a lock is placed on the connection, otherwise a normal return // may not be trusted. - private void ValidateActiveOnConnection(SqlInternalConnection connection) + private void ValidateActiveOnConnection(SqlConnectionInternal connection) { bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this); 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 e070bb7d2e..b392a94521 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.Runtime.Serialization; using System.Text; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -174,7 +175,7 @@ public override string ToString() internal static SqlException CreateException( SqlError error, string serverVersion, - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, Exception innerException = null) { SqlErrorCollection errorCollection = new() { error }; @@ -200,7 +201,7 @@ internal static SqlException CreateException( internal static SqlException CreateException( SqlErrorCollection errorCollection, string serverVersion, - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, Exception innerException = null) { return CreateException( @@ -214,7 +215,7 @@ internal static SqlException CreateException( internal static SqlException CreateException( SqlErrorCollection errorCollection, string serverVersion, - SqlInternalConnectionTds internalConnection, + SqlConnectionInternal internalConnection, Exception innerException = null, SqlBatchCommand batchCommand = null) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs deleted file mode 100644 index 46852a5df4..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ /dev/null @@ -1,611 +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.Data.Common; -using System.Diagnostics; -using System.Transactions; -using Microsoft.Data.Common; -using Microsoft.Data.ProviderBase; - -#if NETFRAMEWORK -using System.Runtime.CompilerServices; -using System.Runtime.ConstrainedExecution; -#endif - -namespace Microsoft.Data.SqlClient -{ - internal abstract class SqlInternalConnection : DbConnectionInternal - { - /// - /// Cache the whereabouts (DTC Address) for exporting. - /// - private byte[] _whereAbouts; - - /// - /// ID of the Azure SQL DB Transaction Manager (Non-MSDTC) - /// - private static readonly Guid s_globalTransactionTMID = new("1c742caf-6680-40ea-9c26-6b6846079764"); - - internal SqlCommand.ExecuteReaderAsyncCallContext CachedCommandExecuteReaderAsyncContext; - internal SqlCommand.ExecuteNonQueryAsyncCallContext CachedCommandExecuteNonQueryAsyncContext; - internal SqlCommand.ExecuteXmlReaderAsyncCallContext CachedCommandExecuteXmlReaderAsyncContext; - internal SqlDataReader.Snapshot CachedDataReaderSnapshot; - internal SqlDataReader.IsDBNullAsyncCallContext CachedDataReaderIsDBNullContext; - internal SqlDataReader.ReadAsyncCallContext CachedDataReaderReadAsyncContext; - - /// - /// Constructs a new SqlInternalConnection object using the provided connection options. - /// - /// The options to use for this connection. - internal SqlInternalConnection(SqlConnectionString connectionOptions) : base() - { - Debug.Assert(connectionOptions != null, "null connectionOptions?"); - ConnectionOptions = connectionOptions; - } - - #region Properties - - // SQLBU 415870 - // Get the internal transaction that should be hooked to a new outer transaction - // during a BeginTransaction API call. In some cases (i.e. connection is going to - // be reset), CurrentTransaction should not be hooked up this way. - /// - /// TODO: need to understand this property better - /// - virtual internal SqlInternalTransaction AvailableInternalTransaction => CurrentTransaction; - - /// - /// A reference to the SqlConnection that owns this internal connection. - /// - internal SqlConnection Connection => (SqlConnection)Owner; - - /// - /// The connection options to be used for this connection. - /// - internal SqlConnectionString ConnectionOptions { get; init; } - - /// - /// The current database for this connection. - /// Null if the connection is not open yet. - /// - internal string CurrentDatabase { get; set; } - - /// - /// The current data source for this connection. - /// - /// if connection is not open yet, CurrentDataSource is null - /// if connection is open: - /// * for regular connections, it is set to the Data Source value from connection string - /// * for failover connections, it is set to the FailoverPartner value from the connection string - /// - internal string CurrentDataSource { get; set; } - - /// - /// The Transaction currently associated with this connection. - /// - abstract internal SqlInternalTransaction CurrentTransaction { get; } - - /// - /// The delegated (or promoted) transaction this connection is responsible for. - /// - internal SqlDelegatedTransaction DelegatedTransaction { get; set; } - - /// - /// Whether this connection has a local (non-delegated) transaction. - /// - internal bool HasLocalTransaction - { - get - { - SqlInternalTransaction currentTransaction = CurrentTransaction; - bool result = currentTransaction != null && currentTransaction.IsLocal; - return result; - } - } - - /// - /// Whether this connection has a local transaction started from the API (i.e., SqlConnection.BeginTransaction) - /// or had a TSQL transaction and later got wrapped by an API transaction. - /// - internal bool HasLocalTransactionFromAPI - { - get - { - SqlInternalTransaction currentTransaction = CurrentTransaction; - bool result = currentTransaction != null && currentTransaction.HasParentTransaction; - return result; - } - } - - /// - /// Whether the server version is SQL Server 2008 or newer. - /// - abstract internal bool Is2008OrNewer { get; } - - /// - /// Whether this connection is to an Azure SQL Database. - /// - internal bool IsAzureSqlConnection { get; set; } - - /// - /// Indicates whether the connection is currently enlisted in a transaction. - /// - internal bool IsEnlistedInTransaction { get; private set; } - - /// - /// Whether this is a Global Transaction (Non-MSDTC, Azure SQL DB Transaction) - /// TODO: overlaps with IsGlobalTransactionsEnabledForServer, need to consolidate to avoid bugs - /// - internal bool IsGlobalTransaction { get; set; } - - /// - /// Whether Global Transactions are enabled. Only supported by Azure SQL. - /// False if disabled or connected to on-prem SQL Server. - /// - internal bool IsGlobalTransactionsEnabledForServer { get; set; } - - /// - /// Whether this connection is locked for bulk copy operations. - /// - abstract internal bool IsLockedForBulkCopy { get; } - - /// - /// Whether this connection is the root of a delegated or promoted transaction. - /// - override internal bool IsTransactionRoot - { - get - { - SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction; - return delegatedTransaction != null && (delegatedTransaction.IsActive); - } - } - - /// - /// TODO: need to understand this property better - /// - abstract internal SqlInternalTransaction PendingTransaction { get; } - - /// - /// A token returned by the server when we promote transaction. - /// - internal byte[] PromotedDtcToken { get; set; } - - #endregion - - override public DbTransaction BeginTransaction(System.Data.IsolationLevel iso) - { - return BeginSqlTransaction(iso, null, false); - } - - virtual internal SqlTransaction BeginSqlTransaction(System.Data.IsolationLevel iso, string transactionName, bool shouldReconnect) - { - SqlStatistics statistics = null; - try - { - statistics = SqlStatistics.StartTimer(Connection.Statistics); - - #if NETFRAMEWORK - SqlConnection.ExecutePermission.Demand(); // MDAC 81476 - #endif - - ValidateConnectionForExecute(null); - - if (HasLocalTransactionFromAPI) - { - throw ADP.ParallelTransactionsNotSupported(Connection); - } - - if (iso == System.Data.IsolationLevel.Unspecified) - { - iso = System.Data.IsolationLevel.ReadCommitted; // Default to ReadCommitted if unspecified. - } - - SqlTransaction transaction = new(this, Connection, iso, AvailableInternalTransaction); - transaction.InternalTransaction.RestoreBrokenConnection = shouldReconnect; - ExecuteTransaction(TransactionRequest.Begin, transactionName, iso, transaction.InternalTransaction, false); - transaction.InternalTransaction.RestoreBrokenConnection = false; - return transaction; - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - finally - { - SqlStatistics.StopTimer(statistics); - } - } - - override public void ChangeDatabase(string database) - { - if (string.IsNullOrEmpty(database)) - { - throw ADP.EmptyDatabaseName(); - } - - ValidateConnectionForExecute(null); - - ChangeDatabaseInternal(database); // do the real work... - } - - abstract protected void ChangeDatabaseInternal(string database); - - override protected void CleanupTransactionOnCompletion(Transaction transaction) - { - // Note: unlocked, potentially multi-threaded code, so pull delegate to local to - // ensure it doesn't change between test and call. - SqlDelegatedTransaction delegatedTransaction = DelegatedTransaction; - if (delegatedTransaction != null) - { - delegatedTransaction.TransactionEnded(transaction); - } - } - - override protected DbReferenceCollection CreateReferenceCollection() - { - return new SqlReferenceCollection(); - } - - /// - override protected void Deactivate() - { - try - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.Deactivate | ADV | Object Id {0} deactivating, Client Connection Id {1}", ObjectID, Connection?.ClientConnectionId); - - SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; - if (referenceCollection != null) - { - referenceCollection.Deactivate(); - } - - // Invoke subclass-specific deactivation logic - InternalDeactivate(); - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - catch (Exception e) - { - if (!ADP.IsCatchableExceptionType(e)) - { - throw; - } - - // if an exception occurred, the inner connection will be - // marked as unusable and destroyed upon returning to the - // pool - DoomThisConnection(); -#if NETFRAMEWORK - ADP.TraceExceptionWithoutRethrow(e); -#endif - } - } - - abstract internal void DisconnectTransaction(SqlInternalTransaction internalTransaction); - - override public void Dispose() - { - _whereAbouts = null; - base.Dispose(); - } - - protected void Enlist(Transaction tx) - { - // This method should not be called while the connection has a - // reference to an active delegated transaction. - // Manual enlistment via SqlConnection.EnlistTransaction - // should catch this case and throw an exception. - // - // Automatic enlistment isn't possible because - // Sys.Tx keeps the connection alive until the transaction is completed. - // TODO: why do we assert pooling status? shouldn't we just be checking - // whether the connection is the root of the transaction? - Debug.Assert(!(IsTransactionRoot && Pool == null), "cannot defect an active delegated transaction!"); // potential race condition, but it's an assert - - if (tx == null) - { - if (IsEnlistedInTransaction) - { - EnlistNull(); - } - else - { - // When IsEnlistedInTransaction is false, it means we are in one of two states: - // 1. EnlistTransaction is null, so the connection is truly not enlisted in a transaction, or - // 2. Connection is enlisted in a SqlDelegatedTransaction. - // - // For #2, we have to consider whether or not the delegated transaction is active. - // If it is not active, we allow the enlistment in the NULL transaction. - // - // If it is active, technically this is an error. - // However, no exception is thrown as this was the precedent (and this case is silently ignored, no error, but no enlistment either). - // There are two mitigations for this: - // 1. SqlConnection.EnlistTransaction checks that the enlisted transaction has completed before allowing a different enlistment. - // 2. For debug builds, the assert at the beginning of this method checks for an enlistment in an active delegated transaction. - Transaction enlistedTransaction = EnlistedTransaction; - if (enlistedTransaction != null && enlistedTransaction.TransactionInformation.Status != TransactionStatus.Active) - { - EnlistNull(); - } - } - } - // Only enlist if it's different... - else if (!tx.Equals(EnlistedTransaction)) - { // WebData 20000024 - Must use Equals, not != - EnlistNonNull(tx); - } - } - - private void EnlistNonNull(Transaction tx) - { - Debug.Assert(tx != null, "null transaction?"); - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object {0}, Transaction Id {1}, attempting to delegate.", ObjectID, tx?.TransactionInformation?.LocalIdentifier); - bool hasDelegatedTransaction = false; - - // Promotable transactions are only supported on 2005 - // servers or newer. - SqlDelegatedTransaction delegatedTransaction = new(this, tx); - - try - { - // NOTE: System.Transactions claims to resolve all - // potential race conditions between multiple delegate - // requests of the same transaction to different - // connections in their code, such that only one - // attempt to delegate will succeed. - - // NOTE: PromotableSinglePhaseEnlist will eventually - // make a round trip to the server; doing this inside - // a lock is not the best choice. We presume that you - // aren't trying to enlist concurrently on two threads - // and leave it at that -- We don't claim any thread - // safety with regard to multiple concurrent requests - // to enlist the same connection in different - // transactions, which is good, because we don't have - // it anyway. - - // PromotableSinglePhaseEnlist may not actually promote - // the transaction when it is already delegated (this is - // the way they resolve the race condition when two - // threads attempt to delegate the same Lightweight - // Transaction) In that case, we can safely ignore - // our delegated transaction, and proceed to enlist - // in the promoted one. - - // NOTE: Global Transactions is an Azure SQL DB only - // feature where the Transaction Manager (TM) is not - // MS-DTC. Sys.Tx added APIs to support Non MS-DTC - // promoter types/TM in .NET 4.6.2. Following directions - // from .NETFX shiproom, to avoid a "hard-dependency" - // (compile time) on Sys.Tx, we use reflection to invoke - // the new APIs. Further, the IsGlobalTransaction flag - // indicates that this is an Azure SQL DB Transaction - // that could be promoted to a Global Transaction (it's - // always false for on-prem Sql Server). The Promote() - // call in SqlDelegatedTransaction makes sure that the - // right Sys.Tx.dll is loaded and that Global Transactions - // are actually allowed for this Azure SQL DB. - - if (IsGlobalTransaction) - { - if (SysTxForGlobalTransactions.EnlistPromotableSinglePhase == null) - { - // This could be a local Azure SQL DB transaction. - hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction); - } - else - { - hasDelegatedTransaction = (bool)SysTxForGlobalTransactions.EnlistPromotableSinglePhase.Invoke(tx, new object[] { delegatedTransaction, s_globalTransactionTMID }); - } - } - else - { - // This is an MS-DTC distributed transaction - hasDelegatedTransaction = tx.EnlistPromotableSinglePhase(delegatedTransaction); - } - - if (hasDelegatedTransaction) - { - DelegatedTransaction = delegatedTransaction; - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object Id {0}, Client Connection Id {1} delegated to transaction {1} with transactionId {2}", ObjectID, Connection?.ClientConnectionId, delegatedTransaction?.ObjectID, delegatedTransaction?.Transaction?.TransactionInformation?.LocalIdentifier); - } - } - catch (SqlException e) - { - // we do not want to eat the error if it is a fatal one - if (e.Class >= TdsEnums.FATAL_ERROR_CLASS) - { - throw; - } - - // if the parser is null or its state is not openloggedin, the connection is no longer good. - if (this is SqlInternalConnectionTds tdsConnection) - { - TdsParser parser = tdsConnection.Parser; - if (parser == null || parser.State != TdsParserState.OpenLoggedIn) - { - throw; - } - } - -#if NETFRAMEWORK - ADP.TraceExceptionWithoutRethrow(e); -#endif - // In this case, SqlDelegatedTransaction.Initialize - // failed and we don't necessarily want to reject - // things -- there may have been a legitimate reason - // for the failure. - } - - if (!hasDelegatedTransaction) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object Id {0}, delegation not possible, enlisting.", ObjectID); - byte[] cookie = null; - - if (IsGlobalTransaction) - { - if (SysTxForGlobalTransactions.GetPromotedToken == null) - { - throw SQL.UnsupportedSysTxForGlobalTransactions(); - } - - cookie = (byte[])SysTxForGlobalTransactions.GetPromotedToken.Invoke(tx, null); - } - else - { - if (_whereAbouts == null) - { - byte[] dtcAddress = GetDTCAddress(); - _whereAbouts = dtcAddress ?? throw SQL.CannotGetDTCAddress(); - } - cookie = GetTransactionCookie(tx, _whereAbouts); - } - - // send cookie to server to finish enlistment - PropagateTransactionCookie(cookie); - - IsEnlistedInTransaction = true; - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNonNull | ADV | Object Id {0}, Client Connection Id {1}, Enlisted in transaction with transactionId {2}", ObjectID, Connection?.ClientConnectionId, tx?.TransactionInformation?.LocalIdentifier); - } - - EnlistedTransaction = tx; // Tell the base class about our enlistment - - - // If we're on a 2005 or newer server, and we delegate the - // transaction successfully, we will have done a begin transaction, - // which produces a transaction id that we should execute all requests - // on. The TdsParser or SmiEventSink will store this information as - // the current transaction. - // - // Likewise, propagating a transaction to a 2005 or newer server will - // produce a transaction id that The TdsParser or SmiEventSink will - // store as the current transaction. - // - // In either case, when we're working with a 2005 or newer server - // we better have a current transaction by now. - - Debug.Assert(CurrentTransaction != null, "delegated/enlisted transaction with null current transaction?"); - } - - internal void EnlistNull() - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNull | ADV | Object Id {0}, unenlisting.", ObjectID); - // We were in a transaction, but now we are not - so send - // message to server with empty transaction - confirmed proper - // behavior from Sameet Agarwal - // - // The connection pooler maintains separate pools for enlisted - // transactions, and only when that transaction is committed or - // rolled back will those connections be taken from that - // separate pool and returned to the general pool of connections - // that are not affiliated with any transactions. When this - // occurs, we will have a new transaction of null and we are - // required to send an empty transaction payload to the server. - - PropagateTransactionCookie(null); - - IsEnlistedInTransaction = false; - EnlistedTransaction = null; // Tell the base class about our enlistment - - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlInternalConnection.EnlistNull | ADV | Object Id {0}, unenlisted.", ObjectID); - - // The EnlistTransaction above will return an TransactionEnded event, - // which causes the TdsParser or SmiEventSink should to clear the - // current transaction. - // - // In either case, when we're working with a 2005 or newer server - // we better not have a current transaction at this point. - - Debug.Assert(CurrentTransaction == null, "unenlisted transaction with non-null current transaction?"); // verify it! - } - - override public void EnlistTransaction(Transaction transaction) - { -#if NETFRAMEWORK - SqlConnection.VerifyExecutePermission(); -#endif - ValidateConnectionForExecute(null); - - // If a connection has a local transaction outstanding and you try - // to enlist in a DTC transaction, SQL Server will rollback the - // local transaction and then do the enlist (7.0 and 2000). So, if - // the user tries to do this, throw. - if (HasLocalTransaction) - { - throw ADP.LocalTransactionPresent(); - } - - if (transaction != null && transaction.Equals(EnlistedTransaction)) - { - // No-op if this is the current transaction - return; - } - - // If a connection is already enlisted in a DTC transaction and you - // try to enlist in another one, in 7.0 the existing DTC transaction - // would roll back and then the connection would enlist in the new - // one. In SQL 2000 & 2005, when you enlist in a DTC transaction - // while the connection is already enlisted in a DTC transaction, - // the connection simply switches enlistments. Regardless, simply - // enlist in the user specified distributed transaction. This - // behavior matches OLEDB and ODBC. - - Enlist(transaction); - // @TODO: CER Exception Handling was removed here (see GH#3581) - } - - abstract internal void ExecuteTransaction(TransactionRequest transactionRequest, string name, System.Data.IsolationLevel iso, SqlInternalTransaction internalTransaction, bool isDelegateControlRequest); - - internal SqlDataReader FindLiveReader(SqlCommand command) - { - SqlDataReader reader = null; - SqlReferenceCollection referenceCollection = (SqlReferenceCollection)ReferenceCollection; - if (referenceCollection != null) - { - reader = referenceCollection.FindLiveReader(command); - } - return reader; - } - - abstract protected byte[] GetDTCAddress(); - - static private byte[] GetTransactionCookie(Transaction transaction, byte[] whereAbouts) - { - byte[] transactionCookie = null; - if (transaction != null) - { - transactionCookie = TransactionInterop.GetExportCookie(transaction, whereAbouts); - } - return transactionCookie; - } - - virtual protected void InternalDeactivate() - { - } - - // If wrapCloseInAction is defined, then the action it defines will be run with the connection close action passed in as a parameter - // The close action also supports being run asynchronously - internal void OnError(SqlException exception, bool breakConnection, Action wrapCloseInAction = null) - { - if (breakConnection) - { - DoomThisConnection(); - } - - SqlConnection connection = Connection; - if (connection != null) - { - connection.OnError(exception, breakConnection, wrapCloseInAction); - } - else if (exception.Class >= TdsEnums.MIN_ERROR_CLASS) - { - // It is an error, and should be thrown. Class of TdsEnums.MIN_ERROR_CLASS - // or above is an error, below TdsEnums.MIN_ERROR_CLASS denotes an info message. - throw exception; - } - } - - abstract protected void PropagateTransactionCookie(byte[] transactionCookie); - - abstract internal void ValidateConnectionForExecute(SqlCommand command); - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs index 5cb7b990dc..cc43c59e99 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Threading; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; namespace Microsoft.Data.SqlClient { @@ -36,7 +37,7 @@ sealed internal class SqlInternalTransaction private readonly TransactionType _transactionType; private long _transactionId; // passed in the MARS headers private int _openResultCount; // passed in the MARS headers - private SqlInternalConnection _innerConnection; + private SqlConnectionInternal _innerConnection; private bool _disposing; // used to prevent us from throwing exceptions while we're disposing private WeakReference _parent; // weak ref to the outer transaction object; needs to be weak to allow GC to occur. @@ -46,11 +47,19 @@ sealed internal class SqlInternalTransaction internal bool RestoreBrokenConnection { get; set; } internal bool ConnectionHasBeenRestored { get; set; } - internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction) : this(innerConnection, type, outerTransaction, NullTransactionId) + internal SqlInternalTransaction( + SqlConnectionInternal innerConnection, + TransactionType type, + SqlTransaction outerTransaction) + : this(innerConnection, type, outerTransaction, NullTransactionId) { } - internal SqlInternalTransaction(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction, long transactionId) + internal SqlInternalTransaction( + SqlConnectionInternal innerConnection, + TransactionType type, + SqlTransaction outerTransaction, + long transactionId) { SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.ctor | RES | CPOOL | Object Id {0}, Created for connection {1}, outer transaction {2}, Type {3}", ObjectID, innerConnection.ObjectID, outerTransaction?.ObjectId, (int)type); _innerConnection = innerConnection; @@ -187,7 +196,7 @@ private void CheckTransactionLevelAndZombie() internal void CloseFromConnection() { - SqlInternalConnection innerConnection = _innerConnection; + SqlConnectionInternal innerConnection = _innerConnection; Debug.Assert(innerConnection != null, "How can we be here if the connection is null?"); SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.CloseFromConnection | RES | CPOOL | Object Id {0}, Closing transaction", ObjectID); @@ -452,7 +461,7 @@ internal void Zombie() ZombieParent(); - SqlInternalConnection innerConnection = _innerConnection; + SqlConnectionInternal innerConnection = _innerConnection; _innerConnection = null; if (innerConnection != null) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs index 011a5ab364..28f8a3d6db 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Threading; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Diagnostics; #if NETFRAMEWORK using System.Runtime.CompilerServices; @@ -28,7 +29,7 @@ public sealed class SqlTransaction : DbTransaction private bool _isFromApi; internal SqlTransaction( - SqlInternalConnection internalConnection, + SqlConnectionInternal internalConnection, SqlConnection con, IsolationLevel iso, SqlInternalTransaction internalTransaction) @@ -41,11 +42,15 @@ internal SqlTransaction( if (internalTransaction == null) { - InternalTransaction = new SqlInternalTransaction(internalConnection, TransactionType.LocalFromAPI, this); + InternalTransaction = new SqlInternalTransaction( + internalConnection, + TransactionType.LocalFromAPI, + this); } else { - Debug.Assert(internalConnection.CurrentTransaction == internalTransaction, "Unexpected Parser.CurrentTransaction state!"); + Debug.Assert(internalConnection.CurrentTransaction == internalTransaction, + "Unexpected Parser.CurrentTransaction state!"); InternalTransaction = internalTransaction; InternalTransaction.InitParent(this); } @@ -289,7 +294,7 @@ internal void Zombie() // For Yukon, we have to defer "zombification" until we get past the users' next // rollback, else we'll throw an exception there that is a breaking change. Of course, // if the connection is already closed, then we're free to zombify... - if (_connection.InnerConnection is SqlInternalConnection internalConnection && !_isFromApi) + if (_connection.InnerConnection is SqlConnectionInternal && !_isFromApi) { SqlClientEventSource.Log.TryAdvancedTraceEvent( "SqlTransaction.Zombie | ADV | Object Id {0} yukon deferred zombie", diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index 540cf3b95c..7c7d3e8c87 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -16,6 +16,7 @@ using System.Transactions; using Interop.Common.Sni; using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.Connection; #if NETFRAMEWORK using System.Runtime.InteropServices; @@ -867,7 +868,9 @@ internal static Exception SqlDependencyNoMatchingServerDatabaseStart() // // SQL.SqlDelegatedTransaction // - static internal Exception CannotCompleteDelegatedTransactionWithOpenResults(SqlInternalConnectionTds internalConnection, bool marsOn) + static internal Exception CannotCompleteDelegatedTransactionWithOpenResults( + SqlConnectionInternal internalConnection, + bool marsOn) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(TdsEnums.TIMEOUT_EXPIRED, (byte)0x00, TdsEnums.MIN_ERROR_CLASS, null, (StringsHelper.GetString(Strings.ADP_OpenReaderExists, marsOn ? ADP.Command : ADP.Connection)), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); @@ -1110,7 +1113,9 @@ internal static Exception UnsupportedSysTxForGlobalTransactions() /// * server-provided failover partner - raising SqlException in this case /// * connection string with failover partner and MultiSubnetFailover=true - raising argument one in this case with the same message /// - internal static Exception MultiSubnetFailoverWithFailoverPartner(bool serverProvidedFailoverPartner, SqlInternalConnectionTds internalConnection) + internal static Exception MultiSubnetFailoverWithFailoverPartner( + bool serverProvidedFailoverPartner, + SqlConnectionInternal internalConnection) { string msg = StringsHelper.GetString(Strings.SQLMSF_FailoverPartnerNotSupported); if (serverProvidedFailoverPartner) @@ -1154,7 +1159,7 @@ internal static Exception ROR_FailoverNotSupportedConnString() return ADP.Argument(StringsHelper.GetString(Strings.SQLROR_FailoverNotSupported)); } - internal static Exception ROR_FailoverNotSupportedServer(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_FailoverNotSupportedServer(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_FailoverNotSupported)), "", 0)); @@ -1163,7 +1168,7 @@ internal static Exception ROR_FailoverNotSupportedServer(SqlInternalConnectionTd return exc; } - internal static Exception ROR_RecursiveRoutingNotSupported(SqlInternalConnectionTds internalConnection, int maxNumberOfRedirectRoute) + internal static Exception ROR_RecursiveRoutingNotSupported(SqlConnectionInternal internalConnection, int maxNumberOfRedirectRoute) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_RecursiveRoutingNotSupported, maxNumberOfRedirectRoute)), "", 0)); @@ -1172,7 +1177,7 @@ internal static Exception ROR_RecursiveRoutingNotSupported(SqlInternalConnection return exc; } - internal static Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_InvalidRoutingInfo(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_InvalidRoutingInfo)), "", 0)); @@ -1181,7 +1186,7 @@ internal static Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds intern return exc; } - internal static Exception ROR_TimeoutAfterRoutingInfo(SqlInternalConnectionTds internalConnection) + internal static Exception ROR_TimeoutAfterRoutingInfo(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_TimeoutAfterRoutingInfo)), "", 0)); @@ -1221,7 +1226,7 @@ internal static Exception CR_NextAttemptWillExceedQueryTimeout(SqlException inne return exc; } - internal static Exception CR_EncryptionChanged(SqlInternalConnectionTds internalConnection) + internal static Exception CR_EncryptionChanged(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_EncryptionChanged), "", 0)); @@ -1237,7 +1242,7 @@ internal static SqlException CR_AllAttemptsFailed(SqlException innerException, G return exc; } - internal static SqlException CR_NoCRAckAtReconnection(SqlInternalConnectionTds internalConnection) + internal static SqlException CR_NoCRAckAtReconnection(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_NoCRAckAtReconnection), "", 0)); @@ -1245,7 +1250,7 @@ internal static SqlException CR_NoCRAckAtReconnection(SqlInternalConnectionTds i return exc; } - internal static SqlException CR_TDSVersionNotPreserved(SqlInternalConnectionTds internalConnection) + internal static SqlException CR_TDSVersionNotPreserved(SqlConnectionInternal internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_TDSVersionNotPreserved), "", 0)); 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 115c62f6c5..07ab80b96a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -101,7 +101,7 @@ internal sealed partial class TdsParser private int _nonTransactedOpenResultCount = 0; // Connection reference - private SqlInternalConnectionTds _connHandler; + private SqlConnectionInternal _connHandler; // Async/Mars variables private bool _fMARS = false; @@ -209,7 +209,7 @@ internal TdsParser(bool MARS, bool fAsynchronous) DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; } - internal SqlInternalConnectionTds Connection + internal SqlConnectionInternal Connection { get { @@ -359,17 +359,19 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) } } - internal void Connect(ServerInfo serverInfo, - SqlInternalConnectionTds connHandler, - TimeoutTimer timeout, - SqlConnectionString connectionOptions, -#if NETFRAMEWORK - bool withFailover, - bool isFirstTransparentAttempt, - bool disableTnir -#else - bool withFailover -#endif + internal void Connect( + ServerInfo serverInfo, + SqlConnectionInternal connHandler, + TimeoutTimer timeout, + SqlConnectionString connectionOptions, + + #if NETFRAMEWORK + bool withFailover, + bool isFirstTransparentAttempt, + bool disableTnir + #else + bool withFailover + #endif ) { SqlConnectionEncryptOption encrypt = connectionOptions.Encrypt; @@ -1689,7 +1691,7 @@ internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand if (asyncClose) { // Wait until we have the parser lock, then try to close - SqlInternalConnectionTds connHandler = _connHandler; + SqlConnectionInternal connHandler = _connHandler; Action wrapCloseAction = closeAction => { Task.Factory.StartNew(() => @@ -6236,7 +6238,9 @@ private TdsOperationStatus TryProcessRow(_SqlMetaDataSet columns, object[] buffe /// Determines if a column value should be transparently decrypted (based on SqlCommand and Connection String settings). /// /// true if the value should be transparently decrypted, false otherwise - internal static bool ShouldHonorTceForRead(SqlCommandColumnEncryptionSetting columnEncryptionSetting, SqlInternalConnectionTds connection) + internal static bool ShouldHonorTceForRead( + SqlCommandColumnEncryptionSetting columnEncryptionSetting, + SqlConnectionInternal connection) { // Command leve setting trumps all switch (columnEncryptionSetting) @@ -6259,7 +6263,7 @@ internal static bool ShouldHonorTceForRead(SqlCommandColumnEncryptionSetting col internal static object GetNullSqlValue(SqlBuffer nullVal, SqlMetaDataPriv md, SqlCommandColumnEncryptionSetting columnEncryptionSetting, - SqlInternalConnectionTds connection) + SqlConnectionInternal connection) { SqlDbType type = md.type; @@ -10000,10 +10004,10 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques static (Task task, object state) => { Debug.Assert(!task.IsCanceled, "Task should not be canceled"); - var parameters = (Tuple)state; + var parameters = (Tuple)state; TdsParser parser = parameters.Item1; TdsParserStateObject tdsParserStateObject = parameters.Item2; - SqlInternalConnectionTds internalConnectionTds = parameters.Item3; + SqlConnectionInternal internalConnectionTds = parameters.Item3; try { if (task.IsFaulted) @@ -10237,7 +10241,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout if (releaseConnectionLock) { task.ContinueWith( - static (Task _, object state) => ((SqlInternalConnectionTds)state)._parserLock.Release(), + static (Task _, object state) => ((SqlConnectionInternal)state)._parserLock.Release(), state: _connHandler, TaskScheduler.Default ); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs index dfc37d2720..435d9cd956 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs @@ -4,7 +4,7 @@ using System; using System.Data; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.Tests.Common; using Microsoft.SqlServer.TDS.Servers; using Xunit; @@ -563,7 +563,7 @@ public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUs // Connect once to the primary to trigger it to send the failover partner connection.Open(); - Assert.Equal("invalidhost", (connection.InnerConnection as SqlInternalConnectionTds)!.ServerProvidedFailoverPartner); + Assert.Equal("invalidhost", (connection.InnerConnection as SqlConnectionInternal)!.ServerProvidedFailoverPartner); // Close the connection to return it to the pool connection.Close();