From e776b674abc620bf390bc039ddc4a95fe224d304 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:09:14 +0000 Subject: [PATCH 1/4] Initial plan From ba3dbad0bcbb026cddb3b2acee0ccbcf0ebdcc8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:18:58 +0000 Subject: [PATCH 2/4] Fix update after nested complex collection shrinks Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- ...ntryBase.InternalComplexCollectionEntry.cs | 21 ++++++-- .../Internal/InternalComplexEntryTest.cs | 50 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs index d4c7996b72e..06c1a281ccf 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs @@ -156,10 +156,8 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP if (!UseOldBehavior37585 && _containingEntry is InternalComplexEntry complexEntry) { - var ordinal = original ? complexEntry.OriginalOrdinal : complexEntry.Ordinal; - if (ordinal < 0) + if (!IsInCollection(complexEntry, original)) { - // Ordinal is -1 (entry is deleted/added), so the collection doesn't exist. return null; } } @@ -169,6 +167,23 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP : (IList?)_containingEntry[_complexCollection]; } + private static bool IsInCollection(InternalComplexEntry entry, bool original) + { + var ordinal = original ? entry.OriginalOrdinal : entry.Ordinal; + if (ordinal < 0 + || (entry.ContainingEntry is InternalComplexEntry containingEntry + && !IsInCollection(containingEntry, original))) + { + return false; + } + + var collection = original + ? (IList?)entry.ContainingEntry.GetOriginalValue(entry.ComplexProperty) + : (IList?)entry.ContainingEntry[entry.ComplexProperty]; + + return ordinal < (collection?.Count ?? 0); + } + public void AcceptChanges() { _isModified = false; diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs index 384f6c72abd..2d196c54049 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs @@ -767,6 +767,32 @@ public void DetectChanges_detects_changes_in_nested_complex_collections() Assert.Equal(EntityState.Added, items[2].State); } + [ConditionalFact] + public void Entity_can_be_marked_modified_after_nested_complex_collection_shrinks() + { + var model = CreateModelWithNestedComplexCollection(); + var serviceProvider = InMemoryTestHelpers.Instance.CreateContextServices(model); + var stateManager = serviceProvider.GetRequiredService(); + + var blog = new BlogWithNestedCollection + { + Items = + [ + new NestedCollectionItem { Name = "foo" }, + new NestedCollectionItem { Name = "bar" } + ] + }; + + var entityEntry = stateManager.GetOrCreateEntry(blog); + entityEntry.SetEntityState(EntityState.Unchanged); + + blog.Items = [new NestedCollectionItem { Name = "foo" }]; + + entityEntry.SetEntityState(EntityState.Modified); + + Assert.Equal(EntityState.Modified, entityEntry.EntityState); + } + private static IModel CreateModel() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); @@ -781,6 +807,18 @@ private static IModel CreateModel() return modelBuilder.FinalizeModel(); } + private static IModel CreateModelWithNestedComplexCollection() + { + var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); + + modelBuilder.Entity( + b => b.ComplexCollection( + e => e.Items, + b => b.ComplexCollection(e => e.NestedItems))); + + return modelBuilder.FinalizeModel(); + } + private static IModel CreateModelWithNestedComplexCollections() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); @@ -831,6 +869,18 @@ private class BlogWithNested public NestedJson NestedJson { get; set; } = new(); } + private class BlogWithNestedCollection + { + public int Id { get; set; } + public List Items { get; set; } = []; + } + + private class NestedCollectionItem + { + public string Name { get; set; } = ""; + public List NestedItems { get; set; } = []; + } + private record NestedJson { public NestedItem Item { get; init; } = new(); From df72b12dcfe228123fae3570d5a730457cdcd2bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:34:51 +0000 Subject: [PATCH 3/4] Refine complex collection state traversal Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- ...ntryBase.InternalComplexCollectionEntry.cs | 35 ++++++------------- .../Internal/InternalEntryBase.cs | 4 ++- .../Internal/InternalComplexEntryTest.cs | 13 ++++++- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs index 06c1a281ccf..14b48d11baa 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs @@ -46,7 +46,7 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP && (defaultState != EntityState.Deleted || original) && (defaultState != EntityState.Added || !original)) { - for (var i = 0; i < entries.Count; i++) + for (var i = 0; i < collection.Count; i++) { if (entries[i] != null) { @@ -151,13 +151,15 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP return _entries; } - private IList? GetCollection(bool original) + public IList? GetCollection(bool original) { if (!UseOldBehavior37585 && _containingEntry is InternalComplexEntry complexEntry) { - if (!IsInCollection(complexEntry, original)) + var ordinal = original ? complexEntry.OriginalOrdinal : complexEntry.Ordinal; + if (ordinal < 0) { + // Ordinal is -1 (entry is deleted/added), so the collection doesn't exist. return null; } } @@ -167,23 +169,6 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP : (IList?)_containingEntry[_complexCollection]; } - private static bool IsInCollection(InternalComplexEntry entry, bool original) - { - var ordinal = original ? entry.OriginalOrdinal : entry.Ordinal; - if (ordinal < 0 - || (entry.ContainingEntry is InternalComplexEntry containingEntry - && !IsInCollection(containingEntry, original))) - { - return false; - } - - var collection = original - ? (IList?)entry.ContainingEntry.GetOriginalValue(entry.ComplexProperty) - : (IList?)entry.ContainingEntry[entry.ComplexProperty]; - - return ordinal < (collection?.Count ?? 0); - } - public void AcceptChanges() { _isModified = false; @@ -420,14 +405,16 @@ public void SetState(EntityState oldState, EntityState newState, bool acceptChan setOriginalState = true; } - EnsureCapacity(GetCollection(original: true)?.Count ?? 0, original: true, trim: false); - EnsureCapacity(GetCollection(original: false)?.Count ?? 0, original: false, trim: false); + var originalCollectionCount = GetCollection(original: true)?.Count ?? 0; + var currentCollectionCount = GetCollection(original: false)?.Count ?? 0; + EnsureCapacity(originalCollectionCount, original: true, trim: false); + EnsureCapacity(currentCollectionCount, original: false, trim: false); var defaultState = newState == EntityState.Modified && !modifyProperties ? EntityState.Unchanged : newState; - var originalEntries = GetOrCreateEntries(original: true, defaultState).ToArray(); - var currentEntries = GetOrCreateEntries(original: false, defaultState).ToArray(); + var originalEntries = GetOrCreateEntries(original: true, defaultState).Take(originalCollectionCount).ToArray(); + var currentEntries = GetOrCreateEntries(original: false, defaultState).Take(currentCollectionCount).ToArray(); if (setOriginalState) { foreach (var originalEntry in originalEntries) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs index bfaed78bae7..b2fd8613773 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs @@ -1165,7 +1165,9 @@ public InternalComplexEntry GetComplexCollectionOriginalEntry(IComplexProperty p /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public IEnumerable GetFlattenedComplexEntries() - => _complexCollectionEntries.SelectMany(c => c.GetOrCreateEntries(original: false)).Where(e => e != null)!; + => _complexCollectionEntries + .SelectMany(c => c.GetOrCreateEntries(original: false).Take(c.GetCollection(original: false)?.Count ?? 0)) + .Where(e => e != null)!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs index 2d196c54049..9e45f154efd 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs @@ -771,15 +771,22 @@ public void DetectChanges_detects_changes_in_nested_complex_collections() public void Entity_can_be_marked_modified_after_nested_complex_collection_shrinks() { var model = CreateModelWithNestedComplexCollection(); + var complexProperty = model.FindEntityType(typeof(BlogWithNestedCollection))! + .FindComplexProperty(nameof(BlogWithNestedCollection.Items))!; var serviceProvider = InMemoryTestHelpers.Instance.CreateContextServices(model); var stateManager = serviceProvider.GetRequiredService(); + var changeDetector = serviceProvider.GetRequiredService(); var blog = new BlogWithNestedCollection { Items = [ new NestedCollectionItem { Name = "foo" }, - new NestedCollectionItem { Name = "bar" } + new NestedCollectionItem + { + Name = "bar", + NestedItems = [new NestedItem { Name = "baz" }] + } ] }; @@ -791,6 +798,10 @@ public void Entity_can_be_marked_modified_after_nested_complex_collection_shrink entityEntry.SetEntityState(EntityState.Modified); Assert.Equal(EntityState.Modified, entityEntry.EntityState); + + changeDetector.DetectChanges(stateManager); + + Assert.Equal(EntityState.Deleted, entityEntry.GetComplexCollectionOriginalEntry(complexProperty, 1).EntityState); } private static IModel CreateModel() From 811bba5750f8f76b3a8f38f922013e1cdb58dc8b Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 24 Sep 2026 19:25:34 -0700 Subject: [PATCH 4/4] Add servicing quirk for complex collection update fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...rnalEntryBase.InternalComplexCollectionEntry.cs | 14 +++++++++++--- .../ChangeTracking/Internal/InternalEntryBase.cs | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs index 14b48d11baa..f4d7d0dc5a1 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs @@ -21,6 +21,9 @@ public partial class InternalEntryBase internal static readonly bool UseOldBehavior38299 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38299", out var enabled) && enabled; + internal static readonly bool UseOldBehavior39073 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue39073", out var enabled) && enabled; + private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexProperty complexCollection) { private static readonly bool UseOldBehavior37585 = @@ -46,7 +49,8 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP && (defaultState != EntityState.Deleted || original) && (defaultState != EntityState.Added || !original)) { - for (var i = 0; i < collection.Count; i++) + var count = UseOldBehavior39073 ? entries.Count : collection.Count; + for (var i = 0; i < count; i++) { if (entries[i] != null) { @@ -413,8 +417,12 @@ public void SetState(EntityState oldState, EntityState newState, bool acceptChan var defaultState = newState == EntityState.Modified && !modifyProperties ? EntityState.Unchanged : newState; - var originalEntries = GetOrCreateEntries(original: true, defaultState).Take(originalCollectionCount).ToArray(); - var currentEntries = GetOrCreateEntries(original: false, defaultState).Take(currentCollectionCount).ToArray(); + var originalEntries = GetOrCreateEntries(original: true, defaultState) + .Take(UseOldBehavior39073 ? int.MaxValue : originalCollectionCount) + .ToArray(); + var currentEntries = GetOrCreateEntries(original: false, defaultState) + .Take(UseOldBehavior39073 ? int.MaxValue : currentCollectionCount) + .ToArray(); if (setOriginalState) { foreach (var originalEntry in originalEntries) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs index b2fd8613773..6d64cef0c6e 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs @@ -1166,7 +1166,9 @@ public InternalComplexEntry GetComplexCollectionOriginalEntry(IComplexProperty p /// public IEnumerable GetFlattenedComplexEntries() => _complexCollectionEntries - .SelectMany(c => c.GetOrCreateEntries(original: false).Take(c.GetCollection(original: false)?.Count ?? 0)) + .SelectMany( + c => c.GetOrCreateEntries(original: false) + .Take(UseOldBehavior39073 ? int.MaxValue : c.GetCollection(original: false)?.Count ?? 0)) .Where(e => e != null)!; ///