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();