diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index 2f97c3bb0aa..da3c10cc4ae 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -102,6 +102,7 @@ private enum Id ForeignKeyTpcPrincipalWarning, TpcStoreGeneratedIdentityWarning, KeyPropertiesNotMappedToTable, + StoredProcedureConcurrencyTokenNotMapped, // Update events BatchReadyForExecution = CoreEventId.RelationalBaseId + 700, @@ -872,6 +873,20 @@ private static EventId MakeValidationId(Id id) public static readonly EventId KeyPropertiesNotMappedToTable = MakeValidationId(Id.KeyPropertiesNotMappedToTable); + /// + /// An entity type is mapped to the stored procedure with a concurrency token not mapped to any original value parameter. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId StoredProcedureConcurrencyTokenNotMapped = + MakeValidationId(Id.StoredProcedureConcurrencyTokenNotMapped); + /// /// A foreign key specifies properties which don't map to the related tables. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index 52b4eba0d40..0b6bf9c850f 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -3122,6 +3122,46 @@ private static string OptionalDependentWithoutIdentifyingPropertyWarning(EventDe return d.GenerateMessage(p.EntityType.DisplayName()); } + /// + /// Logs the event. + /// + /// The diagnostics logger to use. + /// The entity type that the stored procedure is mapped to. + /// The property which represents the concurrency token. + /// The stored procedure name. + public static void StoredProcedureConcurrencyTokenNotMapped( + this IDiagnosticsLogger diagnostics, + IEntityType entityType, + IProperty concurrencyProperty, + string storedProcedureName) + { + var definition = RelationalResources.LogStoredProcedureConcurrencyTokenNotMapped(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, entityType.DisplayName(), storedProcedureName, concurrencyProperty.Name); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new StoredProcedurePropertyEventData( + definition, + StoredProcedureConcurrencyTokenNotMapped, + entityType, + concurrencyProperty, + storedProcedureName); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string StoredProcedureConcurrencyTokenNotMapped(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (StoredProcedurePropertyEventData)payload; + return d.GenerateMessage(p.EntityType.DisplayName(), p.StoredProcedureName, p.Property.Name); + } + /// /// Logs for the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index 86a8eedeaf1..61d56e82c53 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -286,6 +286,15 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogPossibleUnintendedUseOfEquals; + /// + /// 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? LogStoredProcedureConcurrencyTokenNotMapped; + /// /// 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/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 6fca7d36af0..f64fce248fa 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -221,7 +221,7 @@ protected virtual void ValidateStoredProcedures( if (deleteStoredProcedure != null) { AddSproc(StoreObjectType.DeleteStoredProcedure, entityType, storedProcedures); - ValidateSproc(deleteStoredProcedure, mappingStrategy); + ValidateSproc(deleteStoredProcedure, mappingStrategy, logger); sprocCount++; } @@ -229,7 +229,7 @@ protected virtual void ValidateStoredProcedures( if (insertStoredProcedure != null) { AddSproc(StoreObjectType.InsertStoredProcedure, entityType, storedProcedures); - ValidateSproc(insertStoredProcedure, mappingStrategy); + ValidateSproc(insertStoredProcedure, mappingStrategy, logger); sprocCount++; } @@ -237,7 +237,7 @@ protected virtual void ValidateStoredProcedures( if (updateStoredProcedure != null) { AddSproc(StoreObjectType.UpdateStoredProcedure, entityType, storedProcedures); - ValidateSproc(updateStoredProcedure, mappingStrategy); + ValidateSproc(updateStoredProcedure, mappingStrategy, logger); sprocCount++; } @@ -288,7 +288,10 @@ static void AddSproc( } } - private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy) + private static void ValidateSproc( + IStoredProcedure sproc, + string mappingStrategy, + IDiagnosticsLogger logger) { var entityType = sproc.EntityType; var storeObjectIdentifier = sproc.GetStoreIdentifier(); @@ -646,11 +649,7 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy if (originalValueProperties.Values.FirstOrDefault(p => p.IsConcurrencyToken) is { } missedConcurrencyToken) { - throw new InvalidOperationException( - RelationalStrings.StoredProcedureConcurrencyTokenNotMapped( - entityType.DisplayName(), - storeObjectIdentifier.DisplayName(), - missedConcurrencyToken.Name)); + logger.StoredProcedureConcurrencyTokenNotMapped(entityType, missedConcurrencyToken, storeObjectIdentifier.DisplayName()); } if (sproc.ResultColumns.Any(c => c != sproc.FindRowsAffectedResultColumn())) diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs index 0db0c69ee47..1e3d542f2f3 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -386,7 +386,8 @@ public static CoreOptionsExtension WithDefaultWarningConfiguration(CoreOptionsEx .TryWithExplicit(RelationalEventId.AmbientTransactionWarning, WarningBehavior.Throw) .TryWithExplicit(RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, WarningBehavior.Throw) .TryWithExplicit(RelationalEventId.IndexPropertiesMappedToNonOverlappingTables, WarningBehavior.Throw) - .TryWithExplicit(RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables, WarningBehavior.Throw)); + .TryWithExplicit(RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables, WarningBehavior.Throw) + .TryWithExplicit(RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, WarningBehavior.Throw)); /// /// Information/metadata for a . diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 609cb1461dc..4a2a22d0240 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1475,14 +1475,6 @@ public static string SqlQueryUnmappedType(object? elementType) GetString("SqlQueryUnmappedType", nameof(elementType)), elementType); - /// - /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter. - /// - public static string StoredProcedureConcurrencyTokenNotMapped(object? entityType, object? sproc, object? token) - => string.Format( - GetString("StoredProcedureConcurrencyTokenNotMapped", nameof(entityType), nameof(sproc), nameof(token)), - entityType, sproc, token); - /// /// Current value parameter '{parameter}' is not allowed on delete stored procedure '{sproc}'. Use HasOriginalValueParameter() instead. /// @@ -3475,6 +3467,31 @@ public static EventDefinition LogOptionalDependentWithoutIdentifyingProp return (EventDefinition)definition; } + /// + /// The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter. + /// + public static EventDefinition LogStoredProcedureConcurrencyTokenNotMapped(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogStoredProcedureConcurrencyTokenNotMapped; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogStoredProcedureConcurrencyTokenNotMapped, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, + LogLevel.Warning, + "RelationalEventId.StoredProcedureConcurrencyTokenNotMapped", + level => LoggerMessage.Define( + level, + RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, + _resourceManager.GetString("LogStoredProcedureConcurrencyTokenNotMapped")!))); + } + + return (EventDefinition)definition; + } + /// /// Possible unintended use of method 'Equals' for arguments '{left}' and '{right}' of different types in a query. This comparison will always return false. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 69c299c8160..e883a10d1ef 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -769,6 +769,10 @@ The entity type '{entityType}' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance. Warning RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning string + + The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter. + Warning RelationalEventId.StoredProcedureConcurrencyTokenNotMapped string string string + Possible unintended use of method 'Equals' for arguments '{left}' and '{right}' of different types in a query. This comparison will always return false. Warning RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning string string @@ -976,9 +980,6 @@ The element type '{elementType}' used in 'SqlQuery' method is not natively supported by your database provider. Either use a supported element type, or use ModelConfigurationBuilder.DefaultTypeMapping to define a mapping for your type. - - The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter. - Current value parameter '{parameter}' is not allowed on delete stored procedure '{sproc}'. Use HasOriginalValueParameter() instead. diff --git a/src/EFCore/Diagnostics/StoredProcedurePropertyEventData.cs b/src/EFCore/Diagnostics/StoredProcedurePropertyEventData.cs new file mode 100644 index 00000000000..d3eb77a6a2a --- /dev/null +++ b/src/EFCore/Diagnostics/StoredProcedurePropertyEventData.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Diagnostics; + +/// +/// A event payload class for events that have involving mapping of a property to a stored procedure. +/// +/// +/// See Logging, events, and diagnostics for more information and examples. +/// +public class StoredProcedurePropertyEventData : PropertyEventData +{ + /// + /// Constructs the event payload. + /// + /// The event definition. + /// A delegate that generates a log message for this event. + /// The entity type that the stored procedure is mapped to. + /// The property. + /// The stored procedure name. + public StoredProcedurePropertyEventData( + EventDefinitionBase eventDefinition, + Func messageGenerator, + IEntityType entityType, + IProperty property, + string storedProcedureName) + : base(eventDefinition, messageGenerator, property) + { + EntityType = entityType; + StoredProcedureName = storedProcedureName; + } + + /// + /// The entity type that the stored procedure is mapped to. + /// + public virtual IEntityType EntityType { get; } + + /// + /// The stored procedure name. + /// + public virtual string StoredProcedureName { get; } +} diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 81360d1df89..313e9c11ba5 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -2960,9 +2960,9 @@ public virtual void Detects_unmapped_concurrency_token() .HasRowsAffectedReturnValue()) .Property(a => a.Name).IsRowVersion(); - VerifyError( - RelationalStrings.StoredProcedureConcurrencyTokenNotMapped(nameof(Animal), "Animal_Update", nameof(Animal.Name)), - modelBuilder); + VerifyWarning( + RelationalResources.LogStoredProcedureConcurrencyTokenNotMapped(new TestLogger()) + .GenerateMessage(nameof(Animal), "Animal_Update", nameof(Animal.Name)), modelBuilder); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/LoggingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/LoggingSqlServerTest.cs index b35af63537a..5a95ee6b516 100644 --- a/test/EFCore.SqlServer.FunctionalTests/LoggingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/LoggingSqlServerTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; @@ -9,6 +10,44 @@ namespace Microsoft.EntityFrameworkCore; public class LoggingSqlServerTest : LoggingRelationalTestBase { + [ConditionalFact] + public virtual void StoredProcedureConcurrencyTokenNotMapped_throws_by_default() + { + using var context = new StoredProcedureConcurrencyTokenNotMappedContext(CreateOptionsBuilder(new ServiceCollection())); + + var definition = RelationalResources.LogStoredProcedureConcurrencyTokenNotMapped(CreateTestLogger()); + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.StoredProcedureConcurrencyTokenNotMapped.ToString(), + definition.GenerateMessage(nameof(Animal), "Animal_Update", nameof(Animal.Name)), + "RelationalEventId.StoredProcedureConcurrencyTokenNotMapped"), + Assert.Throws( + () => context.Model).Message); + } + + protected class StoredProcedureConcurrencyTokenNotMappedContext : DbContext + { + public StoredProcedureConcurrencyTokenNotMappedContext(DbContextOptionsBuilder optionsBuilder) + : base(optionsBuilder.Options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.Ignore(a => a.FavoritePerson); + b.Property(e => e.Name).IsRowVersion(); + b.UpdateUsingStoredProcedure( + b => + { + b.HasOriginalValueParameter(e => e.Id); + b.HasParameter(e => e.Name, p => p.IsOutput()); + b.HasRowsAffectedReturnValue(); + }); + }); + } + protected override DbContextOptionsBuilder CreateOptionsBuilder( IServiceCollection services, Action> relationalAction) diff --git a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs index 56ea021eb5f..60e8853a4fe 100644 --- a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs +++ b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs @@ -127,6 +127,14 @@ public override void Passes_on_derived_entity_type_not_mapped_to_a_stored_proced Assert.Equal(SqliteStrings.StoredProceduresNotSupported(nameof(Animal)), exception.Message); } + public override void Detects_unmapped_concurrency_token() + { + var exception = + Assert.Throws(() => base.Detects_unmapped_concurrency_token()); + + Assert.Equal(SqliteStrings.StoredProceduresNotSupported(nameof(Animal)), exception.Message); + } + public override void Store_generated_in_composite_key() { var modelBuilder = CreateConventionModelBuilder();