Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/EFCore/ChangeTracking/Internal/EntityGraphAttacher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -114,7 +118,7 @@ private bool PaintAction(

if (internalEntityEntry.StateManager.ResolveToExistingEntry(
internalEntityEntry,
node.InboundNavigation, node.SourceEntry?.GetInfrastructure()))
node.InboundNavigation, sourceEntry))
{
(_visited ??= new HashSet<object>(ReferenceEqualityComparer.Instance)).Add(internalEntityEntry.Entity);
}
Expand All @@ -139,7 +143,10 @@ private async Task<bool> 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;
Expand All @@ -151,7 +158,7 @@ private async Task<bool> PaintActionAsync(

if (internalEntityEntry.StateManager.ResolveToExistingEntry(
internalEntityEntry,
node.InboundNavigation, node.SourceEntry?.GetInfrastructure()))
node.InboundNavigation, sourceEntry))
{
(_visited ??= []).Add(internalEntityEntry.Entity);
}
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,14 @@ public virtual void CascadeChanges(bool force)
/// </summary>
public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumerable<IForeignKey>? 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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