diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs index 93f2cb812fe..61cb0c20068 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs @@ -17,6 +17,9 @@ public partial class InternalEntryBase { private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexProperty complexCollection) { + private static readonly bool UseOldBehavior37585 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37585", out var enabled) && enabled; + private List? _entries; private List? _originalEntries; private bool _isModified; @@ -27,9 +30,7 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP bool original, EntityState defaultState = EntityState.Detached) { - var collection = original - ? (IList?)_containingEntry.GetOriginalValue(_complexCollection) - : (IList?)_containingEntry[_complexCollection]; + var collection = GetCollection(original); var entries = EnsureCapacity(collection?.Count ?? 0, original, trim: false); if (collection != null && defaultState != EntityState.Detached @@ -141,6 +142,24 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP return _entries; } + private IList? GetCollection(bool original) + { + if (!UseOldBehavior37585 + && _containingEntry is InternalComplexEntry complexEntry) + { + 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; + } + } + + return original + ? (IList?)_containingEntry.GetOriginalValue(_complexCollection) + : (IList?)_containingEntry[_complexCollection]; + } + public void AcceptChanges() { _isModified = false; @@ -360,12 +379,8 @@ public void SetState(EntityState oldState, EntityState newState, bool acceptChan setOriginalState = true; } - EnsureCapacity( - ((IList?)_containingEntry.GetOriginalValue(_complexCollection))?.Count ?? 0, - original: true, trim: false); - EnsureCapacity( - ((IList?)_containingEntry[_complexCollection])?.Count ?? 0, - original: false, trim: false); + EnsureCapacity(GetCollection(original: true)?.Count ?? 0, original: true, trim: false); + EnsureCapacity(GetCollection(original: false)?.Count ?? 0, original: false, trim: false); var defaultState = newState == EntityState.Modified && !modifyProperties ? EntityState.Unchanged diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs index 1ecb40e6bb0..9b71bd20abb 100644 --- a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; namespace Microsoft.EntityFrameworkCore; @@ -1376,6 +1377,73 @@ public virtual void Can_detect_duplicates_in_complex_type_collections(bool track Assert.Equal(EntityState.Added, activitiesEntry[2].State); } + [ConditionalTheory, InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreatePubWithCollections); + + [ConditionalTheory(Skip = "Issue #31411"), InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_struct_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreatePubWithStructCollections); + + [ConditionalTheory(Skip = "Issue #31411"), InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_readonly_struct_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreatePubWithReadonlyStructCollections); + + [ConditionalTheory(Skip = "Issue #36483"), InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_record_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreatePubWithRecordCollections); + + [ConditionalTheory, InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_field_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreateFieldCollectionPub); + + [ConditionalTheory(Skip = "Issue #31411"), InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_struct_field_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreateFieldCollectionPubWithStructs); + + [ConditionalTheory(Skip = "Issue #31411"), InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_readonly_struct_field_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreateFieldCollectionPubWithReadonlyStructs); + + [ConditionalTheory(Skip = "Issue #36483"), InlineData(false), InlineData(true)] + public virtual void Can_remove_from_complex_record_field_collection_with_nested_complex_collection(bool trackFromQuery) + => RemoveFromComplexCollectionWithNestedCollectionTest(trackFromQuery, CreateFieldCollectionPubWithRecords); + + private void RemoveFromComplexCollectionWithNestedCollectionTest(bool trackFromQuery, Func createPub) + where TEntity : class + { + using var context = CreateContext(); + var pub = createPub(context); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + + var activitiesProperty = entry.Metadata.FindComplexProperty("Activities")!; + var activities = (IList)activitiesProperty.GetGetter().GetClrValue(pub)!; + var originalCount = activities.Count; + Assert.True(originalCount > 0); + + activities.RemoveAt(0); + + context.ChangeTracker.DetectChanges(); + + var collectionEntry = entry.ComplexCollection("Activities"); + var internalEntry = entry.GetInfrastructure(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(collectionEntry.IsModified); + Assert.Equal([-1, 0], internalEntry.GetComplexCollectionOriginalEntries(collectionEntry.Metadata).Select(e => e?.Ordinal)); + Assert.Equal([1], internalEntry.GetComplexCollectionEntries(collectionEntry.Metadata).Select(e => e?.OriginalOrdinal)); + + context.ChangeTracker.AcceptAllChanges(); + + Assert.Equal(EntityState.Unchanged, entry.State); + Assert.False(collectionEntry.IsModified); + Assert.Equal([0], internalEntry.GetComplexCollectionOriginalEntries(collectionEntry.Metadata).Select(e => e?.Ordinal)); + Assert.Equal([0], internalEntry.GetComplexCollectionEntries(collectionEntry.Metadata).Select(e => e?.OriginalOrdinal)); + } + [ConditionalTheory, InlineData(false), InlineData(true)] public virtual void Can_handle_null_elements_in_complex_type_collections(bool trackFromQuery) { diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs index 0f0df7ca8e7..6f9af8182de 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -262,6 +262,16 @@ public override void Can_detect_swapped_complex_objects_in_collections(bool trac { } + // Issue #36175: Complex types with notification change tracking are not supported + public override void Can_remove_from_complex_collection_with_nested_complex_collection(bool trackFromQuery) + { + } + + // Fields can't be proxied + public override void Can_remove_from_complex_field_collection_with_nested_complex_collection(bool trackFromQuery) + { + } + // Issue #36175: Complex types with notification change tracking are not supported public override void Throws_when_accessing_complex_entries_using_incorrect_cardinality() {