Skip to content
5 changes: 3 additions & 2 deletions src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ private bool PaintAction(
SetReferenceLoaded(node);

var internalEntityEntry = node.GetInfrastructure();
if (internalEntityEntry.EntityState != EntityState.Detached

if (internalEntityEntry.EntityState is not (EntityState.Detached or EntityState.Deleted)
|| (_visited != null && _visited.Contains(internalEntityEntry.Entity)))
{
Comment thread
AndriySvyryd marked this conversation as resolved.
return false;
Expand Down Expand Up @@ -139,7 +140,7 @@ private async Task<bool> PaintActionAsync(
SetReferenceLoaded(node);

var internalEntityEntry = node.GetInfrastructure();
if (internalEntityEntry.EntityState != EntityState.Detached
if (internalEntityEntry.EntityState is not (EntityState.Detached or EntityState.Deleted)
|| (_visited != null && _visited.Contains(internalEntityEntry.Entity)))
{
return false;
Expand Down
11 changes: 11 additions & 0 deletions src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,17 @@ public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumer
continue;
}

// 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 added via SharedIdentityEntry. If a nested
// dependent was re-used by the new entity graph, FindPrincipal returns the replacement principal
// from the identity map rather than the deleted entry. In that case, skip the cascade to avoid
// incorrectly re-deleting the dependent that now belongs to the replacement principal.
Comment thread
AndriySvyryd marked this conversation as resolved.
Outdated
var currentPrincipal = FindPrincipal(dependent, fk);
if (currentPrincipal != null && currentPrincipal != entry)
{
continue;
}

if (detectChangesEnabled)
{
ChangeDetector.DetectChanges(dependent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3671,6 +3671,64 @@ 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,
Comment thread
AndriySvyryd marked this conversation as resolved.
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<JsonEntityBasic>().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());

Expand Down