diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 11a1fe035be..ae9a0cbdc10 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -1913,7 +1913,8 @@ protected virtual Dictionary> DiffData( foreach (var targetProperty in entry.EntityType.GetProperties()) { - if (!(targetProperty.ValueGenerated == ValueGenerated.Never || targetProperty.ValueGenerated == ValueGenerated.OnAdd)) + if (targetProperty.ValueGenerated != ValueGenerated.Never + && targetProperty.ValueGenerated != ValueGenerated.OnAdd) { continue; } diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index 72461d02950..961b26ab976 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -86,8 +86,8 @@ private enum Id ShadowPropertyCreated = CoreBaseId + 600, RedundantIndexRemoved, IncompatibleMatchingForeignKeyProperties, - RequiredAttributeOnDependent, - RequiredAttributeOnBothNavigations, + RequiredAttributeOnDependent, // Unused + RequiredAttributeOnBothNavigations, // Unused ConflictingShadowForeignKeysWarning, MultiplePrimaryKeyCandidates, MultipleNavigationProperties, @@ -99,13 +99,14 @@ private enum Id ConflictingForeignKeyAttributesOnNavigationAndPropertyWarning, RedundantForeignKeyWarning, NonNullableInverted, // Unused - NonNullableReferenceOnBothNavigations, - NonNullableReferenceOnDependent, + NonNullableReferenceOnBothNavigations, // Unused + NonNullableReferenceOnDependent, // Unused RequiredAttributeInverted, // Unused RequiredAttributeOnCollection, CollectionWithoutComparer, ConflictingKeylessAndKeyAttributesWarning, PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning, + RequiredAttributeOnSkipNavigation, // ChangeTracking events DetectChangesStarting = CoreBaseId + 800, @@ -435,6 +436,7 @@ public static readonly EventId InvalidIncludePathError /// This event uses the payload when used with a . /// /// + [Obsolete] public static readonly EventId RequiredAttributeOnBothNavigations = MakeModelId(Id.RequiredAttributeOnBothNavigations); /// @@ -448,6 +450,7 @@ public static readonly EventId InvalidIncludePathError /// This event uses the payload when used with a . /// /// + [Obsolete] public static readonly EventId NonNullableReferenceOnBothNavigations = MakeModelId(Id.NonNullableReferenceOnBothNavigations); /// @@ -461,6 +464,7 @@ public static readonly EventId InvalidIncludePathError /// This event uses the payload when used with a . /// /// + [Obsolete] public static readonly EventId RequiredAttributeOnDependent = MakeModelId(Id.RequiredAttributeOnDependent); /// @@ -474,6 +478,7 @@ public static readonly EventId InvalidIncludePathError /// This event uses the payload when used with a . /// /// + [Obsolete] public static readonly EventId NonNullableReferenceOnDependent = MakeModelId(Id.NonNullableReferenceOnDependent); /// @@ -489,6 +494,19 @@ public static readonly EventId InvalidIncludePathError /// public static readonly EventId RequiredAttributeOnCollection = MakeModelId(Id.RequiredAttributeOnCollection); + /// + /// + /// The on the skip navigation property was ignored. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId RequiredAttributeOnSkipNavigation = MakeModelId(Id.RequiredAttributeOnSkipNavigation); + /// /// /// The properties that best match the foreign key convention are already used by a different foreign key. diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index d937a8359b9..b1994bae16d 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -1284,6 +1284,7 @@ public static void RequiredAttributeInverted( } } + [Obsolete] private static string RequiredAttributeInverted(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; @@ -1319,6 +1320,7 @@ public static void NonNullableInverted( } } + [Obsolete] private static string NonNullableInverted(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; @@ -1332,6 +1334,7 @@ private static string NonNullableInverted(EventDefinitionBase definition, EventD /// The diagnostics logger to use. /// The first navigation property. /// The second navigation property. + [Obsolete] public static void RequiredAttributeOnBothNavigations( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] INavigation firstNavigation, @@ -1361,6 +1364,7 @@ public static void RequiredAttributeOnBothNavigations( } } + [Obsolete] private static string RequiredAttributeOnBothNavigations(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; @@ -1380,6 +1384,7 @@ private static string RequiredAttributeOnBothNavigations(EventDefinitionBase def /// The diagnostics logger to use. /// The first navigation property. /// The second navigation property. + [Obsolete] public static void NonNullableReferenceOnBothNavigations( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] INavigation firstNavigation, @@ -1409,6 +1414,7 @@ public static void NonNullableReferenceOnBothNavigations( } } + [Obsolete] private static string NonNullableReferenceOnBothNavigations(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; @@ -1427,6 +1433,7 @@ private static string NonNullableReferenceOnBothNavigations(EventDefinitionBase /// /// The diagnostics logger to use. /// The navigation property. + [Obsolete] public static void RequiredAttributeOnDependent( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] INavigation navigation) @@ -1451,6 +1458,7 @@ public static void RequiredAttributeOnDependent( } } + [Obsolete] private static string RequiredAttributeOnDependent(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; @@ -1463,6 +1471,7 @@ private static string RequiredAttributeOnDependent(EventDefinitionBase definitio /// /// The diagnostics logger to use. /// The navigation property. + [Obsolete] public static void NonNullableReferenceOnDependent( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] INavigation navigation) @@ -1488,6 +1497,7 @@ public static void NonNullableReferenceOnDependent( } } + [Obsolete] private static string NonNullableReferenceOnDependent(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; @@ -1529,6 +1539,40 @@ private static string RequiredAttributeOnCollection(EventDefinitionBase definiti return d.GenerateMessage(p.Navigation.DeclaringEntityType.DisplayName(), p.Navigation.Name); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The navigation property. + public static void RequiredAttributeOnSkipNavigation( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] ISkipNavigation navigation) + { + var definition = CoreResources.LogRequiredAttributeOnSkipNavigation(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, navigation.DeclaringEntityType.DisplayName(), navigation.Name); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new SkipNavigationEventData( + definition, + RequiredAttributeOnSkipNavigation, + navigation); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string RequiredAttributeOnSkipNavigation(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (SkipNavigationEventData)payload; + return d.GenerateMessage(p.Navigation.DeclaringEntityType.DisplayName(), p.Navigation.Name); + } + /// /// Logs for the event. /// diff --git a/src/EFCore/Diagnostics/LoggingDefinitions.cs b/src/EFCore/Diagnostics/LoggingDefinitions.cs index f28ed7d3b1f..dd5645132a0 100644 --- a/src/EFCore/Diagnostics/LoggingDefinitions.cs +++ b/src/EFCore/Diagnostics/LoggingDefinitions.cs @@ -521,6 +521,7 @@ public abstract class LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] + [Obsolete] public EventDefinitionBase LogRequiredAttributeOnDependent; /// @@ -530,6 +531,7 @@ public abstract class LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] + [Obsolete] public EventDefinitionBase LogNonNullableReferenceOnDependent; /// @@ -539,6 +541,7 @@ public abstract class LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] + [Obsolete] public EventDefinitionBase LogRequiredAttributeOnBothNavigations; /// @@ -548,6 +551,7 @@ public abstract class LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] + [Obsolete] public EventDefinitionBase LogNonNullableReferenceOnBothNavigations; /// @@ -568,6 +572,15 @@ public abstract class LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase LogRequiredAttributeOnCollection; + /// + /// 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 LogRequiredAttributeOnSkipNavigation; + /// /// 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/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index 1e339a3e74d..2f80ef6cd50 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -49,8 +49,7 @@ private WarningsConfiguration _warningsConfiguration .TryWithExplicit(CoreEventId.ManyServiceProvidersCreatedWarning, WarningBehavior.Throw) .TryWithExplicit(CoreEventId.LazyLoadOnDisposedContextWarning, WarningBehavior.Throw) .TryWithExplicit(CoreEventId.DetachedLazyLoadingWarning, WarningBehavior.Throw) - .TryWithExplicit(CoreEventId.InvalidIncludePathError, WarningBehavior.Throw) - .TryWithExplicit(CoreEventId.RequiredAttributeOnDependent, WarningBehavior.Throw); + .TryWithExplicit(CoreEventId.InvalidIncludePathError, WarningBehavior.Throw); /// /// Creates a new set of options with everything set to default values. diff --git a/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs b/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs index d35963da9c2..afa97f40348 100644 --- a/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionForeignKeyBuilder.cs @@ -298,7 +298,7 @@ bool CanSetNavigations( bool fromDataAnnotation = false); /// - /// Configures whether this is a required relationship (i.e. whether none the foreign key properties can + /// Configures whether this is a required relationship (i.e. whether none of the foreign key properties can /// be assigned ). /// /// @@ -313,7 +313,7 @@ bool CanSetNavigations( IConventionForeignKeyBuilder IsRequired(bool? required, bool fromDataAnnotation = false); /// - /// Returns a value indicating whether this relationship requiredness can be configured + /// Returns a value indicating whether the relationship requiredness can be configured /// from the current configuration source. /// /// @@ -324,6 +324,33 @@ bool CanSetNavigations( /// if the relationship requiredness can be configured. bool CanSetIsRequired(bool? required, bool fromDataAnnotation = false); + /// + /// Configures whether the dependent end is required (i.e. whether the principal to dependent navigation can + /// be assigned ). + /// + /// + /// A value indicating whether the dependent end is required. + /// to reset to default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the dependent end requiredness was configured, + /// otherwise. + /// + IConventionForeignKeyBuilder IsRequiredDependent(bool? required, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the dependent end requiredness can be configured + /// from the current configuration source. + /// + /// + /// A value indicating whether this is a required relationship. + /// to reset to default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// if the relationship requiredness can be configured. + bool CanSetIsRequiredDependent(bool? required, bool fromDataAnnotation = false); + /// /// Configures whether this relationship defines an ownership /// (i.e. whether the dependent entity must always be accessed via the navigation from the principal entity). diff --git a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs index 16cae240379..e3b8ece83c3 100644 --- a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs @@ -74,5 +74,27 @@ public interface IConventionNavigationBuilder : IConventionPropertyBaseBuilder /// otherwise. /// IConventionNavigationBuilder AutoInclude(bool? autoInclude, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether this navigation requiredness can be configured + /// from the current configuration source. + /// + /// A value indicating whether the navigation should be required. + /// Indicates whether the configuration was specified using a data annotation. + /// if requiredness can be set for this navigation. + bool CanSetIsRequired(bool? required, bool fromDataAnnotation = false); + + /// + /// Configures whether this navigation is required. + /// + /// + /// A value indicating whether this is a required navigation. + /// to reset to default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the requiredness was configured, otherwise. + /// + IConventionNavigationBuilder IsRequired(bool? required, bool fromDataAnnotation = false); } } diff --git a/src/EFCore/Metadata/Builders/NavigationBuilder.cs b/src/EFCore/Metadata/Builders/NavigationBuilder.cs index 2a4e728d3a1..2848a4d9078 100644 --- a/src/EFCore/Metadata/Builders/NavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/NavigationBuilder.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.ComponentModel; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -35,12 +37,13 @@ public NavigationBuilder([NotNull] IMutableNavigationBase navigationOrSkipNaviga InternalSkipNavigationBuilder = (navigationOrSkipNavigation as SkipNavigation)?.Builder; Metadata = navigationOrSkipNavigation; - Check.DebugAssert(InternalNavigationBuilder != null || InternalSkipNavigationBuilder != null, "Expected either a Navigation or SkipNavigation"); + Check.DebugAssert(InternalNavigationBuilder != null || InternalSkipNavigationBuilder != null, + "Expected either a Navigation or SkipNavigation"); } - private InternalNavigationBuilder InternalNavigationBuilder { get; } + private InternalNavigationBuilder InternalNavigationBuilder { get; set; } - private InternalSkipNavigationBuilder InternalSkipNavigationBuilder { get; } + private InternalSkipNavigationBuilder InternalSkipNavigationBuilder { get; set; } /// /// The navigation being configured. @@ -141,6 +144,28 @@ public virtual NavigationBuilder AutoInclude(bool autoInclude = true) return this; } + /// + /// Configures whether this navigation is required. + /// + /// A value indicating whether the navigation should be required. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual NavigationBuilder IsRequired(bool required = true) + { + if (InternalNavigationBuilder != null) + { + InternalNavigationBuilder = InternalNavigationBuilder.IsRequired(required, ConfigurationSource.Explicit); + } + else + { + throw new InvalidOperationException( + CoreStrings.RequiredSkipNavigation( + InternalSkipNavigationBuilder.Metadata.DeclaringEntityType.DisplayName(), + InternalSkipNavigationBuilder.Metadata.Name)); + } + + return this; + } + /// /// The internal builder being used to configure the skip navigation. /// diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs index 5969000e213..1887836de4c 100644 --- a/src/EFCore/Metadata/Conventions/ConventionSet.cs +++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs @@ -109,6 +109,12 @@ public class ConventionSet public virtual IList ForeignKeyRequirednessChangedConventions { get; } = new List(); + /// + /// Conventions to run when the requiredness of a foreign key is changed. + /// + public virtual IList ForeignKeyDependentRequirednessChangedConventions { get; } + = new List(); + /// /// Conventions to run when the ownership of a foreign key is changed. /// diff --git a/src/EFCore/Metadata/Conventions/IForeignKeyDependentRequirednessChangedConvention.cs b/src/EFCore/Metadata/Conventions/IForeignKeyDependentRequirednessChangedConvention.cs new file mode 100644 index 00000000000..0b328866ef9 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IForeignKeyDependentRequirednessChangedConvention.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// Represents an operation that should be performed when the dependent requiredness for a foreign key is changed. + /// + public interface IForeignKeyDependentRequirednessChangedConvention : IConvention + { + /// + /// Called after the dependent requiredness for a foreign key is changed. + /// + /// The builder for the foreign key. + /// Additional information associated with convention execution. + void ProcessForeignKeyDependentRequirednessChanged( + [NotNull] IConventionForeignKeyBuilder relationshipBuilder, + [NotNull] IConventionContext context); + } +} diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs index f3f53c9bfff..bc58cdd57d3 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs @@ -101,6 +101,9 @@ public abstract IConventionForeignKey OnForeignKeyRemoved( public abstract bool? OnForeignKeyRequirednessChanged( [NotNull] IConventionForeignKeyBuilder relationshipBuilder); + public abstract bool? OnForeignKeyDependentRequirednessChanged( + [NotNull] IConventionForeignKeyBuilder relationshipBuilder); + public abstract bool? OnForeignKeyUniquenessChanged( [NotNull] IConventionForeignKeyBuilder relationshipBuilder); diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs index 2ccc197bae9..7492a741d52 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs @@ -285,6 +285,13 @@ public override IReadOnlyList OnForeignKeyPropertiesChanged return relationshipBuilder.Metadata.IsRequired; } + public override bool? OnForeignKeyDependentRequirednessChanged( + IConventionForeignKeyBuilder relationshipBuilder) + { + Add(new OnForeignKeyDependentRequirednessChangedNode(relationshipBuilder)); + return relationshipBuilder.Metadata.IsRequiredDependent; + } + public override bool? OnForeignKeyOwnershipChanged( IConventionForeignKeyBuilder relationshipBuilder) { @@ -565,6 +572,19 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnForeignKeyRequirednessChanged(RelationshipBuilder); } + private sealed class OnForeignKeyDependentRequirednessChangedNode : ConventionNode + { + public OnForeignKeyDependentRequirednessChangedNode(IConventionForeignKeyBuilder relationshipBuilder) + { + RelationshipBuilder = relationshipBuilder; + } + + public IConventionForeignKeyBuilder RelationshipBuilder { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnForeignKeyDependentRequirednessChanged(RelationshipBuilder); + } + private sealed class OnForeignKeyOwnershipChangedNode : ConventionNode { public OnForeignKeyOwnershipChangedNode(IConventionForeignKeyBuilder relationshipBuilder) diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs index 04490e7ed3c..e8b2bc47d5c 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs @@ -511,6 +511,36 @@ public override IReadOnlyList OnForeignKeyPropertiesChanged return _boolConventionContext.Result; } + public override bool? OnForeignKeyDependentRequirednessChanged( + [NotNull] IConventionForeignKeyBuilder relationshipBuilder) + { + using (_dispatcher.DelayConventions()) + { + _boolConventionContext.ResetState(relationshipBuilder.Metadata.IsRequiredDependent); + foreach (var foreignKeyConvention in _conventionSet.ForeignKeyDependentRequirednessChangedConventions) + { + if (relationshipBuilder.Metadata.Builder == null) + { + return null; + } + + foreignKeyConvention.ProcessForeignKeyDependentRequirednessChanged(relationshipBuilder, _boolConventionContext); + + if (_boolConventionContext.ShouldStopProcessing()) + { + return _boolConventionContext.Result; + } + } + } + + if (relationshipBuilder.Metadata.Builder == null) + { + return null; + } + + return _boolConventionContext.Result; + } + public override bool? OnForeignKeyOwnershipChanged( IConventionForeignKeyBuilder relationshipBuilder) { diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs index 4bec126cd58..d9de87480d0 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs @@ -226,6 +226,16 @@ public virtual IReadOnlyList OnForeignKeyPropertiesChanged( [NotNull] IConventionForeignKeyBuilder relationshipBuilder) => _scope.OnForeignKeyRequirednessChanged(relationshipBuilder); + /// + /// 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. + /// + public virtual bool? OnForeignKeyDependentRequirednessChanged( + [NotNull] IConventionForeignKeyBuilder relationshipBuilder) + => _scope.OnForeignKeyDependentRequirednessChanged(relationshipBuilder); + /// /// 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/Metadata/Conventions/NonNullableNavigationConvention.cs b/src/EFCore/Metadata/Conventions/NonNullableNavigationConvention.cs index 2a411405ddd..563e6c33675 100644 --- a/src/EFCore/Metadata/Conventions/NonNullableNavigationConvention.cs +++ b/src/EFCore/Metadata/Conventions/NonNullableNavigationConvention.cs @@ -60,7 +60,6 @@ private void ProcessNavigation(IConventionNavigationBuilder navigationBuilder) { var navigation = navigationBuilder.Metadata; var foreignKey = navigation.ForeignKey; - var relationshipBuilder = foreignKey.Builder; var modelBuilder = navigationBuilder.ModelBuilder; if (!IsNonNullable(modelBuilder, navigation) @@ -69,28 +68,17 @@ private void ProcessNavigation(IConventionNavigationBuilder navigationBuilder) return; } - if (!navigation.IsOnDependent) + if (foreignKey.GetPrincipalEndConfigurationSource() != null) { - var inverse = navigation.Inverse; - if (inverse != null) + if (navigation.IsOnDependent) { - if (IsNonNullable(modelBuilder, inverse)) - { - Dependencies.Logger.NonNullableReferenceOnBothNavigations(navigation, inverse); - return; - } + foreignKey.Builder.IsRequired(true); } - - if (foreignKey.GetPrincipalEndConfigurationSource() != null) + else { - Dependencies.Logger.NonNullableReferenceOnDependent(navigation.ForeignKey.PrincipalToDependent); - return; + foreignKey.Builder.IsRequiredDependent(true); } - - return; } - - relationshipBuilder.IsRequired(true); } private bool IsNonNullable(IConventionModelBuilder modelBuilder, IConventionNavigation navigation) diff --git a/src/EFCore/Metadata/Conventions/RequiredNavigationAttributeConvention.cs b/src/EFCore/Metadata/Conventions/RequiredNavigationAttributeConvention.cs index 833d3866246..eb6c7b98796 100644 --- a/src/EFCore/Metadata/Conventions/RequiredNavigationAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/RequiredNavigationAttributeConvention.cs @@ -69,30 +69,26 @@ private void ProcessNavigation(IConventionNavigationBuilder navigationBuilder) return; } - var relationshipBuilder = foreignKey.Builder; - if (!navigation.IsOnDependent) + if (foreignKey.GetPrincipalEndConfigurationSource() != null) { - var inverse = navigation.Inverse; - if (inverse != null) + if (navigation.IsOnDependent) { - var attributes = GetAttributes(inverse.DeclaringEntityType, inverse); - if (attributes.Any()) - { - Dependencies.Logger.RequiredAttributeOnBothNavigations(navigation, inverse); - return; - } + foreignKey.Builder.IsRequired(true, fromDataAnnotation: true); } - - if (foreignKey.GetPrincipalEndConfigurationSource() != null) + else { - Dependencies.Logger.RequiredAttributeOnDependent(foreignKey.PrincipalToDependent); - return; + foreignKey.Builder.IsRequiredDependent(true, fromDataAnnotation: true); } - - return; } + } - relationshipBuilder.IsRequired(true, fromDataAnnotation: true); + /// + public override void ProcessSkipNavigationAdded( + IConventionSkipNavigationBuilder skipNavigationBuilder, + RequiredAttribute attribute, + IConventionContext context) + { + Dependencies.Logger.RequiredAttributeOnSkipNavigation(skipNavigationBuilder.Metadata); } } } diff --git a/src/EFCore/Metadata/IConventionForeignKey.cs b/src/EFCore/Metadata/IConventionForeignKey.cs index cc7ebf16011..adf27884cf5 100644 --- a/src/EFCore/Metadata/IConventionForeignKey.cs +++ b/src/EFCore/Metadata/IConventionForeignKey.cs @@ -112,10 +112,10 @@ IReadOnlyList SetProperties( ConfigurationSource? GetIsUniqueConfigurationSource(); /// - /// Sets a value indicating whether this relationship is required. + /// Sets a value indicating whether the principal entity is required. /// If , the dependent entity must always be assigned to a valid principal entity. /// - /// A value indicating whether this relationship is required. + /// A value indicating whether the principal entity is required. /// Indicates whether the configuration was specified using a data annotation. /// The configured requiredness. bool? SetIsRequired(bool? required, bool fromDataAnnotation = false); @@ -126,6 +126,21 @@ IReadOnlyList SetProperties( /// The configuration source for . ConfigurationSource? GetIsRequiredConfigurationSource(); + /// + /// Sets a value indicating whether the dependent entity is required. + /// If , the principal entity must always have a valid dependent entity assigned. + /// + /// A value indicating whether the dependent entity is required. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured requiredness. + bool? SetIsRequiredDependent(bool? required, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetIsRequiredDependentConfigurationSource(); + /// /// Sets a value indicating whether this relationship defines an ownership. /// If , the dependent entity must always be accessed via the navigation from the principal entity. diff --git a/src/EFCore/Metadata/IForeignKey.cs b/src/EFCore/Metadata/IForeignKey.cs index 7d67b1e79e1..edefca787be 100644 --- a/src/EFCore/Metadata/IForeignKey.cs +++ b/src/EFCore/Metadata/IForeignKey.cs @@ -53,11 +53,17 @@ public interface IForeignKey : IAnnotatable bool IsUnique { get; } /// - /// Gets a value indicating whether this relationship is required. + /// Gets a value indicating whether the principal entity is required. /// If , the dependent entity must always be assigned to a valid principal entity. /// bool IsRequired { get; } + /// + /// Gets a value indicating whether the dependent entity is required. + /// If , the principal entity must always have a valid dependent entity assigned. + /// + bool IsRequiredDependent { get; } + /// /// Gets or sets a value indicating whether this relationship defines an ownership. /// If , the dependent entity must always be accessed via the navigation from the principal entity. diff --git a/src/EFCore/Metadata/IMutableForeignKey.cs b/src/EFCore/Metadata/IMutableForeignKey.cs index 846de565ee6..826f8ecf467 100644 --- a/src/EFCore/Metadata/IMutableForeignKey.cs +++ b/src/EFCore/Metadata/IMutableForeignKey.cs @@ -51,11 +51,17 @@ public interface IMutableForeignKey : IForeignKey, IMutableAnnotatable new bool IsUnique { get; set; } /// - /// Gets or sets a value indicating whether this relationship is required. If true, the dependent entity must always be - /// assigned to a valid principal entity. + /// Sets a value indicating whether the principal entity is required. + /// If , the dependent entity must always be assigned to a valid principal entity. /// new bool IsRequired { get; set; } + /// + /// Sets a value indicating whether the dependent entity is required. + /// If , the principal entity must always have a valid dependent entity assigned. + /// + new bool IsRequiredDependent { get; set; } + /// /// Gets or sets a value indicating whether this relationship defines ownership. If true, the dependent entity must always be /// accessed via the navigation from the principal entity. diff --git a/src/EFCore/Metadata/Internal/ForeignKey.cs b/src/EFCore/Metadata/Internal/ForeignKey.cs index a73ae776eda..2dd8c243e89 100644 --- a/src/EFCore/Metadata/Internal/ForeignKey.cs +++ b/src/EFCore/Metadata/Internal/ForeignKey.cs @@ -26,6 +26,7 @@ public class ForeignKey : ConventionAnnotatable, IMutableForeignKey, IConvention private DeleteBehavior? _deleteBehavior; private bool? _isUnique; private bool _isRequired; + private bool? _isRequiredDependent; private bool? _isOwnership; private ConfigurationSource _configurationSource; @@ -33,6 +34,7 @@ public class ForeignKey : ConventionAnnotatable, IMutableForeignKey, IConvention private ConfigurationSource? _principalKeyConfigurationSource; private ConfigurationSource? _isUniqueConfigurationSource; private ConfigurationSource? _isRequiredConfigurationSource; + private ConfigurationSource? _isRequiredDependentConfigurationSource; private ConfigurationSource? _deleteBehaviorConfigurationSource; private ConfigurationSource? _principalEndConfigurationSource; private ConfigurationSource? _isOwnershipConfigurationSource; @@ -509,15 +511,36 @@ public virtual bool IsUnique var oldUnique = IsUnique; _isUnique = unique; - if (unique == null) + if (unique == false + && IsRequiredDependent) { - _isUniqueConfigurationSource = null; + throw new InvalidOperationException( + CoreStrings.NonUniqueRequiredDependentForeignKey(Properties.Format(), DeclaringEntityType.DisplayName())); } - else + + if (unique.HasValue + && PrincipalEntityType.ClrType != null + && PrincipalToDependent != null) { - UpdateIsUniqueConfigurationSource(configurationSource); + if (!Internal.Navigation.IsCompatible( + PrincipalToDependent.GetIdentifyingMemberInfo(), + PrincipalEntityType.ClrType, + DeclaringEntityType.ClrType, + !unique, + shouldThrow: false)) + { + throw new InvalidOperationException( + CoreStrings.UnableToSetIsUnique( + unique.Value, + PrincipalToDependent.PropertyInfo.Name, + PrincipalEntityType.DisplayName())); + } } + _isUniqueConfigurationSource = unique == null + ? null + : (ConfigurationSource?)configurationSource.Max(_isUniqueConfigurationSource); + return IsUnique != oldUnique ? DeclaringEntityType.Model.ConventionDispatcher.OnForeignKeyUniquenessChanged(Builder) : oldUnique; @@ -534,15 +557,6 @@ public virtual bool IsUnique [DebuggerStepThrough] public virtual ConfigurationSource? GetIsUniqueConfigurationSource() => _isUniqueConfigurationSource; - /// - /// 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. - /// - public virtual void UpdateIsUniqueConfigurationSource(ConfigurationSource configurationSource) - => _isUniqueConfigurationSource = configurationSource.Max(_isUniqueConfigurationSource); - /// /// 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 @@ -566,14 +580,9 @@ public virtual bool IsRequired var oldRequired = IsRequired; _isRequired = required ?? DefaultIsRequired; - if (required == null) - { - _isRequiredConfigurationSource = null; - } - else - { - UpdateIsRequiredConfigurationSource(configurationSource); - } + _isRequiredConfigurationSource = required == null + ? null + : (ConfigurationSource?)configurationSource.Max(_isRequiredConfigurationSource); return IsRequired != oldRequired ? DeclaringEntityType.Model.ConventionDispatcher.OnForeignKeyRequirednessChanged(Builder) @@ -606,8 +615,58 @@ public virtual void SetIsRequiredConfigurationSource(ConfigurationSource? config /// 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. /// - public virtual void UpdateIsRequiredConfigurationSource(ConfigurationSource configurationSource) - => _isRequiredConfigurationSource = configurationSource.Max(_isRequiredConfigurationSource); + public virtual bool IsRequiredDependent + { + get => _isRequiredDependent ?? DefaultIsRequiredDependent; + set => SetIsRequiredDependent(value, ConfigurationSource.Explicit); + } + + /// + /// 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. + /// + public virtual bool? SetIsRequiredDependent(bool? required, ConfigurationSource configurationSource) + { + if (!IsUnique + && required == true) + { + throw new InvalidOperationException( + CoreStrings.NonUniqueRequiredDependentForeignKey(Properties.Format(), DeclaringEntityType.DisplayName())); + } + + var oldRequired = IsRequiredDependent; + _isRequiredDependent = required; + + _isRequiredDependentConfigurationSource = required == null + ? null + : (ConfigurationSource?)configurationSource.Max(_isRequiredConfigurationSource); + + return IsRequiredDependent != oldRequired + ? DeclaringEntityType.Model.ConventionDispatcher.OnForeignKeyDependentRequirednessChanged(Builder) + : oldRequired; + } + + private bool DefaultIsRequiredDependent => false; + + /// + /// 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. + /// + [DebuggerStepThrough] + public virtual ConfigurationSource? GetIsRequiredDependentConfigurationSource() => _isRequiredDependentConfigurationSource; + + /// + /// 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. + /// + public virtual void SetIsRequiredDependentConfigurationSource(ConfigurationSource? configurationSource) + => _isRequiredDependentConfigurationSource = configurationSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1034,12 +1093,7 @@ IConventionAnnotatableBuilder IConventionAnnotatable.Builder [DebuggerStepThrough] get => Builder; } - /// - /// 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. - /// + /// [DebuggerStepThrough] IReadOnlyList IConventionForeignKey.SetProperties( IReadOnlyList properties, @@ -1049,42 +1103,22 @@ IReadOnlyList IConventionForeignKey.SetProperties( properties.Cast().ToArray(), (Key)principalKey, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionNavigation IConventionForeignKey.SetDependentToPrincipal(string name, bool fromDataAnnotation) => SetDependentToPrincipal(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionNavigation IConventionForeignKey.SetDependentToPrincipal(MemberInfo property, bool fromDataAnnotation) => SetDependentToPrincipal(property, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionNavigation IConventionForeignKey.SetPrincipalToDependent(string name, bool fromDataAnnotation) => HasPrincipalToDependent(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionNavigation IConventionForeignKey.SetPrincipalToDependent(MemberInfo property, bool fromDataAnnotation) => HasPrincipalToDependent(property, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1092,44 +1126,29 @@ IConventionNavigation IConventionForeignKey.SetPrincipalToDependent(MemberInfo p /// [DebuggerStepThrough] IEnumerable IForeignKey.GetReferencingSkipNavigations() - => GetReferencingSkipNavigations(); + => GetReferencingSkipNavigations(); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool? IConventionForeignKey.SetIsUnique(bool? unique, bool fromDataAnnotation) => SetIsUnique(unique, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool? IConventionForeignKey.SetIsRequired(bool? required, bool fromDataAnnotation) => SetIsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// + [DebuggerStepThrough] + bool? IConventionForeignKey.SetIsRequiredDependent(bool? required, bool fromDataAnnotation) + => SetIsRequiredDependent(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// [DebuggerStepThrough] bool? IConventionForeignKey.SetIsOwnership(bool? ownership, bool fromDataAnnotation) => SetIsOwnership(ownership, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] DeleteBehavior? IConventionForeignKey.SetDeleteBehavior(DeleteBehavior? deleteBehavior, bool fromDataAnnotation) => SetDeleteBehavior(deleteBehavior, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 59c2257de53..b8855e43021 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -3097,10 +3097,10 @@ private InternalForeignKeyBuilder HasOwnership( var ownershipBuilder = existingNavigation.ForeignKey.Builder; ownershipBuilder = ownershipBuilder - .IsRequired(true, configurationSource) - ?.HasEntityTypes( + .HasEntityTypes( Metadata, ownershipBuilder.Metadata.FindNavigationsFromInHierarchy(Metadata).Single().TargetEntityType, configurationSource) + ?.IsRequired(true, configurationSource) ?.HasNavigations(inverse, navigation, configurationSource) ?.IsOwnership(true, configurationSource); diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index dbf1c94ec8a..5da02b7faf3 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -935,6 +935,16 @@ public virtual InternalForeignKeyBuilder IsRequired(bool? required, Configuratio return null; } + if (required == true + && Metadata.GetPrincipalEndConfigurationSource() == null + && configurationSource == ConfigurationSource.Explicit) + { + throw new InvalidOperationException( + CoreStrings.AmbiguousEndRequired( + Metadata.Properties.Format(), + Metadata.DeclaringEntityType.DisplayName())); + } + Metadata.SetIsRequired(required, configurationSource); return this; @@ -950,6 +960,53 @@ public virtual bool CanSetIsRequired(bool? required, ConfigurationSource? config => Metadata.IsRequired == required || configurationSource.Overrides(Metadata.GetIsRequiredConfigurationSource()); + /// + /// 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. + /// + public virtual InternalForeignKeyBuilder IsRequiredDependent(bool? required, ConfigurationSource configurationSource) + { + if (!CanSetIsRequiredDependent(required, configurationSource)) + { + return null; + } + + if (required == true + && Metadata.GetPrincipalEndConfigurationSource() == null + && configurationSource == ConfigurationSource.Explicit) + { + throw new InvalidOperationException( + CoreStrings.AmbiguousEndRequiredDependent( + Metadata.Properties.Format(), + Metadata.DeclaringEntityType.DisplayName())); + } + + if (required == true + && !Metadata.IsUnique) + { + IsUnique(null, configurationSource); + } + + Metadata.SetIsRequiredDependent(required, configurationSource); + + return this; + } + + /// + /// 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. + /// + public virtual bool CanSetIsRequiredDependent(bool? required, ConfigurationSource? configurationSource) + => Metadata.IsRequiredDependent == required + || ((required != true + || Metadata.IsUnique + || configurationSource.Value.Overrides(Metadata.GetIsUniqueConfigurationSource())) + && configurationSource.Overrides(Metadata.GetIsRequiredDependentConfigurationSource())); + /// /// 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 @@ -1220,6 +1277,12 @@ public virtual InternalForeignKeyBuilder IsUnique(bool? unique, ConfigurationSou } } + if (unique == false + && Metadata.IsRequiredDependent) + { + builder.IsRequiredDependent(null, configurationSource); + } + builder.Metadata.SetIsUnique(unique, configurationSource); return batch.Run(builder); @@ -1248,6 +1311,13 @@ private bool CanSetIsUnique(bool? unique, ConfigurationSource? configurationSour return false; } + if (unique == false + && Metadata.IsRequiredDependent + && !configurationSource.Value.Overrides(Metadata.GetIsRequiredDependentConfigurationSource())) + { + return false; + } + if (Metadata.PrincipalToDependent?.IsShadowProperty() == false && !Navigation.IsCompatible( Metadata.PrincipalToDependent.GetIdentifyingMemberInfo(), @@ -1465,11 +1535,6 @@ private InternalForeignKeyBuilder HasEntityTypes( principalEntityType = principalEntityType.LeastDerivedType(Metadata.DeclaringEntityType); dependentEntityType = dependentEntityType.LeastDerivedType(Metadata.PrincipalEntityType); - - if (Metadata.GetIsRequiredConfigurationSource() != ConfigurationSource.Explicit) - { - Metadata.SetIsRequiredConfigurationSource(configurationSource: null); - } } else { @@ -2028,6 +2093,7 @@ private InternalForeignKeyBuilder ReplaceForeignKey( IReadOnlyList principalProperties = null, bool? isUnique = null, bool? isRequired = null, + bool? isRequiredDependent = null, bool? isOwnership = null, DeleteBehavior? deleteBehavior = null, bool removeCurrent = false, @@ -2091,15 +2157,27 @@ private InternalForeignKeyBuilder ReplaceForeignKey( ? Metadata.IsUnique : (bool?)null); - isRequired ??= ((Metadata.GetIsRequiredConfigurationSource()?.Overrides(configurationSource) ?? false) - ? Metadata.IsRequired - : (bool?)null); + isRequired ??= !oldRelationshipInverted + ? ((Metadata.GetIsRequiredConfigurationSource()?.Overrides(configurationSource) ?? false) + ? Metadata.IsRequired + : (bool?)null) + : ((Metadata.GetIsRequiredDependentConfigurationSource()?.Overrides(ConfigurationSource.Explicit) ?? false) + ? Metadata.IsRequiredDependent + : (bool?)null); - isOwnership ??= ((Metadata.GetIsOwnershipConfigurationSource()?.Overrides(configurationSource) ?? false) - && !oldRelationshipInverted - ? Metadata.IsOwnership + isRequiredDependent ??= !oldRelationshipInverted + ? ((Metadata.GetIsRequiredDependentConfigurationSource()?.Overrides(configurationSource) ?? false) + ? Metadata.IsRequiredDependent + : (bool?)null) + : ((Metadata.GetIsRequiredConfigurationSource()?.Overrides(ConfigurationSource.Explicit) ?? false) + ? Metadata.IsRequired : (bool?)null); + isOwnership ??= !oldRelationshipInverted + && (Metadata.GetIsOwnershipConfigurationSource()?.Overrides(configurationSource) ?? false) + ? Metadata.IsOwnership + : (bool?)null; + deleteBehavior ??= ((Metadata.GetDeleteBehaviorConfigurationSource()?.Overrides(configurationSource) ?? false) ? Metadata.DeleteBehavior : (DeleteBehavior?)null); @@ -2123,6 +2201,7 @@ private InternalForeignKeyBuilder ReplaceForeignKey( principalProperties, isUnique, isRequired, + isRequiredDependent, isOwnership, deleteBehavior, removeCurrent, @@ -2141,6 +2220,7 @@ private InternalForeignKeyBuilder ReplaceForeignKey( [CanBeNull] IReadOnlyList principalProperties, bool? isUnique, bool? isRequired, + bool? isRequiredDependent, bool? isOwnership, DeleteBehavior? deleteBehavior, bool removeCurrent, @@ -2433,15 +2513,71 @@ private InternalForeignKeyBuilder ReplaceForeignKey( isRequiredConfigurationSource) ?? newRelationshipBuilder; } - else if (!oldRelationshipInverted - && Metadata.GetIsRequiredConfigurationSource().HasValue - && !newRelationshipBuilder.Metadata.GetIsRequiredConfigurationSource().HasValue) + else { - newRelationshipBuilder = newRelationshipBuilder.IsRequired( - Metadata.IsRequired, - Metadata.GetIsRequiredConfigurationSource().Value) + if (!oldRelationshipInverted) + { + if (Metadata.GetIsRequiredConfigurationSource().HasValue + && !newRelationshipBuilder.Metadata.GetIsRequiredConfigurationSource().HasValue) + { + newRelationshipBuilder = newRelationshipBuilder.IsRequired( + Metadata.IsRequired, + Metadata.GetIsRequiredConfigurationSource().Value) + ?? newRelationshipBuilder; + } + } + else + { + if (Metadata.GetIsRequiredDependentConfigurationSource().Overrides(ConfigurationSource.Explicit) + && !newRelationshipBuilder.Metadata.GetIsRequiredConfigurationSource().HasValue) + { + newRelationshipBuilder = newRelationshipBuilder.IsRequired( + Metadata.IsRequiredDependent, + Metadata.GetIsRequiredDependentConfigurationSource().Value) + ?? newRelationshipBuilder; + } + } + } + + if (isRequiredDependent.HasValue) + { + var isRequiredDependentConfigurationSource = configurationSource; + if (isRequiredDependent.Value == Metadata.IsRequiredDependent) + { + isRequiredDependentConfigurationSource = isRequiredDependentConfigurationSource.Max( + Metadata.GetIsRequiredDependentConfigurationSource()); + } + + newRelationshipBuilder = newRelationshipBuilder.IsRequiredDependent( + isRequiredDependent.Value, + isRequiredDependentConfigurationSource) ?? newRelationshipBuilder; } + else + { + if (!oldRelationshipInverted) + { + if (Metadata.GetIsRequiredDependentConfigurationSource().HasValue + && !newRelationshipBuilder.Metadata.GetIsRequiredDependentConfigurationSource().HasValue) + { + newRelationshipBuilder = newRelationshipBuilder.IsRequiredDependent( + Metadata.IsRequiredDependent, + Metadata.GetIsRequiredDependentConfigurationSource().Value) + ?? newRelationshipBuilder; + } + } + else + { + if (Metadata.GetIsRequiredConfigurationSource().Overrides(ConfigurationSource.Explicit) + && !newRelationshipBuilder.Metadata.GetIsRequiredDependentConfigurationSource().HasValue) + { + newRelationshipBuilder = newRelationshipBuilder.IsRequiredDependent( + Metadata.IsRequired, + Metadata.GetIsRequiredConfigurationSource().Value) + ?? newRelationshipBuilder; + } + } + } if (deleteBehavior.HasValue) { @@ -2627,6 +2763,13 @@ private InternalForeignKeyBuilder MergeFacetsFrom(Navigation newNavigation, Navi builder = builder.HasField(oldNavigation.FieldInfo, oldFieldInfoConfigurationSource.Value); } + var oldIsEagerLoadedConfigurationSource = ((IConventionNavigation)oldNavigation).GetIsEagerLoadedConfigurationSource(); + if (oldIsEagerLoadedConfigurationSource.HasValue + && builder.CanSetAutoInclude(((INavigation)oldNavigation).IsEagerLoaded, oldIsEagerLoadedConfigurationSource.Value)) + { + builder = builder.AutoInclude(((INavigation)oldNavigation).IsEagerLoaded, oldIsEagerLoadedConfigurationSource.Value); + } + return builder.Metadata.ForeignKey.Builder; } @@ -3846,23 +3989,13 @@ private enum Resolution ResetDependentProperties = 1 << 3 } - /// - /// 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. - /// + /// IConventionForeignKey IConventionForeignKeyBuilder.Metadata { [DebuggerStepThrough] get => Metadata; } - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasEntityTypes( IConventionEntityType principalEntityType, IConventionEntityType dependentEntityType, bool fromDataAnnotation) @@ -3870,12 +4003,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasEntityTypes( (EntityType)principalEntityType, (EntityType)dependentEntityType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetEntityTypes( IConventionEntityType principalEntityType, IConventionEntityType dependentEntityType, bool fromDataAnnotation) @@ -3883,24 +4011,14 @@ bool IConventionForeignKeyBuilder.CanSetEntityTypes( (EntityType)principalEntityType, (EntityType)dependentEntityType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanInvert(IReadOnlyList newForeignKeyProperties, bool fromDataAnnotation) => CanInvert( (IReadOnlyList)newForeignKeyProperties, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasForeignKey( [CanBeNull] IReadOnlyList properties, bool fromDataAnnotation) @@ -3908,12 +4026,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasForeignKey( properties, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasForeignKey( IReadOnlyList properties, bool fromDataAnnotation) @@ -3921,12 +4034,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasForeignKey( properties as IReadOnlyList ?? properties?.Cast().ToList(), fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetForeignKey( [CanBeNull] IReadOnlyList properties, bool fromDataAnnotation) @@ -3934,24 +4042,14 @@ bool IConventionForeignKeyBuilder.CanSetForeignKey( properties, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetForeignKey(IReadOnlyList properties, bool fromDataAnnotation) => CanSetForeignKey( properties as IReadOnlyList ?? properties?.Cast().ToList(), fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasPrincipalKey( [CanBeNull] IReadOnlyList properties, bool fromDataAnnotation) @@ -3959,12 +4057,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasPrincipalKey( properties, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasPrincipalKey( IReadOnlyList properties, bool fromDataAnnotation) @@ -3972,12 +4065,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasPrincipalKey( properties as IReadOnlyList ?? properties?.Cast().ToList(), fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetPrincipalKey( [CanBeNull] IReadOnlyList properties, bool fromDataAnnotation) @@ -3985,24 +4073,14 @@ bool IConventionForeignKeyBuilder.CanSetPrincipalKey( properties, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetPrincipalKey(IReadOnlyList properties, bool fromDataAnnotation) => CanSetPrincipalKey( properties as IReadOnlyList ?? properties?.Cast().ToList(), fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigation( string name, bool pointsToPrincipal, bool fromDataAnnotation) @@ -4010,12 +4088,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigation( name, pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigation( MemberInfo property, bool pointsToPrincipal, bool fromDataAnnotation) @@ -4023,12 +4096,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigation( property, pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigations( string navigationToPrincipalName, string navigationToDependentName, bool fromDataAnnotation) @@ -4036,12 +4104,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigations( navigationToPrincipalName, navigationToDependentName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigations( MemberInfo navigationToPrincipal, MemberInfo navigationToDependent, bool fromDataAnnotation) @@ -4049,12 +4112,7 @@ IConventionForeignKeyBuilder IConventionForeignKeyBuilder.HasNavigations( navigationToPrincipal, navigationToDependent, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetNavigation( MemberInfo property, bool pointsToPrincipal, bool fromDataAnnotation) @@ -4062,24 +4120,14 @@ bool IConventionForeignKeyBuilder.CanSetNavigation( property, pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetNavigation(string name, bool pointsToPrincipal, bool fromDataAnnotation) => CanSetNavigation( name, pointsToPrincipal, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetNavigations( MemberInfo navigationToPrincipal, MemberInfo navigationToDependent, bool fromDataAnnotation) @@ -4087,12 +4135,7 @@ bool IConventionForeignKeyBuilder.CanSetNavigations( navigationToPrincipal, navigationToDependent, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetNavigations( string navigationToPrincipalName, string navigationToDependentName, bool fromDataAnnotation) @@ -4100,26 +4143,26 @@ bool IConventionForeignKeyBuilder.CanSetNavigations( navigationToPrincipalName, navigationToDependentName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] IConventionForeignKeyBuilder IConventionForeignKeyBuilder.IsRequired(bool? required, bool fromDataAnnotation) => IsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// + /// [DebuggerStepThrough] bool IConventionForeignKeyBuilder.CanSetIsRequired(bool? required, bool fromDataAnnotation) => CanSetIsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + IConventionForeignKeyBuilder IConventionForeignKeyBuilder.IsRequiredDependent(bool? required, bool fromDataAnnotation) + => IsRequiredDependent(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionForeignKeyBuilder.CanSetIsRequiredDependent(bool? required, bool fromDataAnnotation) + => CanSetIsRequiredDependent(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// 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/Metadata/Internal/InternalNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs index fd270bf0a08..f0c800d638d 100644 --- a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs @@ -1,9 +1,11 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Diagnostics; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Microsoft.EntityFrameworkCore.Metadata.Internal @@ -87,6 +89,75 @@ public virtual InternalNavigationBuilder AutoInclude(bool? autoInclude, Configur return null; } + /// + /// 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. + /// + public virtual bool CanSetIsRequired(bool? required, ConfigurationSource configurationSource) + { + var foreignKey = Metadata.ForeignKey; + return foreignKey.IsUnique + ? foreignKey.GetPrincipalEndConfigurationSource() == null + ? false + : Metadata.IsOnDependent + ? foreignKey.Builder.CanSetIsRequired(required, configurationSource) + : foreignKey.Builder.CanSetIsRequiredDependent(required, configurationSource) + : Metadata.IsOnDependent + ? foreignKey.Builder.CanSetIsRequired(required, configurationSource) + : false; + } + + /// + /// 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. + /// + public virtual InternalNavigationBuilder IsRequired(bool? required, ConfigurationSource configurationSource) + { + if (configurationSource == ConfigurationSource.Explicit + || CanSetIsRequired(required, configurationSource)) + { + var foreignKey = Metadata.ForeignKey; + if (foreignKey.IsUnique) + { + if (foreignKey.GetPrincipalEndConfigurationSource() == null) + { + throw new InvalidOperationException( + CoreStrings.AmbiguousEndRequiredDependentNavigation( + Metadata.DeclaringEntityType.DisplayName(), + Metadata.Name, + foreignKey.Properties.Format())); + } + + return Metadata.IsOnDependent + ? foreignKey.Builder.IsRequired(required, configurationSource) + .Metadata.DependentToPrincipal.Builder + : foreignKey.Builder.IsRequiredDependent(required, configurationSource) + .Metadata.PrincipalToDependent.Builder; + } + else + { + if (Metadata.IsOnDependent) + { + return foreignKey.Builder.IsRequired(required, configurationSource) + .Metadata.DependentToPrincipal.Builder; + } + else + { + throw new InvalidOperationException( + CoreStrings.NonUniqueRequiredDependentNavigation( + foreignKey.PrincipalEntityType.DisplayName(), Metadata.Name)); + } + } + } + + + return null; + } + IConventionPropertyBase IConventionPropertyBaseBuilder.Metadata { [DebuggerStepThrough] @@ -170,5 +241,15 @@ bool IConventionNavigationBuilder.CanSetAutoInclude(bool? autoInclude, bool from [DebuggerStepThrough] IConventionNavigationBuilder IConventionNavigationBuilder.AutoInclude(bool? autoInclude, bool fromDataAnnotation) => AutoInclude(autoInclude, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionNavigationBuilder.CanSetIsRequired(bool? required, bool fromDataAnnotation) + => CanSetIsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionNavigationBuilder IConventionNavigationBuilder.IsRequired(bool? required, bool fromDataAnnotation) + => IsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index d840d45b51d..09c04b4f2d0 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1329,7 +1329,7 @@ public static string InvalidSetType([CanBeNull] object typeName) typeName); /// - /// The child/dependent side could not be determined for the one-to-one relationship between '{dependentToPrincipalNavigationSpecification}' and '{principalToDependentNavigationSpecification}'. To identify the child/dependent side of the relationship, configure the foreign key property. If these navigations should not be part of the same relationship configure them without specifying the inverse. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + /// The dependent side could not be determined for the one-to-one relationship between '{dependentToPrincipalNavigationSpecification}' and '{principalToDependentNavigationSpecification}'. To identify the dependent side of the relationship, configure the foreign key property. If these navigations should not be part of the same relationship configure them without specifying the inverse. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. /// public static string AmbiguousOneToOneRelationship([CanBeNull] object dependentToPrincipalNavigationSpecification, [CanBeNull] object principalToDependentNavigationSpecification) => string.Format( @@ -2748,6 +2748,38 @@ public static string SharedTypeDerivedType([CanBeNull] object entityType) GetString("SharedTypeDerivedType", nameof(entityType)), entityType); + /// + /// '{entityType}.{navigation}' cannot be configured as required since the dependent side of the underlying foreign key {foreignKey} cannot be determined. To identify the dependent side of the relationship, configure the foreign key property. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + /// + public static string AmbiguousEndRequiredDependentNavigation([CanBeNull] object entityType, [CanBeNull] object navigation, [CanBeNull] object foreignKey) + => string.Format( + GetString("AmbiguousEndRequiredDependentNavigation", nameof(entityType), nameof(navigation), nameof(foreignKey)), + entityType, navigation, foreignKey); + + /// + /// The foreign key {foreignKey} on the entity type '{declaringEntityType}' cannot have a required dependent end since it is not unique. + /// + public static string NonUniqueRequiredDependentForeignKey([CanBeNull] object foreignKey, [CanBeNull] object declaringEntityType) + => string.Format( + GetString("NonUniqueRequiredDependentForeignKey", nameof(foreignKey), nameof(declaringEntityType)), + foreignKey, declaringEntityType); + + /// + /// '{principalEntityType}.{principalNavigation}' cannot be configured as required since it contains a collection. + /// + public static string NonUniqueRequiredDependentNavigation([CanBeNull] object principalEntityType, [CanBeNull] object principalNavigation) + => string.Format( + GetString("NonUniqueRequiredDependentNavigation", nameof(principalEntityType), nameof(principalNavigation)), + principalEntityType, principalNavigation); + + /// + /// '{entityType}.{navigation}' cannot be configured as required since it represents a skip navigation. + /// + public static string RequiredSkipNavigation([CanBeNull] object entityType, [CanBeNull] object navigation) + => string.Format( + GetString("RequiredSkipNavigation", nameof(entityType), nameof(navigation)), + entityType, navigation); + /// /// The navigation '{navigation}' on '{entityType}' must be configured using Fluent API with an explicit name for the target shared type entity type or excluded by calling 'EntityTypeBuilder.Ignore'. /// @@ -2756,6 +2788,22 @@ public static string NonconfiguredNavigationToSharedType([CanBeNull] object navi GetString("NonconfiguredNavigationToSharedType", nameof(navigation), nameof(entityType)), navigation, entityType); + /// + /// The foreign key {foreignKey} on entity type '{entityType}' cannot be configured as required since the dependent side cannot be determined. To identify the dependent side of the relationship, configure the foreign key property. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + /// + public static string AmbiguousEndRequired([CanBeNull] object foreignKey, [CanBeNull] object entityType) + => string.Format( + GetString("AmbiguousEndRequired", nameof(foreignKey), nameof(entityType)), + foreignKey, entityType); + + /// + /// The foreign key {foreignKey} on entity type '{entityType}' cannot be configured as having a required dependent since the dependent side cannot be determined. To identify the dependent side of the relationship, configure the foreign key property. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + /// + public static string AmbiguousEndRequiredDependent([CanBeNull] object foreignKey, [CanBeNull] object entityType) + => string.Format( + GetString("AmbiguousEndRequiredDependent", nameof(foreignKey), nameof(entityType)), + foreignKey, entityType); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); @@ -3963,6 +4011,7 @@ public static EventDefinition LogNonNullableInverted([NotNull] I /// /// The RequiredAttribute on '{principalEntityType}.{principalNavigation}' was ignored because there is also a RequiredAttribute on '{dependentEntityType}.{dependentNavigation}'. RequiredAttribute should only be specified on the dependent side of the relationship. /// + [Obsolete] public static EventDefinition LogRequiredAttributeOnBothNavigations([NotNull] IDiagnosticsLogger logger) { var definition = ((LoggingDefinitions)logger.Definitions).LogRequiredAttributeOnBothNavigations; @@ -3987,6 +4036,7 @@ public static EventDefinition LogRequiredAttribu /// /// '{principalEntityType}.{principalNavigation}' may still be null at runtime despite being declared as non-nullable since only the navigation to principal '{dependentEntityType}.{dependentNavigation}' can be configured as required. /// + [Obsolete] public static EventDefinition LogNonNullableReferenceOnBothNavigations([NotNull] IDiagnosticsLogger logger) { var definition = ((LoggingDefinitions)logger.Definitions).LogNonNullableReferenceOnBothNavigations; @@ -4299,6 +4349,7 @@ public static EventDefinition LogRequiredAttributeOnCollection([ /// /// The RequiredAttribute on '{principalEntityType}.{principalNavigation}' is invalid. RequiredAttribute should only be specified on the navigation pointing to the principal side of the relationship. To change the dependent side configure the foreign key properties. /// + [Obsolete] public static EventDefinition LogRequiredAttributeOnDependent([NotNull] IDiagnosticsLogger logger) { var definition = ((LoggingDefinitions)logger.Definitions).LogRequiredAttributeOnDependent; @@ -4323,6 +4374,7 @@ public static EventDefinition LogRequiredAttributeOnDependent([N /// /// '{principalEntityType}.{principalNavigation}' may still be null at runtime despite being declared as non-nullable since only the navigation to principal can be configured as required. /// + [Obsolete] public static EventDefinition LogNonNullableReferenceOnDependent([NotNull] IDiagnosticsLogger logger) { var definition = ((LoggingDefinitions)logger.Definitions).LogNonNullableReferenceOnDependent; @@ -4415,5 +4467,29 @@ public static EventDefinition LogPossibleIncorrectRequiredNaviga return (EventDefinition)definition; } + + /// + /// The RequiredAttribute on '{principalEntityType}.{principalNavigation}' was ignored because it is a skip navigation. Instead configure the underlying foreign keys. + /// + public static EventDefinition LogRequiredAttributeOnSkipNavigation([NotNull] IDiagnosticsLogger logger) + { + var definition = ((LoggingDefinitions)logger.Definitions).LogRequiredAttributeOnSkipNavigation; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((LoggingDefinitions)logger.Definitions).LogRequiredAttributeOnSkipNavigation, + () => new EventDefinition( + logger.Options, + CoreEventId.RequiredAttributeOnSkipNavigation, + LogLevel.Debug, + "CoreEventId.RequiredAttributeOnSkipNavigation", + level => LoggerMessage.Define( + level, + CoreEventId.RequiredAttributeOnSkipNavigation, + _resourceManager.GetString("LogRequiredAttributeOnSkipNavigation")))); + } + + return (EventDefinition)definition; + } } } diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index dd57e58fe7b..96e23e0ef4d 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -761,7 +761,7 @@ Cannot create a DbSet for '{typeName}' because this type is not included in the model for the context. - The child/dependent side could not be determined for the one-to-one relationship between '{dependentToPrincipalNavigationSpecification}' and '{principalToDependentNavigationSpecification}'. To identify the child/dependent side of the relationship, configure the foreign key property. If these navigations should not be part of the same relationship configure them without specifying the inverse. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + The dependent side could not be determined for the one-to-one relationship between '{dependentToPrincipalNavigationSpecification}' and '{principalToDependentNavigationSpecification}'. To identify the dependent side of the relationship, configure the foreign key property. If these navigations should not be part of the same relationship configure them without specifying the inverse. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. Both relationships between '{firstDependentToPrincipalNavigationSpecification}' and '{firstPrincipalToDependentNavigationSpecification}' and between '{secondDependentToPrincipalNavigationSpecification}' and '{secondPrincipalToDependentNavigationSpecification}' could use {foreignKeyProperties} as the foreign key. To resolve this configure the foreign key properties explicitly on at least one of the relationships. @@ -1025,11 +1025,11 @@ The RequiredAttribute on '{principalEntityType}.{principalNavigation}' was ignored because there is also a RequiredAttribute on '{dependentEntityType}.{dependentNavigation}'. RequiredAttribute should only be specified on the dependent side of the relationship. - Debug CoreEventId.RequiredAttributeOnBothNavigations string string string string + Obsolete Debug CoreEventId.RequiredAttributeOnBothNavigations string string string string '{principalEntityType}.{principalNavigation}' may still be null at runtime despite being declared as non-nullable since only the navigation to principal '{dependentEntityType}.{dependentNavigation}' can be configured as required. - Debug CoreEventId.NonNullableReferenceOnBothNavigations string string string string + Obsolete Debug CoreEventId.NonNullableReferenceOnBothNavigations string string string string Navigations '{dependentEntityType}.{dependentNavigation}' and '{principalEntityType}.{principalNavigation}' were separated into two relationships as ForeignKeyAttribute was specified on navigations on both sides. @@ -1222,11 +1222,11 @@ The RequiredAttribute on '{principalEntityType}.{principalNavigation}' is invalid. RequiredAttribute should only be specified on the navigation pointing to the principal side of the relationship. To change the dependent side configure the foreign key properties. - Error CoreEventId.RequiredAttributeOnDependent string string + Obsolete Error CoreEventId.RequiredAttributeOnDependent string string '{principalEntityType}.{principalNavigation}' may still be null at runtime despite being declared as non-nullable since only the navigation to principal can be configured as required. - Debug CoreEventId.NonNullableReferenceOnDependent string string + Obsolete Debug CoreEventId.NonNullableReferenceOnDependent string string 'AddEntityFramework*' was called on the service provider, but 'UseInternalServiceProvider' wasn't called in the DbContext options configuration. Remove the 'AddEntityFramework*' call as in most cases it's not needed and might cause conflicts with other products and services registered in the same service provider. @@ -1444,7 +1444,29 @@ The shared type entity type '{entityType}' cannot have a base type. + + '{entityType}.{navigation}' cannot be configured as required since the dependent side of the underlying foreign key {foreignKey} cannot be determined. To identify the dependent side of the relationship, configure the foreign key property. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + + + The foreign key {foreignKey} on the entity type '{declaringEntityType}' cannot have a required dependent end since it is not unique. + + + '{principalEntityType}.{principalNavigation}' cannot be configured as required since it contains a collection. + + + '{entityType}.{navigation}' cannot be configured as required since it represents a skip navigation. + The navigation '{navigation}' on '{entityType}' must be configured using Fluent API with an explicit name for the target shared type entity type or excluded by calling 'EntityTypeBuilder.Ignore'. + + The foreign key {foreignKey} on entity type '{entityType}' cannot be configured as required since the dependent side cannot be determined. To identify the dependent side of the relationship, configure the foreign key property. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + + + The foreign key {foreignKey} on entity type '{entityType}' cannot be configured as having a required dependent since the dependent side cannot be determined. To identify the dependent side of the relationship, configure the foreign key property. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details. + + + The RequiredAttribute on '{principalEntityType}.{principalNavigation}' was ignored because it is a skip navigation. Instead configure the underlying foreign keys. + Debug CoreEventId.RequiredAttributeOnSkipNavigation string string + \ No newline at end of file diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 03cebd3753f..1d4f434bebf 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -265,7 +265,7 @@ public virtual void Detects_incompatible_primary_keys_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().HasKey(a => a.Id).HasName("Key"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().ToTable("Table"); @@ -281,7 +281,7 @@ public virtual void Detects_incompatible_comments_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table").HasComment("My comment"); modelBuilder.Entity().ToTable("Table").HasComment("my comment"); @@ -296,7 +296,7 @@ public virtual void Passes_on_null_comments() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table").HasComment("My comment"); modelBuilder.Entity().ToTable("Table"); @@ -308,7 +308,7 @@ public virtual void Detects_incompatible_primary_key_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.Id).ValueGeneratedNever().HasColumnName("Key"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(a => a.Id).ValueGeneratedNever().HasColumnName(nameof(B.Id)); @@ -324,7 +324,7 @@ public virtual void Passes_on_not_configured_shared_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(b => b.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); @@ -338,7 +338,7 @@ public virtual void Detects_incompatible_shared_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(b => b.P0).HasColumnName(nameof(A.P0)).HasColumnType("default_int_mapping"); @@ -355,9 +355,9 @@ public virtual void Detects_multiple_shared_table_roots() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table"); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().HasBaseType((string)null).ToTable("Table"); modelBuilder.Entity().ToTable("Table"); @@ -371,9 +371,9 @@ public virtual void Detects_shared_table_root_cycle() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table"); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table"); VerifyError( @@ -386,7 +386,7 @@ public virtual void Passes_for_compatible_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity( @@ -410,7 +410,7 @@ public virtual void Passes_for_compatible_excluded_shared_table_inverted() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table", t => t.IsExcludedFromMigrations()); modelBuilder.Entity().ToTable("Table", t => t.IsExcludedFromMigrations()); @@ -452,7 +452,7 @@ public virtual void Detect_partially_excluded_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table", t => t.IsExcludedFromMigrations()); modelBuilder.Entity().ToTable("Table"); diff --git a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs index cfb8d37ae7a..127f700d83a 100644 --- a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs +++ b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs @@ -2209,19 +2209,6 @@ public virtual void RequiredAttribute_for_navigation_throws_while_inserting_null }); } - [ConditionalFact] - public virtual void RequiredAttribute_throws_when_specified_on_nav_to_dependent_per_convention() - { - var modelBuilder = CreateModelBuilder(); - - Assert.Equal(CoreStrings.WarningAsErrorTemplate( - CoreEventId.RequiredAttributeOnDependent.ToString(), - CoreResources.LogRequiredAttributeOnDependent(new TestLogger()) - .GenerateMessage(nameof(AdditionalBookDetails), nameof(AdditionalBookDetails.BookDetails)), - "CoreEventId.RequiredAttributeOnDependent"), - Assert.Throws(() => modelBuilder.Entity()).Message); - } - [ConditionalFact] public virtual void RequiredAttribute_for_property_throws_while_inserting_null_value() { diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 11b0fcfc7ad..4508a22f80a 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -37,7 +37,7 @@ public override void Detects_incompatible_shared_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); @@ -319,7 +319,7 @@ public virtual void Detects_incompatible_memory_optimized_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table").IsMemoryOptimized(); @@ -335,7 +335,7 @@ public virtual void Detects_incompatible_non_clustered_shared_key() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table") .HasKey(a => a.Id).IsClustered(); diff --git a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs index 734272f54e1..25f447077b2 100644 --- a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs +++ b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs @@ -60,7 +60,7 @@ public override void Detects_incompatible_shared_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 5fb951ff99f..9e7cd2f83fc 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -315,9 +315,9 @@ public virtual void Detects_relationship_cycle() modelBuilder.Entity(); modelBuilder.Entity(); modelBuilder.Entity().HasBaseType((string)null); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); - modelBuilder.Entity().HasOne().WithOne().IsRequired().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); VerifyError( CoreStrings.IdentifyingRelationshipCycle(nameof(A)), diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index d6ce1b4835f..2bb7d5156a4 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Xunit; @@ -1400,6 +1399,108 @@ public void ProcessForeignKeyRequirednessChanged( } } + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnForeignKeyDependentRequirednessChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new ForeignKeyDependentRequirednessChangedConvention(terminate: false); + var convention2 = new ForeignKeyDependentRequirednessChangedConvention(terminate: true); + var convention3 = new ForeignKeyDependentRequirednessChangedConvention(terminate: false); + conventions.ForeignKeyDependentRequirednessChangedConventions.Add(convention1); + conventions.ForeignKeyDependentRequirednessChangedConventions.Add(convention2); + conventions.ForeignKeyDependentRequirednessChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var principalEntityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention); + var dependentEntityBuilder = builder.Entity(typeof(OrderDetails), ConfigurationSource.Convention); + var foreignKey = dependentEntityBuilder.HasRelationship(principalEntityBuilder.Metadata, ConfigurationSource.Convention) + .IsUnique(true, ConfigurationSource.Convention) + .HasEntityTypes(principalEntityBuilder.Metadata, dependentEntityBuilder.Metadata, ConfigurationSource.Convention) + .Metadata; + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + if (useBuilder) + { + foreignKey.Builder.IsRequiredDependent(true, ConfigurationSource.Convention); + } + else + { + foreignKey.IsRequiredDependent = true; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { true }, convention1.Calls); + Assert.Equal(new[] { true }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + foreignKey.Builder.IsRequiredDependent(true, ConfigurationSource.Convention); + } + else + { + foreignKey.IsRequiredDependent = true; + } + + Assert.Equal(new[] { true }, convention1.Calls); + Assert.Equal(new[] { true }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + foreignKey.Builder.IsRequiredDependent(false, ConfigurationSource.Convention); + } + else + { + foreignKey.IsRequiredDependent = false; + } + + Assert.Equal(new[] { true, false }, convention1.Calls); + Assert.Equal(new[] { true, false }, convention2.Calls); + Assert.Empty(convention3.Calls); + + Assert.Same( + foreignKey, + dependentEntityBuilder.Metadata.RemoveForeignKey( + foreignKey.Properties, foreignKey.PrincipalKey, foreignKey.PrincipalEntityType)); + } + + private class ForeignKeyDependentRequirednessChangedConvention : IForeignKeyDependentRequirednessChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public ForeignKeyDependentRequirednessChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessForeignKeyDependentRequirednessChanged( + IConventionForeignKeyBuilder relationshipBuilder, IConventionContext context) + { + Assert.NotNull(relationshipBuilder.Metadata.Builder); + + Calls.Add(relationshipBuilder.Metadata.IsRequiredDependent); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] diff --git a/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs index 3d5845859ac..633aaa7163b 100644 --- a/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs @@ -104,7 +104,8 @@ public void RequiredAttribute_overrides_configuration_from_convention_source() principalEntityTypeBuilder.Metadata, nameof(Post.Blog), nameof(Blog.Posts), - ConfigurationSource.Convention); + ConfigurationSource.Convention, + setTargetAsPrincipal: true); var navigation = dependentEntityTypeBuilder.Metadata.FindNavigation(nameof(Post.Blog)); @@ -172,32 +173,6 @@ public void RequiredAttribute_does_not_set_is_required_for_collection_navigation nameof(Principal), nameof(Principal.Dependents)), logEntry.Message); } - [ConditionalFact] - public void RequiredAttribute_throws_for_navigation_to_dependent() - { - var dependentEntityTypeBuilder = CreateInternalEntityTypeBuilder(); - var principalEntityTypeBuilder = - dependentEntityTypeBuilder.ModelBuilder.Entity(typeof(Principal), ConfigurationSource.Convention); - - var relationshipBuilder = dependentEntityTypeBuilder.HasRelationship( - principalEntityTypeBuilder.Metadata, - nameof(Dependent.Principal), - nameof(Principal.Dependent), - ConfigurationSource.Convention) - .HasEntityTypes - (principalEntityTypeBuilder.Metadata, dependentEntityTypeBuilder.Metadata, ConfigurationSource.Explicit); - - var navigation = principalEntityTypeBuilder.Metadata.FindNavigation(nameof(Principal.Dependent)); - - Assert.False(relationshipBuilder.Metadata.IsRequired); - - Assert.Equal(CoreStrings.WarningAsErrorTemplate( - CoreEventId.RequiredAttributeOnDependent.ToString(), - CoreResources.LogRequiredAttributeOnDependent(new TestLogger()) - .GenerateMessage(nameof(Principal), nameof(Principal.Dependent)), "CoreEventId.RequiredAttributeOnDependent"), - Assert.Throws(() => RunRequiredNavigationAttributeConvention(relationshipBuilder, navigation)).Message); - } - [ConditionalFact] public void RequiredAttribute_does_nothing_when_principal_end_is_ambiguous() { @@ -237,22 +212,35 @@ public void RequiredAttribute_sets_is_required_with_conventional_builder() } [ConditionalFact] - public void RequiredAttribute_can_be_specified_on_both_navigations() - { - var modelBuilder = CreateModelBuilder(); - var model = (Model)modelBuilder.Model; - modelBuilder.Entity(); - modelBuilder.Entity().HasOne(b => b.Blog).WithOne(b => b.BlogDetails); - - Assert.True( - model.FindEntityType(typeof(BlogDetails)).GetForeignKeys() - .Single(fk => fk.PrincipalEntityType?.ClrType == typeof(Blog)).IsRequired); + public void RequiredAttribute_does_not_configure_skip_navigations() + { + var postEntityTypeBuilder = CreateInternalEntityTypeBuilder(); + var blogEntityTypeBuilder = postEntityTypeBuilder.ModelBuilder.Entity( + typeof(Blog), ConfigurationSource.Convention); + + var navigationBuilder = postEntityTypeBuilder.HasSkipNavigation( + new MemberIdentity(nameof(Post.Blogs)), + blogEntityTypeBuilder.Metadata, + new MemberIdentity(nameof(Blog.Posts)), + ConfigurationSource.Convention, + collections: true, + onDependent: false); + + var convention = new RequiredNavigationAttributeConvention(CreateDependencies(CreateLogger())); + convention.ProcessSkipNavigationAdded( + navigationBuilder, + new ConventionContext( + postEntityTypeBuilder.Metadata.Model.ConventionDispatcher)); var logEntry = ListLoggerFactory.Log.Single(); Assert.Equal(LogLevel.Debug, logEntry.Level); Assert.Equal( - CoreResources.LogRequiredAttributeOnBothNavigations(new TestLogger()).GenerateMessage( - nameof(Blog), nameof(Blog.BlogDetails), nameof(BlogDetails), nameof(BlogDetails.Blog)), logEntry.Message); + CoreResources.LogRequiredAttributeOnSkipNavigation(new TestLogger()).GenerateMessage( + nameof(Post), nameof(Post.Blogs)), logEntry.Message); + + Validate(postEntityTypeBuilder); + + Assert.Empty(ListLoggerFactory.Log); } #endregion @@ -1095,6 +1083,7 @@ private class Blog private class BlogDetails { public int Id { get; set; } + public int BlogId { get; set; } [Required] public Blog Blog { get; set; } @@ -1109,6 +1098,10 @@ private class Post [Required] public Blog Blog { get; set; } + + [NotMapped] + [Required] + public ICollection Blogs { get; set; } } private class Principal diff --git a/test/EFCore.Tests/Metadata/Conventions/NonNullableNavigationConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/NonNullableNavigationConventionTest.cs index 9b415f171c3..5060d62428a 100644 --- a/test/EFCore.Tests/Metadata/Conventions/NonNullableNavigationConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/NonNullableNavigationConventionTest.cs @@ -119,46 +119,6 @@ public void Non_nullability_does_not_set_is_required_for_navigation_to_dependent Assert.False(relationshipBuilder.Metadata.IsRequired); } - [ConditionalFact] - public void Non_nullability_logs_when_navigation_to_dependent() - { - var dependentEntityTypeBuilder = CreateInternalEntityTypeBuilder(); - var principalEntityTypeBuilder = - dependentEntityTypeBuilder.ModelBuilder.Entity(typeof(Principal), ConfigurationSource.Convention); - - var relationshipBuilder = dependentEntityTypeBuilder.HasRelationship( - principalEntityTypeBuilder.Metadata, - nameof(Dependent.Principal), - nameof(Principal.Dependent), - ConfigurationSource.Convention); - - Assert.Equal(nameof(Dependent), relationshipBuilder.Metadata.DeclaringEntityType.DisplayName()); - Assert.False(relationshipBuilder.Metadata.IsRequired); - - var navigation = principalEntityTypeBuilder.Metadata.FindNavigation(nameof(Principal.Dependent)); - - navigation = RunConvention(relationshipBuilder, navigation); - - Assert.Equal(nameof(Dependent), navigation.ForeignKey.DeclaringEntityType.DisplayName()); - Assert.False(navigation.ForeignKey.IsRequired); - Assert.Empty(ListLoggerFactory.Log); - - relationshipBuilder.HasEntityTypes( - relationshipBuilder.Metadata.PrincipalEntityType, - relationshipBuilder.Metadata.DeclaringEntityType, - ConfigurationSource.Convention); - - navigation = RunConvention(relationshipBuilder, navigation); - - Assert.Equal(nameof(Dependent), navigation.ForeignKey.DeclaringEntityType.DisplayName()); - Assert.False(navigation.ForeignKey.IsRequired); - var logEntry = ListLoggerFactory.Log.Single(); - Assert.Equal(LogLevel.Debug, logEntry.Level); - Assert.Equal( - CoreResources.LogNonNullableReferenceOnDependent(new TestLogger()).GenerateMessage( - nameof(Dependent.Principal), nameof(Dependent)), logEntry.Message); - } - [ConditionalFact] public void Non_nullability_sets_is_required_with_conventional_builder() { @@ -171,21 +131,6 @@ public void Non_nullability_sets_is_required_with_conventional_builder() .IsRequired); } - [ConditionalFact] - public void Non_nullability_can_be_specified_on_both_navigations() - { - var modelBuilder = CreateModelBuilder(); - var model = (Model)modelBuilder.Model; - modelBuilder.Entity().HasOne(b => b.Blog).WithOne(b => b.BlogDetails); - - var logEntry = ListLoggerFactory.Log.Single(); - - Assert.Equal(LogLevel.Debug, logEntry.Level); - Assert.Equal( - CoreResources.LogNonNullableReferenceOnBothNavigations(new TestLogger()).GenerateMessage( - nameof(Blog), nameof(Blog.BlogDetails), nameof(BlogDetails), nameof(BlogDetails.Blog)), logEntry.Message); - } - private Navigation RunConvention(InternalForeignKeyBuilder relationshipBuilder, Navigation navigation) { var context = new ConventionContext( @@ -259,6 +204,7 @@ private class BlogDetails { public int Id { get; set; } + public int BlogId { get; set; } public Blog Blog { get; set; } private Post Post { get; set; } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs index 692b3a50eb3..bfead8558da 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs @@ -34,7 +34,7 @@ public void Navigation_is_returned_if_it_implements_IClrCollectionAccessor() { var navigation = new FakeNavigation(); - var fk = new FakeForeignKey() { PrincipalToDependent = navigation }; + var fk = new ForeignKeyTest.FakeForeignKey() { PrincipalToDependent = navigation }; navigation.ForeignKey = fk; navigation.PropertyInfo = MyEntity.AsICollectionProperty; @@ -61,23 +61,6 @@ private class FakeNavigation : INavigation, IClrCollectionAccessor public Type CollectionType { get; } } - private class FakeForeignKey : IForeignKey - { - public object this[string name] => throw new NotImplementedException(); - public IAnnotation FindAnnotation(string name) => throw new NotImplementedException(); - public IEnumerable GetAnnotations() => throw new NotImplementedException(); - public IEntityType DeclaringEntityType { get; } - public IReadOnlyList Properties { get; } - public IEntityType PrincipalEntityType { get; } - public IKey PrincipalKey { get; } - public INavigation DependentToPrincipal { get; set; } - public INavigation PrincipalToDependent { get; set; } - public bool IsUnique { get; } - public bool IsRequired { get; } - public bool IsOwnership { get; } - public DeleteBehavior DeleteBehavior { get; } - } - [ConditionalFact] public void Delegate_accessor_is_returned_for_IEnumerable_navigation() { diff --git a/test/EFCore.Tests/Metadata/Internal/ForeignKeyTest.cs b/test/EFCore.Tests/Metadata/Internal/ForeignKeyTest.cs index 89f246d75c5..6375d48c916 100644 --- a/test/EFCore.Tests/Metadata/Internal/ForeignKeyTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ForeignKeyTest.cs @@ -23,7 +23,7 @@ public void Use_of_custom_IForeignKey_throws() Assert.Throws(() => foreignKey.AsForeignKey()).Message); } - private class FakeForeignKey : IForeignKey + public class FakeForeignKey : IForeignKey { public object this[string name] => throw new NotImplementedException(); public IAnnotation FindAnnotation(string name) => throw new NotImplementedException(); @@ -32,12 +32,14 @@ private class FakeForeignKey : IForeignKey public IReadOnlyList Properties { get; } public IEntityType PrincipalEntityType { get; } public IKey PrincipalKey { get; } - public INavigation DependentToPrincipal { get; } - public INavigation PrincipalToDependent { get; } + public INavigation DependentToPrincipal { get; set; } + public INavigation PrincipalToDependent { get; set; } public bool IsUnique { get; } public bool IsRequired { get; } + public bool IsRequiredDependent { get; } public bool IsOwnership { get; } public DeleteBehavior DeleteBehavior { get; } + } [ConditionalFact] @@ -240,6 +242,18 @@ public void Setting_IsRequired_to_false_will_not_configure_FK_properties_as_null Assert.False(dependentProp2.IsNullable); } + [ConditionalFact] + public void IsRequiredDependent_throws_for_incompatible_uniqueness() + { + var foreignKey = CreateOneToManyFK(); + + Assert.Equal( + CoreStrings.NonUniqueRequiredDependentForeignKey( + "{'Id'}", + nameof(OneToManyDependent)), + Assert.Throws(() => foreignKey.IsRequiredDependent = true).Message); + } + private IMutableForeignKey CreateOneToManyFK() { var model = CreateModel(); @@ -366,9 +380,37 @@ public void Throws_when_setting_navigation_to_dependent_on_wrong_FK() nameof(OneToManyDependent), foreignKey2.Properties.Format(), foreignKey1.Properties.Format()), - Assert.Throws( - () - => foreignKey2.SetDependentToPrincipal(OneToManyDependent.DeceptionProperty)).Message); + Assert.Throws(() + => foreignKey2.SetDependentToPrincipal(OneToManyDependent.DeceptionProperty)).Message); + } + + [ConditionalFact] + public void IsUnique_throws_for_incompatible_navigation() + { + var foreignKey = CreateOneToManyFK(); + foreignKey.SetPrincipalToDependent(nameof(OneToManyPrincipal.Deception)); + + Assert.Equal( + CoreStrings.UnableToSetIsUnique( + "True", + nameof(OneToManyPrincipal.Deception), + nameof(OneToManyPrincipal)), + Assert.Throws(() => foreignKey.IsUnique = true).Message); + } + + [ConditionalFact] + public void IsUnique_throws_for_incompatible_required_dependent() + { + var foreignKey = CreateOneToManyFK(); + foreignKey.SetPrincipalToDependent((string)null); + foreignKey.IsUnique = true; + foreignKey.IsRequiredDependent = true; + + Assert.Equal( + CoreStrings.NonUniqueRequiredDependentForeignKey( + "{'Id'}", + nameof(OneToManyDependent)), + Assert.Throws(() => foreignKey.IsUnique = false).Message); } private IMutableForeignKey CreateSelfRefFK(bool useAltKey = false) diff --git a/test/EFCore.Tests/Metadata/Internal/InternalRelationshipBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs similarity index 97% rename from test/EFCore.Tests/Metadata/Internal/InternalRelationshipBuilderTest.cs rename to test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs index 43778c8229a..34764013bd9 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalRelationshipBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs @@ -15,7 +15,7 @@ // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.Metadata.Internal { - public class InternalRelationshipBuilderTest + public class InternalForeignKeyBuilderTest { [ConditionalFact] public void Facets_are_configured_with_the_specified_source() @@ -37,6 +37,7 @@ public void Facets_are_configured_with_the_specified_source() Assert.Null(fk.GetDependentToPrincipalConfigurationSource()); Assert.Null(fk.GetPrincipalToDependentConfigurationSource()); Assert.Null(fk.GetIsRequiredConfigurationSource()); + Assert.Null(fk.GetIsRequiredDependentConfigurationSource()); Assert.Null(fk.GetIsOwnershipConfigurationSource()); Assert.Null(fk.GetIsUniqueConfigurationSource()); Assert.Null(fk.GetDeleteBehaviorConfigurationSource()); @@ -51,6 +52,7 @@ public void Facets_are_configured_with_the_specified_source() ConfigurationSource.Explicit) .IsUnique(false, ConfigurationSource.Explicit) .IsRequired(false, ConfigurationSource.Explicit) + .IsRequiredDependent(false, ConfigurationSource.Explicit) .IsOwnership(false, ConfigurationSource.Explicit) .OnDelete(DeleteBehavior.Cascade, ConfigurationSource.Explicit) .HasForeignKey( @@ -69,6 +71,7 @@ public void Facets_are_configured_with_the_specified_source() new[] { shadowId.Name, Customer.UniqueProperty.Name }, ConfigurationSource.DataAnnotation)); Assert.Null(relationshipBuilder.IsUnique(true, ConfigurationSource.DataAnnotation)); Assert.Null(relationshipBuilder.IsRequired(true, ConfigurationSource.DataAnnotation)); + Assert.Null(relationshipBuilder.IsRequiredDependent(true, ConfigurationSource.DataAnnotation)); Assert.Null(relationshipBuilder.IsOwnership(true, ConfigurationSource.DataAnnotation)); Assert.Null(relationshipBuilder.OnDelete(DeleteBehavior.ClientSetNull, ConfigurationSource.DataAnnotation)); Assert.Null( @@ -105,6 +108,7 @@ public void Existing_facets_are_configured_explicitly() foreignKey.HasPrincipalToDependent(Customer.OrdersProperty, ConfigurationSource.Explicit); foreignKey.IsUnique = false; foreignKey.IsRequired = false; + foreignKey.IsRequiredDependent = false; foreignKey.IsOwnership = false; foreignKey.DeleteBehavior = DeleteBehavior.Cascade; @@ -116,6 +120,7 @@ public void Existing_facets_are_configured_explicitly() Assert.Equal(ConfigurationSource.Explicit, foreignKey.GetPrincipalToDependentConfigurationSource()); Assert.Equal(ConfigurationSource.Explicit, foreignKey.GetIsUniqueConfigurationSource()); Assert.Equal(ConfigurationSource.Explicit, foreignKey.GetIsRequiredConfigurationSource()); + Assert.Equal(ConfigurationSource.Explicit, foreignKey.GetIsRequiredDependentConfigurationSource()); Assert.Equal(ConfigurationSource.Explicit, foreignKey.GetIsOwnershipConfigurationSource()); Assert.Equal(ConfigurationSource.Explicit, foreignKey.GetDeleteBehaviorConfigurationSource()); } @@ -536,6 +541,27 @@ public void Can_set_Required_false_on_non_nullable_properties() relationshipBuilder.Metadata.Properties.Select(p => p.Name)); } + [ConditionalFact] + public void Can_only_override_lower_or_equal_source_RequiredDependent() + { + var modelBuilder = CreateInternalModelBuilder(); + var customerEntityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + var orderEntityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Explicit); + + var relationshipBuilder = orderEntityBuilder.HasRelationship(customerEntityBuilder.Metadata, ConfigurationSource.Convention); + Assert.False(relationshipBuilder.Metadata.IsRequiredDependent); + + relationshipBuilder = relationshipBuilder.IsUnique(true, ConfigurationSource.Convention); + relationshipBuilder = relationshipBuilder.IsRequiredDependent(true, ConfigurationSource.Convention); + Assert.True(relationshipBuilder.Metadata.IsRequiredDependent); + + relationshipBuilder = relationshipBuilder.IsRequiredDependent(false, ConfigurationSource.DataAnnotation); + Assert.False(relationshipBuilder.Metadata.IsRequiredDependent); + + Assert.Null(relationshipBuilder.IsRequiredDependent(true, ConfigurationSource.Convention)); + Assert.False(relationshipBuilder.Metadata.IsRequiredDependent); + } + [ConditionalFact] public void Can_only_override_lower_or_equal_source_Ownership() { diff --git a/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs index 333c6a24a07..5f5246d4600 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalNavigationBuilderTest.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -156,6 +158,53 @@ public void Can_only_override_lower_or_equal_source_IsEagerLoaded() Assert.Null(metadata.GetIsEagerLoadedConfigurationSource()); } + [ConditionalFact] + public void Configuring_IsRequired_on_to_dependent_nonUnique_throws() + { + var builder = CreateInternalNavigationBuilder(); + + Assert.Equal( + CoreStrings.NonUniqueRequiredDependentNavigation(nameof(Order), nameof(Order.Details)), + Assert.Throws(() => builder.IsRequired(true, ConfigurationSource.Explicit)).Message); + } + + [ConditionalFact] + public void Can_configure_IsRequired_on_to_principal_nonUnique() + { + var builder = CreateInternalNavigationBuilder() + .Metadata.ForeignKey.Builder.HasNavigation(nameof(OrderDetails.Order), pointsToPrincipal: true, ConfigurationSource.Explicit) + .Metadata.DependentToPrincipal.Builder; + builder.IsRequired(true, ConfigurationSource.Explicit); + + Assert.True(builder.Metadata.ForeignKey.IsRequired); + } + + [ConditionalFact] + public void Can_configure_IsRequired_on_to_dependent_unique() + { + var foreignKey = CreateInternalNavigationBuilder() + .Metadata.ForeignKey; + foreignKey = foreignKey.Builder.HasNavigations(nameof(OrderDetails.Order), nameof(Order.SingleDetails), ConfigurationSource.Explicit) + .Metadata; + + foreignKey.PrincipalToDependent.Builder.IsRequired(true, ConfigurationSource.Explicit); + + Assert.True(foreignKey.IsRequiredDependent); + } + + [ConditionalFact] + public void Can_configure_IsRequired_on_to_principal_unique() + { + var foreignKey = CreateInternalNavigationBuilder() + .Metadata.ForeignKey; + foreignKey = foreignKey.Builder.HasNavigations(nameof(OrderDetails.Order), nameof(Order.SingleDetails), ConfigurationSource.Explicit) + .Metadata; + + foreignKey.PrincipalToDependent.Builder.IsRequired(true, ConfigurationSource.Explicit); + + Assert.True(foreignKey.IsRequiredDependent); + } + private InternalNavigationBuilder CreateInternalNavigationBuilder() { var modelBuilder = (InternalModelBuilder) @@ -163,7 +212,7 @@ private InternalNavigationBuilder CreateInternalNavigationBuilder() var orderEntityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Convention); var detailsEntityBuilder = modelBuilder.Entity(typeof(OrderDetails), ConfigurationSource.Convention); orderEntityBuilder - .HasRelationship(detailsEntityBuilder.Metadata, nameof(Order.Details), ConfigurationSource.Convention, targetIsPrincipal: false) + .HasRelationship(detailsEntityBuilder.Metadata, nameof(Order.Details), ConfigurationSource.DataAnnotation, targetIsPrincipal: false) .IsUnique(false, ConfigurationSource.Convention); var navigation = (Navigation)orderEntityBuilder.Navigation(nameof(Order.Details)); @@ -181,12 +230,14 @@ protected class Order private ICollection _details; private readonly ICollection _otherDetails = new List(); + public OrderDetails SingleDetails { get; set; } public ICollection Details { get => _details; set => _details = value; } } protected class OrderDetails { public int Id { get; set; } + public Order Order { get; set; } } } } diff --git a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs index 635bfbc714e..04e8825a359 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs @@ -376,6 +376,22 @@ public virtual void Navigation_properties_can_set_access_mode_using_navigation_n Assert.Equal(PropertyAccessMode.Property, dependent.FindSkipNavigation("ManyToManyPrincipals").GetPropertyAccessMode()); } + [ConditionalFact] + public virtual void IsRequired_throws() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .HasMany(n => n.Dependents) + .WithMany(n => n.ManyToManyPrincipals); + + Assert.Equal( + CoreStrings.RequiredSkipNavigation(nameof(ManyToManyNavPrincipal), nameof(ManyToManyNavPrincipal.Dependents)), + Assert.Throws(() => modelBuilder.Entity() + .Navigation("Dependents") + .IsRequired()).Message); + } + [ConditionalFact] public virtual void Can_use_shared_Type_as_join_entity() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index ff822b6f9ea..a07f40a5eb2 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -490,6 +490,15 @@ public override TestNavigationBuilder HasAnnotation(string annotation, object va public override TestNavigationBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) => new GenericTestNavigationBuilder(NavigationBuilder.UsePropertyAccessMode(propertyAccessMode)); + + public override TestNavigationBuilder HasField(string fieldName) + => new GenericTestNavigationBuilder(NavigationBuilder.HasField(fieldName)); + + public override TestNavigationBuilder AutoInclude(bool autoInclude = true) + => new GenericTestNavigationBuilder(NavigationBuilder.AutoInclude(autoInclude)); + + public override TestNavigationBuilder IsRequired(bool required = true) + => new GenericTestNavigationBuilder(NavigationBuilder.IsRequired(required)); } protected class diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index 62d1826f64b..a1b49c2fc4f 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -436,6 +436,15 @@ public override TestNavigationBuilder HasAnnotation(string annotation, object va public override TestNavigationBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) => new NonGenericTestNavigationBuilder(NavigationBuilder.UsePropertyAccessMode(propertyAccessMode)); + + public override TestNavigationBuilder HasField(string fieldName) + => new NonGenericTestNavigationBuilder(NavigationBuilder.HasField(fieldName)); + + public override TestNavigationBuilder AutoInclude(bool autoInclude = true) + => new NonGenericTestNavigationBuilder(NavigationBuilder.AutoInclude(autoInclude)); + + public override TestNavigationBuilder IsRequired(bool required = true) + => new NonGenericTestNavigationBuilder(NavigationBuilder.IsRequired(required)); } protected class NonGenericTestKeyBuilder : TestKeyBuilder, IInfrastructure diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 49a279b07d4..99949f1ec52 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -351,6 +351,9 @@ public abstract class TestNavigationBuilder { public abstract TestNavigationBuilder HasAnnotation(string annotation, object value); public abstract TestNavigationBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode); + public abstract TestNavigationBuilder HasField(string fieldName); + public abstract TestNavigationBuilder AutoInclude(bool autoInclude = true); + public abstract TestNavigationBuilder IsRequired(bool required = true); } public abstract class TestCollectionNavigationBuilder diff --git a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs index 30b264f69f5..66a2ea8ccce 100644 --- a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs @@ -1634,7 +1634,6 @@ public virtual void Principal_and_dependent_cannot_be_flipped_twice() public virtual void Principal_and_dependent_can_be_flipped_twice_separately() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; modelBuilder.Entity(); modelBuilder.Entity() .HasOne(e => e.Order).WithOne(e => e.Details) @@ -1646,10 +1645,17 @@ public virtual void Principal_and_dependent_can_be_flipped_twice_separately() .Entity().HasOne(e => e.Order).WithOne(e => e.Details) .HasForeignKey(e => e.OrderId); + modelBuilder + .Entity().Navigation(e => e.Order).IsRequired(); + modelBuilder + .Entity().Navigation(e => e.Details).IsRequired(); + modelBuilder .Entity().HasOne(e => e.Order).WithOne(e => e.Details) .HasPrincipalKey(e => e.OrderId); + var model = modelBuilder.FinalizeModel(); + var dependentType = model.FindEntityType(typeof(Order)); var principalType = model.FindEntityType(typeof(OrderDetails)); var fk = dependentType.GetForeignKeys().Single(); @@ -1662,6 +1668,9 @@ public virtual void Principal_and_dependent_can_be_flipped_twice_separately() Assert.Same(fk.PrincipalKey, principalType.GetKeys().First(k => !k.IsPrimaryKey())); Assert.Empty(dependentType.GetIndexes()); Assert.Empty(principalType.GetIndexes()); + Assert.True(fk.IsUnique); + Assert.True(fk.IsRequired); + Assert.True(fk.IsRequiredDependent); } [ConditionalFact] @@ -3230,15 +3239,20 @@ public virtual void Nullable_FK_can_be_made_required() modelBuilder .Entity() - .Ignore(e => e.Hobs); + .Ignore(e => e.Hobs) + .Property(e => e.HobId1).IsRequired(false); + + modelBuilder + .Entity() + .Property(e => e.HobId2).IsRequired(false); modelBuilder .Entity() .Ignore(e => e.Nobs) .HasOne(e => e.Nob).WithOne(e => e.Hob) - .IsRequired() .HasForeignKey( - e => new { e.HobId1, e.HobId2 }); + e => new { e.HobId1, e.HobId2 }) + .IsRequired(); modelBuilder.FinalizeModel(); @@ -3376,9 +3390,9 @@ public virtual void Unspecified_FK_can_be_made_required() modelBuilder .Entity().HasOne(e => e.Nob).WithOne(e => e.Hob) - .IsRequired() .HasPrincipalKey( - e => new { e.Id1, e.Id2 }); + e => new { e.Id1, e.Id2 }) + .IsRequired(); var fk = dependentType.GetForeignKeys().Single(); Assert.True(fk.IsRequired); @@ -3389,6 +3403,20 @@ public virtual void Unspecified_FK_can_be_made_required() AssertEqual(expectedDependentProperties, dependentType.GetProperties()); } + [ConditionalFact] + public virtual void Throws_if_ambiguous_FK_made_required() + { + var modelBuilder = HobNobBuilder(); + + Assert.Equal( + CoreStrings.AmbiguousEndRequired("{'NobId11', 'NobId21'}", typeof(Hob).Name), + Assert.Throws(() => + modelBuilder + .Entity() + .HasOne(e => e.Nob).WithOne(e => e.Hob) + .IsRequired()).Message); + } + [ConditionalFact] public virtual void Can_be_defined_before_the_PK_from_principal() { @@ -3722,7 +3750,7 @@ public virtual void Ignoring_properties_on_principal_resolves_ambiguity() b => b.Ignore(e => e.OneToOnePrincipalEntityId)); modelBuilder.Entity().HasOne(e => e.NavOneToOneDependentEntity) - .WithOne(e => e.NavOneToOnePrincipalEntity).IsRequired(); + .WithOne(e => e.NavOneToOnePrincipalEntity); modelBuilder.Entity( b =>