Skip to content
Draft
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
42 changes: 42 additions & 0 deletions src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public class ArrayPropertyValues : PropertyValues
private readonly object?[] _values;
private readonly List<ArrayPropertyValues?>?[] _complexCollectionValues;

// Tracks nullable non-collection complex properties that should be null when materializing via ToObject().
// This is needed because value type properties inside nullable complex types store default values (not null)
// when the complex property is null, making it impossible to detect nullness from the values array alone.
// The array indices correspond to NullableComplexProperties ordering.
private 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
Expand All @@ -30,6 +36,16 @@ public ArrayPropertyValues(InternalEntryBase internalEntry, object?[] values)
_complexCollectionValues = new List<ArrayPropertyValues?>?[ComplexCollectionProperties.Count];
}

/// <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>
[EntityFrameworkInternal]
internal void SetNullComplexPropertyFlags(bool[] flags)
=> _nullComplexPropertyFlags = flags;

/// <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
Expand All @@ -41,6 +57,23 @@ public override object ToObject()
var structuralObject = StructuralType.GetOrCreateMaterializer(MaterializerSource)(
new MaterializationContext(new ValueBuffer(_values), InternalEntry.Context));

// Set null for nullable complex properties that were explicitly marked as null.
// Shadow properties don't have CLR members and aren't part of the materialized object.
if (_nullComplexPropertyFlags != null)
{
for (var i = 0; i < _nullComplexPropertyFlags.Length; i++)
{
if (_nullComplexPropertyFlags[i])
{
var complexProperty = NullableComplexProperties[i];
if (!complexProperty.IsShadowProperty())
{
structuralObject = ((IRuntimeComplexProperty)complexProperty).GetSetter().SetClrValue(structuralObject, null);
}
}
}
}

for (var i = 0; i < _complexCollectionValues.Length; i++)
{
var propertyValuesList = _complexCollectionValues[i];
Expand Down Expand Up @@ -133,6 +166,15 @@ public override PropertyValues Clone()
Array.Copy(_values, copies, _values.Length);

var clone = new ArrayPropertyValues(InternalEntry, copies);

// Copy null complex property tracking
if (_nullComplexPropertyFlags != null)
{
var flagsCopy = new bool[_nullComplexPropertyFlags.Length];
Array.Copy(_nullComplexPropertyFlags, flagsCopy, _nullComplexPropertyFlags.Length);
clone.SetNullComplexPropertyFlags(flagsCopy);
}

for (var i = 0; i < _complexCollectionValues.Length; i++)
{
var list = _complexCollectionValues[i];
Expand Down
15 changes: 15 additions & 0 deletions src/EFCore/ChangeTracking/Internal/EntryPropertyValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,21 @@ public override PropertyValues Clone()
}

var cloned = new ArrayPropertyValues(InternalEntry, values);

// Track null nullable complex properties (non-collection) using bool array
var nullableComplexProperties = NullableComplexProperties;
if (nullableComplexProperties.Count > 0)
{
var flags = new bool[nullableComplexProperties.Count];
for (var i = 0; i < nullableComplexProperties.Count; i++)
{
// Use the entry's indexer to get the current complex property value
flags[i] = InternalEntry[nullableComplexProperties[i]] == null;
}

cloned.SetNullComplexPropertyFlags(flags);
}

foreach (var complexProperty in ComplexCollectionProperties)
{
var collection = (IList?)GetValueInternal(InternalEntry, complexProperty);
Expand Down
15 changes: 15 additions & 0 deletions src/EFCore/ChangeTracking/PropertyValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking;
public abstract class PropertyValues
{
private readonly IReadOnlyList<IComplexProperty> _complexCollectionProperties;
private readonly IReadOnlyList<IComplexProperty> _nullableComplexProperties;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -42,6 +43,7 @@ protected PropertyValues(InternalEntryBase internalEntry)
Check.DebugAssert(
_complexCollectionProperties.Select((p, i) => p.GetIndex() == i).All(e => e),
"Complex collection properties indices are not sequential.");
_nullableComplexProperties = [.. internalEntry.StructuralType.GetFlattenedComplexProperties().Where(p => !p.IsCollection && p.IsNullable)];
}

/// <summary>
Expand Down Expand Up @@ -154,6 +156,19 @@ public virtual IReadOnlyList<IComplexProperty> ComplexCollectionProperties
get => _complexCollectionProperties;
}

/// <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>
[EntityFrameworkInternal]
protected IReadOnlyList<IComplexProperty> NullableComplexProperties
{
[DebuggerStepThrough]
get => _nullableComplexProperties;
}

/// <summary>
/// Gets the underlying structural type for which this object is storing values.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Microsoft.EntityFrameworkCore;
public class PropertyValuesInMemoryTest(PropertyValuesInMemoryTest.PropertyValuesInMemoryFixture fixture)
: PropertyValuesTestBase<PropertyValuesInMemoryTest.PropertyValuesInMemoryFixture>(fixture)
{
// Nullable complex property test - InMemory provider doesn't support complex types in queries
public override void Nullable_complex_property_with_null_value_returns_null_when_using_ToObject()
{
}

public override Task Complex_current_values_can_be_accessed_as_a_property_dictionary_using_IProperty()
=> Assert.ThrowsAsync<NullReferenceException>( // In-memory database cannot query complex types
() => base.Complex_current_values_can_be_accessed_as_a_property_dictionary_using_IProperty());
Expand Down Expand Up @@ -113,6 +118,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
{
b.Ignore(e => e.Culture);
b.Ignore(e => e.Milk);
b.Ignore(e => e.OptionalMilk);
});

// In-memory database doesn't support complex collections
Expand Down
17 changes: 17 additions & 0 deletions test/EFCore.Specification.Tests/PropertyValuesTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3167,6 +3167,21 @@ public virtual void SetValues_throws_for_complex_property_with_non_dictionary_va
Assert.Equal(CoreStrings.ComplexPropertyValueNotDictionary("Culture", "string"), exception.Message);
}

[ConditionalFact]
public virtual void Nullable_complex_property_with_null_value_returns_null_when_using_ToObject()
{
using var context = CreateContext();
var building = context.Set<Building>().Single(b => b.Name == "Building One");

Assert.Null(building.OptionalMilk);

var originalBuilding = (Building)context.Entry(building).OriginalValues.ToObject();
Assert.Null(originalBuilding.OptionalMilk);

var currentBuilding = (Building)context.Entry(building).CurrentValues.ToObject();
Assert.Null(currentBuilding.OptionalMilk);
}

[ConditionalFact]
public virtual void Current_values_can_be_copied_to_object_using_ToObject()
{
Expand Down Expand Up @@ -3587,6 +3602,7 @@ public string NoSetter

public Culture Culture { get; set; }
public required Milk Milk { get; set; }
public Milk? OptionalMilk { get; set; }
}

[ComplexType]
Expand Down Expand Up @@ -3903,6 +3919,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con

b.ComplexProperty(e => e.Culture);
b.ComplexProperty(e => e.Milk);
b.ComplexProperty(e => e.OptionalMilk);
});

modelBuilder.Entity<Contact33307>();
Expand Down
Loading