diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs index d4c7996b72e..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 < entries.Count; i++) + var count = UseOldBehavior39073 ? entries.Count : collection.Count; + for (var i = 0; i < count; i++) { if (entries[i] != null) { @@ -151,7 +155,7 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP return _entries; } - private IList? GetCollection(bool original) + public IList? GetCollection(bool original) { if (!UseOldBehavior37585 && _containingEntry is InternalComplexEntry complexEntry) @@ -405,14 +409,20 @@ 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(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 bfaed78bae7..6d64cef0c6e 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs @@ -1165,7 +1165,11 @@ 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(UseOldBehavior39073 ? int.MaxValue : 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 384f6c72abd..9e45f154efd 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs @@ -767,6 +767,43 @@ 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 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", + NestedItems = [new NestedItem { Name = "baz" }] + } + ] + }; + + 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); + + changeDetector.DetectChanges(stateManager); + + Assert.Equal(EntityState.Deleted, entityEntry.GetComplexCollectionOriginalEntry(complexProperty, 1).EntityState); + } + private static IModel CreateModel() { var modelBuilder = InMemoryTestHelpers.Instance.CreateConventionBuilder(); @@ -781,6 +818,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 +880,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();