-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
InMemory support for DefaultIfEmpty #17278
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,9 +109,19 @@ private Expression BindProperty(Expression source, string propertyName, Type typ | |
} | ||
|
||
var result = BindProperty(entityProjection, entityType.FindProperty(propertyName)); | ||
|
||
// If the entity projection is nullable (e.g. because of DefaultIfEmpty) but the member is a non-nullable value type, | ||
// we coalesce to its default value. | ||
if (IsNullableValueType(result.Type) && !IsNullableValueType(type)) | ||
{ | ||
result = Expression.Coalesce(result, Expression.Default(result.Type.UnwrapNullableType())); | ||
} | ||
|
||
return result.Type == type | ||
? result | ||
: Expression.Convert(result, type); | ||
|
||
static bool IsNullableValueType(Type t) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); | ||
} | ||
|
||
throw new InvalidOperationException(CoreStrings.TranslationFailed(source.Print())); | ||
|
@@ -315,24 +325,37 @@ private static T GetParameterValue<T>(QueryContext queryContext, string paramete | |
protected override Expression VisitUnary(UnaryExpression unaryExpression) | ||
{ | ||
var result = base.VisitUnary(unaryExpression); | ||
|
||
if (result is UnaryExpression outerUnary | ||
&& outerUnary.NodeType == ExpressionType.Convert | ||
&& outerUnary.Operand is UnaryExpression innerUnary | ||
&& innerUnary.NodeType == ExpressionType.Convert) | ||
&& outerUnary.NodeType == ExpressionType.Convert) | ||
{ | ||
var innerMostType = innerUnary.Operand.Type; | ||
var intermediateType = innerUnary.Type; | ||
var outerMostType = outerUnary.Type; | ||
|
||
if (outerMostType == innerMostType | ||
&& intermediateType == innerMostType.UnwrapNullableType()) | ||
if (outerUnary.Operand is UnaryExpression innerUnary | ||
&& innerUnary.NodeType == ExpressionType.Convert) | ||
{ | ||
result = innerUnary.Operand; | ||
var innerMostType = innerUnary.Operand.Type; | ||
var intermediateType = innerUnary.Type; | ||
|
||
if (outerMostType == innerMostType | ||
&& intermediateType == innerMostType.UnwrapNullableType()) | ||
{ | ||
return innerUnary.Operand; | ||
} | ||
} | ||
else if (outerMostType == typeof(object) | ||
&& intermediateType == innerMostType.UnwrapNullableType()) | ||
|
||
// VisitMember introduces a coalesce to default for value-type members being accessed on nullable entities. | ||
// However, if the member access was immediately wrapped by a Convert to object, we don't want the coalesce. | ||
// See test GroupJoin_DefaultIfEmpty_Project | ||
if (outerMostType == typeof(object) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the part I mentioned above, feels problematic. |
||
&& outerUnary.Operand is BinaryExpression binary | ||
&& binary.NodeType == ExpressionType.Coalesce | ||
&& binary.Right is DefaultExpression | ||
// Only discard the coalesce if it was introduced when translating MemberExpression | ||
// (user may have explicitly introduced it) | ||
&& unaryExpression.Operand is MemberExpression) | ||
{ | ||
result = Expression.Convert(innerUnary.Operand, typeof(object)); | ||
return Expression.Convert(binary.Left, typeof(object)); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -301,6 +301,39 @@ private Expression CreateReadValueExpression( | |
Constant(index), | ||
Constant(property, typeof(IPropertyBase))); | ||
|
||
public virtual void ApplyDefaultIfEmpty() | ||
{ | ||
var nullableReadValueExpressionVisitor = new NullableReadValueExpressionVisitor(); | ||
var projectionMapping = new Dictionary<ProjectionMember, Expression>(); | ||
var index = 0; | ||
foreach (var projection in _projectionMapping) | ||
{ | ||
if (projection.Value is EntityProjectionExpression entityProjection) | ||
{ | ||
var readExpressionMap = GetAllPropertiesInHierarchy(entityProjection.EntityType) | ||
.ToDictionary( | ||
p => p, | ||
p => CreateReadValueExpression( | ||
nullableReadValueExpressionVisitor.Visit(entityProjection.BindProperty(p)).Type, index++, p)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is somewhat overkill. You are just using type. You can pass nullable type directly. Also it is coming from EntityProjection so it would be readValueMethod. NullableReadValueEV is for complex expression which may have nested ReadValue calls which needs to be converted to null. Further, for DefaultIfEmpty. you just need to update current projection Mapping rather than change anything going inside so you can just create nullable calls. |
||
|
||
projectionMapping[projection.Key] = new EntityProjectionExpression(entityProjection.EntityType, readExpressionMap); | ||
} | ||
else | ||
{ | ||
projectionMapping[projection.Key] = CreateReadValueExpression( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also incorrect. |
||
nullableReadValueExpressionVisitor.Visit(projection.Value).Type, index++, InferPropertyFromInner(projection.Value)); | ||
} | ||
} | ||
|
||
_projectionMapping = projectionMapping; | ||
|
||
// Rewrite 0-arg DefaultOrEmpty to DefaultOrEmpty to a ValueBuffer containing all nulls | ||
ServerQueryExpression = Call( | ||
InMemoryLinqOperatorProvider.DefaultIfEmptyWithArgument.MakeGenericMethod(typeof(ValueBuffer)), | ||
ServerQueryExpression, | ||
New(_valueBufferConstructor, NewArrayInit(typeof(object), Enumerable.Repeat(Constant(null), index)))); | ||
} | ||
|
||
public virtual void AddInnerJoin( | ||
InMemoryQueryExpression innerQueryExpression, | ||
LambdaExpression outerKeySelector, | ||
|
@@ -486,7 +519,7 @@ public virtual void AddLeftJoin( | |
|
||
var collectionSelector = Lambda( | ||
Call( | ||
InMemoryLinqOperatorProvider.DefaultIfEmptyWithArg.MakeGenericMethod(typeof(ValueBuffer)), | ||
InMemoryLinqOperatorProvider.DefaultIfEmptyWithArgument.MakeGenericMethod(typeof(ValueBuffer)), | ||
collection, | ||
New( | ||
_valueBufferConstructor, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,7 +161,18 @@ protected override ShapedQueryExpression TranslateCount(ShapedQueryExpression so | |
return source; | ||
} | ||
|
||
protected override ShapedQueryExpression TranslateDefaultIfEmpty(ShapedQueryExpression source, Expression defaultValue) => throw new NotImplementedException(); | ||
protected override ShapedQueryExpression TranslateDefaultIfEmpty(ShapedQueryExpression source, Expression defaultValue) | ||
{ | ||
if (defaultValue == null) | ||
{ | ||
((InMemoryQueryExpression)source.QueryExpression).ApplyDefaultIfEmpty(); | ||
source.ShaperExpression = MarkShaperNullable(source.ShaperExpression); | ||
|
||
return source; | ||
} | ||
|
||
throw new InvalidOperationException(CoreStrings.TranslationFailed(defaultValue.Print())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returns null. |
||
} | ||
|
||
protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression source) | ||
{ | ||
|
@@ -319,9 +330,6 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s | |
return source; | ||
} | ||
|
||
private static readonly MethodInfo _defaultIfEmptyWithoutArgMethodInfo = typeof(Enumerable).GetTypeInfo() | ||
.GetDeclaredMethods(nameof(Enumerable.DefaultIfEmpty)).Single(mi => mi.GetParameters().Length == 1); | ||
|
||
protected override ShapedQueryExpression TranslateSelectMany( | ||
ShapedQueryExpression source, LambdaExpression collectionSelector, LambdaExpression resultSelector) | ||
{ | ||
|
@@ -330,7 +338,7 @@ protected override ShapedQueryExpression TranslateSelectMany( | |
|
||
if (collectionSelectorBody is MethodCallExpression collectionEndingMethod | ||
&& collectionEndingMethod.Method.IsGenericMethod | ||
&& collectionEndingMethod.Method.GetGenericMethodDefinition() == _defaultIfEmptyWithoutArgMethodInfo) | ||
&& collectionEndingMethod.Method.GetGenericMethodDefinition() == InMemoryLinqOperatorProvider.DefaultIfEmptyWithoutArgument) | ||
{ | ||
//defaultIfEmpty = true; | ||
collectionSelectorBody = collectionEndingMethod.Arguments[0]; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may not be right thing to do.
There is a lot of overlap in this area between work we all are doing, (mainly @maumar is tackling it).
Due to null propagation needs to introduced by EF core on nav expansion and depending on translation,
Translation of certain expression can return null. (mainly for unmapped property. Also see #17050), That means every expression composition when visiting children needs to take care of encountering null and cascade null forward. I made some changes in the one of the PR I merged.
Translation of certain non-null expression can return nullable expression back. This is due to the fact that property may be non-nullable but it may end up being null due to optional navigation path. In such cases we should be rewriting the expression tree to make things nullable. Coalesce may be ok in some cases but may not represent exact match.
e.g.
(a => a.OptionalNav.Counter == 0)
, if you use coalesce it gives true back. But is counter really 0?