diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs index ac2b693833e..38ec5d3db69 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs @@ -1477,7 +1477,9 @@ public virtual void AcceptChanges() var storeGeneratedIndex = property.GetStoreGeneratedIndex(); if (storeGeneratedIndex != -1 && _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated) - && _storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var value)) + && _storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var value) + && (property.DeclaringType is not IComplexType complexType + || GetCurrentValue(complexType.ComplexProperty) != null)) { this[property] = value; } diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 59c46d11b10..ec5d4b78b9f 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -302,8 +302,20 @@ public static Expression CreateMemberAccess( break; } } - else + else if (!fromDeclaringType + || !addNullCheck + || property?.DeclaringType is not IComplexType declaringComplexType + || instanceExpression.Type.IsValueType + || declaringComplexType.ClrType.IsValueType + || !declaringComplexType.ComplexProperty.IsNullable) { + // Disable null check for all cases except: + // - fromDeclaringType is true AND + // - addNullCheck is true AND + // - property is declared in a complex type AND + // - instance is a reference type AND + // - declaring complex type is a reference type AND + // - the complex property is nullable addNullCheck = false; } diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs index c8523e7e4b3..f71c7a6537f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -8,13 +8,102 @@ public class ComplexTypesTrackingSqlServerTest( ITestOutputHelper testOutputHelper) : ComplexTypesTrackingSqlServerTestBase(fixture, testOutputHelper) { + [ConditionalFact] + public virtual async Task Can_read_original_values_with_TPH_shared_complex_property_column_null() + { + await using var context = CreateContext(); + + await context.Database.CreateExecutionStrategy().ExecuteAsync( + context, + async context => + { + await using var transaction = await context.Database.BeginTransactionAsync(); + + context.Add(new InheritedItem1 { Name = "Item1" }); + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + var item = await context.Set().Where(i => i.Name == "Item1").FirstAsync(); + var entry = context.ChangeTracker.Entries().First(); + var originalItem = (InheritedItemBase)entry.OriginalValues.ToObject(); + + Assert.NotNull(originalItem); + Assert.Equal("Item1", originalItem.Name); + }); + } + + [ConditionalFact] + public virtual async Task Can_read_original_values_with_TPH_shared_complex_property_column_with_value() + { + await using var context = CreateContext(); + + await context.Database.CreateExecutionStrategy().ExecuteAsync( + context, + async context => + { + await using var transaction = await context.Database.BeginTransactionAsync(); + + context.Add(new InheritedItem1 { Name = "Item1", SharedPrice = new SharedPrice { Amount = "10.99" } }); + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + var item = await context.Set().Where(i => i.Name == "Item1").FirstAsync(); + var entry = context.ChangeTracker.Entries().First(); + var originalItem = (InheritedItemBase)entry.OriginalValues.ToObject(); + + Assert.NotNull(originalItem); + Assert.Equal("Item1", originalItem.Name); + var item1 = Assert.IsType(originalItem); + Assert.NotNull(item1.SharedPrice); + Assert.Equal("10.99", item1.SharedPrice.Amount); + }); + } + public class SqlServerFixture : SqlServerFixtureBase { protected override string StoreName => nameof(ComplexTypesTrackingSqlServerTest); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().HasDiscriminator("Discriminator") + .HasValue("Item1") + .HasValue("Item2"); + + modelBuilder.Entity().ComplexProperty( + x => x.SharedPrice, + p => p.Property(a => a.Amount).HasColumnName("SharedPriceAmount")); + + modelBuilder.Entity().ComplexProperty( + x => x.SharedPrice, + p => p.Property(a => a.Amount).HasColumnName("SharedPriceAmount")); + } } } +public abstract class InheritedItemBase +{ + public int Id { get; set; } + public required string Name { get; set; } +} + +public class InheritedItem1 : InheritedItemBase +{ + public SharedPrice? SharedPrice { get; set; } +} + +public class InheritedItem2 : InheritedItemBase +{ + public SharedPrice? SharedPrice { get; set; } +} + +public class SharedPrice +{ + public required string Amount { get; init; } +} + public class ComplexTypesTrackingProxiesSqlServerTest( ComplexTypesTrackingProxiesSqlServerTest.SqlServerFixture fixture, ITestOutputHelper testOutputHelper)