Skip to content

Commit

Permalink
Allow configuration of the sentinel value that indicates a property h…
Browse files Browse the repository at this point in the history
…as not been set (#30760)
  • Loading branch information
ajcvickers authored Apr 29, 2023
1 parent 7b1ef88 commit 3b1ffd9
Show file tree
Hide file tree
Showing 64 changed files with 4,721 additions and 3,280 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,14 @@ private void Create(
.Append("()");
}

var sentinel = property.Sentinel;
if (sentinel != null)
{
mainBuilder.AppendLine(",")
.Append("sentinel: ")
.Append(_code.UnknownLiteral(sentinel));
}

mainBuilder
.AppendLine(");")
.DecrementIndent();
Expand Down
257 changes: 91 additions & 166 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions src/EFCore/ChangeTracking/Internal/KeyPropagator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,8 @@ private static void SetValue(InternalEntityEntry entry, IProperty property, Valu

if (principalProperty != property)
{
var principalValue = principalEntry[principalProperty];
if (generationProperty == null
|| !principalProperty.ClrType.IsDefaultValue(principalValue))
|| principalEntry.HasExplicitValue(principalProperty))
{
entry.PropagateValue(principalEntry, principalProperty, property);

Expand Down
5 changes: 4 additions & 1 deletion src/EFCore/ChangeTracking/Internal/SidecarValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ public bool TryGetValue(int index, out object? value)
return true;
}

public object? GetValue(int index)
=> _values[index];

public T GetValue<T>(int index)
=> IsEmpty ? default! : _values.GetValue<T>(index);
=> _values.GetValue<T>(index);

public void SetValue(IProperty property, object? value, int index)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen
/// </summary>
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.CurrentValueGetter)(entry);
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry);
return key != null;
}

Expand All @@ -73,7 +73,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen
/// </summary>
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry);
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry);
return key != null;
}

Expand All @@ -85,7 +85,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
/// </summary>
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.OriginalValueGetter!)(entry);
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry);
return key != null;
}

Expand All @@ -97,7 +97,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhe
/// </summary>
public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.RelationshipSnapshotGetter)(entry);
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry);
return key != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen
/// </summary>
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.CurrentValueGetter)(entry)!;
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry)!;
return true;
}

Expand All @@ -80,7 +80,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen
/// </summary>
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)!;
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry)!;
return true;
}

Expand All @@ -92,7 +92,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
/// </summary>
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.OriginalValueGetter!)(entry)!;
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry)!;
return true;
}

Expand All @@ -104,7 +104,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhe
/// </summary>
public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = ((Func<IUpdateEntry, TKey>)_propertyAccessors.RelationshipSnapshotGetter)(entry)!;
key = ((Func<InternalEntityEntry, TKey>)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry)!;
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, out TKey key
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key)
=> HandleNullableValue(((Func<IUpdateEntry, TKey?>)_propertyAccessors.CurrentValueGetter)(entry), out key);
=> HandleNullableValue(((Func<InternalEntityEntry, TKey?>)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry), out key);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -76,7 +76,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key
/// </summary>
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, out TKey key)
=> HandleNullableValue(
((Func<IUpdateEntry, TKey?>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry), out key);
((Func<InternalEntityEntry, TKey?>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry), out key);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -85,7 +85,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, out TKey key)
=> HandleNullableValue(((Func<IUpdateEntry, TKey?>)_propertyAccessors.OriginalValueGetter!)(entry), out key);
=> HandleNullableValue(((Func<InternalEntityEntry, TKey?>)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry), out key);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -94,7 +94,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, out TKey ke
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, out TKey key)
=> HandleNullableValue(((Func<IUpdateEntry, TKey?>)_propertyAccessors.RelationshipSnapshotGetter)(entry), out key);
=> HandleNullableValue(((Func<InternalEntityEntry, TKey?>)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry), out key);

private static bool HandleNullableValue(TKey? value, out TKey key)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen
/// </summary>
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = (TKey)(object)((Func<IUpdateEntry, TNonNullableKey>)_propertyAccessors.CurrentValueGetter)(entry)!;
key = (TKey)(object)((Func<InternalEntityEntry, TNonNullableKey>)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry)!;
return true;
}

Expand All @@ -84,7 +84,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen
/// </summary>
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = (TKey)(object)((Func<IUpdateEntry, TNonNullableKey>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)!;
key = (TKey)(object)((Func<InternalEntityEntry, TNonNullableKey>)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry)!;
return true;
}

Expand All @@ -96,7 +96,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
/// </summary>
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = (TKey)(object)((Func<IUpdateEntry, TNonNullableKey>)_propertyAccessors.OriginalValueGetter!)(entry)!;
key = (TKey)(object)((Func<InternalEntityEntry, TNonNullableKey>)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry)!;
return true;
}

Expand All @@ -108,7 +108,7 @@ public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhe
/// </summary>
public virtual bool TryCreateFromRelationshipSnapshot(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
key = (TKey)(object)((Func<IUpdateEntry, TNonNullableKey>)_propertyAccessors.RelationshipSnapshotGetter)(entry)!;
key = (TKey)(object)((Func<InternalEntityEntry, TNonNullableKey>)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry)!;
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public virtual IProperty FindNullPropertyInKeyValues(IReadOnlyList<object?> keyV
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TKey CreateFromCurrentValues(IUpdateEntry entry)
=> ((Func<IUpdateEntry, TKey>)_propertyAccessors.CurrentValueGetter)(entry);
=> ((Func<InternalEntityEntry, TKey>)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -86,7 +86,7 @@ public virtual IProperty FindNullPropertyInCurrentValues(IUpdateEntry entry)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TKey CreateFromOriginalValues(IUpdateEntry entry)
=> ((Func<IUpdateEntry, TKey>)_propertyAccessors.OriginalValueGetter!)(entry);
=> ((Func<InternalEntityEntry, TKey>)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -95,7 +95,7 @@ public virtual TKey CreateFromOriginalValues(IUpdateEntry entry)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TKey CreateFromRelationshipSnapshot(IUpdateEntry entry)
=> ((Func<IUpdateEntry, TKey>)_propertyAccessors.RelationshipSnapshotGetter)(entry);
=> ((Func<InternalEntityEntry, TKey>)_propertyAccessors.RelationshipSnapshotGetter)((InternalEntityEntry)entry);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -119,18 +119,6 @@ public virtual object CreateEquatableKey(IUpdateEntry entry, bool fromOriginalVa
: CreateFromCurrentValues(entry),
EqualityComparer);

private sealed class NoNullsStructuralEqualityComparer : IEqualityComparer<TKey>
{
private readonly IEqualityComparer _comparer
= StructuralComparisons.StructuralEqualityComparer;

public bool Equals(TKey? x, TKey? y)
=> _comparer.Equals(x, y);

public int GetHashCode([DisallowNull] TKey obj)
=> _comparer.GetHashCode(obj);
}

private sealed class NoNullsCustomEqualityComparer : IEqualityComparer<TKey>
{
private readonly Func<TKey?, TKey?, bool> _equals;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected virtual Expression CreateSnapshotExpression(

if (memberAccess.Type != propertyBase.ClrType)
{
var hasDefaultValueExpression = memberAccess.MakeHasDefaultValue(propertyBase);
var hasDefaultValueExpression = memberAccess.MakeHasSentinelValue(propertyBase);

memberAccess = Expression.Condition(
hasDefaultValueExpression,
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore/ChangeTracking/Internal/StateData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ internal enum PropertyFlag
Null = 1,
Unknown = 2,
IsLoaded = 3,
IsTemporary = 4
IsTemporary = 4,
IsStoreGenerated = 5
}

internal readonly struct StateData
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,13 @@ public virtual InternalEntityEntry CreateEntry(IDictionary<string, object?> valu
{
valuesArray[i++] = values.TryGetValue(property.Name, out var value)
? value
: property.ClrType.GetDefaultValue();
: property.Sentinel;

if (property.IsShadowProperty())
{
shadowPropertyValuesArray[property.GetShadowIndex()] = values.TryGetValue(property.Name, out var shadowValue)
? shadowValue
: property.ClrType.GetDefaultValue();
: property.Sentinel;
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public ValueGenerationManager(
InternalEntityEntry? chosenPrincipal = null;
foreach (var property in entry.EntityType.GetForeignKeyProperties())
{
if (!entry.HasDefaultValue(property))
if (!entry.IsUnknown(property)
&& entry.HasExplicitValue(property))
{
continue;
}
Expand All @@ -68,7 +69,7 @@ public ValueGenerationManager(
InternalEntityEntry? chosenPrincipal = null;
foreach (var property in entry.EntityType.GetForeignKeyProperties())
{
if (!entry.HasDefaultValue(property))
if (entry.HasExplicitValue(property))
{
continue;
}
Expand All @@ -94,7 +95,7 @@ public virtual bool Generate(InternalEntityEntry entry, bool includePrimaryKey =

foreach (var property in entry.EntityType.GetValueGeneratingProperties())
{
if (!entry.HasDefaultValue(property)
if (entry.HasExplicitValue(property)
|| (!includePrimaryKey
&& property.IsPrimaryKey()))
{
Expand Down Expand Up @@ -153,7 +154,7 @@ public virtual async Task<bool> GenerateAsync(
var hasNonStableValues = false;
foreach (var property in entry.EntityType.GetValueGeneratingProperties())
{
if (!entry.HasDefaultValue(property)
if (entry.HasExplicitValue(property)
|| (!includePrimaryKey
&& property.IsPrimaryKey()))
{
Expand Down
52 changes: 35 additions & 17 deletions src/EFCore/Extensions/Internal/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,55 @@ public static class ExpressionExtensions
/// 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 static Expression MakeHasDefaultValue(
public static Expression MakeHasSentinelValue(
this Expression currentValueExpression,
IReadOnlyPropertyBase? propertyBase)
{
if (!currentValueExpression.Type.IsValueType)
{
return Expression.ReferenceEqual(
currentValueExpression,
Expression.Constant(null, currentValueExpression.Type));
}
var sentinel = propertyBase?.Sentinel;

if (currentValueExpression.Type.IsGenericType
&& currentValueExpression.Type.GetGenericTypeDefinition() == typeof(Nullable<>))
var isReferenceType = !currentValueExpression.Type.IsValueType;
var isNullableValueType = currentValueExpression.Type.IsGenericType
&& currentValueExpression.Type.GetGenericTypeDefinition() == typeof(Nullable<>);

if (sentinel == null)
{
return Expression.Not(
Expression.Call(
return isReferenceType
? Expression.ReferenceEqual(
currentValueExpression,
Check.NotNull(
currentValueExpression.Type.GetMethod("get_HasValue"), $"get_HasValue on {currentValueExpression.Type.Name}")));
Expression.Constant(null, currentValueExpression.Type))
: isNullableValueType
? Expression.Not(
Expression.Call(
currentValueExpression,
currentValueExpression.Type.GetMethod("get_HasValue")!))
: Expression.Constant(false);
}

var property = propertyBase as IReadOnlyProperty;
var comparer = property?.GetValueComparer()
var comparer = (propertyBase as IProperty)?.GetValueComparer()
?? ValueComparer.CreateDefault(
propertyBase?.ClrType ?? currentValueExpression.Type, favorStructuralComparisons: false);

return comparer.ExtractEqualsBody(
var equalsExpression = comparer.ExtractEqualsBody(
comparer.Type != currentValueExpression.Type
? Expression.Convert(currentValueExpression, comparer.Type)
: currentValueExpression,
Expression.Default(comparer.Type));
Expression.Constant(sentinel, comparer.Type));

if (isReferenceType || isNullableValueType)
{
return Expression.AndAlso(
isReferenceType
? Expression.Not(
Expression.ReferenceEqual(
currentValueExpression,
Expression.Constant(null, currentValueExpression.Type)))
: Expression.Call(
currentValueExpression,
currentValueExpression.Type.GetMethod("get_HasValue")!),
equalsExpression);
}

return equalsExpression;
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ private static RuntimeProperty Create(IProperty property, RuntimeEntityType runt
=> runtimeEntityType.AddProperty(
property.Name,
property.ClrType,
property.Sentinel,
property.PropertyInfo,
property.FieldInfo,
property.GetPropertyAccessMode(),
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/Metadata/IClrPropertyGetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ public interface IClrPropertyGetter
/// </summary>
/// <param name="entity">The entity instance.</param>
/// <returns><see langword="true" /> if the property value is the CLR default; <see langword="false" /> it is any other value.</returns>
bool HasDefaultValue(object entity);
bool HasSentinelValue(object entity);
}
Loading

0 comments on commit 3b1ffd9

Please sign in to comment.