diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 59c46d11b10..b1128d8b157 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -302,8 +302,18 @@ public static Expression CreateMemberAccess( break; } } - else + else if (!fromDeclaringType + || !addNullCheck + || property is not IComplexProperty complexProperty + || instanceExpression.Type.IsValueType + || complexProperty.ClrType.IsValueType) { + // Disable null check for all cases except: + // - fromDeclaringType is true AND + // - addNullCheck is true AND + // - property is a complex property AND + // - instance is a reference type AND + // - complex property type is a reference type addNullCheck = false; } diff --git a/test/EFCore.SqlServer.FunctionalTests/TPHComplexPropertySharingTest.cs b/test/EFCore.SqlServer.FunctionalTests/TPHComplexPropertySharingTest.cs new file mode 100644 index 00000000000..e4576623c1b --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/TPHComplexPropertySharingTest.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class TPHComplexPropertySharingTest(TPHComplexPropertySharingTest.TPHComplexPropertySharingFixture fixture) + : IClassFixture +{ + protected TPHComplexPropertySharingFixture Fixture { get; } = fixture; + + [ConditionalFact] + public virtual async Task Can_save_and_query_TPH_with_shared_complex_property_column() + { + await using var context = CreateContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + context.Items.Add(new Item1 { Name = "Item 1" }); + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + var item = await context.Items.FirstAsync(); + Assert.NotNull(item); + Assert.Equal("Item 1", item.Name); + } + + [ConditionalFact] + public virtual async Task Can_read_original_values_with_shared_complex_property_column() + { + await using var context = CreateContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + context.Items.Add(new Item1 { Name = "Item 1" }); + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + var item = await context.Items.FirstAsync(); + var entry = context.ChangeTracker.Entries().First(); + var originalItem = (Item)entry.OriginalValues.ToObject(); + + Assert.NotNull(originalItem); + Assert.Equal("Item 1", originalItem.Name); + } + + [ConditionalFact] + public virtual async Task Can_save_and_query_TPH_with_shared_complex_property_column_with_value() + { + await using var context = CreateContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + context.Items.Add(new Item1 { Name = "Item 1", Pricing = new ItemPrice { Amount = "10.99" } }); + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + var item = await context.Items.OfType().FirstAsync(); + Assert.NotNull(item.Pricing); + Assert.Equal("10.99", item.Pricing.Amount); + } + + [ConditionalFact] + public virtual async Task Can_read_original_values_with_shared_complex_property_column_with_value() + { + await using var context = CreateContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + context.Items.Add(new Item1 { Name = "Item 1", Pricing = new ItemPrice { Amount = "10.99" } }); + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + var item = await context.Items.OfType().FirstAsync(); + var entry = context.ChangeTracker.Entries().First(); + var originalItem = (Item)entry.OriginalValues.ToObject(); + + Assert.NotNull(originalItem); + Assert.Equal("Item 1", originalItem.Name); + var item1 = Assert.IsType(originalItem); + Assert.NotNull(item1.Pricing); + Assert.Equal("10.99", item1.Pricing.Amount); + } + + protected ItemContext CreateContext() + => Fixture.CreateContext(); + + public class TPHComplexPropertySharingFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "TPHComplexPropertySharingTest"; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + } +} + +public class ItemContext(DbContextOptions options) : DbContext(options) +{ + public required DbSet Items { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasDiscriminator("Discriminator") + .HasValue("Item1") + .HasValue("Item2"); + + modelBuilder.Entity().ComplexProperty( + x => x.Pricing, + p => p.Property(a => a.Amount).HasColumnName("Price")); + + modelBuilder.Entity().ComplexProperty( + x => x.Pricing, + p => p.Property(a => a.Amount).HasColumnName("Price")); + } +} + +public abstract class Item +{ + public int Id { get; set; } + public required string Name { get; set; } +} + +public class Item1 : Item +{ + public ItemPrice? Pricing { get; set; } +} + +public class Item2 : Item +{ + public ItemPrice? Pricing { get; set; } +} + +public class ItemPrice +{ + public required string Amount { get; init; } +}