diff --git a/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs b/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs index c929be5238c..6c87a564a17 100644 --- a/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs +++ b/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs @@ -315,6 +315,93 @@ public virtual Task TransactionRolledBackAsync( CancellationToken cancellationToken = default) => Task.CompletedTask; + /// + public virtual InterceptionResult CreatingSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + => result; + + /// + public virtual void CreatedSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + } + + /// + public virtual Task CreatingSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + public virtual Task CreatedSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + public virtual InterceptionResult RollingBackToSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + => result; + + /// + public virtual void RolledBackToSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + } + + /// + public virtual Task RollingBackToSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + public virtual Task RolledBackToSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + public virtual InterceptionResult ReleasingSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + => result; + + /// + public virtual void ReleasedSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + } + + /// + public virtual Task ReleasingSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + public virtual Task ReleasedSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + /// /// Called when use of a has failed with an exception. /// diff --git a/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs b/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs index e28bb6c89a4..af14c27d32d 100644 --- a/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs +++ b/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs @@ -320,6 +320,213 @@ Task TransactionRolledBackAsync( [NotNull] TransactionEndEventData eventData, CancellationToken cancellationToken = default); + /// + /// Called just before EF intends to create a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// Represents the current result if one exists. + /// This value will have set to if some previous + /// interceptor suppressed execution by calling . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If is false, the EF will continue as normal. + /// If is true, then EF will suppress the operation + /// it was about to perform. + /// A normal implementation of this method for any interceptor that is not attempting to suppress + /// the operation is to return the value passed in. + /// + InterceptionResult CreatingSavepoint( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult result); + + /// + /// Called immediately after EF creates a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + void CreatedSavepoint( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData); + + /// + /// Called just before EF intends to create a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// Represents the current result if one exists. + /// This value will have set to if some previous + /// interceptor suppressed execution by calling . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If is false, the EF will continue as normal. + /// If is true, then EF will suppress the operation + /// it was about to perform. + /// A normal implementation of this method for any interceptor that is not attempting to suppress + /// the operation is to return the value passed in. + /// + Task CreatingSavepointAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + Task CreatedSavepointAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to roll back to a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// Represents the current result if one exists. + /// This value will have set to if some previous + /// interceptor suppressed execution by calling . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If is false, the EF will continue as normal. + /// If is true, then EF will suppress the operation + /// it was about to perform. + /// A normal implementation of this method for any interceptor that is not attempting to suppress + /// the operation is to return the value passed in. + /// + InterceptionResult RollingBackToSavepoint( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult result); + + /// + /// Called immediately after EF rolls back to a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + void RolledBackToSavepoint( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData); + + /// + /// Called just before EF intends to roll back to a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// Represents the current result if one exists. + /// This value will have set to if some previous + /// interceptor suppressed execution by calling . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If is false, the EF will continue as normal. + /// If is true, then EF will suppress the operation + /// it was about to perform. + /// A normal implementation of this method for any interceptor that is not attempting to suppress + /// the operation is to return the value passed in. + /// + Task RollingBackToSavepointAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + Task RolledBackToSavepointAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to release a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// Represents the current result if one exists. + /// This value will have set to if some previous + /// interceptor suppressed execution by calling . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If is false, the EF will continue as normal. + /// If is true, then EF will suppress the operation + /// it was about to perform. + /// A normal implementation of this method for any interceptor that is not attempting to suppress + /// the operation is to return the value passed in. + /// + InterceptionResult ReleasingSavepoint( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult result); + + /// + /// Called immediately after EF releases a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + void ReleasedSavepoint( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData); + + /// + /// Called just before EF intends to release a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// Represents the current result if one exists. + /// This value will have set to if some previous + /// interceptor suppressed execution by calling . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If is false, the EF will continue as normal. + /// If is true, then EF will suppress the operation + /// it was about to perform. + /// A normal implementation of this method for any interceptor that is not attempting to suppress + /// the operation is to return the value passed in. + /// + Task ReleasingSavepointAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default); + + /// + /// Called immediately after EF releases a transaction savepoint. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + Task ReleasedSavepointAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData, + CancellationToken cancellationToken = default); + /// /// Called when use of a has failed with an exception. /// diff --git a/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorAggregator.cs b/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorAggregator.cs index 9323c2c0f52..dc9e336050f 100644 --- a/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorAggregator.cs +++ b/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorAggregator.cs @@ -213,6 +213,150 @@ public async Task TransactionRolledBackAsync( } } + public InterceptionResult CreatingSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].CreatingSavepoint(transaction, eventData, result); + } + + return result; + } + + public void CreatedSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].CreatedSavepoint(transaction, eventData); + } + } + + public async Task CreatingSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].CreatingSavepointAsync(transaction, eventData, result, cancellationToken); + } + + return result; + } + + public async Task CreatedSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].CreatedSavepointAsync(transaction, eventData, cancellationToken); + } + } + + public InterceptionResult RollingBackToSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].RollingBackToSavepoint(transaction, eventData, result); + } + + return result; + } + + public void RolledBackToSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].RolledBackToSavepoint(transaction, eventData); + } + } + + public async Task RollingBackToSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].RollingBackToSavepointAsync(transaction, eventData, result, cancellationToken); + } + + return result; + } + + public async Task RolledBackToSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].RolledBackToSavepointAsync(transaction, eventData, cancellationToken); + } + } + + public InterceptionResult ReleasingSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ReleasingSavepoint(transaction, eventData, result); + } + + return result; + } + + public void ReleasedSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].ReleasedSavepoint(transaction, eventData); + } + } + + public async Task ReleasingSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ReleasingSavepointAsync(transaction, eventData, result, cancellationToken); + } + + return result; + } + + public async Task ReleasedSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].ReleasedSavepointAsync(transaction, eventData, cancellationToken); + } + } + public void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData) { for (var i = 0; i < _interceptors.Length; i++) diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index 59d6799b24c..8fcff84a13e 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -52,6 +52,12 @@ private enum Id TransactionStarting, TransactionCommitting, TransactionRollingBack, + CreatingTransactionSavepoint, + CreatedTransactionSavepoint, + RollingBackToTransactionSavepoint, + RolledBackToTransactionSavepoint, + ReleasingTransactionSavepoint, + ReleasedTransactionSavepoint, // DataReader events DataReaderDisposing = CoreEventId.RelationalBaseId + 300, @@ -314,6 +320,84 @@ private enum Id /// public static readonly EventId TransactionRolledBack = MakeTransactionId(Id.TransactionRolledBack); + /// + /// + /// A database transaction savepoint is being created. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId CreatingTransactionSavepoint = MakeTransactionId(Id.CreatingTransactionSavepoint); + + /// + /// + /// A database transaction savepoint has been created. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId CreatedTransactionSavepoint = MakeTransactionId(Id.CreatedTransactionSavepoint); + + /// + /// + /// A database transaction is being rolled back to a savepoint. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId RollingBackToTransactionSavepoint = MakeTransactionId(Id.RollingBackToTransactionSavepoint); + + /// + /// + /// A database transaction has been rolled back to a savepoint. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId RolledBackToTransactionSavepoint = MakeTransactionId(Id.RolledBackToTransactionSavepoint); + + /// + /// + /// A database transaction savepoint is being released. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ReleasingTransactionSavepoint = MakeTransactionId(Id.ReleasingTransactionSavepoint); + + /// + /// + /// A database transaction savepoint has been released. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ReleasedTransactionSavepoint = MakeTransactionId(Id.ReleasedTransactionSavepoint); + /// /// /// A database transaction has been disposed. diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index 79a53c893c0..42ad2f79d16 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2745,6 +2745,756 @@ private static void LogTransactionRollingBack( } } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult CreatingTransactionSavepoint( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogCreatingTransactionSavepoint(diagnostics); + + LogCreatingTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastCreatingTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + false, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.CreatingSavepoint(transaction, eventData, default); + } + } + + return default; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task CreatingTransactionSavepointAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogCreatingTransactionSavepoint(diagnostics); + + LogCreatingTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastCreatingTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + true, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.CreatingSavepointAsync(transaction, eventData, default, cancellationToken); + } + } + + return Task.FromResult(default(InterceptionResult)); + } + + private static TransactionEventData BroadcastCreatingTransactionSavepoint( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled, + bool simpleLogEnabled) + { + var eventData = new TransactionEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + + return eventData; + } + + private static void LogCreatingTransactionSavepoint( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + public static void CreatedTransactionSavepoint( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogCreatedTransactionSavepoint(diagnostics); + + LogCreatedTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastCreatedTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + false, + diagnosticSourceEnabled, + simpleLogEnabled); + + interceptor?.CreatedSavepoint(transaction, eventData); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task CreatedTransactionSavepointAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogCreatedTransactionSavepoint(diagnostics); + + LogCreatedTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastCreatedTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + true, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.CreatedSavepointAsync(transaction, eventData, cancellationToken); + } + } + + return Task.CompletedTask; + } + + private static TransactionEndEventData BroadcastCreatedTransactionSavepoint( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled, + bool simpleLogEnabled) + { + var eventData = new TransactionEndEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime, + duration); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + + return eventData; + } + + private static void LogCreatedTransactionSavepoint( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult RollingBackToTransactionSavepoint( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogRollingBackToTransactionSavepoint(diagnostics); + + LogRollingBackToTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastRollingBackToTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + false, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.RollingBackToSavepoint(transaction, eventData, default); + } + } + + return default; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task RollingBackToTransactionSavepointAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogRollingBackToTransactionSavepoint(diagnostics); + + LogRollingBackToTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastRollingBackToTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + true, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.RollingBackToSavepointAsync(transaction, eventData, default, cancellationToken); + } + } + + return Task.FromResult(default(InterceptionResult)); + } + + private static TransactionEventData BroadcastRollingBackToTransactionSavepoint( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled, + bool simpleLogEnabled) + { + var eventData = new TransactionEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + + return eventData; + } + + private static void LogRollingBackToTransactionSavepoint( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + public static void RolledBackToTransactionSavepoint( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogRolledBackToTransactionSavepoint(diagnostics); + + LogRolledBackToTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastRolledBackToTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + false, + diagnosticSourceEnabled, + simpleLogEnabled); + + interceptor?.RolledBackToSavepoint(transaction, eventData); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task RolledBackToTransactionSavepointAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogRolledBackToTransactionSavepoint(diagnostics); + + LogCreatedTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastRolledBackToTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + true, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.RolledBackToSavepointAsync(transaction, eventData, cancellationToken); + } + } + + return Task.CompletedTask; + } + + private static TransactionEndEventData BroadcastRolledBackToTransactionSavepoint( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled, + bool simpleLogEnabled) + { + var eventData = new TransactionEndEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime, + duration); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + + return eventData; + } + + private static void LogRolledBackToTransactionSavepoint( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult ReleasingTransactionSavepoint( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogReleasingTransactionSavepoint(diagnostics); + + LogReleasingTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastReleasingTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + false, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.ReleasingSavepoint(transaction, eventData, default); + } + } + + return default; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task ReleasingTransactionSavepointAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogReleasingTransactionSavepoint(diagnostics); + + LogReleasingTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastReleasingTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + true, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.ReleasingSavepointAsync(transaction, eventData, default, cancellationToken); + } + } + + return Task.FromResult(default(InterceptionResult)); + } + + private static TransactionEventData BroadcastReleasingTransactionSavepoint( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled, + bool simpleLogEnabled) + { + var eventData = new TransactionEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + + return eventData; + } + + private static void LogReleasingTransactionSavepoint( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + public static void ReleasedTransactionSavepoint( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogReleasedTransactionSavepoint(diagnostics); + + LogReleasedTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastReleasedTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + false, + diagnosticSourceEnabled, + simpleLogEnabled); + + interceptor?.ReleasedSavepoint(transaction, eventData); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task ReleasedTransactionSavepointAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogReleasedTransactionSavepoint(diagnostics); + + LogReleasedTransactionSavepoint(diagnostics, definition); + + if (diagnostics.NeedsEventData( + definition, out var interceptor, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = BroadcastReleasedTransactionSavepoint( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + true, + diagnosticSourceEnabled, + simpleLogEnabled); + + if (interceptor != null) + { + return interceptor.ReleasedSavepointAsync(transaction, eventData, cancellationToken); + } + } + + return Task.CompletedTask; + } + + private static TransactionEndEventData BroadcastReleasedTransactionSavepoint( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled, + bool simpleLogEnabled) + { + var eventData = new TransactionEndEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime, + duration); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + + return eventData; + } + + private static void LogReleasedTransactionSavepoint( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + } + /// /// Logs for the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index eb528cc2a3d..0e67c8edf53 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -142,6 +142,60 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase LogRolledBackTransaction; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogCreatingTransactionSavepoint; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogRollingBackToTransactionSavepoint; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogCreatedTransactionSavepoint; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogRolledBackToTransactionSavepoint; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogReleasingTransactionSavepoint; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase LogReleasedTransactionSavepoint; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 55ecbf8ed08..bfa56d43f12 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1203,6 +1203,150 @@ public static EventDefinition LogRolledBackTransaction([NotNull] IDiagnosticsLog return (EventDefinition)definition; } + /// + /// Creating transaction savepoint. + /// + public static EventDefinition LogCreatingTransactionSavepoint([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogCreatingTransactionSavepoint; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogCreatingTransactionSavepoint, + () => new EventDefinition( + logger.Options, + RelationalEventId.CreatingTransactionSavepoint, + LogLevel.Debug, + "RelationalEventId.CreatingTransactionSavepoint", + level => LoggerMessage.Define( + level, + RelationalEventId.CreatingTransactionSavepoint, + _resourceManager.GetString("LogCreatingTransactionSavepoint")))); + } + + return (EventDefinition)definition; + } + + /// + /// Created transaction savepoint. + /// + public static EventDefinition LogCreatedTransactionSavepoint([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogCreatedTransactionSavepoint; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogCreatedTransactionSavepoint, + () => new EventDefinition( + logger.Options, + RelationalEventId.CreatedTransactionSavepoint, + LogLevel.Debug, + "RelationalEventId.CreatedTransactionSavepoint", + level => LoggerMessage.Define( + level, + RelationalEventId.CreatedTransactionSavepoint, + _resourceManager.GetString("LogCreatedTransactionSavepoint")))); + } + + return (EventDefinition)definition; + } + + /// + /// Rolling back to transaction savepoint. + /// + public static EventDefinition LogRollingBackToTransactionSavepoint([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogRollingBackToTransactionSavepoint; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogRollingBackToTransactionSavepoint, + () => new EventDefinition( + logger.Options, + RelationalEventId.RollingBackToTransactionSavepoint, + LogLevel.Debug, + "RelationalEventId.RollingBackToTransactionSavepoint", + level => LoggerMessage.Define( + level, + RelationalEventId.RollingBackToTransactionSavepoint, + _resourceManager.GetString("LogRollingBackToTransactionSavepoint")))); + } + + return (EventDefinition)definition; + } + + /// + /// Rolled back to transaction savepoint. + /// + public static EventDefinition LogRolledBackToTransactionSavepoint([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogRolledBackToTransactionSavepoint; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogRolledBackToTransactionSavepoint, + () => new EventDefinition( + logger.Options, + RelationalEventId.RolledBackToTransactionSavepoint, + LogLevel.Debug, + "RelationalEventId.RolledBackToTransactionSavepoint", + level => LoggerMessage.Define( + level, + RelationalEventId.RolledBackToTransactionSavepoint, + _resourceManager.GetString("LogRolledBackToTransactionSavepoint")))); + } + + return (EventDefinition)definition; + } + + /// + /// Releasing transaction savepoint. + /// + public static EventDefinition LogReleasingTransactionSavepoint([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogReleasingTransactionSavepoint; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogReleasingTransactionSavepoint, + () => new EventDefinition( + logger.Options, + RelationalEventId.ReleasingTransactionSavepoint, + LogLevel.Debug, + "RelationalEventId.ReleasingTransactionSavepoint", + level => LoggerMessage.Define( + level, + RelationalEventId.ReleasingTransactionSavepoint, + _resourceManager.GetString("LogReleasingTransactionSavepoint")))); + } + + return (EventDefinition)definition; + } + + /// + /// Released transaction savepoint. + /// + public static EventDefinition LogReleasedTransactionSavepoint([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogReleasedTransactionSavepoint; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogReleasedTransactionSavepoint, + () => new EventDefinition( + logger.Options, + RelationalEventId.ReleasedTransactionSavepoint, + LogLevel.Debug, + "RelationalEventId.ReleasedTransactionSavepoint", + level => LoggerMessage.Define( + level, + RelationalEventId.ReleasedTransactionSavepoint, + _resourceManager.GetString("LogReleasedTransactionSavepoint")))); + } + + return (EventDefinition)definition; + } + /// /// Disposing transaction. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index b59a504cf75..bd038924836 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -208,6 +208,30 @@ Rolled back transaction. Debug RelationalEventId.TransactionRolledBack + + Creating transaction savepoint. + Debug RelationalEventId.CreatingTransactionSavepoint + + + Created transaction savepoint. + Debug RelationalEventId.CreatedTransactionSavepoint + + + Rolling back to transaction savepoint.. + Debug RelationalEventId.RollingBackToTransactionSavepoint + + + Rolled back to transaction savepoint.. + Debug RelationalEventId.RolledBackToTransactionSavepoint + + + Releasing transaction savepoint. + Debug RelationalEventId.ReleasingTransactionSavepoint + + + Released transaction savepoint. + Debug RelationalEventId.ReleasedTransactionSavepoint + Disposing transaction. Debug RelationalEventId.TransactionDisposed diff --git a/src/EFCore.Relational/Storage/RelationalTransaction.cs b/src/EFCore.Relational/Storage/RelationalTransaction.cs index 8523eb48a69..e9478be62f9 100644 --- a/src/EFCore.Relational/Storage/RelationalTransaction.cs +++ b/src/EFCore.Relational/Storage/RelationalTransaction.cs @@ -258,19 +258,92 @@ await Logger.TransactionErrorAsync( /// public virtual void Save(string savepointName) { - using var command = Connection.DbConnection.CreateCommand(); - command.Transaction = _dbTransaction; - command.CommandText = GetSavepointSaveSql(savepointName); - command.ExecuteNonQuery(); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = Logger.CreatingTransactionSavepoint( + Connection, + _dbTransaction, + TransactionId, + startTime); + + if (!interceptionResult.IsSuppressed) + { + using var command = Connection.DbConnection.CreateCommand(); + command.Transaction = _dbTransaction; + command.CommandText = GetSavepointSaveSql(savepointName); + command.ExecuteNonQuery(); + } + + Logger.CreatedTransactionSavepoint( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed); + } + catch (Exception e) + { + Logger.TransactionError( + Connection, + _dbTransaction, + TransactionId, + "CreateSavepoint", + e, + startTime, + stopwatch.Elapsed); + + throw; + } } /// - public virtual Task SaveAsync(string savepointName, CancellationToken cancellationToken = default) + public virtual async Task SaveAsync(string savepointName, CancellationToken cancellationToken = default) { - using var command = Connection.DbConnection.CreateCommand(); - command.Transaction = _dbTransaction; - command.CommandText = GetSavepointSaveSql(savepointName); - return command.ExecuteNonQueryAsync(cancellationToken); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = await Logger.CreatingTransactionSavepointAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + cancellationToken); + + if (!interceptionResult.IsSuppressed) + { + using var command = Connection.DbConnection.CreateCommand(); + command.Transaction = _dbTransaction; + command.CommandText = GetSavepointSaveSql(savepointName); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + await Logger.CreatedTransactionSavepointAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + catch (Exception e) + { + await Logger.TransactionErrorAsync( + Connection, + _dbTransaction, + TransactionId, + "CreateSavepoint", + e, + startTime, + stopwatch.Elapsed, + cancellationToken); + + throw; + } } /// @@ -284,19 +357,92 @@ public virtual Task SaveAsync(string savepointName, CancellationToken cancellati /// public virtual void Rollback(string savepointName) { - using var command = Connection.DbConnection.CreateCommand(); - command.Transaction = _dbTransaction; - command.CommandText = GetSavepointRollbackSql(savepointName); - command.ExecuteNonQuery(); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = Logger.RollingBackToTransactionSavepoint( + Connection, + _dbTransaction, + TransactionId, + startTime); + + if (!interceptionResult.IsSuppressed) + { + using var command = Connection.DbConnection.CreateCommand(); + command.Transaction = _dbTransaction; + command.CommandText = GetSavepointRollbackSql(savepointName); + command.ExecuteNonQuery(); + } + + Logger.RolledBackToTransactionSavepoint( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed); + } + catch (Exception e) + { + Logger.TransactionError( + Connection, + _dbTransaction, + TransactionId, + "RollbackToSavepoint", + e, + startTime, + stopwatch.Elapsed); + + throw; + } } /// - public virtual Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default) + public virtual async Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default) { - using var command = Connection.DbConnection.CreateCommand(); - command.Transaction = _dbTransaction; - command.CommandText = GetSavepointRollbackSql(savepointName); - return command.ExecuteNonQueryAsync(cancellationToken); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = await Logger.RollingBackToTransactionSavepointAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + cancellationToken); + + if (!interceptionResult.IsSuppressed) + { + using var command = Connection.DbConnection.CreateCommand(); + command.Transaction = _dbTransaction; + command.CommandText = GetSavepointRollbackSql(savepointName); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + await Logger.RolledBackToTransactionSavepointAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + catch (Exception e) + { + await Logger.TransactionErrorAsync( + Connection, + _dbTransaction, + TransactionId, + "RollbackToSavepoint", + e, + startTime, + stopwatch.Elapsed, + cancellationToken); + + throw; + } } /// @@ -310,19 +456,92 @@ public virtual Task RollbackAsync(string savepointName, CancellationToken cancel /// public virtual void Release(string savepointName) { - using var command = Connection.DbConnection.CreateCommand(); - command.Transaction = _dbTransaction; - command.CommandText = GetSavepointReleaseSql(savepointName); - command.ExecuteNonQuery(); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = Logger.ReleasingTransactionSavepoint( + Connection, + _dbTransaction, + TransactionId, + startTime); + + if (!interceptionResult.IsSuppressed) + { + using var command = Connection.DbConnection.CreateCommand(); + command.Transaction = _dbTransaction; + command.CommandText = GetSavepointReleaseSql(savepointName); + command.ExecuteNonQuery(); + } + + Logger.ReleasedTransactionSavepoint( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed); + } + catch (Exception e) + { + Logger.TransactionError( + Connection, + _dbTransaction, + TransactionId, + "ReleaseSavepoint", + e, + startTime, + stopwatch.Elapsed); + + throw; + } } /// - public virtual Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default) + public virtual async Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default) { - using var command = Connection.DbConnection.CreateCommand(); - command.Transaction = _dbTransaction; - command.CommandText = GetSavepointReleaseSql(savepointName); - return command.ExecuteNonQueryAsync(cancellationToken); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = await Logger.ReleasingTransactionSavepointAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + cancellationToken); + + if (!interceptionResult.IsSuppressed) + { + using var command = Connection.DbConnection.CreateCommand(); + command.Transaction = _dbTransaction; + command.CommandText = GetSavepointReleaseSql(savepointName); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + await Logger.ReleasedTransactionSavepointAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + catch (Exception e) + { + await Logger.TransactionErrorAsync( + Connection, + _dbTransaction, + TransactionId, + "ReleaseSavepoint", + e, + startTime, + stopwatch.Elapsed, + cancellationToken); + + throw; + } } /// diff --git a/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs index 846a4b39ffc..9523c48a801 100644 --- a/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs @@ -375,6 +375,109 @@ public virtual async Task Intercept_Rollback_to_suppress(bool async) } } + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_CreateSavepoint(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction(); + interceptor.Reset(); + + using var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId); + if (async) + { + await contextTransaction.SaveAsync("dummy"); + } + else + { + contextTransaction.Save("dummy"); + } + + AssertCreateSavepoint(context, contextTransaction, interceptor, async); + + AssertCreateSavepointEvents(listener); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_RollbackToSavepoint(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction(); + if (async) + { + await contextTransaction.SaveAsync("dummy"); + } + else + { + contextTransaction.Save("dummy"); + } + interceptor.Reset(); + + using var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId); + if (async) + { + await contextTransaction.RollbackAsync("dummy"); + } + else + { + contextTransaction.Rollback("dummy"); + } + + AssertRollbackToSavepoint(context, contextTransaction, interceptor, async); + + AssertRollbackToSavepointEvents(listener); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_ReleaseSavepoint(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction(); + if (async) + { + await contextTransaction.SaveAsync("dummy"); + } + else + { + contextTransaction.Save("dummy"); + } + interceptor.Reset(); + + using var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId); + if (async) + { + await contextTransaction.ReleaseAsync("dummy"); + } + else + { + contextTransaction.Release("dummy"); + } + + AssertReleaseSavepoint(context, contextTransaction, interceptor, async); + + AssertReleaseSavepointEvents(listener); + } + } + protected class CommitSuppressingTransactionInterceptor : TransactionInterceptor { public override InterceptionResult TransactionCommitting( @@ -554,6 +657,12 @@ private static void AssertBeginTransaction(DbContext context, TransactionInterce Assert.False(interceptor.CommittedCalled); Assert.False(interceptor.RollingBackCalled); Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.CreatingSavepointCalled); + Assert.False(interceptor.CreatedSavepointCalled); + Assert.False(interceptor.RollingBackToSavepointCalled); + Assert.False(interceptor.RolledBackToSavepointCalled); + Assert.False(interceptor.ReleasingSavepointCalled); + Assert.False(interceptor.ReleasedSavepointCalled); Assert.False(interceptor.FailedCalled); Assert.Same(context, interceptor.Context); } @@ -573,6 +682,12 @@ private static void AssertUseTransaction( Assert.False(interceptor.CommittedCalled); Assert.False(interceptor.RollingBackCalled); Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.CreatingSavepointCalled); + Assert.False(interceptor.CreatedSavepointCalled); + Assert.False(interceptor.RollingBackToSavepointCalled); + Assert.False(interceptor.RolledBackToSavepointCalled); + Assert.False(interceptor.ReleasingSavepointCalled); + Assert.False(interceptor.ReleasedSavepointCalled); Assert.False(interceptor.StartedCalled); Assert.False(interceptor.FailedCalled); Assert.Same(context, interceptor.Context); @@ -592,6 +707,12 @@ private static void AssertCommit( Assert.True(interceptor.CommittedCalled); Assert.False(interceptor.RollingBackCalled); Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.CreatingSavepointCalled); + Assert.False(interceptor.CreatedSavepointCalled); + Assert.False(interceptor.RollingBackToSavepointCalled); + Assert.False(interceptor.RolledBackToSavepointCalled); + Assert.False(interceptor.ReleasingSavepointCalled); + Assert.False(interceptor.ReleasedSavepointCalled); Assert.False(interceptor.UsedCalled); Assert.False(interceptor.StartingCalled); Assert.False(interceptor.StartedCalled); @@ -613,6 +734,93 @@ private static void AssertRollBack( Assert.False(interceptor.CommittedCalled); Assert.True(interceptor.RollingBackCalled); Assert.True(interceptor.RolledBackCalled); + Assert.False(interceptor.CreatingSavepointCalled); + Assert.False(interceptor.CreatedSavepointCalled); + Assert.False(interceptor.RollingBackToSavepointCalled); + Assert.False(interceptor.RolledBackToSavepointCalled); + Assert.False(interceptor.ReleasingSavepointCalled); + Assert.False(interceptor.ReleasedSavepointCalled); + Assert.False(interceptor.UsedCalled); + Assert.False(interceptor.StartingCalled); + Assert.False(interceptor.StartedCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + Assert.Equal(contextTransaction.TransactionId, interceptor.TransactionId); + } + + private static void AssertCreateSavepoint( + DbContext context, + IDbContextTransaction contextTransaction, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.False(interceptor.CommittingCalled); + Assert.False(interceptor.CommittedCalled); + Assert.False(interceptor.RollingBackCalled); + Assert.False(interceptor.RolledBackCalled); + Assert.True(interceptor.CreatingSavepointCalled); + Assert.True(interceptor.CreatedSavepointCalled); + Assert.False(interceptor.RollingBackToSavepointCalled); + Assert.False(interceptor.RolledBackToSavepointCalled); + Assert.False(interceptor.ReleasingSavepointCalled); + Assert.False(interceptor.ReleasedSavepointCalled); + Assert.False(interceptor.UsedCalled); + Assert.False(interceptor.StartingCalled); + Assert.False(interceptor.StartedCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + Assert.Equal(contextTransaction.TransactionId, interceptor.TransactionId); + } + + private static void AssertRollbackToSavepoint( + DbContext context, + IDbContextTransaction contextTransaction, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.False(interceptor.CommittingCalled); + Assert.False(interceptor.CommittedCalled); + Assert.False(interceptor.RollingBackCalled); + Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.CreatingSavepointCalled); + Assert.False(interceptor.CreatedSavepointCalled); + Assert.True(interceptor.RollingBackToSavepointCalled); + Assert.True(interceptor.RolledBackToSavepointCalled); + Assert.False(interceptor.ReleasingSavepointCalled); + Assert.False(interceptor.ReleasedSavepointCalled); + Assert.False(interceptor.UsedCalled); + Assert.False(interceptor.StartingCalled); + Assert.False(interceptor.StartedCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + Assert.Equal(contextTransaction.TransactionId, interceptor.TransactionId); + } + + private static void AssertReleaseSavepoint( + DbContext context, + IDbContextTransaction contextTransaction, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.False(interceptor.CommittingCalled); + Assert.False(interceptor.CommittedCalled); + Assert.False(interceptor.RollingBackCalled); + Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.CreatingSavepointCalled); + Assert.False(interceptor.CreatedSavepointCalled); + Assert.False(interceptor.RollingBackToSavepointCalled); + Assert.False(interceptor.RolledBackToSavepointCalled); + Assert.True(interceptor.ReleasingSavepointCalled); + Assert.True(interceptor.ReleasedSavepointCalled); Assert.False(interceptor.UsedCalled); Assert.False(interceptor.StartingCalled); Assert.False(interceptor.StartedCalled); @@ -651,6 +859,21 @@ private static void AssertRollBackEvents(ITestDiagnosticListener listener) RelationalEventId.TransactionRollingBack.Name, RelationalEventId.TransactionRolledBack.Name); + private static void AssertCreateSavepointEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.CreatingTransactionSavepoint.Name, + RelationalEventId.CreatedTransactionSavepoint.Name); + + private static void AssertRollbackToSavepointEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.RollingBackToTransactionSavepoint.Name, + RelationalEventId.RolledBackToTransactionSavepoint.Name); + + private static void AssertReleaseSavepointEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.ReleasingTransactionSavepoint.Name, + RelationalEventId.ReleasedTransactionSavepoint.Name); + protected class TransactionInterceptor : IDbTransactionInterceptor { public DbContext Context { get; set; } @@ -667,6 +890,12 @@ protected class TransactionInterceptor : IDbTransactionInterceptor public bool CommittedCalled { get; set; } public bool RollingBackCalled { get; set; } public bool RolledBackCalled { get; set; } + public bool CreatingSavepointCalled { get; set; } + public bool CreatedSavepointCalled { get; set; } + public bool RollingBackToSavepointCalled { get; set; } + public bool RolledBackToSavepointCalled { get; set; } + public bool ReleasingSavepointCalled { get; set; } + public bool ReleasedSavepointCalled { get; set; } public bool FailedCalled { get; set; } public void Reset() @@ -683,6 +912,12 @@ public void Reset() CommittedCalled = false; RollingBackCalled = false; RolledBackCalled = false; + CreatingSavepointCalled = false; + CreatedSavepointCalled = false; + RollingBackToSavepointCalled = false; + RolledBackToSavepointCalled = false; + ReleasingSavepointCalled = false; + ReleasedSavepointCalled = false; FailedCalled = false; } @@ -762,6 +997,72 @@ protected virtual void AssertRolledBack(TransactionEndEventData eventData) RolledBackCalled = true; } + protected virtual void AssertCreatingSavepoint(TransactionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + + CreatingSavepointCalled = true; + } + + protected virtual void AssertCreatedSavepoint(TransactionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + CreatedSavepointCalled = true; + } + + protected virtual void AssertRollingBackToSavepoint(TransactionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + + RollingBackToSavepointCalled = true; + } + + protected virtual void AssertRolledBackToSavepoint(TransactionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + RolledBackToSavepointCalled = true; + } + + protected virtual void AssertReleasingSavepoint(TransactionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + + ReleasingSavepointCalled = true; + } + + protected virtual void AssertReleasedSavepoint(TransactionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + ReleasedSavepointCalled = true; + } + protected virtual void AssertFailed(TransactionErrorEventData eventData) { Assert.Same(Context, eventData.Context); @@ -952,6 +1253,144 @@ public virtual Task TransactionRolledBackAsync( return Task.CompletedTask; } + public virtual InterceptionResult CreatingSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertCreatingSavepoint(eventData); + + return result; + } + + public virtual void CreatedSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertCreatedSavepoint(eventData); + } + + public virtual Task CreatingSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertCreatingSavepoint(eventData); + + return Task.FromResult(result); + } + + public virtual Task CreatedSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertCreatedSavepoint(eventData); + + return Task.CompletedTask; + } + + public virtual InterceptionResult RollingBackToSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertRollingBackToSavepoint(eventData); + + return result; + } + + public virtual void RolledBackToSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertRolledBackToSavepoint(eventData); + } + + public virtual Task RollingBackToSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertRollingBackToSavepoint(eventData); + + return Task.FromResult(result); + } + + public virtual Task RolledBackToSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertRolledBackToSavepoint(eventData); + + return Task.CompletedTask; + } + + public virtual InterceptionResult ReleasingSavepoint( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertReleasingSavepoint(eventData); + + return result; + } + + public virtual void ReleasedSavepoint( + DbTransaction transaction, + TransactionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertReleasedSavepoint(eventData); + } + + public virtual Task ReleasingSavepointAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertReleasingSavepoint(eventData); + + return Task.FromResult(result); + } + + public virtual Task ReleasedSavepointAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertReleasedSavepoint(eventData); + + return Task.CompletedTask; + } + public virtual void TransactionFailed( DbTransaction transaction, TransactionErrorEventData eventData) diff --git a/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs index de15b2cfacc..2e7b2e3e157 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; @@ -35,6 +36,9 @@ public TransactionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture { } + // ReleaseSavepoint is unsupported by SQL Server and is ignored + public override Task Intercept_ReleaseSavepoint(bool async) => Task.CompletedTask; + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener => false; @@ -50,6 +54,9 @@ public TransactionInterceptionWithDiagnosticsSqlServerTest(InterceptionSqlServer { } + // ReleaseSavepoint is unsupported by SQL Server and is ignored + public override Task Intercept_ReleaseSavepoint(bool async) => Task.CompletedTask; + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase { protected override bool ShouldSubscribeToDiagnosticListener => true;