diff --git a/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs
index d3d0c18d328..595fdb87596 100644
--- a/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs
+++ b/src/EFCore/ChangeTracking/Internal/ArrayPropertyValues.cs
@@ -24,34 +24,12 @@ public class ArrayPropertyValues : PropertyValues
/// 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
- /// 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, bool[]? nullComplexPropertyFlags)
- : this(internalEntry, values, nullComplexPropertyFlags, computeNullComplexPropertyFlags: false)
- {
- }
-
- private ArrayPropertyValues(
- InternalEntryBase internalEntry,
- object?[] values,
- bool[]? nullComplexPropertyFlags,
- bool computeNullComplexPropertyFlags)
+ public ArrayPropertyValues(InternalEntryBase internalEntry, object?[] values, bool[]? nullComplexPropertyFlags = null)
: base(internalEntry)
{
_values = values;
_complexCollectionValues = new List?[ComplexCollectionProperties.Count];
- _nullComplexPropertyFlags = computeNullComplexPropertyFlags
- ? CreateNullComplexPropertyFlags(values)
- : nullComplexPropertyFlags;
+ _nullComplexPropertyFlags = nullComplexPropertyFlags ?? CreateNullComplexPropertyFlags(values);
}
private bool[]? CreateNullComplexPropertyFlags(object?[] values)
@@ -118,7 +96,7 @@ public override object ToObject()
if (_nullComplexPropertyFlags[i])
{
var complexProperty = NullableComplexProperties[i];
- structuralObject = ((IRuntimeComplexProperty)complexProperty).GetSetter().SetClrValue(structuralObject, null);
+ structuralObject = SetNestedComplexPropertyValue(structuralObject, complexProperty, null);
}
}
}
@@ -136,7 +114,7 @@ public override object ToObject()
!complexProperty.IsShadowProperty(),
$"Shadow complex property {complexProperty.Name} is not supported. Issue #31243");
var list = (IList)((IRuntimeComplexProperty)complexProperty).GetIndexedCollectionAccessor().Create(propertyValuesList.Count);
- structuralObject = ((IRuntimeComplexProperty)complexProperty).GetSetter().SetClrValue(structuralObject, list);
+ structuralObject = SetNestedComplexPropertyValue(structuralObject, complexProperty, list);
foreach (var propertyValues in propertyValuesList)
{
@@ -147,6 +125,31 @@ public override object ToObject()
return structuralObject;
}
+ private object SetNestedComplexPropertyValue(object structuralObject, IComplexProperty complexProperty, object? value)
+ {
+ return SetValueRecursively(structuralObject, complexProperty.GetChainToComplexProperty(fromEntity: false), 0, value);
+
+ static object SetValueRecursively(object instance, IReadOnlyList chain, int index, object? value)
+ {
+ var currentProperty = (IRuntimeComplexProperty)chain[index];
+ if (index == chain.Count - 1)
+ {
+ return currentProperty.GetSetter().SetClrValue(instance, value);
+ }
+
+ var child = currentProperty.GetGetter().GetClrValue(instance);
+ if (child is null)
+ {
+ return instance;
+ }
+
+ var updated = SetValueRecursively(child, chain, index + 1, value);
+ // Need to update the child value as well because it could be a value type
+ // TODO: Improve this, see #36041
+ return currentProperty.GetSetter().SetClrValue(instance, updated);
+ }
+ }
+
///
/// 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
@@ -166,7 +169,8 @@ public override void SetValues(object obj)
{
Check.NotNull(obj);
- if (obj.GetType() == StructuralType.ClrType)
+ var isSameType = obj.GetType() == StructuralType.ClrType;
+ if (isSameType)
{
for (var i = 0; i < _values.Length; i++)
{
@@ -210,6 +214,78 @@ public override void SetValues(object obj)
}
}
}
+
+ UpdateNullComplexPropertyFlags(cp => IsNullAlongChain(obj, cp, isSameType));
+ }
+
+ private void UpdateNullComplexPropertyFlags(Func isNull)
+ {
+ var nullableComplexProperties = NullableComplexProperties;
+ if (nullableComplexProperties == null
+ || _nullComplexPropertyFlags == null)
+ {
+ return;
+ }
+
+ for (var i = 0; i < nullableComplexProperties.Count; i++)
+ {
+ var result = isNull(nullableComplexProperties[i]);
+ if (result.HasValue)
+ {
+ _nullComplexPropertyFlags[i] = result.Value;
+ }
+ }
+
+ for (var i = 0; i < _complexCollectionValues.Length; i++)
+ {
+ var list = _complexCollectionValues[i];
+ if (list == null)
+ {
+ continue;
+ }
+
+ foreach (var propertyValues in list)
+ {
+ propertyValues?.UpdateNullComplexPropertyFlags(
+ cp => IsNullAlongChain(propertyValues.ToObject(), cp, isSameType: true));
+ }
+ }
+ }
+
+ ///
+ /// Walks the chain from the structural type root to the given complex property
+ /// and returns if the value is null, if non-null,
+ /// or if the chain could not be fully read (e.g. DTO missing a property).
+ ///
+ private static bool? IsNullAlongChain(object obj, IComplexProperty complexProperty, bool isSameType)
+ {
+ var chain = complexProperty.GetChainToComplexProperty(fromEntity: false);
+ object? current = obj;
+
+ for (var j = 0; j < chain.Count; j++)
+ {
+ if (current == null)
+ {
+ break;
+ }
+
+ if (isSameType)
+ {
+ current = chain[j].GetGetter().GetClrValue(current);
+ }
+ else
+ {
+ var getter = current.GetType().GetAnyProperty(chain[j].Name)?.FindGetterProperty();
+ if (getter == null)
+ {
+ return null;
+ }
+
+ current = getter.GetValue(current);
+ }
+ }
+
+ return current == null;
}
///
@@ -272,6 +348,16 @@ public override void SetValues(PropertyValues propertyValues)
var list = propertyValues[ComplexCollectionProperties[i]];
_complexCollectionValues[i] = GetComplexCollectionPropertyValues(ComplexCollectionProperties[i], list);
}
+
+ var nullableComplexProperties = NullableComplexProperties;
+ if (nullableComplexProperties != null
+ && _nullComplexPropertyFlags != null)
+ {
+ for (var i = 0; i < nullableComplexProperties.Count; i++)
+ {
+ _nullComplexPropertyFlags[i] = propertyValues.IsNullableComplexPropertyNull(i);
+ }
+ }
}
///
@@ -376,7 +462,30 @@ public override IList? this[IComplexProperty complexProperty]
///
public override void SetValues(IDictionary values)
- => SetValuesFromDictionary((IRuntimeTypeBase)StructuralType, Check.NotNull(values));
+ {
+ Check.NotNull(values);
+ SetValuesFromDictionary((IRuntimeTypeBase)StructuralType, values);
+ UpdateNullComplexPropertyFlags(cp => IsNullInDictionary(values, cp));
+ }
+
+ private static bool IsNullInDictionary(IDictionary rootDict, IComplexProperty cp)
+ {
+ var chain = cp.GetChainToComplexProperty(fromEntity: false);
+ object? current = rootDict;
+ for (var i = 0; i < chain.Count; i++)
+ {
+ if (current is not IDictionary currentDict
+ || !currentDict.TryGetValue(chain[i].Name, out var value)
+ || value == null)
+ {
+ return true;
+ }
+
+ current = value;
+ }
+
+ return false;
+ }
private void SetValuesFromDictionary(IRuntimeTypeBase structuralType, IDictionary values)
{
@@ -453,7 +562,19 @@ private void SetValuesFromDictionary(IRuntimeTypeBase structuralType,
var complexEntry = new InternalComplexEntry((IRuntimeComplexType)complexProperty.ComplexType, InternalEntry, i);
var complexType = complexEntry.StructuralType;
var values = new object?[complexType.GetFlattenedProperties().Count()];
- var complexPropertyValues = new ArrayPropertyValues(complexEntry, values, null);
+
+ bool[]? nullValues = null;
+ var nullableComplexProperties = ComputeNullableComplexProperties(complexEntry.StructuralType);
+ if (nullableComplexProperties != null && nullableComplexProperties.Count > 0)
+ {
+ nullValues = new bool[nullableComplexProperties.Count];
+ for (var j = 0; j < nullableComplexProperties.Count; j++)
+ {
+ nullValues[j] = IsNullInDictionary(itemDict, nullableComplexProperties[j]);
+ }
+ }
+
+ var complexPropertyValues = new ArrayPropertyValues(complexEntry, values, nullValues);
complexPropertyValues.SetValues(itemDict);
propertyValuesList.Add(complexPropertyValues);
@@ -563,20 +684,60 @@ ArrayPropertyValues CreateComplexPropertyValues(object complexObject, InternalCo
for (var i = 0; i < properties.Count; i++)
{
var property = properties[i];
- var getter = property.GetGetter();
- values[i] = getter.GetClrValue(complexObject);
+ var targetObject = NavigateToDeclaringType(complexObject, property.DeclaringType, complexType);
+ values[i] = targetObject == null ? null : property.GetGetter().GetClrValue(targetObject);
}
- var complexPropertyValues = new ArrayPropertyValues(entry, values, null);
+ bool[]? nullValues = null;
+ var nullableComplexProperties = ComputeNullableComplexProperties(complexType);
+ if (nullableComplexProperties != null && nullableComplexProperties.Count > 0)
+ {
+ nullValues = new bool[nullableComplexProperties.Count];
+
+ for (var i = 0; i < nullableComplexProperties.Count; i++)
+ {
+ var cp = nullableComplexProperties[i];
+ var targetObject = NavigateToDeclaringType(complexObject, cp.DeclaringType, complexType);
+ if (targetObject != null)
+ {
+ nullValues[i] = cp.GetGetter().GetClrValue(targetObject) == null;
+ }
+ }
+ }
+
+ var complexPropertyValues = new ArrayPropertyValues(entry, values, nullValues);
foreach (var nestedComplexProperty in complexPropertyValues.ComplexCollectionProperties)
{
- var nestedCollection = (IList?)nestedComplexProperty.GetGetter().GetClrValue(complexObject);
- var propertyValuesList = GetComplexCollectionPropertyValues(nestedComplexProperty, nestedCollection);
- complexPropertyValues.SetComplexCollectionValue(nestedComplexProperty, propertyValuesList);
+ var targetObject = NavigateToDeclaringType(complexObject, nestedComplexProperty.DeclaringType, complexType);
+ var nestedCollection = targetObject == null ? null : (IList?)nestedComplexProperty.GetGetter().GetClrValue(targetObject);
+ var nestedPropertyValuesList = GetComplexCollectionPropertyValues(nestedComplexProperty, nestedCollection);
+ complexPropertyValues.SetComplexCollectionValue(nestedComplexProperty, nestedPropertyValuesList);
}
return complexPropertyValues;
}
+
+ static object? NavigateToDeclaringType(object root, ITypeBase declaringType, IRuntimeTypeBase rootType)
+ {
+ if (declaringType == rootType)
+ {
+ return root;
+ }
+
+ if (declaringType is not IComplexType ct)
+ {
+ return root;
+ }
+
+ var chain = ct.ComplexProperty.GetChainToComplexProperty(fromEntity: false);
+ object? target = root;
+ for (var i = 0; i < chain.Count && target != null; i++)
+ {
+ target = chain[i].GetGetter().GetClrValue(target);
+ }
+
+ return target;
+ }
}
}
diff --git a/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs
index ce61c477464..4755f226f04 100644
--- a/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs
+++ b/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs
@@ -48,7 +48,7 @@ public override TValue GetValue(IProperty property)
/// 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.
///
- protected override void SetValueInternal(IInternalEntry entry, IPropertyBase property, object? value)
+ protected override void SetValueInternal(IInternalEntry entry, IPropertyBase property, object? value, bool skipChangeDetection = false)
=> entry[property] = value;
///
diff --git a/src/EFCore/ChangeTracking/Internal/EntryPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/EntryPropertyValues.cs
index 880b50fd607..22b6eda3658 100644
--- a/src/EFCore/ChangeTracking/Internal/EntryPropertyValues.cs
+++ b/src/EFCore/ChangeTracking/Internal/EntryPropertyValues.cs
@@ -34,6 +34,15 @@ protected EntryPropertyValues(InternalEntryBase internalEntry)
public override object ToObject()
=> Clone().ToObject();
+ ///
+ /// 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 override bool IsNullableComplexPropertyNull(int index)
+ => NullableComplexProperties != null && GetValueInternal(InternalEntry, NullableComplexProperties[index]) == null;
+
///
/// 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
@@ -57,7 +66,17 @@ public override void SetValues(object obj)
}
}
- private void SetValuesFromInstance(InternalEntryBase entry, IRuntimeTypeBase structuralType, object obj)
+ ///
+ /// 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.
+ ///
+ protected virtual void SetValuesFromInstance(
+ InternalEntryBase entry,
+ IRuntimeTypeBase structuralType,
+ object obj,
+ bool skipChangeDetection = false)
{
foreach (var property in structuralType.GetProperties())
{
@@ -66,7 +85,7 @@ private void SetValuesFromInstance(InternalEntryBase entry, IRuntimeTypeBase str
continue;
}
- SetValueInternal(entry, property, property.GetGetter().GetClrValue(obj));
+ SetValueInternal(entry, property, property.GetGetter().GetClrValue(obj), skipChangeDetection: skipChangeDetection);
}
foreach (var complexProperty in structuralType.GetComplexProperties())
@@ -79,7 +98,7 @@ private void SetValuesFromInstance(InternalEntryBase entry, IRuntimeTypeBase str
if (complexProperty.IsCollection)
{
var complexList = (IList?)complexProperty.GetGetter().GetClrValue(obj);
- SetValueInternal(entry, complexProperty, complexList);
+ SetValueInternal(entry, complexProperty, complexList, skipChangeDetection: skipChangeDetection);
for (var i = 0; i < complexList?.Count; i++)
{
@@ -90,15 +109,20 @@ private void SetValuesFromInstance(InternalEntryBase entry, IRuntimeTypeBase str
}
var complexEntry = GetComplexCollectionEntry(entry, complexProperty, i);
- SetValuesFromInstance(complexEntry, complexEntry.StructuralType, complexObject);
+ SetValuesFromInstance(complexEntry, complexEntry.StructuralType, complexObject, skipChangeDetection);
}
}
else
{
var complexObject = complexProperty.GetGetter().GetClrValue(obj);
+ if (complexProperty.IsNullable)
+ {
+ SetValueInternal(entry, complexProperty, complexObject, skipChangeDetection: skipChangeDetection);
+ }
+
if (complexObject != null)
{
- SetValuesFromInstance(entry, (IRuntimeTypeBase)complexProperty.ComplexType, complexObject);
+ SetValuesFromInstance(entry, (IRuntimeTypeBase)complexProperty.ComplexType, complexObject, skipChangeDetection);
}
}
}
@@ -171,9 +195,17 @@ private void SetValuesFromDto(InternalEntryBase entry, IRuntimeTypeBase structur
if (dtoComplexValue != null)
{
var complexObject = CreateComplexObjectFromDto((IRuntimeComplexType)complexProperty.ComplexType, dtoComplexValue);
- SetValueInternal(entry, complexProperty, complexObject);
+ if (complexProperty.IsNullable)
+ {
+ SetValueInternal(entry, complexProperty, complexObject);
+ }
+
SetValuesFromDto(entry, (IRuntimeComplexType)complexProperty.ComplexType, dtoComplexValue);
}
+ else if (complexProperty.IsNullable)
+ {
+ SetValueInternal(entry, complexProperty, null);
+ }
}
}
}
@@ -282,18 +314,21 @@ public override PropertyValues Clone()
values[i] = GetValueInternal(InternalEntry, Properties[i]);
}
- bool[]? flags = null;
+ bool[]? nullValues = null;
var nullableComplexProperties = NullableComplexProperties;
if (nullableComplexProperties != null && nullableComplexProperties.Count > 0)
{
- flags = new bool[nullableComplexProperties.Count];
+ nullValues = new bool[nullableComplexProperties.Count];
+
for (var i = 0; i < nullableComplexProperties.Count; i++)
{
- flags[i] = GetValueInternal(InternalEntry, nullableComplexProperties[i]) == null;
+ var complexProperty = nullableComplexProperties[i];
+
+ nullValues[i] = GetValueInternal(InternalEntry, complexProperty) == null;
}
}
- var cloned = new ArrayPropertyValues(InternalEntry, values, flags);
+ var cloned = new ArrayPropertyValues(InternalEntry, values, nullValues);
foreach (var complexProperty in ComplexCollectionProperties)
{
@@ -317,10 +352,11 @@ public override void SetValues(PropertyValues propertyValues)
{
Check.NotNull(propertyValues);
- var nullableComplexProperties = NullableComplexProperties;
HashSet? nullComplexProperties = null;
+ var nullableComplexProperties = NullableComplexProperties;
if (nullableComplexProperties != null)
{
+ object? materializedObject = null;
for (var i = 0; i < nullableComplexProperties.Count; i++)
{
if (propertyValues.IsNullableComplexPropertyNull(i))
@@ -328,12 +364,34 @@ public override void SetValues(PropertyValues propertyValues)
nullComplexProperties ??= [];
nullComplexProperties.Add(nullableComplexProperties[i]);
}
+ else
+ {
+ // Ensure the CLR complex object exists on the target entry so child property setters
+ // can navigate through it. Create it from the source property values if needed.
+ var complexProperty = nullableComplexProperties[i];
+ var currentValue = GetValueInternal(InternalEntry, complexProperty);
+ if (currentValue == null)
+ {
+ materializedObject ??= propertyValues.ToObject();
+ var chain = complexProperty.GetChainToComplexProperty(fromEntity: false);
+ var value = materializedObject;
+ for (var j = 0; j < chain.Count && value != null; j++)
+ {
+ value = chain[j].GetGetter().GetClrValue(value);
+ }
+
+ if (value != null)
+ {
+ SetValueInternal(InternalEntry, complexProperty, value);
+ }
+ }
+ }
}
}
foreach (var property in Properties)
{
- if (nullComplexProperties != null && IsPropertyInNullComplexType(property, nullComplexProperties))
+ if (nullComplexProperties != null && IsInNullComplexType(property, nullComplexProperties))
{
continue;
}
@@ -343,7 +401,31 @@ public override void SetValues(PropertyValues propertyValues)
foreach (var complexProperty in ComplexCollectionProperties)
{
+ if (IsInNullComplexType(complexProperty, nullComplexProperties))
+ {
+ continue;
+ }
+
SetValueInternal(InternalEntry, complexProperty, propertyValues[complexProperty]);
+
+ // Also propagate individual element values into complex collection entries
+ // so that OriginalPropertyValues/CurrentPropertyValues backed by tracked entries
+ // can properly reconstruct collection elements via their complex entries.
+ var sourceCollection = propertyValues[complexProperty];
+ if (sourceCollection != null)
+ {
+ for (var i = 0; i < sourceCollection.Count; i++)
+ {
+ var element = sourceCollection[i];
+ if (element == null)
+ {
+ continue;
+ }
+
+ var targetEntry = GetComplexCollectionEntry(InternalEntry, complexProperty, i);
+ SetValuesFromInstance(targetEntry, (IRuntimeTypeBase)complexProperty.ComplexType, element);
+ }
+ }
}
if (nullComplexProperties != null)
@@ -355,10 +437,15 @@ public override void SetValues(PropertyValues propertyValues)
}
}
- private static bool IsPropertyInNullComplexType(IProperty property, HashSet nullComplexProperties)
+ private static bool IsInNullComplexType(IPropertyBase property, HashSet? nullComplexProperties)
{
+ if (nullComplexProperties == null)
+ {
+ return false;
+ }
+
var declaringType = property.DeclaringType;
- while (declaringType is IComplexType complexType)
+ while (declaringType is IComplexType complexType && !complexType.ComplexProperty.IsCollection)
{
if (nullComplexProperties.Contains(complexType.ComplexProperty))
{
@@ -449,7 +536,10 @@ private void SetValuesFromDictionary(
}
var complexObject = CreateComplexObjectFromDictionary((IRuntimeComplexType)complexProperty.ComplexType, complexDict);
- SetValueInternal(entry, complexProperty, complexObject);
+ if (complexProperty.IsNullable)
+ {
+ SetValueInternal(entry, complexProperty, complexObject);
+ }
if (complexDict != null)
{
@@ -541,7 +631,7 @@ public override IList? this[IComplexProperty complexProperty]
/// 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.
///
- protected abstract void SetValueInternal(IInternalEntry entry, IPropertyBase property, object? value);
+ protected abstract void SetValueInternal(IInternalEntry entry, IPropertyBase property, object? value, bool skipChangeDetection = false);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -564,7 +654,11 @@ public override IList? this[IComplexProperty complexProperty]
return null;
}
- var values = new object?[complexType.PropertyCount];
+ // For non-collection complex types property indices are in the containing type's value space.
+ var valueCount = complexType.ComplexProperty.IsCollection
+ ? complexType.PropertyCount
+ : complexType.ContainingEntryType.PropertyCount;
+ var values = new object?[valueCount];
foreach (var property in complexType.GetProperties())
{
if (dictionary.TryGetValue(property.Name, out var value)
diff --git a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs
index 357d258e532..0acc3977fc0 100644
--- a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs
+++ b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs
@@ -324,7 +324,7 @@ public object Entity
/// 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.
///
- void SetOriginalValue(IPropertyBase propertyBase, object? value, int index = -1);
+ void SetOriginalValue(IPropertyBase propertyBase, object? value, int index = -1, bool skipChangeDetection = false);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs
index 66a06c7a841..76480fb8998 100644
--- a/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs
+++ b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs
@@ -159,8 +159,9 @@ public int OriginalOrdinal
///
public override object? ReadPropertyValue(IPropertyBase propertyBase)
=> EntityState == EntityState.Deleted
- ? GetOriginalValue(propertyBase)
- : base.ReadPropertyValue(propertyBase);
+ && HasOriginalValuesSnapshot
+ ? GetOriginalValue(propertyBase)
+ : base.ReadPropertyValue(propertyBase);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs
index 93f2cb812fe..d06e3c94580 100644
--- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs
+++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs
@@ -273,7 +273,7 @@ public InternalComplexEntry GetEntry(int ordinal, bool original = false)
return complexEntry;
}
- // The currentEntry is created in Detached state, so it's not added to the entries list yet.
+ // The entry is created in Detached state, so it's not added to the entries list yet.
// HandleStateChange will add it when the state changes.
return new InternalComplexEntry((IRuntimeComplexType)_complexCollection.ComplexType, _containingEntry, ordinal);
}
@@ -360,6 +360,8 @@ public void SetState(EntityState oldState, EntityState newState, bool acceptChan
setOriginalState = true;
}
+ _containingEntry.EnsureOriginalValues();
+
EnsureCapacity(
((IList?)_containingEntry.GetOriginalValue(_complexCollection))?.Count ?? 0,
original: true, trim: false);
diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.OriginalValues.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.OriginalValues.cs
index 0f0d585f59b..330fc4e9352 100644
--- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.OriginalValues.cs
+++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.OriginalValues.cs
@@ -8,9 +8,12 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
public partial class InternalEntryBase
{
- private struct OriginalValues(InternalEntryBase entry)
+ private struct OriginalValues
{
- private ISnapshot _values = entry.StructuralType.OriginalValuesFactory(entry);
+ private ISnapshot _values;
+
+ public OriginalValues(InternalEntryBase entry)
+ => _values = entry.StructuralType.OriginalValuesFactory(entry);
public object? GetValue(IInternalEntry entry, IPropertyBase property)
=> property.GetOriginalValueIndex() is var index && index == -1
diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs
index ac2b693833e..99dd4e0010e 100644
--- a/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs
+++ b/src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs
@@ -949,34 +949,52 @@ public bool CanHaveOriginalValue(IPropertyBase propertyBase)
public void SetOriginalValue(
IPropertyBase propertyBase,
object? value,
- int index = -1)
+ int index = -1,
+ bool skipChangeDetection = false)
{
StructuralType.CheckContains(propertyBase);
EnsureOriginalValues();
var complexProperty = propertyBase as IComplexProperty;
- var isComplexCollection = complexProperty != null && complexProperty.IsCollection;
- if (isComplexCollection)
+ if (complexProperty != null && complexProperty.IsCollection)
{
ReorderOriginalComplexCollectionEntries(complexProperty!, (IList?)value);
}
_originalValues.SetValue(propertyBase, value, index);
- if (propertyBase is IProperty property)
+ if (skipChangeDetection)
{
- if ((EntityState == EntityState.Unchanged
- || (EntityState == EntityState.Modified && !IsModified(property)))
- && !_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown))
- {
- ((StateManager as StateManager)?.ChangeDetector as ChangeDetector)?.DetectValueChange(this, property);
- }
+ return;
+ }
+
+ if (complexProperty != null)
+ {
+ DetectChanges(complexProperty);
+ }
+ else if (propertyBase is IProperty property)
+ {
+ DetectChanges(property);
}
- else if (isComplexCollection
- && EntityState is EntityState.Unchanged or EntityState.Modified)
+ }
+
+ private void DetectChanges(IProperty property)
+ {
+ if ((EntityState == EntityState.Unchanged
+ || (EntityState == EntityState.Modified && !IsModified(property)))
+ && !_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown))
+ {
+ ((StateManager as StateManager)?.ChangeDetector as ChangeDetector)?.DetectValueChange(this, property);
+ }
+ }
+
+ private void DetectChanges(IComplexProperty complexProperty)
+ {
+ if (EntityState is EntityState.Unchanged or EntityState.Modified
+ && complexProperty.IsCollection)
{
- ((StateManager as StateManager)?.ChangeDetector as ChangeDetector)?.DetectComplexCollectionChanges(this, complexProperty!);
+ ((StateManager as StateManager)?.ChangeDetector as ChangeDetector)?.DetectComplexCollectionChanges(this, complexProperty);
}
}
diff --git a/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs
index 7f9a1f4a0a7..41263b6a718 100644
--- a/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs
+++ b/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs
@@ -49,8 +49,8 @@ public override TValue GetValue(IProperty property)
/// 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.
///
- protected override void SetValueInternal(IInternalEntry entry, IPropertyBase property, object? value)
- => entry.SetOriginalValue(property, value);
+ protected override void SetValueInternal(IInternalEntry entry, IPropertyBase property, object? value, bool skipChangeDetection = false)
+ => entry.SetOriginalValue(property, value, skipChangeDetection: skipChangeDetection);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -69,46 +69,34 @@ protected override void SetValueInternal(IInternalEntry entry, IPropertyBase pro
return null;
}
- // The stored original collection might contain references to the current elements,
- // so we need to recreate it using stored values.
- var clonedCollection = (IList)((IRuntimePropertyBase)complexProperty).GetIndexedCollectionAccessor()
+ // The stored original collection contains references to the current CLR elements
+ // (see SnapshotComplexCollection), so we must reconstruct each element from the
+ // per-entry original value snapshots to get true original values.
+ var reconstructed = (IList)((IRuntimePropertyBase)complexProperty).GetIndexedCollectionAccessor()
.Create(originalCollection.Count);
for (var i = 0; i < originalCollection.Count; i++)
{
- clonedCollection.Add(
- originalCollection[i] == null
- ? null
- : GetPropertyValues(entry.GetComplexCollectionOriginalEntry(complexProperty, i)).ToObject());
- }
-
- return clonedCollection;
- }
-
- return originalValue;
- }
-
- private PropertyValues GetPropertyValues(InternalEntryBase entry)
- {
- var structuralType = entry.StructuralType;
- var properties = structuralType.GetFlattenedProperties().AsList();
- var values = new object?[properties.Count];
- for (var i = 0; i < values.Length; i++)
- {
- values[i] = entry.GetOriginalValue(properties[i]);
- }
+ var element = originalCollection[i];
+ if (element == null)
+ {
+ reconstructed.Add(null);
+ continue;
+ }
- var cloned = new ArrayPropertyValues(entry, values, null);
+ var complexEntry = entry.GetComplexCollectionOriginalEntry(complexProperty, i);
+ if (!complexEntry.HasOriginalValuesSnapshot)
+ {
+ complexEntry.EnsureOriginalValues();
+ SetValuesFromInstance(complexEntry, (IRuntimeTypeBase)complexProperty.ComplexType, element, skipChangeDetection: true);
+ }
- foreach (var nestedComplexProperty in cloned.ComplexCollectionProperties)
- {
- var collection = (IList?)GetValueInternal(entry, nestedComplexProperty);
- if (collection != null)
- {
- cloned[nestedComplexProperty] = collection;
+ reconstructed.Add(new OriginalPropertyValues(complexEntry).Clone().ToObject());
}
+
+ return reconstructed;
}
- return cloned;
+ return originalValue;
}
///
diff --git a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs
index 0ce1e772c06..f18ae8505a8 100644
--- a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs
@@ -63,4 +63,13 @@ protected override int GetPropertyCount(IRuntimeTypeBase structuralType)
///
protected override MethodInfo? GetValueComparerMethod()
=> _getValueComparerMethod;
+
+ ///
+ /// 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.
+ ///
+ protected override bool ShouldThrowOnMissingCollectionElement
+ => false;
}
diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
index 93e2997608e..b4da0cb54ad 100644
--- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
@@ -116,17 +116,6 @@ protected virtual Expression CreateSnapshotExpression(
var count = types.Length;
var arguments = new Expression[count];
- var structuralTypeVariable = clrType == null
- ? null
- : Expression.Variable(clrType, "structuralType");
-
- Check.DebugAssert(
- structuralTypeVariable != null || count == 0,
- "If there are any properties then the entity parameter must be used");
- var indicesExpression = parameter == null || !parameter.Type.IsAssignableTo(typeof(IInternalEntry))
- ? (Expression)Expression.Property(null, typeof(ReadOnlySpan), nameof(ReadOnlySpan<>.Empty))
- : Expression.Call(parameter, PropertyAccessorsFactory.GetOrdinalsMethod);
-
for (var i = 0; i < count; i++)
{
var propertyBase = propertyBases[i];
@@ -171,45 +160,79 @@ protected virtual Expression CreateSnapshotExpression(
arguments),
typeof(ISnapshot));
+ var structuralTypeVariable = clrType == null
+ ? null
+ : Expression.Variable(clrType, "structuralType");
+
+ Check.DebugAssert(
+ structuralTypeVariable != null || count == 0,
+ "If there are any properties then the entity parameter must be used");
Check.DebugAssert(
!UseEntityVariable || structuralTypeVariable == null || parameter != null,
"Parameter can only be null when not using entity variable.");
- return UseEntityVariable
- && structuralTypeVariable != null
- ? Expression.Block(
- new List { structuralTypeVariable },
- new List
- {
- Expression.Assign(
- structuralTypeVariable,
- (IRuntimeTypeBase)propertyBases[0]!.DeclaringType switch
- {
- IComplexType { ComplexProperty.IsCollection: true } declaringComplexType => PropertyAccessorsFactory.CreateComplexCollectionElementAccess(
- declaringComplexType.ComplexProperty,
- Expression.Convert(
- Expression.Property(parameter!, nameof(IInternalEntry.Entity)),
- declaringComplexType.ComplexProperty.DeclaringType.ContainingEntityType.ClrType),
- indicesExpression,
- fromDeclaringType: false,
- fromEntity: true),
- { ContainingEntryType: IComplexType collectionComplexType }
- => PropertyAccessorsFactory.CreateComplexCollectionElementAccess(
- collectionComplexType.ComplexProperty,
- Expression.Convert(
- Expression.Property(parameter!, nameof(IInternalEntry.Entity)),
- collectionComplexType.ComplexProperty.DeclaringType.ContainingEntityType.ClrType),
- indicesExpression,
- fromDeclaringType: false,
- fromEntity: true),
- _
- => Expression.Convert(
- Expression.Property(parameter!, nameof(IInternalEntry.Entity)),
- structuralTypeVariable.Type)
- }),
- constructorExpression
- })
- : constructorExpression;
+ if (!UseEntityVariable || structuralTypeVariable == null)
+ {
+ return constructorExpression;
+ }
+
+ Expression? isMissingExpression = null;
+ var indicesExpression = parameter == null || !parameter.Type.IsAssignableTo(typeof(IInternalEntry))
+ ? (Expression)Expression.Property(null, typeof(ReadOnlySpan), nameof(ReadOnlySpan<>.Empty))
+ : Expression.Call(parameter, PropertyAccessorsFactory.GetOrdinalsMethod);
+ var declaringType = (IRuntimeTypeBase)propertyBases[0]!.DeclaringType;
+ var entityAccessExpression = declaringType switch
+ {
+ IComplexType { ComplexProperty.IsCollection: true } declaringComplexType => PropertyAccessorsFactory.CreateComplexCollectionElementAccess(
+ declaringComplexType.ComplexProperty,
+ Expression.Convert(
+ Expression.Property(parameter!, nameof(IInternalEntry.Entity)),
+ declaringComplexType.ComplexProperty.DeclaringType.ContainingEntityType.ClrType),
+ indicesExpression,
+ fromDeclaringType: false,
+ fromEntity: true,
+ shouldThrowIfMissing: ShouldThrowOnMissingCollectionElement,
+ isMissingExpression: out isMissingExpression),
+ {ContainingEntryType: IComplexType collectionComplexType } => PropertyAccessorsFactory.CreateComplexCollectionElementAccess(
+ collectionComplexType.ComplexProperty,
+ Expression.Convert(
+ Expression.Property(parameter!, nameof(IInternalEntry.Entity)),
+ collectionComplexType.ComplexProperty.DeclaringType.ContainingEntityType.ClrType),
+ indicesExpression,
+ fromDeclaringType: false,
+ fromEntity: true,
+ shouldThrowIfMissing: ShouldThrowOnMissingCollectionElement,
+ isMissingExpression: out isMissingExpression),
+ _ => Expression.Convert(
+ Expression.Property(parameter!, nameof(IInternalEntry.Entity)),
+ structuralTypeVariable.Type),
+ };
+
+ var snapshotBlock = Expression.Block(
+ [structuralTypeVariable],
+ Expression.Assign(structuralTypeVariable, entityAccessExpression),
+ constructorExpression);
+
+ if (isMissingExpression != null)
+ {
+ // When the collection element is missing (null collection or out of bounds),
+ // return a default-valued snapshot instead of accessing the element.
+ var defaultArguments = new Expression[count];
+ for (var i = 0; i < count; i++)
+ {
+ defaultArguments[i] = Expression.Default(types[i]);
+ }
+
+ var emptySnapshotExpression = Expression.Convert(
+ Expression.New(
+ Snapshot.CreateSnapshotType(types).GetDeclaredConstructor(types)!,
+ defaultArguments),
+ typeof(ISnapshot));
+
+ return Expression.Condition(isMissingExpression, emptySnapshotExpression, snapshotBlock);
+ }
+
+ return snapshotBlock;
}
private Expression CreateSnapshotValueExpression(Expression expression, IPropertyBase propertyBase)
@@ -342,6 +365,15 @@ protected virtual Expression CreateReadValueExpression(
protected virtual bool UseEntityVariable
=> 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
+ /// 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.
+ ///
+ protected virtual bool ShouldThrowOnMissingCollectionElement
+ => true;
+
private static readonly MethodInfo SnapshotCollectionMethod
= typeof(SnapshotFactoryFactory).GetTypeInfo().GetDeclaredMethod(nameof(SnapshotCollection))!;
diff --git a/src/EFCore/ChangeTracking/PropertyValues.cs b/src/EFCore/ChangeTracking/PropertyValues.cs
index 79cc7750d29..cf8a44aedd8 100644
--- a/src/EFCore/ChangeTracking/PropertyValues.cs
+++ b/src/EFCore/ChangeTracking/PropertyValues.cs
@@ -41,7 +41,6 @@ protected PropertyValues(InternalEntryBase internalEntry)
InternalEntry = internalEntry;
var complexCollectionProperties = new List();
- var nullableComplexProperties = new List();
foreach (var complexProperty in internalEntry.StructuralType.GetFlattenedComplexProperties())
{
@@ -49,17 +48,36 @@ protected PropertyValues(InternalEntryBase internalEntry)
{
complexCollectionProperties.Add(complexProperty);
}
- else if (complexProperty.IsNullable && !complexProperty.IsShadowProperty())
- {
- nullableComplexProperties!.Add(complexProperty);
- }
}
_complexCollectionProperties = complexCollectionProperties;
Check.DebugAssert(
_complexCollectionProperties.Select((p, i) => p.GetIndex() == i).All(e => e),
"Complex collection properties indices are not sequential.");
- _nullableComplexProperties = nullableComplexProperties?.Count > 0 ? nullableComplexProperties : null;
+ _nullableComplexProperties = ComputeNullableComplexProperties(internalEntry.StructuralType);
+ }
+
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ protected static IReadOnlyList? ComputeNullableComplexProperties(ITypeBase structuralType)
+ {
+ List? nullableComplexProperties = null;
+
+ foreach (var complexProperty in structuralType.GetFlattenedComplexProperties())
+ {
+ if (!complexProperty.IsCollection && complexProperty.IsNullable && !complexProperty.IsShadowProperty())
+ {
+ nullableComplexProperties ??= [];
+ nullableComplexProperties.Add(complexProperty);
+ }
+ }
+
+ return nullableComplexProperties;
}
///
diff --git a/src/EFCore/Design/Internal/CSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore/Design/Internal/CSharpRuntimeAnnotationCodeGenerator.cs
index 89c3546a52b..65daf72ad25 100644
--- a/src/EFCore/Design/Internal/CSharpRuntimeAnnotationCodeGenerator.cs
+++ b/src/EFCore/Design/Internal/CSharpRuntimeAnnotationCodeGenerator.cs
@@ -42,6 +42,7 @@ public virtual void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorP
{
annotations.Remove(CoreAnnotationNames.ModelDependencies);
annotations.Remove(CoreAnnotationNames.ReadOnlyModel);
+ annotations.Remove(CoreAnnotationNames.DetailedErrorsEnabled);
}
GenerateSimpleAnnotations(parameters);
diff --git a/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs b/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs
index eee799fd065..88952bef9e3 100644
--- a/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs
+++ b/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs
@@ -73,6 +73,13 @@ public virtual IModel Initialize(
{
model.ModelDependencies = Dependencies.ModelDependencies;
+ if (Dependencies.CoreSingletonOptions.AreDetailedErrorsEnabled)
+ {
+ model.SetRuntimeAnnotation(
+ CoreAnnotationNames.DetailedErrorsEnabled,
+ true);
+ }
+
InitializeModel(model, designTime, prevalidation: true);
if (validationLogger != null
diff --git a/src/EFCore/Infrastructure/ModelRuntimeInitializerDependencies.cs b/src/EFCore/Infrastructure/ModelRuntimeInitializerDependencies.cs
index 844ff0bafdb..95654ba5e3c 100644
--- a/src/EFCore/Infrastructure/ModelRuntimeInitializerDependencies.cs
+++ b/src/EFCore/Infrastructure/ModelRuntimeInitializerDependencies.cs
@@ -47,10 +47,12 @@ public sealed record ModelRuntimeInitializerDependencies
[EntityFrameworkInternal]
public ModelRuntimeInitializerDependencies(
RuntimeModelDependencies runtimeModelDependencies,
- IModelValidator modelValidator)
+ IModelValidator modelValidator,
+ ICoreSingletonOptions coreSingletonOptions)
{
ModelDependencies = runtimeModelDependencies;
ModelValidator = modelValidator;
+ CoreSingletonOptions = coreSingletonOptions;
}
///
@@ -58,6 +60,14 @@ public ModelRuntimeInitializerDependencies(
///
public RuntimeModelDependencies ModelDependencies { get; init; }
+ ///
+ /// 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 ICoreSingletonOptions CoreSingletonOptions { get; init; }
+
///
/// The model validator.
///
diff --git a/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs
index 3e4ed948761..4ecf6c75500 100644
--- a/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs
+++ b/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs
@@ -244,7 +244,9 @@ static Expression CreateMemberAssignment(
previousLevel,
indicesParameter,
fromDeclaringType: true,
- fromEntity: false)));
+ fromEntity: false,
+ shouldThrowIfMissing: true,
+ isMissingExpression: out _)));
var indexExpression = MakeIndex(
indicesParameter,
@@ -304,7 +306,9 @@ static Expression CreateMemberAssignment(
i == (chainCount - 1) ? instanceParameter : variables[chainCount - 2 - i],
indicesParameter,
fromDeclaringType: true,
- fromEntity: false);
+ fromEntity: false,
+ shouldThrowIfMissing: true,
+ isMissingExpression: out _);
statements.Add(memberExpression.Assign(variables[chainCount - 1 - i]));
}
diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs
index 1246a351809..7709e272e61 100644
--- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs
+++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs
@@ -227,6 +227,14 @@ public static class CoreAnnotationNames
///
public const string ModelDependencies = "ModelDependencies";
+ ///
+ /// 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 const string DetailedErrorsEnabled = "DetailedErrorsEnabled";
+
///
/// 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
@@ -406,6 +414,7 @@ public static class CoreAnnotationNames
LazyLoadingEnabled,
ProviderClrType,
ModelDependencies,
+ DetailedErrorsEnabled,
ReadOnlyModel,
PreUniquificationName,
DerivedTypes,
diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
index 59c46d11b10..8fbd8f04804 100644
--- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
+++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
@@ -274,7 +274,8 @@ public static Expression CreateMemberAccess(
MemberInfo memberInfo,
bool fromDeclaringType,
bool fromEntity,
- bool addNullCheck = true)
+ bool addNullCheck = true,
+ bool shouldThrowIfMissing = true)
{
Check.DebugAssert(!fromEntity || !fromDeclaringType, "fromEntity and fromDeclaringType can't both be true");
@@ -285,7 +286,17 @@ public static Expression CreateMemberAccess(
{
case { IsCollection: true } complexProperty when fromEntity:
instanceExpression = CreateComplexCollectionElementAccess(
- complexProperty, instanceExpression, indicesExpression, fromDeclaringType, fromEntity);
+ complexProperty, instanceExpression, indicesExpression, fromDeclaringType, fromEntity,
+ shouldThrowIfMissing, out var isMissingExpression);
+
+ if (isMissingExpression != null)
+ {
+ instanceExpression = Expression.Condition(
+ isMissingExpression,
+ Expression.Default(instanceExpression.Type),
+ instanceExpression);
+ }
+
break;
case { IsCollection: false } complexProperty:
instanceExpression = CreateMemberAccess(
@@ -340,6 +351,22 @@ public static Expression CreateMemberAccess(
private static readonly MethodInfo ComplexCollectionNotInitializedMethod
= typeof(CoreStrings).GetMethod(nameof(CoreStrings.ComplexCollectionNotInitialized), BindingFlags.Static | BindingFlags.Public)!;
+ private static readonly MethodInfo ComplexCollectionOrdinalOutOfRangeMethod
+ = typeof(CoreStrings).GetMethod(nameof(CoreStrings.ComplexCollectionOrdinalOutOfRange), BindingFlags.Static | BindingFlags.Public)!;
+
+ private static readonly PropertyInfo ReadOnlyListIndexer
+ = typeof(IReadOnlyList).GetProperty("Item")!;
+
+ private static PropertyInfo GetCollectionCountProperty(Type collectionType)
+ => collectionType.GetGenericTypeImplementations(typeof(ICollection<>))
+ .First()
+ .GetProperty(nameof(ICollection