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 e1483a26a8..222cdc8403 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -43,6 +43,9 @@ Microsoft\Data\Sql\SqlNotificationRequest.cs + + Microsoft\Data\SqlClient\SqlInternalTransaction.cs + Microsoft\Data\Common\ActivityCorrelator.cs @@ -586,7 +589,6 @@ - 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 a4ecd51c67..9066ce2e98 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -98,6 +98,9 @@ Microsoft\Data\SqlClient\SqlClientLogger.cs + + Microsoft\Data\SqlClient\SqlInternalTransaction.cs + Microsoft\Data\Common\ActivityCorrelator.cs @@ -561,7 +564,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/sqlinternaltransaction.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/sqlinternaltransaction.cs deleted file mode 100644 index df63fef962..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/sqlinternaltransaction.cs +++ /dev/null @@ -1,588 +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; -using System.Diagnostics; -using System.Threading; -using Microsoft.Data.Common; - -namespace Microsoft.Data.SqlClient -{ - internal enum TransactionState - { - Pending = 0, - Active = 1, - Aborted = 2, - Committed = 3, - Unknown = 4, - } - - internal enum TransactionType - { - LocalFromTSQL = 1, - LocalFromAPI = 2, - Delegated = 3, - Distributed = 4, - Context = 5, // only valid in proc. - } - - sealed internal class SqlInternalTransaction - { - internal const long NullTransactionId = 0; - - private TransactionState _transactionState; - private TransactionType _transactionType; - private long _transactionId; // passed in the MARS headers - private int _openResultCount; // passed in the MARS headers - private SqlInternalConnection _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. - - private static int _objectTypeCount; // EventSource Counter - internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); - - 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(SqlInternalConnection innerConnection, TransactionType type, SqlTransaction outerTransaction, long transactionId) - { - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Created for connection {1}, outer transaction {2}, Type {3}", ObjectID, innerConnection.ObjectID, (null != outerTransaction) ? outerTransaction.ObjectID : -1, (int)type); - - _innerConnection = innerConnection; - _transactionType = type; - - if (null != outerTransaction) - { - _parent = new WeakReference(outerTransaction); - } - - _transactionId = transactionId; - RestoreBrokenConnection = false; - ConnectionHasBeenRestored = false; - } - - internal bool HasParentTransaction - { - get - { - // Return true if we are an API started local transaction, or if we were a TSQL - // started local transaction and were then wrapped with a parent transaction as - // a result of a later API begin transaction. - bool result = ((TransactionType.LocalFromAPI == _transactionType) || - (TransactionType.LocalFromTSQL == _transactionType && _parent != null)); - return result; - } - } - - internal bool IsAborted - { - get - { - return (TransactionState.Aborted == _transactionState); - } - } - - internal bool IsActive - { - get - { - return (TransactionState.Active == _transactionState); - } - } - - internal bool IsCommitted - { - get - { - return (TransactionState.Committed == _transactionState); - } - } - - internal bool IsCompleted - { - get - { - return (TransactionState.Aborted == _transactionState - || TransactionState.Committed == _transactionState - || TransactionState.Unknown == _transactionState); - } - } - - internal bool IsContext - { - get - { - bool result = (TransactionType.Context == _transactionType); - return result; - } - } - - internal bool IsDelegated - { - get - { - bool result = (TransactionType.Delegated == _transactionType); - return result; - } - } - - internal bool IsDistributed - { - get - { - bool result = (TransactionType.Distributed == _transactionType); - return result; - } - } - - internal bool IsLocal - { - get - { - bool result = (TransactionType.LocalFromTSQL == _transactionType - || TransactionType.LocalFromAPI == _transactionType - || TransactionType.Context == _transactionType); - return result; - } - } - - internal bool IsOrphaned - { - get - { - // An internal transaction is orphaned when its parent has been - // reclaimed by GC. - bool result; - if (null == _parent) - { - // No parent, so we better be LocalFromTSQL. Should we even return in this case - - // since it could be argued this is invalid? - Debug.Fail("Why are we calling IsOrphaned with no parent?"); - Debug.Assert(_transactionType == TransactionType.LocalFromTSQL, "invalid state"); - result = false; - } - else if (!_parent.TryGetTarget(out SqlTransaction _)) - { - // We had a parent, but parent was GC'ed. - result = true; - } - else - { - // We have a parent, and parent is alive. - result = false; - } - - return result; - } - } - - internal bool IsZombied - { - get - { - return (null == _innerConnection); - } - } - - internal int ObjectID - { - get - { - return _objectID; - } - } - - internal int OpenResultsCount - { - get - { - return _openResultCount; - } - } - - internal SqlTransaction Parent - { - get - { - SqlTransaction result = null; - // Should we protect against this, since this probably is an invalid state? - Debug.Assert(null != _parent, "Why are we calling Parent with no parent?"); - if (_parent != null && _parent.TryGetTarget(out SqlTransaction target)) - { - result = target; - } - return result; - } - } - - internal long TransactionId - { - get - { - return _transactionId; - } - set - { - Debug.Assert(NullTransactionId == _transactionId, "setting transaction cookie while one is active?"); - _transactionId = value; - } - } - - internal void Activate() - { - _transactionState = TransactionState.Active; - } - - private void CheckTransactionLevelAndZombie() - { - try - { - if (!IsZombied && GetServerTransactionLevel() == 0) - { - // If not zombied, not closed, and not in transaction, zombie. - Zombie(); - } - } - catch (Exception e) - { - // UNDONE - should not be catching all exceptions!!! - if (!ADP.IsCatchableExceptionType(e)) - { - throw; - } - - ADP.TraceExceptionWithoutRethrow(e); - Zombie(); // If exception caught when trying to check level, zombie. - } - } - - internal void CloseFromConnection() - { - SqlInternalConnection innerConnection = _innerConnection; - - Debug.Assert(innerConnection != null, "How can we be here if the connection is null?"); - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Closing", ObjectID); - - bool processFinallyBlock = true; - try - { - innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.IfRollback, null, IsolationLevel.Unspecified, null, false); - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - TdsParser.ReliabilitySection.Assert("unreliable call to CloseFromConnection"); // you need to setup for a thread abort somewhere before you call this method - if (processFinallyBlock) - { - // Always ensure we're zombied; 2005 will send an EnvChange that - // will cause the zombie, but only if we actually go to the wire; - // 7.0 and 2000 won't send the env change, so we have to handle - // them ourselves. - Zombie(); - } - } - } - - internal void Commit() - { - using (TryEventScope.Create(" {0}", ObjectID)) - { - if (_innerConnection.IsLockedForBulkCopy) - { - throw SQL.ConnectionLockedForBcpEvent(); - } - - _innerConnection.ValidateConnectionForExecute(null); - - // If this transaction has been completed, throw exception since it is unusable. - try - { - // COMMIT ignores transaction names, and so there is no reason to pass it anything. COMMIT - // simply commits the transaction from the most recent BEGIN, nested or otherwise. - _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, IsolationLevel.Unspecified, null, false); - - // SQL BU DT 291159 - perform full Zombie on pre-2005, but do not actually - // complete internal transaction until informed by server in the case of 2005 - // or later. - if (!IsZombied && !_innerConnection.Is2005OrNewer) - { - // Since nested transactions are no longer allowed, set flag to false. - // This transaction has been completed. - Zombie(); - } - else - { - ZombieParent(); - } - } - catch (Exception e) - { - // UNDONE - should not be catching all exceptions!!! - if (ADP.IsCatchableExceptionType(e)) - { - CheckTransactionLevelAndZombie(); - } - - throw; - } - } - } - - internal void Completed(TransactionState transactionState) - { - Debug.Assert(TransactionState.Active < transactionState, "invalid transaction completion state?"); - _transactionState = transactionState; - Zombie(); - } - - internal Int32 DecrementAndObtainOpenResultCount() - { - Int32 openResultCount = Interlocked.Decrement(ref _openResultCount); - if (openResultCount < 0) - { - throw SQL.OpenResultCountExceeded(); - } - return openResultCount; - } - - internal void Dispose() - { - this.Dispose(true); - System.GC.SuppressFinalize(this); - } - - private /*protected override*/ void Dispose(bool disposing) - { - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Disposing", ObjectID); - - if (disposing) - { - if (null != _innerConnection) - { - // implicitly rollback if transaction still valid - _disposing = true; - this.Rollback(); - } - } - } - - private int GetServerTransactionLevel() - { - // This function is needed for those times when it is impossible to determine the server's - // transaction level, unless the user's arguments were parsed - which is something we don't want - // to do. An example when it is impossible to determine the level is after a rollback. - - // TODO: we really ought to be able to execute without using the public objects... - - using (SqlCommand transactionLevelCommand = new SqlCommand("set @out = @@trancount", (SqlConnection)(_innerConnection.Owner))) - { - transactionLevelCommand.Transaction = Parent; - - SqlParameter parameter = new SqlParameter("@out", SqlDbType.Int); - parameter.Direction = ParameterDirection.Output; - transactionLevelCommand.Parameters.Add(parameter); - - // UNDONE: use a singleton select here - // UNDONE: execute without SqlClientPermission.Demand() - transactionLevelCommand.RunExecuteReader(0, RunBehavior.UntilDone, false /* returnDataStream */, nameof(GetServerTransactionLevel)); - - return (int)parameter.Value; - } - } - - internal Int32 IncrementAndObtainOpenResultCount() - { - Int32 openResultCount = Interlocked.Increment(ref _openResultCount); - - if (openResultCount < 0) - { - throw SQL.OpenResultCountExceeded(); - } - return openResultCount; - } - - internal void InitParent(SqlTransaction transaction) - { - Debug.Assert(_parent == null, "Why do we have a parent on InitParent?"); - _parent = new WeakReference(transaction); - } - - internal void Rollback() - { - using (TryEventScope.Create(" {0}", ObjectID)) - { - if (_innerConnection.IsLockedForBulkCopy) - { - throw SQL.ConnectionLockedForBcpEvent(); - } - - _innerConnection.ValidateConnectionForExecute(null); - - try - { - // If no arg is given to ROLLBACK it will rollback to the outermost begin - rolling back - // all nested transactions as well as the outermost transaction. - _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.IfRollback, null, IsolationLevel.Unspecified, null, false); - - // Since Rollback will rollback to outermost begin, no need to check - // server transaction level. This transaction has been completed. - Zombie(); - } - catch (Exception e) - { - // UNDONE - should not be catching all exceptions!!! - if (ADP.IsCatchableExceptionType(e)) - { - CheckTransactionLevelAndZombie(); - - if (!_disposing) - { - throw; - } - } - else - { - throw; - } - } - } - } - - internal void Rollback(string transactionName) - { - using (TryEventScope.Create(" {0}, transactionName='{1}'", ObjectID, transactionName)) - { - if (_innerConnection.IsLockedForBulkCopy) - { - throw SQL.ConnectionLockedForBcpEvent(); - } - - _innerConnection.ValidateConnectionForExecute(null); - - // ROLLBACK takes either a save point name or a transaction name. It will rollback the - // transaction to either the save point with the save point name or begin with the - // transaction name. NOTE: for simplicity it is possible to give all save point names - // the same name, and ROLLBACK will simply rollback to the most recent save point with the - // save point name. - if (ADP.IsEmpty(transactionName)) - { - throw SQL.NullEmptyTransactionName(); - } - - try - { - _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Rollback, transactionName, IsolationLevel.Unspecified, null, false); - - if (!IsZombied && !_innerConnection.Is2005OrNewer) - { - // Check if Zombied before making round-trip to server. - // Against 2005 we receive an envchange on the ExecuteTransaction above on the - // parser that calls back into SqlTransaction for the Zombie() call. - CheckTransactionLevelAndZombie(); - } - } - catch (Exception e) - { - // UNDONE - should not be catching all exceptions!!! - if (ADP.IsCatchableExceptionType(e)) - { - CheckTransactionLevelAndZombie(); - } - throw; - } - } - } - - internal void Save(string savePointName) - { - using (TryEventScope.Create(" {0}, savePointName='{1}'", ObjectID, savePointName)) - { - _innerConnection.ValidateConnectionForExecute(null); - - // ROLLBACK takes either a save point name or a transaction name. It will rollback the - // transaction to either the save point with the save point name or begin with the - // transaction name. So, to rollback a nested transaction you must have a save point. - // SAVE TRANSACTION MUST HAVE AN ARGUMENT!!! Save Transaction without an arg throws an - // exception from the server. So, an overload for SaveTransaction without an arg doesn't make - // sense to have. Save Transaction does not affect the transaction level. - if (ADP.IsEmpty(savePointName)) - { - throw SQL.NullEmptyTransactionName(); - } - - try - { - _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Save, savePointName, IsolationLevel.Unspecified, null, false); - } - catch (Exception e) - { - // UNDONE - should not be catching all exceptions!!! - if (ADP.IsCatchableExceptionType(e)) - { - CheckTransactionLevelAndZombie(); - } - - throw; - } - } - } - - internal void Zombie() - { - // Called by several places in the code to ensure that the outer - // transaction object has been zombied and the parser has broken - // it's reference to us. - - // NOTE: we'll be called from the TdsParser when it gets appropriate - // ENVCHANGE events that indicate the transaction has completed, however - // we cannot rely upon those events occuring in the case of pre-2005 - // servers (and when we don't go to the wire because the connection - // is broken) so we can also be called from the Commit/Rollback/Save - // methods to handle that case as well. - - // There are two parts to a full zombie: - // 1) Zombie parent and disconnect outer transaction from internal transaction - // 2) Disconnect internal transaction from connection and parser - // Number 1 needs to be done whenever a SqlTransaction object is completed. Number - // 2 is only done when a transaction is actually completed. Since users can begin - // transactions both in and outside of the API, and since nested begins are not actual - // transactions we need to distinguish between #1 and #2. See SQL BU DT 291159 - // for further details. - - ZombieParent(); - - SqlInternalConnection innerConnection = _innerConnection; - _innerConnection = null; - - if (null != innerConnection) - { - innerConnection.DisconnectTransaction(this); - } - } - - private void ZombieParent() - { - if (_parent != null && _parent.TryGetTarget(out SqlTransaction parent)) - { - parent.Zombie(); - } - _parent = null; - } - - internal string TraceString() - { - return string.Format(/*IFormatProvider*/ null, "(ObjId={0}, tranId={1}, state={2}, type={3}, open={4}, disp={5}", - ObjectID, _transactionId, _transactionState, _transactionType, _openResultCount, _disposing); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs similarity index 80% rename from src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs index 5d706c6c0a..114363f5a6 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs @@ -26,22 +26,22 @@ internal enum TransactionType Delegated = 3, Distributed = 4, Context = 5, // only valid in proc. - }; + } sealed internal class SqlInternalTransaction { internal const long NullTransactionId = 0; private TransactionState _transactionState; - private TransactionType _transactionType; + private readonly TransactionType _transactionType; private long _transactionId; // passed in the MARS headers private int _openResultCount; // passed in the MARS headers private SqlInternalConnection _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. - private static int _objectTypeCount; // EventSource counter - internal readonly int _objectID = Interlocked.Increment(ref _objectTypeCount); + private static int s_objectTypeCount; // EventSource counter + internal readonly int _objectID = Interlocked.Increment(ref s_objectTypeCount); internal bool RestoreBrokenConnection { get; set; } internal bool ConnectionHasBeenRestored { get; set; } @@ -66,81 +66,37 @@ internal SqlInternalTransaction(SqlInternalConnection innerConnection, Transacti ConnectionHasBeenRestored = false; } - internal bool HasParentTransaction - { - get - { + internal bool HasParentTransaction => // Return true if we are an API started local transaction, or if we were a TSQL // started local transaction and were then wrapped with a parent transaction as // a result of a later API begin transaction. - bool result = ((TransactionType.LocalFromAPI == _transactionType) || - (TransactionType.LocalFromTSQL == _transactionType && _parent != null)); - return result; - } - } + (_transactionType == TransactionType.LocalFromAPI) || + (_transactionType == TransactionType.LocalFromTSQL && _parent != null); - internal bool IsAborted - { - get - { - return (TransactionState.Aborted == _transactionState); - } - } + internal bool IsAborted => _transactionState == TransactionState.Aborted; - internal bool IsActive - { - get - { - return (TransactionState.Active == _transactionState); - } - } + internal bool IsActive => _transactionState == TransactionState.Active; - internal bool IsCommitted - { - get - { - return (TransactionState.Committed == _transactionState); - } - } + internal bool IsCommitted => _transactionState == TransactionState.Committed; - internal bool IsCompleted - { - get - { - return (TransactionState.Aborted == _transactionState - || TransactionState.Committed == _transactionState - || TransactionState.Unknown == _transactionState); - } - } + internal bool IsCompleted => _transactionState == TransactionState.Aborted + || _transactionState == TransactionState.Committed + || _transactionState == TransactionState.Unknown; - internal bool IsDelegated - { - get - { - bool result = (TransactionType.Delegated == _transactionType); - return result; - } - } + internal bool IsDelegated =>_transactionType == TransactionType.Delegated; - internal bool IsDistributed - { - get - { - bool result = (TransactionType.Distributed == _transactionType); - return result; - } - } + internal bool IsDistributed => _transactionType == TransactionType.Distributed; - internal bool IsLocal - { - get - { - bool result = (TransactionType.LocalFromTSQL == _transactionType - || TransactionType.LocalFromAPI == _transactionType - ); - return result; - } - } +#if NETFRAMEWORK + internal bool IsContext => _transactionType == TransactionType.Context; +#endif + + internal bool IsLocal => _transactionType == TransactionType.LocalFromTSQL + || _transactionType == TransactionType.LocalFromAPI +#if NETFRAMEWORK + || IsContext +#endif + ; internal bool IsOrphaned { @@ -149,7 +105,7 @@ internal bool IsOrphaned // An internal transaction is orphaned when its parent has been // reclaimed by GC. bool result; - if (null == _parent) + if (_parent == null) { // No parent, so we better be LocalFromTSQL. Should we even return in this case - // since it could be argued this is invalid? @@ -172,29 +128,11 @@ internal bool IsOrphaned } } - internal bool IsZombied - { - get - { - return (null == _innerConnection); - } - } + internal bool IsZombied => _innerConnection == null; - internal int ObjectID - { - get - { - return _objectID; - } - } + internal int ObjectID => _objectID; - internal int OpenResultsCount - { - get - { - return _openResultCount; - } - } + internal int OpenResultsCount => _openResultCount; internal SqlTransaction Parent { @@ -202,7 +140,7 @@ internal SqlTransaction Parent { SqlTransaction result = null; // Should we protect against this, since this probably is an invalid state? - Debug.Assert(null != _parent, "Why are we calling Parent with no parent?"); + Debug.Assert(_parent != null, "Why are we calling Parent with no parent?"); if (_parent != null && _parent.TryGetTarget(out SqlTransaction target)) { result = target; @@ -213,10 +151,7 @@ internal SqlTransaction Parent internal long TransactionId { - get - { - return _transactionId; - } + get => _transactionId; set { Debug.Assert(NullTransactionId == _transactionId, "setting transaction cookie while one is active?"); @@ -224,10 +159,7 @@ internal long TransactionId } } - internal void Activate() - { - _transactionState = TransactionState.Active; - } + internal void Activate() => _transactionState = TransactionState.Active; private void CheckTransactionLevelAndZombie() { @@ -245,6 +177,10 @@ private void CheckTransactionLevelAndZombie() { throw; } +#if NETFRAMEWORK + ADP.TraceExceptionWithoutRethrow(e); +#endif + Zombie(); // If exception caught when trying to check level, zombie. } } @@ -267,6 +203,9 @@ internal void CloseFromConnection() } finally { +#if NETFRAMEWORK + TdsParser.ReliabilitySection.Assert("unreliable call to CloseFromConnection"); // you need to setup for a thread abort somewhere before you call this method +#endif if (processFinallyBlock) { // Always ensure we're zombied; 2005 will send an EnvChange that @@ -295,6 +234,18 @@ internal void Commit() // COMMIT ignores transaction names, and so there is no reason to pass it anything. COMMIT // simply commits the transaction from the most recent BEGIN, nested or otherwise. _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, IsolationLevel.Unspecified, null, false); +#if NETFRAMEWORK + // SQL BU DT 291159 - perform full Zombie on pre-2005, but do not actually + // complete internal transaction until informed by server in the case of 2005 + // or later. + if (!IsZombied && !_innerConnection.Is2005OrNewer) + { + // Since nested transactions are no longer allowed, set flag to false. + // This transaction has been completed. + Zombie(); + } + else +#endif { ZombieParent(); } @@ -330,8 +281,8 @@ internal int DecrementAndObtainOpenResultCount() internal void Dispose() { - this.Dispose(true); - System.GC.SuppressFinalize(this); + Dispose(true); + GC.SuppressFinalize(this); } private void Dispose(bool disposing) @@ -339,7 +290,7 @@ private void Dispose(bool disposing) SqlClientEventSource.Log.TryPoolerTraceEvent("SqlInternalTransaction.Dispose | RES | CPOOL | Object Id {0}, Disposing", ObjectID); if (disposing) { - if (null != _innerConnection) + if (_innerConnection != null) { // implicitly rollback if transaction still valid _disposing = true; @@ -347,13 +298,14 @@ private void Dispose(bool disposing) } } } - + /// + /// This function is needed for those times when it is impossible to determine the server's + /// transaction level, unless the user's arguments were parsed - which is something we don't want + ///to do. An example when it is impossible to determine the level is after a rollback. + /// + /// private int GetServerTransactionLevel() { - // This function is needed for those times when it is impossible to determine the server's - // transaction level, unless the user's arguments were parsed - which is something we don't want - // to do. An example when it is impossible to determine the level is after a rollback. - using (SqlCommand transactionLevelCommand = new SqlCommand("set @out = @@trancount", (SqlConnection)(_innerConnection.Owner))) { transactionLevelCommand.Transaction = Parent; @@ -361,8 +313,7 @@ private int GetServerTransactionLevel() SqlParameter parameter = new SqlParameter("@out", SqlDbType.Int); parameter.Direction = ParameterDirection.Output; transactionLevelCommand.Parameters.Add(parameter); - - transactionLevelCommand.RunExecuteReader(CommandBehavior.Default, RunBehavior.UntilDone, returnStream: false); + transactionLevelCommand.RunExecuteReader(CommandBehavior.Default, RunBehavior.UntilDone, returnStream: false, nameof(GetServerTransactionLevel)); return (int)parameter.Value; } @@ -447,6 +398,15 @@ internal void Rollback(string transactionName) try { _innerConnection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Rollback, transactionName, IsolationLevel.Unspecified, null, false); +#if NETFRAMEWORK + if (!IsZombied && !_innerConnection.Is2005OrNewer) + { + // Check if Zombied before making round-trip to server. + // Against 2005 we receive an envchange on the ExecuteTransaction above on the + // parser that calls back into SqlTransaction for the Zombie() call. + CheckTransactionLevelAndZombie(); + } +#endif } catch (Exception e) { @@ -509,7 +469,8 @@ internal void Zombie() // Number 1 needs to be done whenever a SqlTransaction object is completed. Number // 2 is only done when a transaction is actually completed. Since users can begin // transactions both in and outside of the API, and since nested begins are not actual - // transactions we need to distinguish between #1 and #2. + // transactions we need to distinguish between #1 and #2.See SQL BU DT 291159 + // for further details. ZombieParent(); @@ -526,15 +487,15 @@ private void ZombieParent() { if (_parent != null && _parent.TryGetTarget(out SqlTransaction parent)) { - parent.Zombie(); + parent.Zombie(); } _parent = null; } - internal string TraceString() - { - return string.Format(/*IFormatProvider*/ null, "(ObjId={0}, tranId={1}, state={2}, type={3}, open={4}, disp={5}", - ObjectID, _transactionId, _transactionState, _transactionType, _openResultCount, _disposing); - } + internal string TraceString() => string.Format(/*IFormatProvider*/ null, + "(ObjId={0}, tranId={1}, state={2}, type={3}, open={4}, disp={5}", + ObjectID, _transactionId, _transactionState, _transactionType, _openResultCount, _disposing); + + } }