Skip to content

Commit

Permalink
Use nullable backing field to check for default value
Browse files Browse the repository at this point in the history
Fixes #15182

This allows a null backing field to be used for a non-nullable property while at the same time checking the backing field for null in order to determine if a property value has been set.
  • Loading branch information
ajcvickers committed Jun 21, 2020
1 parent 61f3c0b commit d20c01e
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 74 deletions.
5 changes: 5 additions & 0 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,11 @@ private CurrentValueType GetValueType(
equals = ValuesEqualFunc(property);
}

if (!PropertyHasDefaultValue(property))
{
return CurrentValueType.Normal;
}

var defaultValue = property.ClrType.GetDefaultValue();
var value = ReadPropertyValue(property);
if (!equals(value, defaultValue))
Expand Down
39 changes: 38 additions & 1 deletion src/EFCore/Extensions/Internal/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Utilities;

// ReSharper disable once CheckNamespace
Expand All @@ -21,6 +22,42 @@ namespace Microsoft.EntityFrameworkCore.Internal
/// </summary>
public static class ExpressionExtensions
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static Expression MakeHasDefaultValue<TProperty>(
[NotNull] this Expression currentValueExpression, [NotNull] IPropertyBase propertyBase)
{
if (!currentValueExpression.Type.IsValueType)
{
return Expression.ReferenceEqual(
currentValueExpression,
Expression.Constant(null, currentValueExpression.Type));
}

if (currentValueExpression.Type.IsGenericType
&& currentValueExpression.Type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return Expression.Not(
Expression.Call(
currentValueExpression,
currentValueExpression.Type.GetMethod("get_HasValue")));
}

var property = propertyBase as IProperty;
var comparer = property?.GetValueComparer()
?? ValueComparer.CreateDefault(typeof(TProperty), favorStructuralComparisons: false);

return comparer.ExtractEqualsBody(
comparer.Type != typeof(TProperty)
? Expression.Convert(currentValueExpression, comparer.Type)
: currentValueExpression,
Expression.Default(comparer.Type));
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
40 changes: 7 additions & 33 deletions src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal
{
Expand Down Expand Up @@ -61,40 +61,14 @@ protected override IClrPropertyGetter CreateGeneric<TEntity, TValue, TNonNullabl
});
}

if (readExpression.Type != typeof(TValue))
{
readExpression = Expression.Convert(readExpression, typeof(TValue));
}
var hasDefaultValueExpression = readExpression.MakeHasDefaultValue<TValue>(propertyBase);

Expression hasDefaultValueExpression;

if (!readExpression.Type.IsValueType)
{
hasDefaultValueExpression
= Expression.ReferenceEqual(
readExpression,
Expression.Constant(null, readExpression.Type));
}
else if (readExpression.Type.IsGenericType
&& readExpression.Type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
hasDefaultValueExpression
= Expression.Not(
Expression.Call(
readExpression,
readExpression.Type.GetMethod("get_HasValue")));
}
else
if (readExpression.Type != typeof(TValue))
{
var property = propertyBase as IProperty;
var comparer = property?.GetValueComparer()
?? ValueComparer.CreateDefault(typeof(TValue), favorStructuralComparisons: false);

hasDefaultValueExpression = comparer.ExtractEqualsBody(
comparer.Type != typeof(TValue)
? Expression.Convert(readExpression, comparer.Type)
: readExpression,
Expression.Default(comparer.Type));
readExpression = Expression.Condition(
hasDefaultValueExpression,
Expression.Constant(default(TValue), typeof(TValue)),
Expression.Convert(readExpression, typeof(TValue)));
}

return new ClrPropertyGetter<TEntity, TValue>(
Expand Down
6 changes: 5 additions & 1 deletion src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Update;

Expand Down Expand Up @@ -76,7 +77,10 @@ private static Func<IUpdateEntry, TProperty> CreateCurrentValueGetter<TProperty>

if (currentValueExpression.Type != typeof(TProperty))
{
currentValueExpression = Expression.Convert(currentValueExpression, typeof(TProperty));
currentValueExpression = Expression.Condition(
currentValueExpression.MakeHasDefaultValue<TProperty>(propertyBase),
Expression.Constant(default(TProperty), typeof(TProperty)),
Expression.Convert(currentValueExpression, typeof(TProperty)));
}
}

Expand Down
4 changes: 2 additions & 2 deletions test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1916,7 +1916,7 @@ public virtual void Can_insert_and_read_back_non_nullable_backed_data_types()
DateTimeOffset = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)),
TimeSpan = new TimeSpan(0, 10, 9, 8, 7),
Single = -1.234F,
Boolean = false,
Boolean = true,
Byte = 255,
UnsignedInt16 = 1234,
UnsignedInt32 = 1234565789U,
Expand Down Expand Up @@ -1953,7 +1953,7 @@ public virtual void Can_insert_and_read_back_non_nullable_backed_data_types()
() => dt.DateTimeOffset);
AssertEqualIfMapped(entityType, new TimeSpan(0, 10, 9, 8, 7), () => dt.TimeSpan);
AssertEqualIfMapped(entityType, -1.234F, () => dt.Single);
AssertEqualIfMapped(entityType, false, () => dt.Boolean);
AssertEqualIfMapped(entityType, true, () => dt.Boolean);
AssertEqualIfMapped(entityType, (byte)255, () => dt.Byte);
AssertEqualIfMapped(entityType, Enum64.SomeValue, () => dt.Enum64);
AssertEqualIfMapped(entityType, Enum32.SomeValue, () => dt.Enum32);
Expand Down
2 changes: 1 addition & 1 deletion test/EFCore.Specification.Tests/FieldMappingTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected class User2 : IUser2

protected class LoginSession
{
private object _id = 0;
private object _id;
private IUser2 _user;
private object _users;

Expand Down
Loading

0 comments on commit d20c01e

Please sign in to comment.