diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index e8b7eccba1f..7d852de2869 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -130,6 +130,16 @@ protected virtual void ValidateRelationships( CoreStrings.SkipNavigationNoInverse( skipNavigation.Name, skipNavigation.DeclaringEntityType.DisplayName())); } + + if (skipNavigation.IsShadowProperty()) + { + throw new InvalidOperationException( + CoreStrings.ShadowManyToManyNavigation( + skipNavigation.DeclaringEntityType.DisplayName(), + skipNavigation.Name, + skipNavigation.Inverse.DeclaringEntityType.DisplayName(), + skipNavigation.Inverse.Name)); + } } } } diff --git a/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs index 5e473fb4ab5..2e4043be2a2 100644 --- a/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs @@ -95,6 +95,15 @@ public virtual ReferenceCollectionBuilder WithOne( /// An object to further configure the relationship. public new virtual CollectionCollectionBuilder WithMany(string navigationName) { + if (Builder != null + && Builder.Metadata.PrincipalToDependent == null) + { + throw new InvalidOperationException( + CoreStrings.MissingInverseManyToManyNavigation( + Builder.Metadata.PrincipalEntityType.DisplayName(), + Builder.Metadata.DeclaringEntityType.DisplayName())); + } + var leftName = Builder?.Metadata.PrincipalToDependent!.Name; var collectionCollectionBuilder = new CollectionCollectionBuilder( diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index c7234d5386f..93ce7785d44 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -352,7 +352,7 @@ public static string ClashingNonOwnedDerivedEntityType(object? entityType, objec entityType, derivedType); /// - /// The entity type '{entityType}' cannot be configured as owned because it has already been configured as a non-owned. If you want to override previous configuration first remove the entity type from the model by calling 'Ignore'. See https://aka.ms/efcore-docs-owned for more information. + /// The entity type '{entityType}' cannot be configured as owned because it has already been configured as a non-owned. If you want to override previous configuration first remove the entity type from the model by calling 'Ignore'. See https://aka.ms/efcore-docs-owned for more information. /// public static string ClashingNonOwnedEntityType(object? entityType) => string.Format( @@ -1546,11 +1546,11 @@ public static string LiteralGenerationNotSupported(object? type) type); /// - /// The navigation '{entityType}.{navigation}' cannot be used for both sides of a many-to-many relationship. Many-to-many relationships must use different navigation properties for either end of the relationship. + /// The navigation '{entityType}.{navigation}' cannot be used for both sides of a many-to-many relationship. Many-to-many relationships must use two distinct navigation properties. /// public static string ManyToManyOneNav(object? entityType, object? navigation) => string.Format( - GetString("ManyToManyOneNav", "entityType", "navigation"), + GetString("ManyToManyOneNav", nameof(entityType), nameof(navigation)), entityType, navigation); /// @@ -1562,7 +1562,7 @@ public static string MissingBackingField(object? field, object? property, object field, property, entityType); /// - /// Unable to set up a many-to-many relationship between the entity types '{principalEntityType}' and '{declaringEntityType}' because one of the navigations was not specified. Provide a navigation in the 'HasMany' call in 'OnModelCreating'. + /// Unable to set up a many-to-many relationship between the entity types '{principalEntityType}' and '{declaringEntityType}' because one of the navigations was not specified. Provide a navigation in the 'HasMany' call in 'OnModelCreating'. Consider adding a private property for this. /// public static string MissingInverseManyToManyNavigation(object? principalEntityType, object? declaringEntityType) => string.Format( @@ -2603,6 +2603,14 @@ public static string ShadowEntity(object? entityType) GetString("ShadowEntity", nameof(entityType)), entityType); + /// + /// Unable to set up a many-to-many relationship between '{leftEntityType}.{leftNavigation}' and '{rightEntityType}.{rightNavigation}' because one or both of the navigations doesn't have a corresponding CLR property. Consider adding a corresponding private property to the entity CLR type. + /// + public static string ShadowManyToManyNavigation(object? leftEntityType, object? leftNavigation, object? rightEntityType, object? rightNavigation) + => string.Format( + GetString("ShadowManyToManyNavigation", nameof(leftEntityType), nameof(leftNavigation), nameof(rightEntityType), nameof(rightNavigation)), + leftEntityType, leftNavigation, rightEntityType, rightNavigation); + /// /// The shared-type entity type '{entityType}' cannot have a base type. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index d5573b70723..874bc842f06 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1025,7 +1025,7 @@ The specified field '{field}' could not be found for property '{2_entityType}.{1_property}'. - Unable to set up a many-to-many relationship between the entity types '{principalEntityType}' and '{declaringEntityType}' because one of the navigations was not specified. Provide a navigation in the 'HasMany' call in 'OnModelCreating'. + Unable to set up a many-to-many relationship between the entity types '{principalEntityType}' and '{declaringEntityType}' because one of the navigations was not specified. Provide a navigation in the 'HasMany' call in 'OnModelCreating'. Consider adding a private property for this. Runtime metadata changes are not allowed when the model hasn't been marked as read-only. @@ -1437,6 +1437,9 @@ The entity type '{entityType}' is in shadow state. A valid model requires all entity types to have a corresponding CLR type. Obsolete + + Unable to set up a many-to-many relationship between '{leftEntityType}.{leftNavigation}' and '{rightEntityType}.{rightNavigation}' because one or both of the navigations doesn't have a corresponding CLR property. Consider adding a corresponding private property to the entity CLR type. + The shared-type entity type '{entityType}' cannot have a base type. diff --git a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs index b1597475458..f8545755363 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs @@ -467,6 +467,28 @@ public virtual void Throws_for_many_to_many_with_only_one_navigation_configured( .WithMany(d => d.ManyToManyPrincipals)).Message); } + [ConditionalFact] + public virtual void Throws_for_many_to_many_with_a_shadow_navigation() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Entity().Ignore(d => d.ManyToManyPrincipals); + + modelBuilder.Entity() + .HasMany(d => d.Dependents) + .WithMany("Shadow"); + + Assert.Equal( + CoreStrings.ShadowManyToManyNavigation( + nameof(NavDependent), + "Shadow", + nameof(ManyToManyNavPrincipal), + nameof(ManyToManyNavPrincipal.Dependents)), + Assert.Throws( + () => modelBuilder.FinalizeModel()).Message); + } + [ConditionalFact] public virtual void Throws_for_self_ref_with_same_navigation() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs index 87d09250734..f417c6a83f1 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs @@ -28,6 +28,12 @@ protected override TestModelBuilder CreateTestModelBuilder(TestHelpers testHelpe => new GenericStringTestModelBuilder(testHelpers, configure); } + public class GenericManyToManyString : ManyToManyTestBase + { + protected override TestModelBuilder CreateTestModelBuilder(TestHelpers testHelpers, Action? configure) + => new GenericStringTestModelBuilder(testHelpers, configure); + } + public class GenericOneToOneString : OneToOneTestBase { protected override TestModelBuilder CreateTestModelBuilder(TestHelpers testHelpers, Action? configure)