Skip to content

Commit 68744b8

Browse files
committed
Query: Add support for GroupBy patterns beyond aggregate
- Allow expanding navigations after GroupBy operator applied before reducing it to non-grouping Fixes #22609 - Translate FirstOrDefault over grouping element Fixes #12088 - Add ability to select N element over grouping element Fixes #13805 Overall approach: A grouping element (the range variable you get after applying GroupBy operator) is of type `IGrouping<TKey, TElement>` which implements `IEnumerable<TElement>`. Hence we treat this enumerable as if it is queryable during nav expansion phase. During translation phase we inject ShapedQueryExpression in place of the grouping element which is being enumerated. What this allows us is to expand navigation just like any other query root and translate a subquery similar to other subqueries to facilitate reusing same code for the tasks. During translation phase in relational layer, since aggregate operation can be lifted into projection for SelectExpression containing SQL GROUP BY. This code path works in 2 ways, when translating we try to combine predicate/distinct into the aggregate operation (so in future when we support custom aggregate operators, we don't have to understand the shape of it to modify it later. When adding this scalar subquery to SelectExpression, we try to pattern match it to see if we can lift it. Further during lifting, we also lift any additional joins in the subquery (which implies there were some joins expanded on grouping element before aggregate) including the navigation expanded from owned navigations. A pending TODO is to de-dupe navigation expanded. It is not straight forward since aliases of table would have changed when previous was lifted. Given every enumerable grouping element act as query root, every time we replace it inside a lambda expression, we need to create a copy of the root. Navigation expansion and individual queryableMethodTranslatingEV does this. So each root act and translate independently from each other. Bug fixes: - Fix a bug in identifying single result in InMemory to convert it to enumerable - Null out _groupingParameter in InMemoryQueryExpression once the projection to reduce it has been applied - Throw better error message when translating Min/Max over an entity type for InMemory
1 parent 527e8b5 commit 68744b8

File tree

33 files changed

+2326
-1125
lines changed

33 files changed

+2326
-1125
lines changed

src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs

+25
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,31 @@ public virtual void AddNavigationBinding(INavigation navigation, EntityShaperExp
148148
: null;
149149
}
150150

151+
/// <summary>
152+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
153+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
154+
/// any release. You should only use it directly in your code with extreme caution and knowing that
155+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
156+
/// </summary>
157+
public virtual EntityProjectionExpression Clone()
158+
{
159+
var readExpressionMap = new Dictionary<IProperty, MethodCallExpression>();
160+
foreach (var kvp in _readExpressionMap)
161+
{
162+
readExpressionMap.Add(kvp.Key, kvp.Value);
163+
}
164+
var entityProjectionExpression = new EntityProjectionExpression(EntityType, readExpressionMap);
165+
foreach (var kvp in _navigationExpressionsCache)
166+
{
167+
entityProjectionExpression._navigationExpressionsCache[kvp.Key] = new EntityShaperExpression(
168+
kvp.Value.EntityType,
169+
((EntityProjectionExpression)kvp.Value.ValueBufferExpression).Clone(),
170+
kvp.Value.IsNullable);
171+
}
172+
173+
return entityProjectionExpression;
174+
}
175+
151176
/// <summary>
152177
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
153178
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs

+8-290
Large diffs are not rendered by default.

src/EFCore.InMemory/Query/Internal/InMemoryGroupByShaperExpression.cs

-50
This file was deleted.

src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs

+60
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Linq.Expressions;
9+
using Microsoft.EntityFrameworkCore.Metadata;
910
using Microsoft.EntityFrameworkCore.Query;
1011
using Microsoft.EntityFrameworkCore.Storage;
1112
using Microsoft.EntityFrameworkCore.Utilities;
@@ -173,5 +174,64 @@ protected override Expression VisitExtension(Expression extensionExpression)
173174
: base.VisitExtension(extensionExpression);
174175
}
175176
}
177+
178+
private sealed class QueryExpressionReplacingExpressionVisitor : ExpressionVisitor
179+
{
180+
private readonly Expression _oldQuery;
181+
private readonly Expression _newQuery;
182+
183+
public QueryExpressionReplacingExpressionVisitor(Expression oldQuery, Expression newQuery)
184+
{
185+
_oldQuery = oldQuery;
186+
_newQuery = newQuery;
187+
}
188+
189+
[return: NotNullIfNotNull("expression")]
190+
public override Expression? Visit(Expression? expression)
191+
{
192+
return expression is ProjectionBindingExpression projectionBindingExpression
193+
&& ReferenceEquals(projectionBindingExpression.QueryExpression, _oldQuery)
194+
? projectionBindingExpression.ProjectionMember != null
195+
? new ProjectionBindingExpression(
196+
_newQuery, projectionBindingExpression.ProjectionMember!, projectionBindingExpression.Type)
197+
: new ProjectionBindingExpression(
198+
_newQuery, projectionBindingExpression.Index!.Value, projectionBindingExpression.Type)
199+
: base.Visit(expression);
200+
}
201+
}
202+
203+
private sealed class CloningExpressionVisitor : ExpressionVisitor
204+
{
205+
[return: NotNullIfNotNull("expression")]
206+
public override Expression? Visit(Expression? expression)
207+
{
208+
if (expression is InMemoryQueryExpression inMemoryQueryExpression)
209+
{
210+
var clonedInMemoryQueryExpression = new InMemoryQueryExpression(
211+
inMemoryQueryExpression.ServerQueryExpression, inMemoryQueryExpression._valueBufferParameter)
212+
{
213+
_groupingParameter = inMemoryQueryExpression._groupingParameter,
214+
_singleResultMethodInfo = inMemoryQueryExpression._singleResultMethodInfo,
215+
_scalarServerQuery = inMemoryQueryExpression._scalarServerQuery
216+
};
217+
218+
clonedInMemoryQueryExpression._clientProjections.AddRange(inMemoryQueryExpression._clientProjections.Select(e => Visit(e)));
219+
clonedInMemoryQueryExpression._projectionMappingExpressions.AddRange(inMemoryQueryExpression._projectionMappingExpressions);
220+
foreach (var item in inMemoryQueryExpression._projectionMapping)
221+
{
222+
clonedInMemoryQueryExpression._projectionMapping[item.Key] = Visit(item.Value);
223+
}
224+
225+
return clonedInMemoryQueryExpression;
226+
}
227+
228+
if (expression is EntityProjectionExpression entityProjectionExpression)
229+
{
230+
return entityProjectionExpression.Clone();
231+
}
232+
233+
return base.Visit(expression);
234+
}
235+
}
176236
}
177237
}

src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs

+50-8
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,20 @@ private static readonly ConstructorInfo _resultEnumerableConstructor
4141
private MethodInfo? _singleResultMethodInfo;
4242
private bool _scalarServerQuery;
4343

44+
private CloningExpressionVisitor? _cloningExpressionVisitor;
45+
4446
private Dictionary<ProjectionMember, Expression> _projectionMapping = new();
4547
private readonly List<Expression> _clientProjections = new();
4648
private readonly List<Expression> _projectionMappingExpressions = new();
4749

50+
private InMemoryQueryExpression(
51+
Expression serverQueryExpression,
52+
ParameterExpression valueBufferParameter)
53+
{
54+
ServerQueryExpression = serverQueryExpression;
55+
_valueBufferParameter = valueBufferParameter;
56+
}
57+
4858
/// <summary>
4959
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
5060
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -333,15 +343,17 @@ public virtual void ApplyProjection()
333343
ServerQueryExpression,
334344
selectorLambda);
335345

346+
_groupingParameter = null;
347+
336348
if (_singleResultMethodInfo != null)
337349
{
338350
ServerQueryExpression = Call(
339351
_singleResultMethodInfo.MakeGenericMethod(CurrentParameter.Type),
340352
ServerQueryExpression);
341353

342-
_singleResultMethodInfo = null;
343-
344354
ConvertToEnumerable();
355+
356+
_singleResultMethodInfo = null;
345357
}
346358
}
347359

@@ -540,7 +552,7 @@ public virtual void ApplyDistinct()
540552
/// any release. You should only use it directly in your code with extreme caution and knowing that
541553
/// doing so can result in application failures when updating to a new Entity Framework Core release.
542554
/// </summary>
543-
public virtual InMemoryGroupByShaperExpression ApplyGrouping(
555+
public virtual GroupByShaperExpression ApplyGrouping(
544556
Expression groupingKey,
545557
Expression shaperExpression,
546558
bool defaultElementSelector)
@@ -583,11 +595,15 @@ public virtual InMemoryGroupByShaperExpression ApplyGrouping(
583595
keySelector,
584596
selector);
585597

586-
return new InMemoryGroupByShaperExpression(
598+
var clonedInMemoryQueryExpression = Clone();
599+
clonedInMemoryQueryExpression.UpdateServerQueryExpression(_groupingParameter);
600+
clonedInMemoryQueryExpression._groupingParameter = null;
601+
602+
return new GroupByShaperExpression(
587603
groupingKey,
588-
shaperExpression,
589-
_groupingParameter,
590-
_valueBufferParameter);
604+
new ShapedQueryExpression(
605+
clonedInMemoryQueryExpression,
606+
new QueryExpressionReplacingExpressionVisitor(this, clonedInMemoryQueryExpression).Visit(shaperExpression)));
591607
}
592608

593609
/// <summary>
@@ -711,6 +727,22 @@ public virtual EntityShaperExpression AddNavigationToWeakEntityType(
711727
return entityShaper;
712728
}
713729

730+
/// <summary>
731+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
732+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
733+
/// any release. You should only use it directly in your code with extreme caution and knowing that
734+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
735+
/// </summary>
736+
public virtual ShapedQueryExpression Clone(Expression shaperExpression)
737+
{
738+
var clonedInMemoryQueryExpression = Clone();
739+
740+
return new ShapedQueryExpression(
741+
clonedInMemoryQueryExpression,
742+
new QueryExpressionReplacingExpressionVisitor(this, clonedInMemoryQueryExpression).Visit(shaperExpression));
743+
744+
}
745+
714746
/// <summary>
715747
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
716748
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -811,6 +843,16 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
811843
}
812844
}
813845

846+
private InMemoryQueryExpression Clone()
847+
{
848+
if (_cloningExpressionVisitor == null)
849+
{
850+
_cloningExpressionVisitor = new();
851+
}
852+
853+
return (InMemoryQueryExpression)_cloningExpressionVisitor.Visit(this);
854+
}
855+
814856
private Expression GetGroupingKey(Expression key, List<Expression> groupingExpressions, Expression groupingKeyAccessExpression)
815857
{
816858
switch (key)
@@ -1061,7 +1103,7 @@ static Expression MakeNullable(Expression expression, bool nullable)
10611103

10621104
private void ConvertToEnumerable()
10631105
{
1064-
if (ServerQueryExpression.Type.TryGetSequenceType() == null)
1106+
if (_scalarServerQuery || _singleResultMethodInfo != null)
10651107
{
10661108
if (ServerQueryExpression.Type != typeof(ValueBuffer))
10671109
{

src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Linq;
78
using System.Linq.Expressions;
89
using System.Reflection;
@@ -70,6 +71,24 @@ protected InMemoryQueryableMethodTranslatingExpressionVisitor(
7071
protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
7172
=> new InMemoryQueryableMethodTranslatingExpressionVisitor(this);
7273

74+
/// <summary>
75+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
76+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
77+
/// any release. You should only use it directly in your code with extreme caution and knowing that
78+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
79+
/// </summary>
80+
protected override Expression VisitExtension(Expression extensionExpression)
81+
{
82+
if (extensionExpression is GroupByShaperExpression groupByShaperExpression)
83+
{
84+
var shapedQueryExpression = groupByShaperExpression.GroupingEnumerable;
85+
return ((InMemoryQueryExpression)shapedQueryExpression.QueryExpression)
86+
.Clone(shapedQueryExpression.ShaperExpression);
87+
}
88+
89+
return base.VisitExtension(extensionExpression);
90+
}
91+
7392
/// <summary>
7493
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
7594
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -1513,7 +1532,8 @@ private static Expression AccessField(
15131532
inMemoryQueryExpression.CurrentParameter)
15141533
: TranslateLambdaExpression(source, selector, preserveType: true);
15151534

1516-
if (selector == null)
1535+
if (selector == null
1536+
|| selector.Body is EntityProjectionExpression)
15171537
{
15181538
return null;
15191539
}

0 commit comments

Comments
 (0)