Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow configuration of the sentinel value that indicates a property has not been set #30760

Merged
merged 2 commits into from
Apr 29, 2023
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
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
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
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