From 2d884ea60af91bc4d743573f5480c20992916bc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:45:25 +0000 Subject: [PATCH] Port PR #37944 to release/10.0: fix persisting null optional complex property with default values Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../ChangeTracking/Internal/ChangeDetector.cs | 7 ++- .../ComplexTypesTrackingInMemoryTest.cs | 5 ++ .../ComplexTypesTrackingTestBase.cs | 50 +++++++++++++++++++ .../ComplexTypesTrackingSqlServerTest.cs | 4 ++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 6497a886d8b..5d2600518a3 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -21,6 +21,9 @@ public class ChangeDetector : IChangeDetector private static readonly bool UseOldBehavior37387 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37387", out var enabled) && enabled; + private static readonly bool UseOldBehavior37890 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37890", out var enabled) && enabled; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -339,9 +342,9 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple if ((currentValue is null) != (originalValue is null)) { - // If it changed from null to non-null, mark all inner properties as modified + // If it changed from null to non-null or from non-null to null, mark all inner properties as modified // to ensure the entity is detected as modified and the complex type properties are persisted - if (currentValue is not null) + if (!UseOldBehavior37890 || currentValue is not null) { foreach (var innerProperty in complexProperty.ComplexType.GetFlattenedProperties()) { diff --git a/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs index 720f9ef74ed..e420a64e5ce 100644 --- a/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs @@ -27,6 +27,11 @@ public override Task Can_save_default_values_in_optional_complex_property_with_m // See https://github.com/dotnet/efcore/issues/31464 => Task.CompletedTask; + public override Task Can_null_complex_property_with_default_values_and_multiple_properties(bool async) + // InMemory provider has issues with complex type query compilation and materialization + // 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) diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs index 9da17d6ac8f..4cddf3f8cd9 100644 --- a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -4619,6 +4619,56 @@ await ExecuteWithStrategyInTransactionAsync( }); } + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Can_null_complex_property_with_default_values_and_multiple_properties(bool async) + { + await ExecuteWithStrategyInTransactionAsync( + async context => + { + var entity = Fixture.UseProxies + ? context.CreateProxy() + : new EntityWithOptionalMultiPropComplex(); + + entity.Id = Guid.NewGuid(); + // Set the complex property with default values + entity.ComplexProp = new MultiPropComplex + { + IntValue = 0, + BoolValue = false, + DateValue = default, + }; + + _ = async ? await context.AddAsync(entity) : context.Add(entity); + _ = async ? await context.SaveChangesAsync() : context.SaveChanges(); + + Assert.NotNull(entity.ComplexProp); + }, + async context => + { + var entity = async + ? await context.Set().SingleAsync() + : context.Set().Single(); + + Assert.NotNull(entity.ComplexProp); + + entity.ComplexProp = null; + + _ = async ? await context.SaveChangesAsync() : context.SaveChanges(); + + Assert.Null(entity.ComplexProp); + }, + async context => + { + var entity = async + ? await context.Set().SingleAsync() + : context.Set().Single(); + + Assert.Null(entity.ComplexProp); + }); + } + public class EntityWithOptionalMultiPropComplex { public virtual Guid Id { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs index 336dfb181b9..387decc4724 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -352,6 +352,10 @@ 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; + // Issue #36175: Complex types with notification change tracking are not supported + public override Task Can_null_complex_property_with_default_values_and_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;