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 efc9d50ac1..bb77ffba3e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -620,15 +620,15 @@ Microsoft\Data\SqlClient\SqlSymmetricKeyCache.cs + + Microsoft\Data\SqlClient\SqlTransaction.cs + Microsoft\Data\SqlClient\SqlUdtInfo.cs Microsoft\Data\SqlClient\SqlUtil.cs - - Microsoft\Data\SqlClient\SqlTransaction.Common.cs - Microsoft\Data\SqlClient\SSPI\NegotiateSSPIContextProvider.cs @@ -724,7 +724,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlTransaction.cs deleted file mode 100644 index aef40c173b..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ /dev/null @@ -1,304 +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.ComponentModel; -using System.Data.Common; -using Microsoft.Data.Common; -using Microsoft.Data.SqlClient.Diagnostics; - -namespace Microsoft.Data.SqlClient -{ - /// - public sealed partial class SqlTransaction : DbTransaction - { - private static readonly SqlDiagnosticListener s_diagnosticListener = new(); - - //////////////////////////////////////////////////////////////////////////////////////// - // PUBLIC METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - /// - public override void Commit() - { - using (DiagnosticTransactionScope diagnosticScope = s_diagnosticListener.CreateTransactionCommitScope(_isolationLevel, _connection, InternalTransaction)) - { - ZombieCheck(); - - using (TryEventScope.Create("SqlTransaction.Commit | API | Object Id {0}", ObjectID)) - { - SqlStatistics statistics = null; - TdsParser bestEffortCleanupTarget = null; - - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlTransaction.Commit | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId); -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _isFromAPI = true; - - _internalTransaction.Commit(); - } - catch (System.OutOfMemoryException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); -#if NETFRAMEWORK - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); -#endif - throw; - } - catch (SqlException ex) - { - diagnosticScope.SetException(ex); - // GitHub Issue #130 - When a timeout exception has occurred on transaction completion request, - // this connection may not be in reusable state. - // We will abort this connection and make sure it does not go back to the pool. - if (ex.InnerException is Win32Exception innerException && innerException.NativeErrorCode == TdsEnums.SNI_WAIT_TIMEOUT) - { - _connection.Abort(ex); - } - throw; - } - catch (Exception ex) - { - diagnosticScope.SetException(ex); - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - _isFromAPI = false; - } - } - } - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - TdsParser bestEffortCleanupTarget = null; -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - if (!IsZombied && !Is2005PartialZombie) - { - _internalTransaction.Dispose(); - } - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); -#if NETFRAMEWORK - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); -#endif - throw; - } - } - base.Dispose(disposing); - } - - /// - public override void Rollback() - { - using (DiagnosticTransactionScope diagnosticScope = s_diagnosticListener.CreateTransactionRollbackScope(_isolationLevel, _connection, InternalTransaction, null)) - { - if (Is2005PartialZombie) - { - // Put something in the trace in case a customer has an issue - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlTransaction.Rollback | ADV | Object Id {0}, partial zombie no rollback required", ObjectID); - _internalTransaction = null; // 2005 zombification - } - else - { - ZombieCheck(); - - SqlStatistics statistics = null; - using (TryEventScope.Create("SqlTransaction.Rollback | API | Object Id {0}", ObjectID)) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlTransaction.Rollback | API | Correlation | Object Id {0}, ActivityID {1}, Client Connection Id {2}", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId); - - TdsParser bestEffortCleanupTarget = null; -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _isFromAPI = true; - - _internalTransaction.Rollback(); - } - catch (System.OutOfMemoryException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); -#if NETFRAMEWORK - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); -#endif - throw; - } - catch (Exception ex) - { - diagnosticScope.SetException(ex); - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - _isFromAPI = false; - } - } - } - } - } - - /// - public override void Rollback(string transactionName) - { - using (DiagnosticTransactionScope diagnosticScope = s_diagnosticListener.CreateTransactionRollbackScope(_isolationLevel, _connection, InternalTransaction, transactionName)) - { - ZombieCheck(); - - using (TryEventScope.Create(SqlClientEventSource.Log.TryScopeEnterEvent("SqlTransaction.Rollback | API | Object Id {0}, Transaction Name='{1}', ActivityID {2}, Client Connection Id {3}", ObjectID, transactionName, ActivityCorrelator.Current, Connection?.ClientConnectionId))) - { - SqlStatistics statistics = null; - TdsParser bestEffortCleanupTarget = null; -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _isFromAPI = true; - - _internalTransaction.Rollback(transactionName); - } - catch (System.OutOfMemoryException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - diagnosticScope.SetException(e); - _connection.Abort(e); -#if NETFRAMEWORK - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); -#endif - throw; - } - catch (Exception ex) - { - diagnosticScope.SetException(ex); - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - _isFromAPI = false; - } - } - } - } - - /// - public override void Save(string savePointName) - { - ZombieCheck(); - - SqlStatistics statistics = null; - using (TryEventScope.Create("SqlTransaction.Save | API | Object Id {0} | Save Point Name '{1}'", ObjectID, savePointName)) - { - TdsParser bestEffortCleanupTarget = null; -#if NETFRAMEWORK - RuntimeHelpers.PrepareConstrainedRegions(); -#endif - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _internalTransaction.Save(savePointName); - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); -#if NETFRAMEWORK - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); -#endif - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - } - } -} 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 e1ddaa6926..05d4626119 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -550,9 +550,6 @@ Microsoft\Data\SqlClient\Server\SqlRecordBuffer.cs - - Microsoft\Data\SqlClient\SqlTransaction.Common.cs - Microsoft\Data\SqlClient\Server\ValueUtilsSmi.cs @@ -793,6 +790,9 @@ Microsoft\Data\SqlClient\SqlSymmetricKeyCache.cs + + Microsoft\Data\SqlClient\SqlTransaction.cs + Microsoft\Data\SqlClient\SqlUdtInfo.cs @@ -890,7 +890,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlTransaction.cs deleted file mode 100644 index b08f109e65..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlTransaction.cs +++ /dev/null @@ -1,257 +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.ComponentModel; -using System.Data.Common; -using System.Runtime.CompilerServices; -using Microsoft.Data.Common; - -namespace Microsoft.Data.SqlClient -{ - /// - public sealed partial class SqlTransaction : DbTransaction - { - //////////////////////////////////////////////////////////////////////////////////////// - // PUBLIC METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - /// - public override void Commit() - { - SqlConnection.ExecutePermission.Demand(); // MDAC 81476 - - ZombieCheck(); - - using (TryEventScope.Create(" {0}", ObjectID)) - { - SqlStatistics statistics = null; - TdsParser bestEffortCleanupTarget = null; - - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _isFromAPI = true; - - _internalTransaction.Commit(); - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - catch (SqlException e) - { - // GitHub Issue #130 - When a timeout exception has occurred on transaction completion request, - // this connection may not be in reusable state. - // We will abort this connection and make sure it does not go back to the pool. - if (e.InnerException is Win32Exception innerException && innerException.NativeErrorCode == TdsEnums.SNI_WAIT_TIMEOUT) - { - _connection.Abort(e); - } - throw; - } - finally - { - _isFromAPI = false; - - SqlStatistics.StopTimer(statistics); - } - } - } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - TdsParser bestEffortCleanupTarget = null; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - if (!IsZombied && !Is2005PartialZombie) - { - _internalTransaction.Dispose(); - } - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - } - base.Dispose(disposing); - } - - /// - public override void Rollback() - { - if (Is2005PartialZombie) - { - // Put something in the trace in case a customer has an issue - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0} partial zombie no rollback required", ObjectID); - - _internalTransaction = null; // 2005 zombification - } - else - { - ZombieCheck(); - - SqlStatistics statistics = null; - using (TryEventScope.Create(" {0}", ObjectID)) - { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - - TdsParser bestEffortCleanupTarget = null; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _isFromAPI = true; - - _internalTransaction.Rollback(); - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - finally - { - _isFromAPI = false; - - SqlStatistics.StopTimer(statistics); - } - } - } - } - - /// - public void Rollback(string transactionName) - { - SqlConnection.ExecutePermission.Demand(); // MDAC 81476 - - ZombieCheck(); - - using (TryEventScope.Create(" {0} transactionName='{1}'", ObjectID, transactionName)) - { - SqlStatistics statistics = null; - TdsParser bestEffortCleanupTarget = null; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _isFromAPI = true; - - _internalTransaction.Rollback(transactionName); - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - finally - { - _isFromAPI = false; - - SqlStatistics.StopTimer(statistics); - } - } - } - - /// - public void Save(string savePointName) - { - SqlConnection.ExecutePermission.Demand(); // MDAC 81476 - - ZombieCheck(); - - SqlStatistics statistics = null; - using (TryEventScope.Create(" {0} savePointName='{1}'", ObjectID, savePointName)) - { - TdsParser bestEffortCleanupTarget = null; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); - statistics = SqlStatistics.StartTimer(Statistics); - - _internalTransaction.Save(savePointName); - } - catch (System.OutOfMemoryException e) - { - _connection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _connection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _connection.Abort(e); - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - finally - { - SqlStatistics.StopTimer(statistics); - } - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.stub.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.stub.cs index d16088849e..791201951d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.stub.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.stub.cs @@ -12,6 +12,12 @@ public class SqlConnection { internal Guid ClientConnectionId { get; set; } + #if NETFRAMEWORK + internal static System.Security.CodeAccessPermission ExecutePermission { get; set; } + #endif + internal SqlStatistics Statistics { get; set; } + + internal void Abort(Exception e) { } } } 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 31884f4b96..ad89176fe6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalTransaction.cs @@ -52,7 +52,7 @@ internal SqlInternalTransaction(SqlInternalConnection innerConnection, Transacti internal SqlInternalTransaction(SqlInternalConnection 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); + 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; _transactionType = type; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.Common.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.Common.cs deleted file mode 100644 index e099712fc2..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.Common.cs +++ /dev/null @@ -1,141 +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.Data; -using System.Data.Common; -using System.Diagnostics; -using Microsoft.Data.Common; - -namespace Microsoft.Data.SqlClient -{ - /// - public sealed partial class SqlTransaction : DbTransaction - { - private static int s_objectTypeCount; // EventSource Counter - internal readonly int _objectID = System.Threading.Interlocked.Increment(ref s_objectTypeCount); - internal readonly IsolationLevel _isolationLevel = IsolationLevel.ReadCommitted; - - private SqlInternalTransaction _internalTransaction; - private readonly SqlConnection _connection; - - private bool _isFromAPI; - - internal SqlTransaction(SqlInternalConnection internalConnection, SqlConnection con, - IsolationLevel iso, SqlInternalTransaction internalTransaction) - { -#if NETFRAMEWORK - SqlConnection.VerifyExecutePermission(); -#endif - _isolationLevel = iso; - _connection = con; - - if (internalTransaction == null) - { - _internalTransaction = new SqlInternalTransaction(internalConnection, TransactionType.LocalFromAPI, this); - } - else - { - Debug.Assert(internalConnection.CurrentTransaction == internalTransaction, "Unexpected Parser.CurrentTransaction state!"); - _internalTransaction = internalTransaction; - _internalTransaction.InitParent(this); - } - } - - //////////////////////////////////////////////////////////////////////////////////////// - // PROPERTIES - //////////////////////////////////////////////////////////////////////////////////////// - - /// - public new SqlConnection Connection - {// MDAC 66655 - get - { - if (IsZombied) - { - return null; - } - else - { - return _connection; - } - } - } - - /// - protected override DbConnection DbConnection => Connection; - - internal SqlInternalTransaction InternalTransaction => _internalTransaction; - - /// - public override IsolationLevel IsolationLevel - { - get - { - ZombieCheck(); - return _isolationLevel; - } - } - - private bool Is2005PartialZombie => _internalTransaction !=null && _internalTransaction.IsCompleted; - - internal bool IsZombied => _internalTransaction == null || _internalTransaction.IsCompleted; - - internal int ObjectID => _objectID; - - internal SqlStatistics Statistics - { - get - { - if (_connection != null) - { - if (_connection.StatisticsEnabled) - { - return _connection.Statistics; - } - } - return null; - } - } - - //////////////////////////////////////////////////////////////////////////////////////// - // INTERNAL METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - 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... - SqlInternalConnection internalConnection = (_connection.InnerConnection as SqlInternalConnection); - if (internalConnection != null && !_isFromAPI) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlTransaction.Zombie | ADV | Object Id {0} yukon deferred zombie", ObjectID); - } - else - { - _internalTransaction = null; // pre SQL 2005 zombification - } - } - - //////////////////////////////////////////////////////////////////////////////////////// - // PRIVATE METHODS - //////////////////////////////////////////////////////////////////////////////////////// - - private void ZombieCheck() - { - // If this transaction has been completed, throw exception since it is unusable. - if (IsZombied) - { - if (Is2005PartialZombie) - { - _internalTransaction = null; // SQL 2005 zombification - } - - throw ADP.TransactionZombied(this); - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs new file mode 100644 index 0000000000..8130b529d7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlTransaction.cs @@ -0,0 +1,470 @@ +// 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.ComponentModel; +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Threading; +using Microsoft.Data.Common; + +#if NET +using Microsoft.Data.SqlClient.Diagnostics; +#else +using System.Runtime.CompilerServices; +#endif + +namespace Microsoft.Data.SqlClient +{ + /// + public sealed class SqlTransaction : DbTransaction + { + #if NET + private static readonly SqlDiagnosticListener s_diagnosticListener = new(); + #endif + + private static int s_objectTypeCount; // EventSource Counter + + private readonly SqlConnection _connection; + private readonly IsolationLevel _isolationLevel; + private bool _isFromApi; + + internal SqlTransaction( + SqlInternalConnection internalConnection, + SqlConnection con, + IsolationLevel iso, + SqlInternalTransaction internalTransaction) + { + #if NETFRAMEWORK + SqlConnection.VerifyExecutePermission(); + #endif + _isolationLevel = iso; + _connection = con; + + if (internalTransaction == null) + { + InternalTransaction = new SqlInternalTransaction(internalConnection, TransactionType.LocalFromAPI, this); + } + else + { + Debug.Assert(internalConnection.CurrentTransaction == internalTransaction, "Unexpected Parser.CurrentTransaction state!"); + InternalTransaction = internalTransaction; + InternalTransaction.InitParent(this); + } + } + + #region Properties + + /// + public new SqlConnection Connection => IsZombied ? null : _connection; + + /// + public override IsolationLevel IsolationLevel + { + get + { + ZombieCheck(); + return _isolationLevel; + } + } + + internal SqlInternalTransaction InternalTransaction { get; private set; } + + internal bool IsZombied => InternalTransaction == null || InternalTransaction.IsCompleted; + + internal int ObjectId { get; } = Interlocked.Increment(ref s_objectTypeCount); + + /// + protected override DbConnection DbConnection => Connection; + + private bool Is2005PartialZombie => InternalTransaction?.IsCompleted == true; + + private SqlStatistics Statistics => _connection?.StatisticsEnabled == true ? _connection.Statistics : null; + + #endregion + + /// + public override void Commit() + { + #if NET + using DiagnosticTransactionScope diagnosticScope = s_diagnosticListener.CreateTransactionCommitScope( + _isolationLevel, + _connection, + InternalTransaction); + #endif + + ZombieCheck(); + + using (TryEventScope.Create("SqlTransaction.Commit | API | Object Id {0}", ObjectId)) + { + SqlStatistics statistics = null; + + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlTransaction.Commit | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}", + ObjectId, + ActivityCorrelator.Current, + Connection?.ClientConnectionId); + + #if NETFRAMEWORK + TdsParser bestEffortCleanupTarget = null; + RuntimeHelpers.PrepareConstrainedRegions(); + #endif + try + { + #if NETFRAMEWORK + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); + #endif + + statistics = SqlStatistics.StartTimer(Statistics); + + _isFromApi = true; + + InternalTransaction.Commit(); + } + #if NETFRAMEWORK + catch (OutOfMemoryException e) + { + _connection.Abort(e); + throw; + } + catch (StackOverflowException e) + { + _connection.Abort(e); + throw; + } + catch (ThreadAbortException e) + { + _connection.Abort(e); + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + throw; + } + #endif + catch (SqlException ex) + { + #if NET + diagnosticScope.SetException(ex); + #endif + + // GitHub Issue #130 - When a timeout exception has occurred on transaction completion request, + // this connection may not be in reusable state. + // We will abort this connection and make sure it does not go back to the pool. + if (ex.InnerException is Win32Exception innerException && innerException.NativeErrorCode == TdsEnums.SNI_WAIT_TIMEOUT) + { + _connection.Abort(ex); + } + throw; + } + #if NET + catch (Exception ex) + { + diagnosticScope.SetException(ex); + throw; + } + #endif + finally + { + SqlStatistics.StopTimer(statistics); + _isFromApi = false; + } + } + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + #if NETFRAMEWORK + TdsParser bestEffortCleanupTarget = null; + RuntimeHelpers.PrepareConstrainedRegions(); + #endif + try + { + #if NETFRAMEWORK + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); + #endif + + if (!IsZombied && !Is2005PartialZombie) + { + InternalTransaction.Dispose(); + } + } + catch (OutOfMemoryException e) + { + _connection.Abort(e); + throw; + } + catch (StackOverflowException e) + { + _connection.Abort(e); + throw; + } + catch (ThreadAbortException e) + { + _connection.Abort(e); + + #if NETFRAMEWORK + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + #endif + + throw; + } + } + + base.Dispose(disposing); + } + + /// + public override void Rollback() + { + #if NET + using DiagnosticTransactionScope diagnosticScope = s_diagnosticListener.CreateTransactionRollbackScope( + _isolationLevel, + _connection, + InternalTransaction, + transactionName: null); + #endif + + if (Is2005PartialZombie) + { + // Put something in the trace in case a customer has an issue + SqlClientEventSource.Log.TryAdvancedTraceEvent( + "SqlTransaction.Rollback | ADV | Object Id {0}, partial zombie no rollback required", + ObjectId); + + InternalTransaction = null; // 2005 zombification + } + else + { + ZombieCheck(); + + SqlStatistics statistics = null; + using (TryEventScope.Create("SqlTransaction.Rollback | API | Object Id {0}", ObjectId)) + { + SqlClientEventSource.Log.TryCorrelationTraceEvent( + "SqlTransaction.Rollback | API | Correlation | Object Id {0}, ActivityID {1}, Client Connection Id {2}", + ObjectId, + ActivityCorrelator.Current, + Connection?.ClientConnectionId); + + #if NETFRAMEWORK + TdsParser bestEffortCleanupTarget = null; + RuntimeHelpers.PrepareConstrainedRegions(); + #endif + try + { + #if NETFRAMEWORK + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); + #endif + + statistics = SqlStatistics.StartTimer(Statistics); + + _isFromApi = true; + InternalTransaction.Rollback(); + } + #if NETFRAMEWORK + catch (OutOfMemoryException e) + { + _connection.Abort(e); + throw; + } + catch (StackOverflowException e) + { + _connection.Abort(e); + throw; + } + catch (ThreadAbortException e) + { + _connection.Abort(e); + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + throw; + } + #else + catch (Exception ex) + { + diagnosticScope.SetException(ex); + throw; + } + #endif + finally + { + SqlStatistics.StopTimer(statistics); + _isFromApi = false; + } + } + } + } + + /// + #if NET + public override void Rollback(string transactionName) + #else + public void Rollback(string transactionName) + #endif + { + #if NET + using DiagnosticTransactionScope diagnosticScope = s_diagnosticListener.CreateTransactionRollbackScope( + _isolationLevel, + _connection, + InternalTransaction, + transactionName); + #endif + + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); // MDAC 81476 + #endif + + ZombieCheck(); + + var eventScopeEnter = TryEventScope.Create(SqlClientEventSource.Log.TryScopeEnterEvent( + "SqlTransaction.Rollback | API | Object Id {0}, Transaction Name='{1}', ActivityID {2}, Client Connection Id {3}", + ObjectId, + transactionName, + ActivityCorrelator.Current, + Connection?.ClientConnectionId)); + using (eventScopeEnter) + { + SqlStatistics statistics = null; + + #if NETFRAMEWORK + TdsParser bestEffortCleanupTarget = null; + RuntimeHelpers.PrepareConstrainedRegions(); + #endif + try + { + #if NETFRAMEWORK + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); + #endif + + statistics = SqlStatistics.StartTimer(Statistics); + + _isFromApi = true; + InternalTransaction.Rollback(transactionName); + } + #if NETFRAMEWORK + catch (OutOfMemoryException e) + { + _connection.Abort(e); + throw; + } + catch (StackOverflowException e) + { + _connection.Abort(e); + throw; + } + catch (ThreadAbortException e) + { + _connection.Abort(e); + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + throw; + } + #else + catch (Exception ex) + { + diagnosticScope.SetException(ex); + throw; + } + #endif + finally + { + SqlStatistics.StopTimer(statistics); + _isFromApi = false; + } + } + } + + /// + #if NET + public override void Save(string savePointName) + #else + public void Save(string savePointName) + #endif + { + #if NETFRAMEWORK + SqlConnection.ExecutePermission.Demand(); // MDAC 81476 + #endif + + ZombieCheck(); + + SqlStatistics statistics = null; + using (TryEventScope.Create("SqlTransaction.Save | API | Object Id {0} | Save Point Name '{1}'", ObjectId, savePointName)) + { + #if NETFRAMEWORK + TdsParser bestEffortCleanupTarget = null; + RuntimeHelpers.PrepareConstrainedRegions(); + #endif + try + { + #if NETFRAMEWORK + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_connection); + #endif + + statistics = SqlStatistics.StartTimer(Statistics); + + InternalTransaction.Save(savePointName); + } + #if NETFRAMEWORK + catch (OutOfMemoryException e) + { + _connection.Abort(e); + throw; + } + catch (StackOverflowException e) + { + _connection.Abort(e); + throw; + } + catch (ThreadAbortException e) + { + _connection.Abort(e); + + #if NETFRAMEWORK + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + #endif + + throw; + } + #endif + finally + { + SqlStatistics.StopTimer(statistics); + } + } + } + + 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) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent( + "SqlTransaction.Zombie | ADV | Object Id {0} yukon deferred zombie", + ObjectId); + } + else + { + // pre SQL 2005 zombification + InternalTransaction = null; + } + } + + private void ZombieCheck() + { + // If this transaction has been completed, throw exception since it is unusable. + if (IsZombied) + { + if (Is2005PartialZombie) + { + // SQL 2005 zombification + InternalTransaction = null; + } + + throw ADP.TransactionZombied(this); + } + } + } +}