diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index 09c7fa839f8..1bd92025a42 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -104,6 +104,7 @@ private enum Id RequiredAttributeOnCollection, CollectionWithoutComparer, ConflictingKeylessAndKeyAttributesWarning, + PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning, // ChangeTracking events DetectChangesStarting = CoreBaseId + 800, @@ -646,6 +647,22 @@ public static readonly EventId InvalidIncludePathError /// public static readonly EventId ConflictingKeylessAndKeyAttributesWarning = MakeModelId(Id.ConflictingKeylessAndKeyAttributesWarning); + /// + /// + /// Required navigation with principal entity having global query filter defined + /// and the principal entity not having a matching filter + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a + /// . + /// + /// + public static readonly EventId PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning + = MakeModelId(Id.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning); + private static readonly string _changeTrackingPrefix = DbLoggerCategory.ChangeTracking.Name + "."; private static EventId MakeChangeTrackingId(Id id) => new EventId((int)id, _changeTrackingPrefix + id); diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index 41ba14f9b02..828635fc1ce 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -2996,5 +2996,44 @@ private static string ConflictingKeylessAndKeyAttributesWarning(EventDefinitionB p.Property.Name, p.Property.DeclaringEntityType.DisplayName()); } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// Foreign key which is used in the incorrectly setup navigation. + public static void PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IForeignKey foreignKey) + { + var definition = CoreResources.LogPossibleIncorrectRequiredNavigationWithQueryFilterInteraction(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + foreignKey.PrincipalEntityType.DisplayName(), + foreignKey.DeclaringEntityType.DisplayName()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new ForeignKeyEventData( + definition, + PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning, + foreignKey); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (ForeignKeyEventData)payload; + return d.GenerateMessage( + p.ForeignKey.PrincipalEntityType.DisplayName(), + p.ForeignKey.DeclaringEntityType.DisplayName()); + } } } diff --git a/src/EFCore/Diagnostics/LoggingDefinitions.cs b/src/EFCore/Diagnostics/LoggingDefinitions.cs index 82a3f4de886..149a7606de6 100644 --- a/src/EFCore/Diagnostics/LoggingDefinitions.cs +++ b/src/EFCore/Diagnostics/LoggingDefinitions.cs @@ -654,5 +654,14 @@ public abstract class LoggingDefinitions /// [EntityFrameworkInternal] public EventDefinitionBase LogConflictingKeylessAndKeyAttributes; + + /// + /// 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 LogPossibleIncorrectRequiredNavigationWithQueryFilterInteraction; } } diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 1102505715b..e69c8946f76 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -947,6 +947,18 @@ protected virtual void ValidateQueryFilters( throw new InvalidOperationException( CoreStrings.BadFilterDerivedType(entityType.GetQueryFilter(), entityType.DisplayName())); } + + var requiredNavigationWithQueryFilter = entityType.GetNavigations() + .Where(n => !n.IsCollection + && n.ForeignKey.IsRequired + && n.IsOnDependent + && n.ForeignKey.PrincipalEntityType.GetQueryFilter() != null + && n.ForeignKey.DeclaringEntityType.GetQueryFilter() == null).FirstOrDefault(); + + if (requiredNavigationWithQueryFilter != null) + { + logger.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning(requiredNavigationWithQueryFilter.ForeignKey); + } } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index a2e9c58c9b8..c0cf21e082d 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2596,6 +2596,14 @@ public static string InvalidStateEncountered([CanBeNull] object state) public static string DefaultIfEmptyAppliedAfterProjection => GetString("DefaultIfEmptyAppliedAfterProjection"); + /// + /// Entitiy '{principalEntityType}' has global query filter defined and is a required end of a relationship with the entity '{declaringEntityType}'. This may lead to unexpected results when the required entity is filtered out. Either use optional navigation or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information. + /// + public static string PossibleIncorrectRequiredNavigationWithQueryFilterInteraction([CanBeNull] object principalEntityType, [CanBeNull] object declaringEntityType) + => string.Format( + GetString("PossibleIncorrectRequiredNavigationWithQueryFilterInteraction", nameof(principalEntityType), nameof(declaringEntityType)), + principalEntityType, declaringEntityType); + /// /// Invalid {name}: {value} /// @@ -4243,5 +4251,29 @@ public static EventDefinition LogConflictingKeylessAndKeyAttribu return (EventDefinition)definition; } + + /// + /// Entitiy '{principalEntityType}' has global query filter defined and is a required end of a relationship with the entity '{declaringEntityType}'. This may lead to unexpected results when the required entity is filtered out. Either use optional navigation or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information. + /// + public static EventDefinition LogPossibleIncorrectRequiredNavigationWithQueryFilterInteraction([NotNull] IDiagnosticsLogger logger) + { + var definition = ((LoggingDefinitions)logger.Definitions).LogPossibleIncorrectRequiredNavigationWithQueryFilterInteraction; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((LoggingDefinitions)logger.Definitions).LogPossibleIncorrectRequiredNavigationWithQueryFilterInteraction, + () => new EventDefinition( + logger.Options, + CoreEventId.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning, + LogLevel.Warning, + "CoreEventId.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning", + level => LoggerMessage.Define( + level, + CoreEventId.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning, + _resourceManager.GetString("LogPossibleIncorrectRequiredNavigationWithQueryFilterInteraction")))); + } + + return (EventDefinition)definition; + } } } diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 0072be98d56..6d8dacb184b 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1375,6 +1375,13 @@ Conflicting attributes have been applied: the 'Key' attribute on property '{property}' and the 'Keyless' attribute on its entity '{entity}'. Note that the entity will have no key unless you use fluent API to override this. Warning CoreEventId.ConflictingKeylessAndKeyAttributesWarning string string + + Entitiy '{principalEntityType}' has global query filter defined and is a required end of a relationship with the entity '{declaringEntityType}'. This may lead to unexpected results when the required entity is filtered out. Either use optional navigation or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information. + Warning CoreEventId.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning string string + + + Entitiy '{principalEntityType}' has global query filter defined and is a required end of a relationship with the entity '{declaringEntityType}'. This may lead to unexpected results when the required entity is filtered out. Either use optional navigation or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information. + Invalid {name}: {value} diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 8c52c04a70d..c2bf183dfa3 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -1300,6 +1300,40 @@ public virtual void Detects_duplicate_discriminator_values() VerifyError(CoreStrings.DuplicateDiscriminatorValue(typeof(C).Name, 1, typeof(A).Name), model); } + [ConditionalFact] + public virtual void Required_navigation_with_query_filter_on_one_side_issues_a_warning() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().HasMany(x => x.Orders).WithOne(x => x.Customer).IsRequired(); + modelBuilder.Entity().HasQueryFilter(x => x.Id > 5); + + var model = modelBuilder.Model; + VerifyWarning(CoreStrings.PossibleIncorrectRequiredNavigationWithQueryFilterInteraction(nameof(Customer), nameof(Order)), modelBuilder.Model); + } + + [ConditionalFact] + public virtual void Optional_navigation_with_query_filter_on_one_side_doesnt_issue_a_warning() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().HasMany(x => x.Orders).WithOne(x => x.Customer).IsRequired(false); + modelBuilder.Entity().HasQueryFilter(x => x.Id > 5); + + var model = modelBuilder.Model; + VerifyLogDoesNotContain(CoreStrings.PossibleIncorrectRequiredNavigationWithQueryFilterInteraction(nameof(Customer), nameof(Order)), modelBuilder.Model); + } + + [ConditionalFact] + public virtual void Required_navigation_with_query_filter_on_both_sides_doesnt_issue_a_warning() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().HasMany(x => x.Orders).WithOne(x => x.Customer).IsRequired(); + modelBuilder.Entity().HasQueryFilter(x => x.Id > 5); + modelBuilder.Entity().HasQueryFilter(x => x.Customer.Id > 5); + + var model = modelBuilder.Model; + VerifyLogDoesNotContain(CoreStrings.PossibleIncorrectRequiredNavigationWithQueryFilterInteraction(nameof(Customer), nameof(Order)), modelBuilder.Model); + } + // INotify interfaces not really implemented; just marking the classes to test metadata construction private class FullNotificationEntity : INotifyPropertyChanging, INotifyPropertyChanged {