From 44121f0d7275ac50aedfcd4929acb20df17009e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:02:26 +0000 Subject: [PATCH 1/8] Initial plan From 550d3fb7900da72a07f25a28bd22c5a03f644cd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:30:56 +0000 Subject: [PATCH 2/8] Pass known principal type into InternalForeignKeyBuilder.Attach to short-circuit re-resolution Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Metadata/Internal/InternalForeignKeyBuilder.cs | 13 ++++++++++--- .../Metadata/Internal/RelationshipSnapshot.cs | 11 ++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index 2e39914f4ce..8bf229f49fc 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -3492,13 +3492,20 @@ private static IReadOnlyList FindRelationships( /// 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? Attach(InternalEntityTypeBuilder entityTypeBuilder) + public virtual InternalForeignKeyBuilder? Attach( + InternalEntityTypeBuilder entityTypeBuilder, + EntityType? principalEntityType = null) { var configurationSource = Metadata.GetConfigurationSource(); var model = Metadata.DeclaringEntityType.Model; InternalEntityTypeBuilder principalEntityTypeBuilder; - EntityType? principalEntityType; - if (Metadata.PrincipalEntityType.IsInModel) + if (principalEntityType is { IsInModel: true } + && (Metadata.PrincipalEntityType.Name == principalEntityType.Name + || Metadata.PrincipalEntityType.ClrType == principalEntityType.ClrType)) + { + principalEntityTypeBuilder = principalEntityType.Builder; + } + else if (Metadata.PrincipalEntityType.IsInModel) { principalEntityTypeBuilder = Metadata.PrincipalEntityType.Builder; principalEntityType = Metadata.PrincipalEntityType; diff --git a/src/EFCore/Metadata/Internal/RelationshipSnapshot.cs b/src/EFCore/Metadata/Internal/RelationshipSnapshot.cs index bcfb41abc2a..18c10abbfc7 100644 --- a/src/EFCore/Metadata/Internal/RelationshipSnapshot.cs +++ b/src/EFCore/Metadata/Internal/RelationshipSnapshot.cs @@ -23,10 +23,19 @@ public RelationshipSnapshot( List<(SkipNavigation, ConfigurationSource)>? referencingSkipNavigations) { Relationship = relationship; + PrincipalEntityType = relationship.Metadata.PrincipalEntityType; OwnedEntityTypeSnapshot = ownedEntityTypeSnapshot; ReferencingSkipNavigations = referencingSkipNavigations; } + /// + /// 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 EntityType PrincipalEntityType { [DebuggerStepThrough] get; } + /// /// 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 @@ -65,7 +74,7 @@ public RelationshipSnapshot( { entityTypeBuilder ??= Relationship.Metadata.DeclaringEntityType.Builder; - var newRelationship = Relationship.Attach(entityTypeBuilder); + var newRelationship = Relationship.Attach(entityTypeBuilder, PrincipalEntityType); if (newRelationship != null) { OwnedEntityTypeSnapshot?.Attach( From 2b7a9a1caa361c7d9a43b247eca4c60fef81c37e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:43:33 +0000 Subject: [PATCH 3/8] Fix inverting identifying FK to keep derived dependent end Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/InternalForeignKeyBuilder.cs | 11 +++++-- .../Internal/InternalForeignKeyBuilderTest.cs | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index 8bf229f49fc..5f942c992b7 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -1437,8 +1437,15 @@ private bool CanSetIsUnique(bool? unique, ConfigurationSource? configurationSour configurationSource.Overrides(Metadata.GetPrincipalKeyConfigurationSource()), "configurationSource does not override Metadata.GetPrincipalKeyConfigurationSource"); - principalEntityType = principalEntityType.LeastDerivedType(Metadata.DeclaringEntityType)!; - dependentEntityType = dependentEntityType.LeastDerivedType(Metadata.PrincipalEntityType)!; + // When inverting, the new principal/dependent ends take the place of the old dependent/principal + // ends. If the requested type is a base of the old opposite end, keep the old (more derived) type + // so that an identifying relationship isn't moved to a base type. See #15898. + principalEntityType = principalEntityType.IsAssignableFrom(Metadata.DeclaringEntityType) + ? Metadata.DeclaringEntityType + : principalEntityType.LeastDerivedType(Metadata.DeclaringEntityType)!; + dependentEntityType = dependentEntityType.IsAssignableFrom(Metadata.PrincipalEntityType) + ? Metadata.PrincipalEntityType + : dependentEntityType.LeastDerivedType(Metadata.PrincipalEntityType)!; } else { diff --git a/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs index bea8afdf953..76888d86183 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs @@ -1062,6 +1062,38 @@ public void Can_set_IsConstrained_and_respects_configuration_source() Assert.False(relationshipBuilder.Metadata.IsConstrained); } + [Fact] + public void Inverting_identifying_relationship_keeps_derived_dependent() + { + var modelBuilder = CreateInternalModelBuilder(); + var customerEntityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + customerEntityBuilder.PrimaryKey([Customer.IdProperty], ConfigurationSource.Explicit); + var orderEntityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Explicit); + orderEntityBuilder.PrimaryKey([Order.IdProperty], ConfigurationSource.Explicit); + var specialOrderEntityBuilder = modelBuilder.Entity(typeof(SpecialOrder), ConfigurationSource.Explicit); + specialOrderEntityBuilder.HasBaseType(orderEntityBuilder.Metadata, ConfigurationSource.Explicit); + + // Identifying FK: principal = SpecialOrder (derived), dependent = Customer (non-derived), + // where the FK property is the dependent's primary key. + var relationshipBuilder = customerEntityBuilder + .HasRelationship(specialOrderEntityBuilder.Metadata, ConfigurationSource.Convention) + .HasForeignKey([Customer.IdProperty], ConfigurationSource.Explicit) + .HasPrincipalKey(specialOrderEntityBuilder.Metadata.FindPrimaryKey().Properties, ConfigurationSource.Explicit) + .IsUnique(true, ConfigurationSource.Explicit); + + Assert.Same(specialOrderEntityBuilder.Metadata, relationshipBuilder.Metadata.PrincipalEntityType); + Assert.Same(customerEntityBuilder.Metadata, relationshipBuilder.Metadata.DeclaringEntityType); + + // Invert the relationship, requesting the base Order as the new dependent. The inverted FK must + // remain between the derived SpecialOrder and the non-derived Customer rather than collapsing to Order. + var inverted = relationshipBuilder.HasEntityTypes( + customerEntityBuilder.Metadata, orderEntityBuilder.Metadata, ConfigurationSource.Explicit); + + Assert.NotNull(inverted); + Assert.Same(customerEntityBuilder.Metadata, inverted.Metadata.PrincipalEntityType); + Assert.Same(specialOrderEntityBuilder.Metadata, inverted.Metadata.DeclaringEntityType); + } + private InternalModelBuilder CreateInternalModelBuilder() => new(new Model()); From 1c16761a9110e7d445a66104e2f53ee08f777c69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:56:33 +0000 Subject: [PATCH 4/8] Create FK with dependent properties in a single step Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/InternalEntityTypeBuilder.cs | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index a5fecf0b7e1..e64710b170b 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -2620,7 +2620,8 @@ public static InternalIndexBuilder DetachIndex(Index indexToDetach) return null; } - var newRelationship = HasRelationshipInternal(principalEntityType, principalKey, configurationSource)!; + var newRelationship = HasRelationshipInternal( + principalEntityType, principalKey, configurationSource, dependentProperties: dependentProperties)!; var relationship = newRelationship.HasForeignKey(dependentProperties, configurationSource); if (relationship == null @@ -3126,7 +3127,8 @@ public static InternalIndexBuilder DetachIndex(Index indexToDetach) Key? principalKey, ConfigurationSource configurationSource, bool? required = null, - string? propertyBaseName = null) + string? propertyBaseName = null, + IReadOnlyList? dependentProperties = null) { InternalForeignKeyBuilder? relationship; InternalForeignKeyBuilder? newRelationship; @@ -3134,7 +3136,7 @@ public static InternalIndexBuilder DetachIndex(Index indexToDetach) { relationship = CreateForeignKey( targetEntityType.Builder, - null, + dependentProperties, principalKey, propertyBaseName, required, @@ -3972,6 +3974,25 @@ private static bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignK { var principalType = principalEntityTypeBuilder.Metadata; var principalBaseEntityTypeBuilder = principalType.GetRootType().Builder; + + if (foreignKey == null + && dependentProperties != null) + { + // When creating a new foreign key, defer to shadow properties if a matching foreign key already + // exists in the hierarchy so the relationship builder can match or promote it instead of adding a + // conflicting duplicate. The matching and promotion is handled by the subsequent HasForeignKey call. + var matchPrincipalKey = principalKey ?? principalType.FindPrimaryKey(); + if (matchPrincipalKey != null) + { + var actualDependentProperties = + dependentEntityTypeBuilder.GetActualProperties(dependentProperties, ConfigurationSource.Convention); + if (actualDependentProperties != null + && Metadata.FindForeignKeysInHierarchy(actualDependentProperties, matchPrincipalKey, principalType).Any()) + { + dependentProperties = null; + } + } + } if (principalKey == null) { if (principalType.IsKeyless From 5cc32c39b24a30f1e8a5a0e53ec0ab05c7090f3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:02:42 +0000 Subject: [PATCH 5/8] Tighten Attach principal short-circuit for shared CLR types Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs index 5f942c992b7..c21aa505d49 100644 --- a/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs @@ -3508,7 +3508,8 @@ private static IReadOnlyList FindRelationships( InternalEntityTypeBuilder principalEntityTypeBuilder; if (principalEntityType is { IsInModel: true } && (Metadata.PrincipalEntityType.Name == principalEntityType.Name - || Metadata.PrincipalEntityType.ClrType == principalEntityType.ClrType)) + || (!principalEntityType.HasSharedClrType + && Metadata.PrincipalEntityType.ClrType == principalEntityType.ClrType))) { principalEntityTypeBuilder = principalEntityType.Builder; } From 1c62f5a1c8af28a52d7990f83c20f5da2cb81d68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:14:35 +0000 Subject: [PATCH 6/8] Revert single-step FK creation to fix CI failures in Can_access_relationship tests Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/InternalEntityTypeBuilder.cs | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index e64710b170b..a5fecf0b7e1 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -2620,8 +2620,7 @@ public static InternalIndexBuilder DetachIndex(Index indexToDetach) return null; } - var newRelationship = HasRelationshipInternal( - principalEntityType, principalKey, configurationSource, dependentProperties: dependentProperties)!; + var newRelationship = HasRelationshipInternal(principalEntityType, principalKey, configurationSource)!; var relationship = newRelationship.HasForeignKey(dependentProperties, configurationSource); if (relationship == null @@ -3127,8 +3126,7 @@ public static InternalIndexBuilder DetachIndex(Index indexToDetach) Key? principalKey, ConfigurationSource configurationSource, bool? required = null, - string? propertyBaseName = null, - IReadOnlyList? dependentProperties = null) + string? propertyBaseName = null) { InternalForeignKeyBuilder? relationship; InternalForeignKeyBuilder? newRelationship; @@ -3136,7 +3134,7 @@ public static InternalIndexBuilder DetachIndex(Index indexToDetach) { relationship = CreateForeignKey( targetEntityType.Builder, - dependentProperties, + null, principalKey, propertyBaseName, required, @@ -3974,25 +3972,6 @@ private static bool Contains(IReadOnlyForeignKey? inheritedFk, IReadOnlyForeignK { var principalType = principalEntityTypeBuilder.Metadata; var principalBaseEntityTypeBuilder = principalType.GetRootType().Builder; - - if (foreignKey == null - && dependentProperties != null) - { - // When creating a new foreign key, defer to shadow properties if a matching foreign key already - // exists in the hierarchy so the relationship builder can match or promote it instead of adding a - // conflicting duplicate. The matching and promotion is handled by the subsequent HasForeignKey call. - var matchPrincipalKey = principalKey ?? principalType.FindPrimaryKey(); - if (matchPrincipalKey != null) - { - var actualDependentProperties = - dependentEntityTypeBuilder.GetActualProperties(dependentProperties, ConfigurationSource.Convention); - if (actualDependentProperties != null - && Metadata.FindForeignKeysInHierarchy(actualDependentProperties, matchPrincipalKey, principalType).Any()) - { - dependentProperties = null; - } - } - } if (principalKey == null) { if (principalType.IsKeyless From f199036a9f1555811e1f5ed65d332f39a410f41f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:43:13 +0000 Subject: [PATCH 7/8] Add tests for Attach principalEntityType short-circuit and shared-type guard Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/InternalForeignKeyBuilderTest.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs index 76888d86183..9d525f76032 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs @@ -1094,6 +1094,84 @@ public void Inverting_identifying_relationship_keeps_derived_dependent() Assert.Same(specialOrderEntityBuilder.Metadata, inverted.Metadata.DeclaringEntityType); } + [Fact] + public void Attach_uses_provided_principal_type_when_in_model() + { + var modelBuilder = CreateInternalModelBuilder(); + var principalEntityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + principalEntityBuilder.PrimaryKey([Customer.IdProperty], ConfigurationSource.Explicit); + var dependentEntityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Explicit); + + var fkBuilder = dependentEntityBuilder + .HasRelationship(principalEntityBuilder.Metadata, ConfigurationSource.Convention); + + var snapshot = InternalEntityTypeBuilder.DetachRelationship(fkBuilder.Metadata); + + // snapshot.PrincipalEntityType is Customer (in model, name matches) — the short-circuit fires + var reattached = snapshot.Attach(dependentEntityBuilder); + + Assert.NotNull(reattached); + Assert.Same(principalEntityBuilder.Metadata, reattached.Metadata.PrincipalEntityType); + Assert.Same(dependentEntityBuilder.Metadata, reattached.Metadata.DeclaringEntityType); + } + + [Fact] + public void Attach_does_not_bind_to_wrong_shared_type_principal_by_clr_type() + { + // Two shared-type entity types with the same CLR type but different names. + // Passing PrincipalB (wrong entity, same CLR type) should NOT cause the FK to bind to it; + // the !HasSharedClrType guard disables CLR-type matching for shared types. + var modelBuilder = CreateInternalModelBuilder(); + + var principalABuilder = modelBuilder.SharedTypeEntity("PrincipalA", typeof(Customer), ConfigurationSource.Explicit)!; + principalABuilder.PrimaryKey([Customer.IdProperty], ConfigurationSource.Explicit); + var principalBBuilder = modelBuilder.SharedTypeEntity("PrincipalB", typeof(Customer), ConfigurationSource.Explicit)!; + + var dependentEntityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Explicit); + + var fkBuilder = dependentEntityBuilder + .HasRelationship(principalABuilder.Metadata, ConfigurationSource.Convention); + + var snapshot = InternalEntityTypeBuilder.DetachRelationship(fkBuilder.Metadata); + + // Pass PrincipalB (same CLR type as PrincipalA but different name) as principalEntityType. + // Because PrincipalB.HasSharedClrType = true the CLR-type path is skipped; name matching also + // fails, so the fallback uses the FK's recorded PrincipalA (still in model). + var reattached = snapshot.Relationship.Attach(dependentEntityBuilder, principalBBuilder.Metadata); + + Assert.NotNull(reattached); + Assert.Same(principalABuilder.Metadata, reattached.Metadata.PrincipalEntityType); + } + + [Fact] + public void Attach_falls_back_to_model_lookup_when_provided_principal_is_not_in_model() + { + var modelBuilder = CreateInternalModelBuilder(); + var principalEntityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + principalEntityBuilder.PrimaryKey([Customer.IdProperty], ConfigurationSource.Explicit); + var dependentEntityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Explicit); + + var fkBuilder = dependentEntityBuilder + .HasRelationship(principalEntityBuilder.Metadata, ConfigurationSource.Convention); + + var stalePrincipal = principalEntityBuilder.Metadata; + var snapshot = InternalEntityTypeBuilder.DetachRelationship(fkBuilder.Metadata); + + // Remove and re-add the principal entity to produce a stale captured reference. + modelBuilder.HasNoEntityType(stalePrincipal, ConfigurationSource.Explicit); + var newPrincipalEntityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit)!; + newPrincipalEntityBuilder.PrimaryKey([Customer.IdProperty], ConfigurationSource.Explicit); + + Assert.False(stalePrincipal.IsInModel); + + // snapshot.PrincipalEntityType is stale (IsInModel = false), so the short-circuit is skipped; + // Attach falls back to model lookup by name and finds the new Customer instance. + var reattached = snapshot.Attach(dependentEntityBuilder); + + Assert.NotNull(reattached); + Assert.Same(newPrincipalEntityBuilder.Metadata, reattached.Metadata.PrincipalEntityType); + } + private InternalModelBuilder CreateInternalModelBuilder() => new(new Model()); From 96551d1503dd963c45bd90fe2d97128c313c8189 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:43:57 +0000 Subject: [PATCH 8/8] Add explicit NotSame assertion to shared-type Attach test Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Metadata/Internal/InternalForeignKeyBuilderTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs index 9d525f76032..c500645ef30 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalForeignKeyBuilderTest.cs @@ -1141,6 +1141,7 @@ public void Attach_does_not_bind_to_wrong_shared_type_principal_by_clr_type() Assert.NotNull(reattached); Assert.Same(principalABuilder.Metadata, reattached.Metadata.PrincipalEntityType); + Assert.NotSame(principalBBuilder.Metadata, reattached.Metadata.PrincipalEntityType); } [Fact]