Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<InternalComplexEntry?>? _entries;
private List<InternalComplexEntry?>? _originalEntries;
private bool _isModified;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<TEntity>(bool trackFromQuery, Func<DbContext, TEntity> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down