Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/EFCore/ChangeTracking/EntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down
70 changes: 69 additions & 1 deletion src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,86 @@ public class ArrayPropertyValues : PropertyValues
private readonly List<ArrayPropertyValues?>?[] _complexCollectionValues;
private readonly bool[]? _nullComplexPropertyFlags;

/// <summary>
/// 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.
/// </summary>
public ArrayPropertyValues(InternalEntryBase internalEntry, object?[] values)
: this(internalEntry, values, nullComplexPropertyFlags: null, computeNullComplexPropertyFlags: true)
{
}

/// <summary>
/// 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.
/// </summary>
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<ArrayPropertyValues?>?[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;
}

/// <summary>
Expand Down
31 changes: 23 additions & 8 deletions src/EFCore/Internal/EntityFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -858,16 +858,32 @@ private static Expression<Func<object, object[]>> BuildProjection(IEntityType en
foreach (var property in entityType.GetFlattenedProperties())
{
var path = new List<IPropertyBase> { 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;
}
}
Comment thread
AndriySvyryd marked this conversation as resolved.

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)));

Expand All @@ -877,12 +893,11 @@ private static Expression<Func<object, object[]>> 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<Func<object, object[]>>(
Expand Down
18 changes: 17 additions & 1 deletion test/EFCore.Specification.Tests/PropertyValuesTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Building>().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()
{
Expand Down Expand Up @@ -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>
{
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)
Expand Down
Loading