From b4381d279453b4ef92a6232b688af8fdfd258452 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 3 Aug 2022 16:02:56 -0700 Subject: [PATCH] BulkUpdates: Add custom CommandSource and exception messages Resolves #28568 --- .../Diagnostics/CommandSource.cs | 13 ++- .../Diagnostics/RelationalEventId.cs | 34 ++++++- .../Diagnostics/RelationalLoggerExtensions.cs | 90 +++++++++++++++++-- .../RelationalLoggingDefinitions.cs | 20 ++++- .../Properties/RelationalStrings.Designer.cs | 66 ++++++++++++-- .../Properties/RelationalStrings.resx | 14 ++- .../Query/NonQueryExpression.cs | 10 ++- ...alShapedQueryCompilingExpressionVisitor.cs | 45 ++++++++-- 8 files changed, 262 insertions(+), 30 deletions(-) diff --git a/src/EFCore.Relational/Diagnostics/CommandSource.cs b/src/EFCore.Relational/Diagnostics/CommandSource.cs index 7489926ee45..c0f7c12b471 100644 --- a/src/EFCore.Relational/Diagnostics/CommandSource.cs +++ b/src/EFCore.Relational/Diagnostics/CommandSource.cs @@ -61,5 +61,16 @@ public enum CommandSource /// /// The command was generated as part of a bulk update. /// - BulkUpdate + [Obsolete("Use ExecuteDelete or ExecuteUpdate instead.")] + BulkUpdate, + + /// + /// The command was generated as part of an 'ExecuteDelete' operation. + /// + ExecuteDelete = 9, + + /// + /// The command was generated as part of an 'ExecuteUpdate' operation. + /// + ExecuteUpdate, } diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index 94851772997..626c1f6bb72 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -85,7 +85,9 @@ private enum Id Obsolete_QueryPossibleExceptionWithAggregateOperatorWarning, Obsolete_ValueConversionSqlLiteralWarning, MultipleCollectionIncludeWarning, - BulkOperationFailed, + NonQueryOperationFailed, + ExecuteDeleteFailed, + ExecuteUpdateFailed, // Model validation events ModelValidationKeyDefaultValueWarning = CoreEventId.RelationalBaseId + 600, @@ -743,7 +745,7 @@ private static EventId MakeQueryId(Id id) public static readonly EventId MultipleCollectionIncludeWarning = MakeQueryId(Id.MultipleCollectionIncludeWarning); /// - /// An error occurred while executing a bulk operation. + /// An error occurred while executing a non-query operation. /// /// /// @@ -753,7 +755,33 @@ private static EventId MakeQueryId(Id id) /// This event uses the payload when used with a . /// /// - public static readonly EventId BulkOperationFailed = MakeQueryId(Id.BulkOperationFailed); + public static readonly EventId NonQueryOperationFailed = MakeQueryId(Id.NonQueryOperationFailed); + + /// + /// An error occurred while executing an 'ExecuteDelete' operation. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ExecuteDeleteFailed = MakeQueryId(Id.ExecuteDeleteFailed); + + /// + /// An error occurred while executing an 'ExecuteUpdate' operation. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ExecuteUpdateFailed = MakeQueryId(Id.ExecuteUpdateFailed); private static readonly string _validationPrefix = DbLoggerCategory.Model.Validation.Name + "."; diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index e3041fabb5f..0a0940c7af0 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2313,17 +2313,17 @@ private static string QueryPossibleUnintendedUseOfEqualsWarning(EventDefinitionB } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The type being used. /// The exception that caused this failure. - public static void BulkOperationFailed( + public static void ExecuteDeleteFailed( this IDiagnosticsLogger diagnostics, Type contextType, Exception exception) { - var definition = RelationalResources.LogExceptionDuringBulkOperation(diagnostics); + var definition = RelationalResources.LogExceptionDuringExecuteDelete(diagnostics); if (diagnostics.ShouldLog(definition)) { @@ -2337,7 +2337,7 @@ public static void BulkOperationFailed( { var eventData = new DbContextTypeErrorEventData( definition, - BulkOperationFailed, + ExecuteDeleteFailed, contextType, exception); @@ -2345,7 +2345,87 @@ public static void BulkOperationFailed( } } - private static string BulkOperationFailed(EventDefinitionBase definition, EventData payload) + private static string ExecuteDeleteFailed(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (DbContextTypeErrorEventData)payload; + return d.GenerateMessage(p.ContextType, Environment.NewLine, p.Exception); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The type being used. + /// The exception that caused this failure. + public static void ExecuteUpdateFailed( + this IDiagnosticsLogger diagnostics, + Type contextType, + Exception exception) + { + var definition = RelationalResources.LogExceptionDuringExecuteUpdate(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + contextType, Environment.NewLine, exception, + exception); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new DbContextTypeErrorEventData( + definition, + ExecuteUpdateFailed, + contextType, + exception); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string ExecuteUpdateFailed(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (DbContextTypeErrorEventData)payload; + return d.GenerateMessage(p.ContextType, Environment.NewLine, p.Exception); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The type being used. + /// The exception that caused this failure. + public static void NonQueryOperationFailed( + this IDiagnosticsLogger diagnostics, + Type contextType, + Exception exception) + { + var definition = RelationalResources.LogExceptionDuringNonQueryOperation(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + contextType, Environment.NewLine, exception, + exception); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new DbContextTypeErrorEventData( + definition, + NonQueryOperationFailed, + contextType, + exception); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string NonQueryOperationFailed(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; var p = (DbContextTypeErrorEventData)payload; diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index 8e0c2545278..80558d28f3e 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -644,5 +644,23 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public EventDefinitionBase? LogExceptionDuringBulkOperation; + public EventDefinitionBase? LogExceptionDuringNonQueryOperation; + + /// + /// 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? LogExceptionDuringExecuteDelete; + + /// + /// 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? LogExceptionDuringExecuteUpdate; } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 9015aed25f3..7313e514f49 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -2502,25 +2502,75 @@ public static EventDefinition LogDuplicateColumnOrders(IDiagnost } /// - /// An exception occurred while executing a bulk operation for context type '{contextType}'.{newline}{error} + /// An exception occurred while executing an 'ExecuteDelete' operation for context type '{contextType}'.{newline}{error} /// - public static EventDefinition LogExceptionDuringBulkOperation(IDiagnosticsLogger logger) + public static EventDefinition LogExceptionDuringExecuteDelete(IDiagnosticsLogger logger) { - var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringBulkOperation; + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringExecuteDelete; if (definition == null) { definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringBulkOperation, + ref ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringExecuteDelete, logger, static logger => new EventDefinition( logger.Options, - RelationalEventId.BulkOperationFailed, + RelationalEventId.ExecuteDeleteFailed, LogLevel.Error, - "RelationalEventId.BulkOperationFailed", + "RelationalEventId.ExecuteDeleteFailed", level => LoggerMessage.Define( level, - RelationalEventId.BulkOperationFailed, - _resourceManager.GetString("LogExceptionDuringBulkOperation")!))); + RelationalEventId.ExecuteDeleteFailed, + _resourceManager.GetString("LogExceptionDuringExecuteDelete")!))); + } + + return (EventDefinition)definition; + } + + /// + /// An exception occurred while executing an 'ExecuteUpdate' operation for context type '{contextType}'.{newline}{error} + /// + public static EventDefinition LogExceptionDuringExecuteUpdate(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringExecuteUpdate; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringExecuteUpdate, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.ExecuteUpdateFailed, + LogLevel.Error, + "RelationalEventId.ExecuteUpdateFailed", + level => LoggerMessage.Define( + level, + RelationalEventId.ExecuteUpdateFailed, + _resourceManager.GetString("LogExceptionDuringExecuteUpdate")!))); + } + + return (EventDefinition)definition; + } + + /// + /// An exception occurred while executing a non-query operation for context type '{contextType}'.{newline}{error} + /// + public static EventDefinition LogExceptionDuringNonQueryOperation(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringNonQueryOperation; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogExceptionDuringNonQueryOperation, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.NonQueryOperationFailed, + LogLevel.Error, + "RelationalEventId.NonQueryOperationFailed", + level => LoggerMessage.Define( + level, + RelationalEventId.NonQueryOperationFailed, + _resourceManager.GetString("LogExceptionDuringNonQueryOperation")!))); } return (EventDefinition)definition; diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 032df1b706e..8eb2534e682 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -612,9 +612,17 @@ The configured column orders for the table '{table}' contains duplicates. Ensure the specified column order values are distinct. Conflicting columns: {columns} Error RelationalEventId.DuplicateColumnOrders string string - - An exception occurred while executing a bulk operation for context type '{contextType}'.{newline}{error} - Error RelationalEventId.BulkOperationFailed Type string Exception + + An exception occurred while executing an 'ExecuteDelete' operation for context type '{contextType}'.{newline}{error} + Error RelationalEventId.ExecuteDeleteFailed Type string Exception + + + An exception occurred while executing an 'ExecuteUpdate' operation for context type '{contextType}'.{newline}{error} + Error RelationalEventId.ExecuteUpdateFailed Type string Exception + + + An exception occurred while executing a non-query operation for context type '{contextType}'.{newline}{error} + Error RelationalEventId.NonQueryOperationFailed Type string Exception Executed DbCommand ({elapsed}ms) [Parameters=[{parameters}], CommandType='{commandType}', CommandTimeout='{commandTimeout}']{newLine}{commandText} diff --git a/src/EFCore.Relational/Query/NonQueryExpression.cs b/src/EFCore.Relational/Query/NonQueryExpression.cs index 3aa0cc47bce..f90adc658ab 100644 --- a/src/EFCore.Relational/Query/NonQueryExpression.cs +++ b/src/EFCore.Relational/Query/NonQueryExpression.cs @@ -9,12 +9,20 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NonQueryExpression : Expression, IPrintableExpression { public NonQueryExpression(DeleteExpression deleteExpression) + : this(deleteExpression, CommandSource.ExecuteDelete) { - DeleteExpression = deleteExpression; + } + + public NonQueryExpression(DeleteExpression expression, CommandSource commandSource) + { + DeleteExpression = expression; + CommandSource = commandSource; } public virtual DeleteExpression DeleteExpression { get; } + public virtual CommandSource CommandSource { get; } + /// public override Type Type => typeof(int); diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 68cd41e1b74..7c3a5ffe687 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -68,23 +68,25 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression Expression.Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), Expression.Constant(relationalCommandCache), Expression.Constant(_contextType), + Expression.Constant(nonQueryExpression.CommandSource), Expression.Constant(_threadSafetyChecksEnabled)); } private static readonly MethodInfo NonQueryMethodInfo = typeof(RelationalShapedQueryCompilingExpressionVisitor).GetTypeInfo() .GetDeclaredMethods(nameof(NonQueryResult)) - .Single(mi => mi.GetParameters().Length == 4); + .Single(mi => mi.GetParameters().Length == 5); private static readonly MethodInfo NonQueryAsyncMethodInfo = typeof(RelationalShapedQueryCompilingExpressionVisitor).GetTypeInfo() .GetDeclaredMethods(nameof(NonQueryResultAsync)) - .Single(mi => mi.GetParameters().Length == 4); + .Single(mi => mi.GetParameters().Length == 5); private static int NonQueryResult( RelationalQueryContext relationalQueryContext, RelationalCommandCache relationalCommandCache, Type contextType, + CommandSource commandSource, bool threadSafetyChecksEnabled) { try @@ -97,7 +99,7 @@ private static int NonQueryResult( try { return relationalQueryContext.ExecutionStrategy.Execute( - (relationalQueryContext, relationalCommandCache), + (relationalQueryContext, relationalCommandCache, commandSource), static (_, state) => { EntityFrameworkEventSource.Log.QueryExecuting(); @@ -110,7 +112,7 @@ private static int NonQueryResult( null, state.relationalQueryContext.Context, state.relationalQueryContext.CommandLogger, - CommandSource.BulkUpdate)); + state.commandSource)); }, null); } @@ -130,7 +132,20 @@ private static int NonQueryResult( } else { - relationalQueryContext.QueryLogger.BulkOperationFailed(contextType, exception); + switch (commandSource) + { + case CommandSource.ExecuteDelete: + relationalQueryContext.QueryLogger.ExecuteDeleteFailed(contextType, exception); + break; + + case CommandSource.ExecuteUpdate: + relationalQueryContext.QueryLogger.ExecuteUpdateFailed(contextType, exception); + break; + + default: + relationalQueryContext.QueryLogger.NonQueryOperationFailed(contextType, exception); + break; + } } throw; @@ -141,6 +156,7 @@ private static Task NonQueryResultAsync( RelationalQueryContext relationalQueryContext, RelationalCommandCache relationalCommandCache, Type contextType, + CommandSource commandSource, bool threadSafetyChecksEnabled) { try @@ -153,7 +169,7 @@ private static Task NonQueryResultAsync( try { return relationalQueryContext.ExecutionStrategy.ExecuteAsync( - (relationalQueryContext, relationalCommandCache), + (relationalQueryContext, relationalCommandCache, commandSource), static (_, state, cancellationToken) => { EntityFrameworkEventSource.Log.QueryExecuting(); @@ -166,7 +182,7 @@ private static Task NonQueryResultAsync( null, state.relationalQueryContext.Context, state.relationalQueryContext.CommandLogger, - CommandSource.BulkUpdate), + state.commandSource), cancellationToken); }, null); @@ -187,7 +203,20 @@ private static Task NonQueryResultAsync( } else { - relationalQueryContext.QueryLogger.BulkOperationFailed(contextType, exception); + switch (commandSource) + { + case CommandSource.ExecuteDelete: + relationalQueryContext.QueryLogger.ExecuteDeleteFailed(contextType, exception); + break; + + case CommandSource.ExecuteUpdate: + relationalQueryContext.QueryLogger.ExecuteUpdateFailed(contextType, exception); + break; + + default: + relationalQueryContext.QueryLogger.NonQueryOperationFailed(contextType, exception); + break; + } } throw;