From 9042fdbea72754f9612bcf87b707a38eca5632f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:19:34 +0000 Subject: [PATCH 1/6] Initial plan From 4bcecf33cf1e61ef9ed9df1b664eecb24ea99b94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:34:32 +0000 Subject: [PATCH 2/6] Fix nullable complex property discriminator changes not being recognized (#38119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop marking complex type discriminators as PropertySaveBehavior.Throw so they can be properly included in update operations. Set discriminator values when nullable complex properties transition between null and non-null states. Add AppContext quirk for backward compatibility. Add tests for null→non-null, non-null→null, and nested scenarios. Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/ac816eeb-ca12-43b0-8cfe-a7e4aeb83167 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../ChangeTracking/Internal/ChangeDetector.cs | 16 ++ .../Internal/InternalComplexTypeBuilder.cs | 24 +++ .../Query/AdHocComplexTypeQueryTestBase.cs | 174 +++++++++++++++++- 3 files changed, 209 insertions(+), 5 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 5d2600518a3..a4e9afc79c7 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -24,6 +24,9 @@ public class ChangeDetector : IChangeDetector private static readonly bool UseOldBehavior37890 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37890", out var enabled) && enabled; + private static readonly bool UseOldBehavior38119 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", 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 @@ -346,6 +349,19 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple // to ensure the entity is detected as modified and the complex type properties are persisted if (!UseOldBehavior37890 || currentValue is not null) { + if (!UseOldBehavior38119) + { + // Set the discriminator value for the complex type when transitioning from null to non-null. + // The discriminator is a shadow property whose value needs to be updated to reflect the new state. + var discriminatorProperty = complexProperty.ComplexType.FindDiscriminatorProperty(); + if (discriminatorProperty != null) + { + entry[discriminatorProperty] = currentValue is not null + ? complexProperty.ComplexType.GetDiscriminatorValue() + : discriminatorProperty.ClrType.GetDefaultValue(); + } + } + foreach (var innerProperty in complexProperty.ComplexType.GetFlattenedProperties()) { // Only mark properties that are tracked and can be modified diff --git a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs index aa03bb7c0ff..de2c00c52e1 100644 --- a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs @@ -500,6 +500,30 @@ public virtual bool CanSetServiceOnlyConstructorBinding( => configurationSource.Overrides(Metadata.GetServiceOnlyConstructorBindingConfigurationSource()) || Metadata.ServiceOnlyConstructorBinding == constructorBinding; + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override InternalPropertyBuilder? GetOrCreateDiscriminatorProperty( + Type? type, + string? name, + MemberInfo? memberInfo, + ConfigurationSource configurationSource) + { + var builder = base.GetOrCreateDiscriminatorProperty(type, name, memberInfo, configurationSource); + if (!UseOldBehavior38119) + { + builder?.AfterSave(PropertySaveBehavior.Save, ConfigurationSource.Convention); + } + + return builder; + } + + private static readonly bool UseOldBehavior38119 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", 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 diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index b2123250eac..3ad24d5c34f 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -148,13 +148,23 @@ public virtual async Task Optional_complex_type_with_discriminator() return context.SaveChangesAsync(); }); - await using var context = contextFactory.CreateContext(); + await using (var context = contextFactory.CreateContext()) + { + var complexTypeNull = await context.Set() + .SingleAsync(b => b.AllOptionalsComplexType == null); + Assert.Null(complexTypeNull.AllOptionalsComplexType); - var complexTypeNull = await context.Set().SingleAsync(b => b.AllOptionalsComplexType == null); - Assert.Null(complexTypeNull.AllOptionalsComplexType); + complexTypeNull.AllOptionalsComplexType = + new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "New thing" }; + await context.SaveChangesAsync(); + } - complexTypeNull.AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "New thing" }; - await context.SaveChangesAsync(); + await using (var context = contextFactory.CreateContext()) + { + var entities = await context.Set().ToListAsync(); + Assert.Equal(3, entities.Count); + Assert.All(entities, e => Assert.NotNull(e.AllOptionalsComplexType)); + } } private class ContextShadowDiscriminator(DbContextOptions options) : DbContext(options) @@ -401,6 +411,160 @@ public class OptionalComplexProperty #endregion Issue37337 + #region Issue38119 + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add(new Context38119.EntityType { Id = Guid.NewGuid() }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.Null(entity.Prop); + + entity.Prop = new Context38119.OptionalComplexProperty { OptionalValue = true }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + + entity.Prop = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.Null(entity.Prop); + } + } + + [ConditionalFact] + public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context38119_Nested.EntityType + { + Id = Guid.NewGuid(), + Outer = new Context38119_Nested.OuterComplexProperty { Name = "outer" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Outer); + Assert.Null(entity.Outer.Inner); + + entity.Outer.Inner = new Context38119_Nested.InnerComplexProperty { Value = 42 }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Outer); + Assert.NotNull(entity.Outer.Inner); + Assert.Equal(42, entity.Outer.Inner.Value); + } + } + + private class Context38119(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).ValueGeneratedNever(); + + var compl = entity.ComplexProperty(p => p.Prop); + compl.HasDiscriminator(); + } + + public class EntityType + { + public Guid Id { get; set; } + public OptionalComplexProperty? Prop { get; set; } + } + + public class OptionalComplexProperty + { + public bool? OptionalValue { get; set; } + } + } + + private class Context38119_Nested(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).ValueGeneratedNever(); + + entity.ComplexProperty( + p => p.Outer, outer => + { + outer.ComplexProperty( + p => p.Inner, inner => inner.HasDiscriminator()); + }); + } + + public class EntityType + { + public Guid Id { get; set; } + public OuterComplexProperty Outer { get; set; } = null!; + } + + public class OuterComplexProperty + { + public string? Name { get; set; } + public InnerComplexProperty? Inner { get; set; } + } + + public class InnerComplexProperty + { + public int? Value { get; set; } + } + } + + #endregion Issue38119 + protected override string StoreName => "AdHocComplexTypeQueryTest"; } From 2c0997f8b40b70395734c20c22321f243c9b7dec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:38:44 +0000 Subject: [PATCH 3/6] Move UseOldBehavior38119 field declaration before method that uses it Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/ac816eeb-ca12-43b0-8cfe-a7e4aeb83167 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs index de2c00c52e1..0f56664eaba 100644 --- a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs @@ -500,6 +500,9 @@ public virtual bool CanSetServiceOnlyConstructorBinding( => configurationSource.Overrides(Metadata.GetServiceOnlyConstructorBindingConfigurationSource()) || Metadata.ServiceOnlyConstructorBinding == constructorBinding; + private static readonly bool UseOldBehavior38119 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", 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 @@ -521,9 +524,6 @@ public virtual bool CanSetServiceOnlyConstructorBinding( return builder; } - private static readonly bool UseOldBehavior38119 = - AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", 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 From 3161062d382ab1fb36d45e24b03f184aedce6da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:17:42 +0000 Subject: [PATCH 4/6] Address review: remove duplicate quirk field from ChangeDetector, use InternalComplexTypeBuilder.UseOldBehavior38119; fix comment Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/519f1a56-e9a0-4b07-acaf-d153dc80b390 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/ChangeTracking/Internal/ChangeDetector.cs | 7 ++----- src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index a4e9afc79c7..158214e385c 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -24,9 +24,6 @@ public class ChangeDetector : IChangeDetector private static readonly bool UseOldBehavior37890 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37890", out var enabled) && enabled; - private static readonly bool UseOldBehavior38119 = - AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", 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 @@ -349,9 +346,9 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple // to ensure the entity is detected as modified and the complex type properties are persisted if (!UseOldBehavior37890 || currentValue is not null) { - if (!UseOldBehavior38119) + if (!InternalComplexTypeBuilder.UseOldBehavior38119) { - // Set the discriminator value for the complex type when transitioning from null to non-null. + // Set the discriminator value for the complex type when transitioning from null to non-null or viceversa. // The discriminator is a shadow property whose value needs to be updated to reflect the new state. var discriminatorProperty = complexProperty.ComplexType.FindDiscriminatorProperty(); if (discriminatorProperty != null) diff --git a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs index 0f56664eaba..ff79ac03542 100644 --- a/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs @@ -500,7 +500,7 @@ public virtual bool CanSetServiceOnlyConstructorBinding( => configurationSource.Overrides(Metadata.GetServiceOnlyConstructorBindingConfigurationSource()) || Metadata.ServiceOnlyConstructorBinding == constructorBinding; - private static readonly bool UseOldBehavior38119 = + internal static readonly bool UseOldBehavior38119 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38119", out var enabled) && enabled; /// From b3d900ed475ed603e3e10bc7242805d56b1c3d53 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Fri, 17 Apr 2026 15:34:29 -0700 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ChangeTracking/Internal/ChangeDetector.cs | 2 +- .../Query/AdHocComplexTypeQueryTestBase.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 158214e385c..9f111a545f1 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -348,7 +348,7 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple { if (!InternalComplexTypeBuilder.UseOldBehavior38119) { - // Set the discriminator value for the complex type when transitioning from null to non-null or viceversa. + // Set the discriminator value for the complex type when transitioning from null to non-null or vice versa. // The discriminator is a shadow property whose value needs to be updated to reflect the new state. var discriminatorProperty = complexProperty.ComplexType.FindDiscriminatorProperty(); if (discriminatorProperty != null) diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index 3ad24d5c34f..01b117159f5 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -474,31 +474,31 @@ public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_n [ConditionalFact] public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeAsync( seed: context => { context.Add( - new Context38119_Nested.EntityType + new Context38119Nested.EntityType { Id = Guid.NewGuid(), - Outer = new Context38119_Nested.OuterComplexProperty { Name = "outer" } + Outer = new Context38119Nested.OuterComplexProperty { Name = "outer" } }); return context.SaveChangesAsync(); }); await using (var context = contextFactory.CreateContext()) { - var entity = await context.Set().SingleAsync(); + var entity = await context.Set().SingleAsync(); Assert.NotNull(entity.Outer); Assert.Null(entity.Outer.Inner); - entity.Outer.Inner = new Context38119_Nested.InnerComplexProperty { Value = 42 }; + entity.Outer.Inner = new Context38119Nested.InnerComplexProperty { Value = 42 }; await context.SaveChangesAsync(); } await using (var context = contextFactory.CreateContext()) { - var entity = await context.Set().SingleAsync(); + var entity = await context.Set().SingleAsync(); Assert.NotNull(entity.Outer); Assert.NotNull(entity.Outer.Inner); Assert.Equal(42, entity.Outer.Inner.Value); @@ -529,7 +529,7 @@ public class OptionalComplexProperty } } - private class Context38119_Nested(DbContextOptions options) : DbContext(options) + private class Context38119Nested(DbContextOptions options) : DbContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { From 689c8d874c798acf76125eecb8ac98274c36b572 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:01:35 +0000 Subject: [PATCH 6/6] Guard discriminator with IsShadowProperty when null; add Update, explicit discriminator value, and null discriminator tests Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/153c52d0-a077-43d9-8ef7-dbf5e588039f Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../ChangeTracking/Internal/ChangeDetector.cs | 11 ++- .../Query/AdHocComplexTypeQueryTestBase.cs | 96 +++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 9f111a545f1..d3f23016795 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -353,9 +353,14 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple var discriminatorProperty = complexProperty.ComplexType.FindDiscriminatorProperty(); if (discriminatorProperty != null) { - entry[discriminatorProperty] = currentValue is not null - ? complexProperty.ComplexType.GetDiscriminatorValue() - : discriminatorProperty.ClrType.GetDefaultValue(); + if (currentValue is not null) + { + entry[discriminatorProperty] = complexProperty.ComplexType.GetDiscriminatorValue(); + } + else if (discriminatorProperty.IsShadowProperty()) + { + entry[discriminatorProperty] = discriminatorProperty.ClrType.GetDefaultValue(); + } } } diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index 01b117159f5..0ce294f63c2 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -471,6 +471,102 @@ public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_n } } + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + + context.Update(entity); + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_set_to_different_value() + { + var contextFactory = await InitializeAsync(); + + Guid entityId; + await using (var context = contextFactory.CreateContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + // Override the discriminator value before saving + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = "SomeOtherValue"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + // The discriminator is non-null so the complex property is still materialized + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_set_to_null() + { + var contextFactory = await InitializeAsync(); + + Guid entityId; + await using (var context = contextFactory.CreateContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + // Set discriminator to null before saving, which should cause the complex property to be null on reload + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateContext()) + { + // With null discriminator, the complex property should be materialized as null + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.Null(entity.Prop); + } + } + [ConditionalFact] public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() {