From cea6df7966c7a341ba12712009b13012e752a491 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:51:01 +0000 Subject: [PATCH 01/15] Initial plan From f226f3293ea8e5362e24deff2851350fbd7396cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:05:48 +0000 Subject: [PATCH 02/15] Throw by default when same instance is used for multiple owned entity navigations Detect when the same object instance is referenced from multiple owned navigation properties on the same entity during graph traversal, and throw a clear InvalidOperationException with an actionable error message instead of the confusing "unknown FK value" error that occurred during SaveChanges. Closes #24614 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/EntityEntryGraphIterator.cs | 31 +++++++++++++++++++ src/EFCore/Properties/CoreStrings.Designer.cs | 8 +++++ src/EFCore/Properties/CoreStrings.resx | 3 ++ .../ChangeTracking/Internal/OwnedFixupTest.cs | 18 +++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs b/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs index f015f2c7c56..4acbd7cd319 100644 --- a/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs +++ b/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs @@ -23,6 +23,8 @@ public virtual void TraverseGraph( EntityEntryGraphNode node, Func, bool> handleNode) { + CheckForDuplicateOwnedInstances(node.GetInfrastructure()); + if (!handleNode(node)) { return; @@ -73,6 +75,8 @@ public virtual async Task TraverseGraphAsync( Func, CancellationToken, Task> handleNode, CancellationToken cancellationToken = default) { + CheckForDuplicateOwnedInstances(node.GetInfrastructure()); + if (!await handleNode(node, cancellationToken).ConfigureAwait(false)) { return; @@ -114,4 +118,31 @@ await TraverseGraphAsync( } } } + + private static void CheckForDuplicateOwnedInstances(InternalEntityEntry entry) + { + Dictionary? ownedInstances = null; + foreach (var navigation in entry.EntityType.GetNavigations()) + { + if (navigation is { IsCollection: false, ForeignKey.IsOwnership: true }) + { + var navigationValue = entry[navigation]; + if (navigationValue != null) + { + ownedInstances ??= new Dictionary(ReferenceEqualityComparer.Instance); + if (ownedInstances.TryGetValue(navigationValue, out var existingNavigation)) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + navigationValue.GetType().ShortDisplayName(), + existingNavigation.Name, + navigation.Name, + entry.EntityType.DisplayName())); + } + + ownedInstances[navigationValue] = navigation; + } + } + } + } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 186af2df532..ac3606f7a95 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1186,6 +1186,14 @@ public static string DuplicateEntityType(object? entityType) GetString("DuplicateEntityType", nameof(entityType)), entityType); + /// + /// The same object instance of type '{entityType}' is being referenced from the navigations '{firstNavigation}' and '{secondNavigation}' on the entity type '{ownerType}'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for one of the navigations. + /// + public static string DuplicateOwnedEntityInstance(object? entityType, object? firstNavigation, object? secondNavigation, object? ownerType) + => string.Format( + GetString("DuplicateOwnedEntityInstance", nameof(entityType), nameof(firstNavigation), nameof(secondNavigation), nameof(ownerType)), + entityType, firstNavigation, secondNavigation, ownerType); + /// /// The foreign key {foreignKeyProperties} cannot be added to the entity type '{entityType}' because a foreign key on the same properties already exists on entity type '{duplicateEntityType}' and also targets the key {keyProperties} on '{principalType}'. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 09ee563d2d1..3a4797367c8 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -558,6 +558,9 @@ The entity type '{entityType}' cannot be added to the model because an entity type with the same name already exists. + + The same object instance of type '{entityType}' is being referenced from the navigations '{firstNavigation}' and '{secondNavigation}' on the entity type '{ownerType}'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for one of the navigations. + The foreign key {foreignKeyProperties} cannot be added to the entity type '{entityType}' because a foreign key on the same properties already exists on entity type '{duplicateEntityType}' and also targets the key {keyProperties} on '{principalType}'. diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index fae6c7fa5d8..e34b0990af1 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5019,6 +5019,24 @@ public async Task SaveChanges_when_owner_has_PK_with_default_values(bool async) } } + [Fact] + public void Throws_when_same_instance_is_used_for_multiple_owned_navigations() + { + using var context = new FixupContext(); + var dependent = new Child { Name = "1" }; + var principal = new Parent + { + Id = 77, + Child1 = dependent, + Child2 = dependent + }; + + Assert.Equal( + CoreStrings.DuplicateOwnedEntityInstance( + nameof(Child), nameof(Parent.Child1), nameof(Parent.Child2), nameof(Parent)), + Assert.Throws(() => context.Add(principal)).Message); + } + private class OneRowContext(bool async) : DbContext { private readonly bool _async = async; From 9f8810c146c60b2cd175695244f90bfccbc4fa62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:08:01 +0000 Subject: [PATCH 03/15] Move duplicate owned entity check to StateManager.RecordReferencedUntrackedEntity Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/EntityEntryGraphIterator.cs | 31 ------------------- .../ChangeTracking/Internal/StateManager.cs | 17 ++++++++++ 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs b/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs index 4acbd7cd319..f015f2c7c56 100644 --- a/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs +++ b/src/EFCore/ChangeTracking/Internal/EntityEntryGraphIterator.cs @@ -23,8 +23,6 @@ public virtual void TraverseGraph( EntityEntryGraphNode node, Func, bool> handleNode) { - CheckForDuplicateOwnedInstances(node.GetInfrastructure()); - if (!handleNode(node)) { return; @@ -75,8 +73,6 @@ public virtual async Task TraverseGraphAsync( Func, CancellationToken, Task> handleNode, CancellationToken cancellationToken = default) { - CheckForDuplicateOwnedInstances(node.GetInfrastructure()); - if (!await handleNode(node, cancellationToken).ConfigureAwait(false)) { return; @@ -118,31 +114,4 @@ await TraverseGraphAsync( } } } - - private static void CheckForDuplicateOwnedInstances(InternalEntityEntry entry) - { - Dictionary? ownedInstances = null; - foreach (var navigation in entry.EntityType.GetNavigations()) - { - if (navigation is { IsCollection: false, ForeignKey.IsOwnership: true }) - { - var navigationValue = entry[navigation]; - if (navigationValue != null) - { - ownedInstances ??= new Dictionary(ReferenceEqualityComparer.Instance); - if (ownedInstances.TryGetValue(navigationValue, out var existingNavigation)) - { - throw new InvalidOperationException( - CoreStrings.DuplicateOwnedEntityInstance( - navigationValue.GetType().ShortDisplayName(), - existingNavigation.Name, - navigation.Name, - entry.EntityType.DisplayName())); - } - - ownedInstances[navigationValue] = navigation; - } - } - } - } } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 1a6e473e476..5ea27148f04 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -849,6 +849,23 @@ public virtual void RecordReferencedUntrackedEntity( _referencedUntrackedEntities.Add(referencedEntity, danglers); } + if (navigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) + { + foreach (var (existingNavigation, existingEntry) in danglers) + { + if (ReferenceEquals(existingEntry, referencedFromEntry) + && existingNavigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } existingOwnedNavigation) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + referencedEntity.GetType().ShortDisplayName(), + existingOwnedNavigation.Name, + ownedNavigation.Name, + referencedFromEntry.EntityType.DisplayName())); + } + } + } + danglers.Add(Tuple.Create(navigation, referencedFromEntry)); } From 560c5096ca5921eea586a98ab1b159baee8c53a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:15:24 +0000 Subject: [PATCH 04/15] Update EFCore API baseline for DuplicateOwnedEntityInstance Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/EFCore.baseline.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 7c9812b71e2..0628155df27 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -3973,6 +3973,9 @@ { "Member": "static string DuplicateNamedIndex(object? indexName, object? indexProperties, object? entityType, object? duplicateEntityType);" }, + { + "Member": "static string DuplicateOwnedEntityInstance(object? entityType, object? firstNavigation, object? secondNavigation, object? ownerType);" + }, { "Member": "static string DuplicatePropertiesOnBase(object? entityType, object? baseType, object? derivedPropertyType, object? derivedProperty, object? basePropertyType, object? baseProperty);" }, From 17dca3460a27c942fefb817eef61efd2c64022cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:39:05 +0000 Subject: [PATCH 05/15] Add duplicate owned entity detection for all scenarios - Check when owned entity is already tracked (different owner or same owner/different nav) - Check when different owners reference same owned instance from same navigation - Update error message to show both navigations and owners - Add tests for new scenarios Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/NavigationFixer.cs | 43 ++++++++++++++ .../ChangeTracking/Internal/StateManager.cs | 59 ++++++++++++++++--- src/EFCore/Properties/CoreStrings.Designer.cs | 8 +-- src/EFCore/Properties/CoreStrings.resx | 2 +- .../ChangeTracking/Internal/OwnedFixupTest.cs | 53 ++++++++++++++++- 5 files changed, 150 insertions(+), 15 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 1a7342cd017..9da6558af33 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -134,6 +134,32 @@ public virtual void NavigationReferenceChanged( newTargetEntry = null; } + // Check for duplicate owned entity instance + if (newTargetEntry != null + && newValue != null + && foreignKey.IsOwnership + && !navigation.IsCollection) + { + // Find the existing owner of this owned entity + var existingOwner = stateManager.FindPrincipal(newTargetEntry, foreignKey); + if (existingOwner != null) + { + var existingOwnerNav = foreignKey.PrincipalToDependent; + // Check if it's a different owner or a different navigation on the same owner + if (!ReferenceEquals(existingOwner, entry) + || (existingOwnerNav != null && existingOwnerNav.Name != navigation.Name)) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + newValue.GetType().ShortDisplayName(), + existingOwnerNav?.Name ?? "", + existingOwner.EntityType.DisplayName(), + navigation.Name, + entry.EntityType.DisplayName())); + } + } + } + var delayingFixup = BeginDelayedFixup(); try { @@ -868,6 +894,23 @@ private void InitialFixup( } else { + // Check for duplicate owned entity instance + if (foreignKey.IsOwnership) + { + var existingOwner = stateManager.FindPrincipal(dependentEntry, foreignKey); + if (existingOwner != null + && !ReferenceEquals(existingOwner, entry)) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + navigationValue.GetType().ShortDisplayName(), + principalToDependent.Name, + existingOwner.EntityType.DisplayName(), + principalToDependent.Name, + entry.EntityType.DisplayName())); + } + } + FixupToDependent(entry, dependentEntry, foreignKey, setModified, fromQuery); } } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 5ea27148f04..b4e41bd092a 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -840,6 +840,42 @@ public virtual void RecordReferencedUntrackedEntity( INavigationBase navigation, InternalEntityEntry referencedFromEntry) { + if (navigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) + { + // Check if the entity is already tracked + var existingEntry = TryGetEntry(referencedEntity); + if (existingEntry != null) + { + // Find the owning navigation for the already-tracked entity + foreach (var fk in existingEntry.EntityType.GetForeignKeys()) + { + if (fk.IsOwnership) + { + var principalToDependent = fk.PrincipalToDependent; + if (principalToDependent is { IsCollection: false }) + { + var existingOwner = existingEntry.StateManager.FindPrincipal(existingEntry, fk); + + if (existingOwner != null) + { + // Check if it's a different owner or a different navigation on the same owner + if (!ReferenceEquals(existingOwner, referencedFromEntry) || principalToDependent.Name != ownedNavigation.Name) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + referencedEntity.GetType().ShortDisplayName(), + principalToDependent.Name, + existingOwner.EntityType.DisplayName(), + ownedNavigation.Name, + referencedFromEntry.EntityType.DisplayName())); + } + } + } + } + } + } + } + _referencedUntrackedEntities ??= new Dictionary>>(ReferenceEqualityComparer.Instance); @@ -849,19 +885,24 @@ public virtual void RecordReferencedUntrackedEntity( _referencedUntrackedEntities.Add(referencedEntity, danglers); } - if (navigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) + if (navigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true }) { + // Check danglers (untracked entities referenced from navigations) foreach (var (existingNavigation, existingEntry) in danglers) { - if (ReferenceEquals(existingEntry, referencedFromEntry) - && existingNavigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } existingOwnedNavigation) + if (existingNavigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } existingOwnedNavigation) { - throw new InvalidOperationException( - CoreStrings.DuplicateOwnedEntityInstance( - referencedEntity.GetType().ShortDisplayName(), - existingOwnedNavigation.Name, - ownedNavigation.Name, - referencedFromEntry.EntityType.DisplayName())); + // Throw if it's the same owner with a different navigation, or a different owner (same or different navigation) + if (!ReferenceEquals(existingEntry, referencedFromEntry) || existingOwnedNavigation.Name != navigation.Name) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + referencedEntity.GetType().ShortDisplayName(), + existingOwnedNavigation.Name, + existingEntry.EntityType.DisplayName(), + navigation.Name, + referencedFromEntry.EntityType.DisplayName())); + } } } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index ac3606f7a95..480d7b33b6e 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1187,12 +1187,12 @@ public static string DuplicateEntityType(object? entityType) entityType); /// - /// The same object instance of type '{entityType}' is being referenced from the navigations '{firstNavigation}' and '{secondNavigation}' on the entity type '{ownerType}'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for one of the navigations. + /// The same object instance of type '{entityType}' is being referenced from the navigation '{firstNavigation}' on '{firstOwnerType}' and from the navigation '{secondNavigation}' on '{secondOwnerType}'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation. /// - public static string DuplicateOwnedEntityInstance(object? entityType, object? firstNavigation, object? secondNavigation, object? ownerType) + public static string DuplicateOwnedEntityInstance(object? entityType, object? firstNavigation, object? firstOwnerType, object? secondNavigation, object? secondOwnerType) => string.Format( - GetString("DuplicateOwnedEntityInstance", nameof(entityType), nameof(firstNavigation), nameof(secondNavigation), nameof(ownerType)), - entityType, firstNavigation, secondNavigation, ownerType); + GetString("DuplicateOwnedEntityInstance", nameof(entityType), nameof(firstNavigation), nameof(firstOwnerType), nameof(secondNavigation), nameof(secondOwnerType)), + entityType, firstNavigation, firstOwnerType, secondNavigation, secondOwnerType); /// /// The foreign key {foreignKeyProperties} cannot be added to the entity type '{entityType}' because a foreign key on the same properties already exists on entity type '{duplicateEntityType}' and also targets the key {keyProperties} on '{principalType}'. diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 3a4797367c8..48c5deb8608 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -559,7 +559,7 @@ The entity type '{entityType}' cannot be added to the model because an entity type with the same name already exists. - The same object instance of type '{entityType}' is being referenced from the navigations '{firstNavigation}' and '{secondNavigation}' on the entity type '{ownerType}'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for one of the navigations. + The same object instance of type '{entityType}' is being referenced from the navigation '{firstNavigation}' on '{firstOwnerType}' and from the navigation '{secondNavigation}' on '{secondOwnerType}'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation. The foreign key {foreignKeyProperties} cannot be added to the entity type '{entityType}' because a foreign key on the same properties already exists on entity type '{duplicateEntityType}' and also targets the key {keyProperties} on '{principalType}'. diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index e34b0990af1..2a460a9561f 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5033,10 +5033,61 @@ public void Throws_when_same_instance_is_used_for_multiple_owned_navigations() Assert.Equal( CoreStrings.DuplicateOwnedEntityInstance( - nameof(Child), nameof(Parent.Child1), nameof(Parent.Child2), nameof(Parent)), + nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child2), nameof(Parent)), Assert.Throws(() => context.Add(principal)).Message); } + [Fact] + public void Throws_when_already_tracked_owned_instance_is_assigned_to_another_navigation() + { + using var context = new FixupContext(); + var dependent = new Child { Name = "1" }; + var principal1 = new Parent + { + Id = 77, + Child1 = dependent + }; + + context.Add(principal1); + + var principal2 = new Parent + { + Id = 78, + Child1 = dependent // Same instance, already tracked + }; + + var ex = Assert.Throws(() => context.Add(principal2)); + Assert.Equal( + CoreStrings.DuplicateOwnedEntityInstance( + nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child1), nameof(Parent)), + ex.Message); + } + + [Fact] + public void Throws_when_different_owners_reference_same_owned_instance_from_same_navigation() + { + using var context = new FixupContext(); + var dependent = new Child { Name = "1" }; + var principal1 = new Parent + { + Id = 77, + Child1 = dependent + }; + var principal2 = new Parent + { + Id = 78, + Child1 = dependent // Same instance, different owner + }; + + context.Add(principal1); + + var ex = Assert.Throws(() => context.Add(principal2)); + Assert.Equal( + CoreStrings.DuplicateOwnedEntityInstance( + nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child1), nameof(Parent)), + ex.Message); + } + private class OneRowContext(bool async) : DbContext { private readonly bool _async = async; From e866222cbd85715f2481e68e6fcf05f25da72f64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:41:03 +0000 Subject: [PATCH 06/15] Update API baseline for DuplicateOwnedEntityInstance signature change Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/EFCore.baseline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 0628155df27..4fa5cd2b2a6 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -3974,7 +3974,7 @@ "Member": "static string DuplicateNamedIndex(object? indexName, object? indexProperties, object? entityType, object? duplicateEntityType);" }, { - "Member": "static string DuplicateOwnedEntityInstance(object? entityType, object? firstNavigation, object? secondNavigation, object? ownerType);" + "Member": "static string DuplicateOwnedEntityInstance(object? entityType, object? firstNavigation, object? firstOwnerType, object? secondNavigation, object? secondOwnerType);" }, { "Member": "static string DuplicatePropertiesOnBase(object? entityType, object? baseType, object? derivedPropertyType, object? derivedProperty, object? basePropertyType, object? baseProperty);" From ba59da4e5a7265783884b946682a91153fa87008 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:44:50 +0000 Subject: [PATCH 07/15] Address code review feedback: improve error message and remove trailing whitespace Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/ChangeTracking/Internal/NavigationFixer.cs | 2 +- test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 9da6558af33..2a6c7ed5f0c 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -152,7 +152,7 @@ public virtual void NavigationReferenceChanged( throw new InvalidOperationException( CoreStrings.DuplicateOwnedEntityInstance( newValue.GetType().ShortDisplayName(), - existingOwnerNav?.Name ?? "", + existingOwnerNav?.Name ?? "", existingOwner.EntityType.DisplayName(), navigation.Name, entry.EntityType.DisplayName())); diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index 2a460a9561f..43cc3901ff7 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5080,7 +5080,7 @@ public void Throws_when_different_owners_reference_same_owned_instance_from_same }; context.Add(principal1); - + var ex = Assert.Throws(() => context.Add(principal2)); Assert.Equal( CoreStrings.DuplicateOwnedEntityInstance( From fc768291e24d82d7d09164260da1d951a6dc1725 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:00:41 +0000 Subject: [PATCH 08/15] Move duplicate owned entity check to where entity is actually tracked Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/NavigationFixer.cs | 37 +++++++++++- .../ChangeTracking/Internal/StateManager.cs | 58 ------------------- 2 files changed, 36 insertions(+), 59 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 2a6c7ed5f0c..6f0c54bd607 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -1025,7 +1025,42 @@ private void InitialFixup( // If the entity was previously referenced while it was still untracked, go back and do the fixup // that we would have done then now that the entity is tracked. - foreach (var (navigationBase, internalEntityEntry) in stateManager.GetRecordedReferrers(entry.Entity, clear: true)) + var referrers = stateManager.GetRecordedReferrers(entry.Entity, clear: true).ToList(); + + // Check for duplicate owned entity instances + if (referrers.Count > 1) + { + INavigation? firstOwnedNavigation = null; + InternalEntityEntry? firstOwnerEntry = null; + + foreach (var (navigationBase, ownerEntry) in referrers) + { + if (navigationBase is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) + { + if (firstOwnedNavigation == null) + { + firstOwnedNavigation = ownedNavigation; + firstOwnerEntry = ownerEntry; + } + else + { + // Throw if it's the same owner with a different navigation, or a different owner + if (!ReferenceEquals(firstOwnerEntry, ownerEntry) || firstOwnedNavigation.Name != ownedNavigation.Name) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + entry.Entity.GetType().ShortDisplayName(), + firstOwnedNavigation.Name, + firstOwnerEntry!.EntityType.DisplayName(), + ownedNavigation.Name, + ownerEntry.EntityType.DisplayName())); + } + } + } + } + } + + foreach (var (navigationBase, internalEntityEntry) in referrers) { DelayedFixup(internalEntityEntry, navigationBase, entry, fromQuery); } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index b4e41bd092a..1a6e473e476 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -840,42 +840,6 @@ public virtual void RecordReferencedUntrackedEntity( INavigationBase navigation, InternalEntityEntry referencedFromEntry) { - if (navigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) - { - // Check if the entity is already tracked - var existingEntry = TryGetEntry(referencedEntity); - if (existingEntry != null) - { - // Find the owning navigation for the already-tracked entity - foreach (var fk in existingEntry.EntityType.GetForeignKeys()) - { - if (fk.IsOwnership) - { - var principalToDependent = fk.PrincipalToDependent; - if (principalToDependent is { IsCollection: false }) - { - var existingOwner = existingEntry.StateManager.FindPrincipal(existingEntry, fk); - - if (existingOwner != null) - { - // Check if it's a different owner or a different navigation on the same owner - if (!ReferenceEquals(existingOwner, referencedFromEntry) || principalToDependent.Name != ownedNavigation.Name) - { - throw new InvalidOperationException( - CoreStrings.DuplicateOwnedEntityInstance( - referencedEntity.GetType().ShortDisplayName(), - principalToDependent.Name, - existingOwner.EntityType.DisplayName(), - ownedNavigation.Name, - referencedFromEntry.EntityType.DisplayName())); - } - } - } - } - } - } - } - _referencedUntrackedEntities ??= new Dictionary>>(ReferenceEqualityComparer.Instance); @@ -885,28 +849,6 @@ public virtual void RecordReferencedUntrackedEntity( _referencedUntrackedEntities.Add(referencedEntity, danglers); } - if (navigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true }) - { - // Check danglers (untracked entities referenced from navigations) - foreach (var (existingNavigation, existingEntry) in danglers) - { - if (existingNavigation is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } existingOwnedNavigation) - { - // Throw if it's the same owner with a different navigation, or a different owner (same or different navigation) - if (!ReferenceEquals(existingEntry, referencedFromEntry) || existingOwnedNavigation.Name != navigation.Name) - { - throw new InvalidOperationException( - CoreStrings.DuplicateOwnedEntityInstance( - referencedEntity.GetType().ShortDisplayName(), - existingOwnedNavigation.Name, - existingEntry.EntityType.DisplayName(), - navigation.Name, - referencedFromEntry.EntityType.DisplayName())); - } - } - } - } - danglers.Add(Tuple.Create(navigation, referencedFromEntry)); } From df9527db0c255bc06b276086b8596a46ba080838 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:05:44 +0000 Subject: [PATCH 09/15] Fix: Only check for duplicate owned entities during Add, not during DetectChanges Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/NavigationFixer.cs | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 6f0c54bd607..e89beb5928c 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -134,32 +134,6 @@ public virtual void NavigationReferenceChanged( newTargetEntry = null; } - // Check for duplicate owned entity instance - if (newTargetEntry != null - && newValue != null - && foreignKey.IsOwnership - && !navigation.IsCollection) - { - // Find the existing owner of this owned entity - var existingOwner = stateManager.FindPrincipal(newTargetEntry, foreignKey); - if (existingOwner != null) - { - var existingOwnerNav = foreignKey.PrincipalToDependent; - // Check if it's a different owner or a different navigation on the same owner - if (!ReferenceEquals(existingOwner, entry) - || (existingOwnerNav != null && existingOwnerNav.Name != navigation.Name)) - { - throw new InvalidOperationException( - CoreStrings.DuplicateOwnedEntityInstance( - newValue.GetType().ShortDisplayName(), - existingOwnerNav?.Name ?? "", - existingOwner.EntityType.DisplayName(), - navigation.Name, - entry.EntityType.DisplayName())); - } - } - } - var delayingFixup = BeginDelayedFixup(); try { @@ -895,7 +869,9 @@ private void InitialFixup( else { // Check for duplicate owned entity instance - if (foreignKey.IsOwnership) + // This handles the case where an already-tracked owned entity is being assigned + // to a different owner during the initial tracking phase (not during DetectChanges) + if (foreignKey.IsOwnership && entry.EntityState == EntityState.Added) { var existingOwner = stateManager.FindPrincipal(dependentEntry, foreignKey); if (existingOwner != null From 71ca911fc7dc4b49c44dff25fcfb14bb9a276384 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:09:26 +0000 Subject: [PATCH 10/15] Address code review feedback: avoid unnecessary ToList and add debug assertion Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/NavigationFixer.cs | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index e89beb5928c..7ba80cd5013 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -1001,44 +1001,42 @@ private void InitialFixup( // If the entity was previously referenced while it was still untracked, go back and do the fixup // that we would have done then now that the entity is tracked. - var referrers = stateManager.GetRecordedReferrers(entry.Entity, clear: true).ToList(); + var referrers = stateManager.GetRecordedReferrers(entry.Entity, clear: true); // Check for duplicate owned entity instances - if (referrers.Count > 1) + // We only allocate a list if we find at least one owned navigation referrer + INavigation? firstOwnedNavigation = null; + InternalEntityEntry? firstOwnerEntry = null; + + foreach (var (navigationBase, ownerEntry) in referrers) { - INavigation? firstOwnedNavigation = null; - InternalEntityEntry? firstOwnerEntry = null; - - foreach (var (navigationBase, ownerEntry) in referrers) + if (navigationBase is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) { - if (navigationBase is INavigation { IsCollection: false, ForeignKey.IsOwnership: true } ownedNavigation) + if (firstOwnedNavigation == null) { - if (firstOwnedNavigation == null) - { - firstOwnedNavigation = ownedNavigation; - firstOwnerEntry = ownerEntry; - } - else + firstOwnedNavigation = ownedNavigation; + firstOwnerEntry = ownerEntry; + } + else + { + // Throw if it's the same owner with a different navigation, or a different owner (with same or different navigation) + // Both scenarios are invalid: an owned entity can only have one owner with one navigation + if (!ReferenceEquals(firstOwnerEntry, ownerEntry) || firstOwnedNavigation.Name != ownedNavigation.Name) { - // Throw if it's the same owner with a different navigation, or a different owner - if (!ReferenceEquals(firstOwnerEntry, ownerEntry) || firstOwnedNavigation.Name != ownedNavigation.Name) - { - throw new InvalidOperationException( - CoreStrings.DuplicateOwnedEntityInstance( - entry.Entity.GetType().ShortDisplayName(), - firstOwnedNavigation.Name, - firstOwnerEntry!.EntityType.DisplayName(), - ownedNavigation.Name, - ownerEntry.EntityType.DisplayName())); - } + Check.DebugAssert(firstOwnerEntry != null, "firstOwnerEntry should not be null when firstOwnedNavigation is set"); + + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + entry.Entity.GetType().ShortDisplayName(), + firstOwnedNavigation.Name, + firstOwnerEntry.EntityType.DisplayName(), + ownedNavigation.Name, + ownerEntry.EntityType.DisplayName())); } } } - } - - foreach (var (navigationBase, internalEntityEntry) in referrers) - { - DelayedFixup(internalEntityEntry, navigationBase, entry, fromQuery); + + DelayedFixup(ownerEntry, navigationBase, entry, fromQuery); } } } From 74d1059054713749ad5fb8ccb0910cd8d247fa1c Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 17 Jun 2026 19:08:49 -0700 Subject: [PATCH 11/15] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/EFCore/ChangeTracking/Internal/NavigationFixer.cs | 4 ++-- test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 7ba80cd5013..1f22bad4a1f 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -871,7 +871,7 @@ private void InitialFixup( // Check for duplicate owned entity instance // This handles the case where an already-tracked owned entity is being assigned // to a different owner during the initial tracking phase (not during DetectChanges) - if (foreignKey.IsOwnership && entry.EntityState == EntityState.Added) + if (foreignKey.IsOwnership) { var existingOwner = stateManager.FindPrincipal(dependentEntry, foreignKey); if (existingOwner != null @@ -886,7 +886,7 @@ private void InitialFixup( entry.EntityType.DisplayName())); } } - + FixupToDependent(entry, dependentEntry, foreignKey, setModified, fromQuery); } } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index 43cc3901ff7..77cd1ae4e1a 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5038,7 +5038,7 @@ public void Throws_when_same_instance_is_used_for_multiple_owned_navigations() } [Fact] - public void Throws_when_already_tracked_owned_instance_is_assigned_to_another_navigation() + public void Throws_when_already_tracked_owned_instance_is_assigned_to_another_owner() { using var context = new FixupContext(); var dependent = new Child { Name = "1" }; From fc157ec4ea50bda8d33626d9fcaebcfc7d1219bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:36:05 +0000 Subject: [PATCH 12/15] Fix duplicate owned entity check to allow legitimate move/swap operations The check in NavigationReferenceChanged was too aggressive: it threw DuplicateOwnedEntityInstance for all cases where an already-tracked owned entity was assigned to a navigation pointing to a different entity type (shared CLR type scenario). This broke the existing identity-swap and identity-changed tests where an entity is legitimately MOVED from one navigation to another (with the original navigation already cleared). The fix: only throw when the existing owner's navigation STILL references the entity being assigned (genuine duplicate). If the existing navigation has already been cleared on the CLR object, it's a legitimate move/swap rather than a duplicate reference. Added 2 new tests: - Throws_when_already_tracked_owned_instance_is_later_assigned_to_different_navigation_on_same_owner - Throws_when_owned_instance_in_unchanged_state_is_assigned_to_another_owner Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/NavigationFixer.cs | 33 +++++++++++++ .../ChangeTracking/Internal/OwnedFixupTest.cs | 48 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 1f22bad4a1f..2330e81ce3e 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -247,6 +247,39 @@ public virtual void NavigationReferenceChanged( if (newValue != null && newTargetEntry == null) { + // Check if this owned entity is already tracked under a different entity type + // (shared CLR type scenario: same CLR instance used for two different owned navigations) + if (foreignKey.IsOwnership && !navigation.IsOnDependent) + { + var existingEntry = stateManager.TryGetEntry(newValue, throwOnNonUniqueness: false); + if (existingEntry != null && existingEntry.EntityState != EntityState.Detached) + { + var existingOwnership = existingEntry.EntityType.FindOwnership(); + if (existingOwnership != null) + { + var existingNavigation = existingOwnership.PrincipalToDependent; + if (existingNavigation != null) + { + var existingOwner = stateManager.FindPrincipal(existingEntry, existingOwnership); + // Only throw if the existing owner's navigation still points to this entity. + // If the navigation has already been cleared on the CLR object, this is a + // legitimate move/swap rather than a duplicate reference. + if (existingOwner != null + && ReferenceEquals(existingOwner[existingNavigation], newValue)) + { + throw new InvalidOperationException( + CoreStrings.DuplicateOwnedEntityInstance( + newValue.GetType().ShortDisplayName(), + existingNavigation.Name, + existingOwner.EntityType.DisplayName(), + navigation.Name, + entry.EntityType.DisplayName())); + } + } + } + } + } + stateManager.RecordReferencedUntrackedEntity(newValue, navigation, entry); entry.SetRelationshipSnapshotValue(navigation, newValue); diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index 77cd1ae4e1a..81cac36ee01 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5088,6 +5088,54 @@ public void Throws_when_different_owners_reference_same_owned_instance_from_same ex.Message); } + [Fact] + public void Throws_when_already_tracked_owned_instance_is_later_assigned_to_different_navigation_on_same_owner() + { + using var context = new FixupContext(); + var dependent = new Child { Name = "1" }; + var principal = new Parent + { + Id = 79, + Child1 = dependent + }; + + context.Add(principal); + + principal.Child2 = dependent; // Same instance, same owner, different navigation + + Assert.Equal( + CoreStrings.DuplicateOwnedEntityInstance( + nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child2), nameof(Parent)), + Assert.Throws(() => context.ChangeTracker.DetectChanges()).Message); + } + + [Fact] + public void Throws_when_owned_instance_in_unchanged_state_is_assigned_to_another_owner() + { + using var context = new FixupContext(); + var dependent = new Child { Name = "1" }; + var principal1 = new Parent + { + Id = 80, + Child1 = dependent + }; + + context.Add(principal1); + context.Entry(principal1).State = EntityState.Unchanged; + context.Entry(dependent).State = EntityState.Unchanged; + + var principal2 = new Parent + { + Id = 81, + Child1 = dependent + }; + + Assert.Equal( + CoreStrings.DuplicateOwnedEntityInstance( + nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child1), nameof(Parent)), + Assert.Throws(() => context.Add(principal2)).Message); + } + private class OneRowContext(bool async) : DbContext { private readonly bool _async = async; From 77d7468490cb1a09c37519d82dd4ea81d991d61f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:38:47 +0000 Subject: [PATCH 13/15] Remove redundant inline comment and condense NavigationFixer comment Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/ChangeTracking/Internal/NavigationFixer.cs | 4 ++-- test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 2330e81ce3e..68f6d18b95c 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -247,8 +247,8 @@ public virtual void NavigationReferenceChanged( if (newValue != null && newTargetEntry == null) { - // Check if this owned entity is already tracked under a different entity type - // (shared CLR type scenario: same CLR instance used for two different owned navigations) + // The owned entity is not yet tracked under the target entity type. Check if it's tracked + // under a different entity type (shared CLR type) and its current owner still references it. if (foreignKey.IsOwnership && !navigation.IsOnDependent) { var existingEntry = stateManager.TryGetEntry(newValue, throwOnNonUniqueness: false); diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index 81cac36ee01..8cccc64ad95 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5101,7 +5101,7 @@ public void Throws_when_already_tracked_owned_instance_is_later_assigned_to_diff context.Add(principal); - principal.Child2 = dependent; // Same instance, same owner, different navigation + principal.Child2 = dependent; Assert.Equal( CoreStrings.DuplicateOwnedEntityInstance( From d55bc920b4972c4a28f1e3658fd637c90842f02c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:11:24 +0000 Subject: [PATCH 14/15] Modify redundant test to cover different navigation + different owner via DetectChanges Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../ChangeTracking/Internal/OwnedFixupTest.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index 8cccc64ad95..a2421d9ccd6 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -5064,7 +5064,7 @@ public void Throws_when_already_tracked_owned_instance_is_assigned_to_another_ow } [Fact] - public void Throws_when_different_owners_reference_same_owned_instance_from_same_navigation() + public void Throws_when_already_tracked_owned_instance_is_later_assigned_to_different_navigation_on_different_owner() { using var context = new FixupContext(); var dependent = new Child { Name = "1" }; @@ -5073,19 +5073,17 @@ public void Throws_when_different_owners_reference_same_owned_instance_from_same Id = 77, Child1 = dependent }; - var principal2 = new Parent - { - Id = 78, - Child1 = dependent // Same instance, different owner - }; + var principal2 = new Parent { Id = 78 }; context.Add(principal1); + context.Add(principal2); + + principal2.Child2 = dependent; // Same instance, different navigation, different owner - var ex = Assert.Throws(() => context.Add(principal2)); Assert.Equal( CoreStrings.DuplicateOwnedEntityInstance( - nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child1), nameof(Parent)), - ex.Message); + nameof(Child), nameof(Parent.Child1), nameof(Parent), nameof(Parent.Child2), nameof(Parent)), + Assert.Throws(() => context.ChangeTracker.DetectChanges()).Message); } [Fact] From c609265feb15a7f5781b390e06aab977c07d20d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:19:26 +0000 Subject: [PATCH 15/15] Fix misleading comment about list allocation in NavigationFixer Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/ChangeTracking/Internal/NavigationFixer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 68f6d18b95c..e2213cc5ba2 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -1035,9 +1035,9 @@ private void InitialFixup( // If the entity was previously referenced while it was still untracked, go back and do the fixup // that we would have done then now that the entity is tracked. var referrers = stateManager.GetRecordedReferrers(entry.Entity, clear: true); - + // Check for duplicate owned entity instances - // We only allocate a list if we find at least one owned navigation referrer + // Track the first owned navigation referrer to compare against subsequent ones INavigation? firstOwnedNavigation = null; InternalEntityEntry? firstOwnerEntry = null;