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
5 changes: 5 additions & 0 deletions src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ protected override void SetEntityState(EntityState oldState, EntityState newStat
if (oldState is EntityState.Detached or EntityState.Deleted
&& newState is not EntityState.Detached and not EntityState.Deleted)
{
if (!UseOldBehavior37724 && Ordinal == -1)
{
Ordinal = OriginalOrdinal;
}

ContainingEntry.ValidateOrdinal(this, original: false);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
/// </summary>
public partial class InternalEntryBase
{
internal static readonly bool UseOldBehavior37724 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37724", out var enabled) && enabled;

private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexProperty complexCollection)
{
private static readonly bool UseOldBehavior37585 =
Expand Down Expand Up @@ -463,7 +466,7 @@ public void HandleStateChange(InternalComplexEntry entry, EntityState oldState,
}

// When going from Deleted to Unchanged, restore the currentEntry to the original collection
if (newState == EntityState.Unchanged)
if (UseOldBehavior37724 && newState == EntityState.Unchanged)
{
InsertEntry(entry, original: true);
}
Expand Down Expand Up @@ -629,8 +632,12 @@ public readonly void InsertEntry(InternalComplexEntry entry, bool original = fal
}

var ordinal = ValidateOrdinal(entry, original, entries);
if (entries[ordinal] == entry
|| entries[ordinal] == null)
if (entries[ordinal] == entry)
{
return;
}

if (entries[ordinal] == null)
{
entries[ordinal] = entry;
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ public override Task Can_save_default_values_in_optional_complex_property_with_m
// See https://github.com/dotnet/efcore/issues/31464
=> Task.CompletedTask;

// Complex type collections are not supported in InMemory provider
// See https://github.com/dotnet/efcore/issues/31464
public override Task Can_change_state_from_Deleted_with_complex_collection(EntityState newState, bool async)
=> Task.CompletedTask;

// Complex type collections are not supported in InMemory provider
// See https://github.com/dotnet/efcore/issues/31464
public override Task Can_change_state_from_Deleted_with_complex_field_collection(EntityState newState, bool async)
=> Task.CompletedTask;

// Complex type collections are not supported in InMemory provider
// See https://github.com/dotnet/efcore/issues/31464
public override Task Can_change_state_from_Deleted_with_complex_field_record_collection(EntityState newState, bool async)
=> Task.CompletedTask;

// Complex type collections are not supported in InMemory provider
// See https://github.com/dotnet/efcore/issues/31464
public override Task Can_change_state_from_Deleted_with_complex_record_collection(EntityState newState, bool async)
=> Task.CompletedTask;

public class InMemoryFixture : FixtureBase
{
protected override ITestStoreFactory TestStoreFactory
Expand Down
98 changes: 98 additions & 0 deletions test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,104 @@ public virtual void Can_read_original_values_for_properties_of_complex_type_coll
public virtual void Can_write_original_values_for_properties_of_complex_type_collections(bool trackFromQuery)
=> WriteOriginalValuesTest(trackFromQuery, CreatePubWithCollections);

[ConditionalTheory]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreatePubWithCollections);

[ConditionalTheory(Skip = "Issue #31411")]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_struct_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreatePubWithStructCollections);

[ConditionalTheory(Skip = "Issue #31621")]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_readonly_struct_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreatePubWithReadonlyStructCollections);

[ConditionalTheory]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_record_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreatePubWithRecordCollections);

[ConditionalTheory]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_field_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreateFieldCollectionPub);

[ConditionalTheory(Skip = "Issue #31411")]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_field_struct_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreateFieldCollectionPubWithStructs);

[ConditionalTheory(Skip = "Issue #31621")]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_field_readonly_struct_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreateFieldCollectionPubWithReadonlyStructs);

[ConditionalTheory]
[InlineData(EntityState.Unchanged, false)]
[InlineData(EntityState.Unchanged, true)]
[InlineData(EntityState.Modified, false)]
[InlineData(EntityState.Modified, true)]
public virtual Task Can_change_state_from_Deleted_with_complex_field_record_collection(EntityState newState, bool async)
=> ChangeStateFromDeletedTest(newState, async, CreateFieldCollectionPubWithRecords);

private async Task ChangeStateFromDeletedTest<TEntity>(
EntityState newState,
bool async,
Func<DbContext, TEntity> createPub)
where TEntity : class
{
await ExecuteWithStrategyInTransactionAsync(
async context =>
{
var pub = createPub(context);
context.Add(pub);
_ = async ? await context.SaveChangesAsync() : context.SaveChanges();
},
async context =>
{
var pub = async
? await context.Set<TEntity>().Where(e => EF.Property<string>(e, "Name") == "The FBI").FirstAsync()
: context.Set<TEntity>().Where(e => EF.Property<string>(e, "Name") == "The FBI").First();
var entry = context.Entry(pub);

entry.State = EntityState.Deleted;

// Change to target state - this should not throw an exception
entry.State = newState;
Assert.Equal(newState, entry.State);

// Verify the complex collection is still accessible
var activitiesEntry = entry.ComplexCollection("Activities");
Assert.NotNull(activitiesEntry);
var activitiesValue = activitiesEntry.CurrentValue;
Assert.Equal(2, ((System.Collections.IList)activitiesValue!).Count);
});
}

[ConditionalTheory(Skip = "Issue #31411"), InlineData(EntityState.Added, false), InlineData(EntityState.Added, true),
InlineData(EntityState.Unchanged, false), InlineData(EntityState.Unchanged, true), InlineData(EntityState.Modified, false),
InlineData(EntityState.Modified, true), InlineData(EntityState.Deleted, false), InlineData(EntityState.Deleted, true)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,22 @@ public override void Can_write_original_values_for_properties_of_complex_propert
public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
=> Task.CompletedTask;

// Fields can't be proxied
public override Task Can_change_state_from_Deleted_with_complex_field_collection(EntityState newState, bool async)
=> Task.CompletedTask;

// Fields can't be proxied
public override Task Can_change_state_from_Deleted_with_complex_field_record_collection(EntityState newState, bool async)
=> Task.CompletedTask;

// Issue #36175: Complex types with notification change tracking are not supported
public override Task Can_change_state_from_Deleted_with_complex_collection(EntityState newState, bool async)
=> Task.CompletedTask;

// Issue #36175: Complex types with notification change tracking are not supported
public override Task Can_change_state_from_Deleted_with_complex_record_collection(EntityState newState, bool async)
=> Task.CompletedTask;

public class SqlServerFixture : SqlServerFixtureBase
{
protected override string StoreName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,47 @@ public void Multiple_complex_entries_state_changes_maintain_correct_ordinals()
Assert.True(entityEntry.IsModified(complexProperty));
}

[ConditionalFact]
public void Can_change_entity_state_from_Deleted_to_Unchanged_with_complex_collection()
{
var model = CreateModel();
var entityType = model.FindEntityType(typeof(Blog))!;
var complexProperty = entityType.FindComplexProperty(nameof(Blog.Tags))!;

var serviceProvider = InMemoryTestHelpers.Instance.CreateContextServices(model);
var stateManager = serviceProvider.GetRequiredService<IStateManager>();

var blog = new Blog
{
Tags =
[
new Tag { Name = "Tag0" },
new Tag { Name = "Tag1" },
new Tag { Name = "Tag2" }
]
};
var entityEntry = stateManager.GetOrCreateEntry(blog);
entityEntry.SetEntityState(EntityState.Unchanged);

// Simulate soft delete pattern: set to Deleted then back to Unchanged
entityEntry.SetEntityState(EntityState.Deleted);
Assert.Equal(EntityState.Deleted, entityEntry.EntityState);

// This should not throw an exception about invalid ordinal
entityEntry.SetEntityState(EntityState.Unchanged);
Assert.Equal(EntityState.Unchanged, entityEntry.EntityState);

// Verify complex collection entries are still accessible
var entries = entityEntry.GetComplexCollectionEntries(complexProperty);
Assert.Equal(3, entries.Count);
Assert.NotNull(entries[0]);
Assert.NotNull(entries[1]);
Assert.NotNull(entries[2]);
Assert.Equal(0, entries[0]!.Ordinal);
Assert.Equal(1, entries[1]!.Ordinal);
Assert.Equal(2, entries[2]!.Ordinal);
}

[ConditionalTheory, InlineData(EntityState.Unchanged), InlineData(EntityState.Modified), InlineData(EntityState.Added),
InlineData(EntityState.Deleted)]
public void Complex_collection_detects_additions_and_deletions(EntityState entityState)
Expand Down