diff --git a/src/EFCore/ChangeTracking/EntityEntry.cs b/src/EFCore/ChangeTracking/EntityEntry.cs index f628c229c13..c719da3ec06 100644 --- a/src/EFCore/ChangeTracking/EntityEntry.cs +++ b/src/EFCore/ChangeTracking/EntityEntry.cs @@ -603,7 +603,8 @@ public virtual PropertyValues OriginalValues { var values = Finder.GetDatabaseValues(InternalEntry); - return values == null ? null : new ArrayPropertyValues(InternalEntry, values, null); + // ArrayPropertyValues will compute null-complex-property flags from the returned value buffer. + return values == null ? null : new ArrayPropertyValues(InternalEntry, values); } /// @@ -634,7 +635,8 @@ public virtual PropertyValues OriginalValues { var values = await Finder.GetDatabaseValuesAsync(InternalEntry, cancellationToken).ConfigureAwait(false); - return values == null ? null : new ArrayPropertyValues(InternalEntry, values, null); + // ArrayPropertyValues will compute null-complex-property flags from the returned value buffer. + return values == null ? null : new ArrayPropertyValues(InternalEntry, values); } /// diff --git a/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs index 5b7a3e651f8..d3d0c18d328 100644 --- a/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs +++ b/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs @@ -18,6 +18,17 @@ public class ArrayPropertyValues : PropertyValues private readonly List?[] _complexCollectionValues; private readonly bool[]? _nullComplexPropertyFlags; + /// + /// 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. + /// + public ArrayPropertyValues(InternalEntryBase internalEntry, object?[] values) + : this(internalEntry, values, nullComplexPropertyFlags: null, computeNullComplexPropertyFlags: true) + { + } + /// /// 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 @@ -25,11 +36,68 @@ public class ArrayPropertyValues : PropertyValues /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public ArrayPropertyValues(InternalEntryBase internalEntry, object?[] values, bool[]? nullComplexPropertyFlags) + : this(internalEntry, values, nullComplexPropertyFlags, computeNullComplexPropertyFlags: false) + { + } + + private ArrayPropertyValues( + InternalEntryBase internalEntry, + object?[] values, + bool[]? nullComplexPropertyFlags, + bool computeNullComplexPropertyFlags) : base(internalEntry) { _values = values; _complexCollectionValues = new List?[ComplexCollectionProperties.Count]; - _nullComplexPropertyFlags = nullComplexPropertyFlags; + _nullComplexPropertyFlags = computeNullComplexPropertyFlags + ? CreateNullComplexPropertyFlags(values) + : nullComplexPropertyFlags; + } + + private bool[]? CreateNullComplexPropertyFlags(object?[] values) + { + var nullableComplexProperties = NullableComplexProperties; + if (nullableComplexProperties == null) + { + return null; + } + + var flags = new bool[nullableComplexProperties.Count]; + for (var i = 0; i < nullableComplexProperties.Count; i++) + { + var complexProperty = nullableComplexProperties[i]; + var scalarProperties = complexProperty.ComplexType.GetFlattenedProperties(); + + IProperty? requiredProperty = null; + foreach (var property in scalarProperties) + { + if (!property.IsNullable) + { + requiredProperty = property; + break; + } + } + + if (requiredProperty != null) + { + flags[i] = values[requiredProperty.GetIndex()] == null; + continue; + } + + var allNull = true; + foreach (var property in scalarProperties) + { + if (values[property.GetIndex()] != null) + { + allNull = false; + break; + } + } + + flags[i] = allNull; + } + + return flags; } /// diff --git a/src/EFCore/Internal/EntityFinder.cs b/src/EFCore/Internal/EntityFinder.cs index 904bf22ddf4..2e199fb6d1b 100644 --- a/src/EFCore/Internal/EntityFinder.cs +++ b/src/EFCore/Internal/EntityFinder.cs @@ -858,16 +858,32 @@ private static Expression> BuildProjection(IEntityType en foreach (var property in entityType.GetFlattenedProperties()) { var path = new List { property }; + var isInNullableComplexProperty = false; while (path[^1].DeclaringType is IComplexType complexType) { - path.Add(complexType.ComplexProperty); + var complexProperty = complexType.ComplexProperty; + path.Add(complexProperty); + + if (!complexProperty.IsCollection && complexProperty.IsNullable) + { + isInNullableComplexProperty = true; + } + } + + var readType = property.ClrType; + if (readType.IsValueType + && !readType.IsNullableType() + && isInNullableComplexProperty) + { + readType = typeof(Nullable<>).MakeGenericType(readType); } Expression instanceExpression = entityParameter; for (var i = path.Count - 1; i >= 0; i--) { + var currentType = i == 0 ? readType : path[i].ClrType; instanceExpression = Expression.Call( - EF.PropertyMethod.MakeGenericMethod(path[i].ClrType), + EF.PropertyMethod.MakeGenericMethod(currentType), instanceExpression, Expression.Constant(path[i].Name, typeof(string))); @@ -877,12 +893,11 @@ private static Expression> BuildProjection(IEntityType en } } - projections.Add( - Expression.Convert( - Expression.Convert( - instanceExpression, - property.ClrType), - typeof(object))); + var projection = instanceExpression.Type == property.ClrType + ? Expression.Convert(Expression.Convert(instanceExpression, property.ClrType), typeof(object)) + : Expression.Convert(instanceExpression, typeof(object)); + + projections.Add(projection); } return Expression.Lambda>( diff --git a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs index db0be471c8a..af7f7ff7194 100644 --- a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs +++ b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs @@ -3195,6 +3195,19 @@ public virtual void Nullable_complex_property_with_null_value_returns_null_when_ Assert.NotNull(originalBuilding.OptionalMilk); } + [ConditionalFact] + public virtual async Task Reloading_optional_complex_property_with_null_does_not_throw() + { + using var context = CreateContext(); + var building = context.Set().Single(b => b.Name == "Building Two"); + Assert.Null(building.OptionalMilk); + + await context.Entry(building).ReloadAsync(); + + Assert.Null(building.OptionalMilk); + Assert.Equal(EntityState.Unchanged, context.Entry(building).State); + } + [ConditionalFact] public virtual void Setting_current_values_from_cloned_values_sets_nullable_complex_property_to_null() { @@ -3990,10 +4003,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con protected override Task SeedAsync(PoolableDbContext context) { + var buildingTwo = Building.Create(Guid.NewGuid(), "Building Two", 1000000m); + buildingTwo.OptionalMilk = null; + var buildings = new List { Building.Create(new Guid("21EC2020-3AEA-1069-A2DD-08002B30309D"), "Building One", 1500000), - Building.Create(Guid.NewGuid(), "Building Two", 1000000m) + buildingTwo }; foreach (var building in buildings)