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

Fixes #701

Part of #13224, #15070, #13613

This PR contains the underlying model and change tracking changes needed to support sentinel values. Future PRs will add model building API surface and setting the sentinel automatically based on the database default.

There is a breaking change here: explicitly setting the value of a property with a temporary value no longer automatically makes the value non temporary.
  • Loading branch information
ajcvickers committed Apr 27, 2023
1 parent 2edfca8 commit e1fedf2
Show file tree
Hide file tree
Showing 62 changed files with 4,657 additions and 3,243 deletions.
249 changes: 83 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 e1fedf2

Please sign in to comment.