diff --git a/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs b/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs index ba7048a022e..711ab05a93a 100644 --- a/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs +++ b/src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs @@ -102,7 +102,11 @@ private bool PaintAction( SetReferenceLoaded(node); var internalEntityEntry = node.GetInfrastructure(); - if (internalEntityEntry.EntityState != EntityState.Detached + var sourceEntry = node.SourceEntry?.GetInfrastructure(); + + if ((internalEntityEntry.EntityState != EntityState.Detached + && (internalEntityEntry.EntityState != EntityState.Deleted + || sourceEntry?.SharedIdentityEntry == null)) || (_visited != null && _visited.Contains(internalEntityEntry.Entity))) { return false; @@ -114,7 +118,7 @@ private bool PaintAction( if (internalEntityEntry.StateManager.ResolveToExistingEntry( internalEntityEntry, - node.InboundNavigation, node.SourceEntry?.GetInfrastructure())) + node.InboundNavigation, sourceEntry)) { (_visited ??= new HashSet(ReferenceEqualityComparer.Instance)).Add(internalEntityEntry.Entity); } @@ -139,7 +143,10 @@ private async Task PaintActionAsync( SetReferenceLoaded(node); var internalEntityEntry = node.GetInfrastructure(); - if (internalEntityEntry.EntityState != EntityState.Detached + var sourceEntry = node.SourceEntry?.GetInfrastructure(); + if ((internalEntityEntry.EntityState != EntityState.Detached + && (internalEntityEntry.EntityState != EntityState.Deleted + || sourceEntry?.SharedIdentityEntry == null)) || (_visited != null && _visited.Contains(internalEntityEntry.Entity))) { return false; @@ -151,7 +158,7 @@ private async Task PaintActionAsync( if (internalEntityEntry.StateManager.ResolveToExistingEntry( internalEntityEntry, - node.InboundNavigation, node.SourceEntry?.GetInfrastructure())) + node.InboundNavigation, sourceEntry)) { (_visited ??= []).Add(internalEntityEntry.Entity); } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 44b2ab405e9..9a5a169ef1b 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -1208,6 +1208,14 @@ public virtual void CascadeChanges(bool force) /// public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumerable? foreignKeys = null) { + // When an owned entity is replaced (e.g., via record 'with' expression), the old entry is + // marked Deleted and a new entry with the same key is linked via SharedIdentityEntry. + // Skip cascade from the old entry since the replacement handles its own dependents. + if (entry.SharedIdentityEntry != null) + { + return; + } + var doCascadeDelete = force || CascadeDeleteTiming != CascadeTiming.Never; var principalIsDetached = entry.EntityState == EntityState.Detached; diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs index 54c37c517d0..2dd1a172555 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs @@ -3671,6 +3671,67 @@ public virtual Task Edit_single_property_with_non_ascii_characters() Assert.Equal("测试1", result.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); }); + [ConditionalFact] + public virtual Task Replace_json_reference_root_preserves_nested_owned_entities_in_memory() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(); + + // Save original leaf value + var originalLeaf = entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf; + var originalLeafValue = originalLeaf.SomethingSomething; + + // Replace the owned reference with a new instance that shares nested reference navigations + var oldRoot = entity.OwnedReferenceRoot; + entity.OwnedReferenceRoot = new JsonOwnedRoot + { + Name = "Modified", + Number = oldRoot.Number, + Names = oldRoot.Names, + Numbers = oldRoot.Numbers, + OwnedReferenceBranch = new JsonOwnedBranch + { + Id = oldRoot.OwnedReferenceBranch.Id, + Date = oldRoot.OwnedReferenceBranch.Date, + Enum = oldRoot.OwnedReferenceBranch.Enum, + Fraction = oldRoot.OwnedReferenceBranch.Fraction, + NullableEnum = oldRoot.OwnedReferenceBranch.NullableEnum, + Enums = oldRoot.OwnedReferenceBranch.Enums, + NullableEnums = oldRoot.OwnedReferenceBranch.NullableEnums, + OwnedReferenceLeaf = originalLeaf, + OwnedCollectionLeaf = [], + }, + OwnedCollectionBranch = [], + }; + + // Before DetectChanges, leaf should be accessible + Assert.Same(originalLeaf, entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf); + + context.ChangeTracker.DetectChanges(); + + // After DetectChanges, leaf should still be accessible + Assert.NotNull(entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf); + + ClearLog(); + await context.SaveChangesAsync(); + + // After SaveChanges, nested owned entities should still be accessible in memory + Assert.NotNull(entity.OwnedReferenceRoot.OwnedReferenceBranch); + Assert.NotNull(entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf); + Assert.Equal(originalLeafValue, entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); + }, + async context => + { + var result = await context.Set().SingleAsync(); + Assert.Equal("Modified", result.OwnedReferenceRoot.Name); + Assert.NotNull(result.OwnedReferenceRoot.OwnedReferenceBranch); + Assert.NotNull(result.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf); + }); + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction());