diff --git a/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs index 66a06c7a841..184d7fd993f 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs @@ -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); } diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs index 61cb0c20068..779e6e22814 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs @@ -15,6 +15,9 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal; /// 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 = @@ -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); } @@ -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; diff --git a/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs index 6d8eb0b6c12..720f9ef74ed 100644 --- a/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs @@ -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 diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs index 9b71bd20abb..9da17d6ac8f 100644 --- a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -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( + EntityState newState, + bool async, + Func 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().Where(e => EF.Property(e, "Name") == "The FBI").FirstAsync() + : context.Set().Where(e => EF.Property(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)] diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs index 6f9af8182de..336dfb181b9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -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 diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs index 3f500ae99d6..384f6c72abd 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalComplexEntryTest.cs @@ -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(); + + 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)