Skip to content

Commit 1d629b2

Browse files
committed
Query: Support for GroupBy entity type
Resolves #17653
1 parent be26865 commit 1d629b2

File tree

6 files changed

+129
-65
lines changed

6 files changed

+129
-65
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,22 @@ private static Expression GetGroupingKey(Expression key, List<Expression> groupi
870870

871871
return memberInitExpression.Update(updatedNewExpression, memberBindings);
872872

873+
case EntityShaperExpression entityShaperExpression
874+
when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression:
875+
var entityProjectionExpression = (EntityProjectionExpression)((InMemoryQueryExpression)projectionBindingExpression.QueryExpression)
876+
.GetProjection(projectionBindingExpression);
877+
var readExpressions = new Dictionary<IProperty, MethodCallExpression>();
878+
foreach (var property in GetAllPropertiesInHierarchy(entityProjectionExpression.EntityType))
879+
{
880+
readExpressions[property] = (MethodCallExpression)GetGroupingKey(
881+
entityProjectionExpression.BindProperty(property),
882+
groupingExpressions,
883+
groupingKeyAccessExpression);
884+
}
885+
886+
return entityShaperExpression.Update(
887+
new EntityProjectionExpression(entityProjectionExpression.EntityType, readExpressions));
888+
873889
default:
874890
var index = groupingExpressions.Count;
875891
groupingExpressions.Add(key);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,10 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy
455455

456456
return memberInitExpression.Update(updatedNewExpression, newBindings);
457457

458+
case EntityShaperExpression entityShaperExpression
459+
when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression:
460+
return entityShaperExpression;
461+
458462
default:
459463
var translation = TranslateExpression(expression);
460464
if (translation == null)

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,16 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
450450
var translatedKey = TranslateGroupingKey(remappedKeySelector);
451451
if (translatedKey == null)
452452
{
453-
return null;
453+
// This could be group by entity type
454+
if (remappedKeySelector is not EntityShaperExpression
455+
{ ValueBufferExpression : ProjectionBindingExpression })
456+
{
457+
// ValueBufferExpression can be JsonQuery, ProjectionBindingExpression, EntityProjection
458+
// We only allow ProjectionBindingExpression which represents a regular entity
459+
return null;
460+
}
461+
462+
translatedKey = remappedKeySelector;
454463
}
455464

456465
if (elementSelector != null)

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,6 +1857,23 @@ private static void PopulateGroupByTerms(
18571857
PopulateGroupByTerms(unaryExpression.Operand, groupByTerms, groupByAliases, name);
18581858
break;
18591859

1860+
case EntityShaperExpression entityShaperExpression
1861+
when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression:
1862+
var entityProjectionExpression = (EntityProjectionExpression)((SelectExpression)projectionBindingExpression.QueryExpression)
1863+
.GetProjection(projectionBindingExpression);
1864+
foreach (var property in GetAllPropertiesInHierarchy(entityProjectionExpression.EntityType))
1865+
{
1866+
PopulateGroupByTerms(entityProjectionExpression.BindProperty(property), groupByTerms, groupByAliases, name: null);
1867+
}
1868+
1869+
if (entityProjectionExpression.DiscriminatorExpression != null)
1870+
{
1871+
PopulateGroupByTerms(
1872+
entityProjectionExpression.DiscriminatorExpression, groupByTerms, groupByAliases, name: DiscriminatorColumnAlias);
1873+
}
1874+
1875+
break;
1876+
18601877
default:
18611878
throw new InvalidOperationException(RelationalStrings.InvalidKeySelectorForGroupBy(keySelector, keySelector.GetType()));
18621879
}

test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs

Lines changed: 66 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,6 +2688,19 @@ public virtual Task GroupBy_selecting_grouping_key_list(bool async)
26882688
AssertCollection(e.Data, a.Data);
26892689
});
26902690

2691+
[ConditionalTheory]
2692+
[MemberData(nameof(IsAsyncData))]
2693+
public virtual Task Select_GroupBy_SelectMany(bool async)
2694+
// Entity equality. Issue #15938.
2695+
=> AssertTranslationFailed(
2696+
() => AssertQuery(
2697+
async,
2698+
ss => ss.Set<Order>().Select(
2699+
o => new ProjectedType { Order = o.OrderID, Customer = o.CustomerID })
2700+
.GroupBy(p => p.Customer)
2701+
.SelectMany(g => g),
2702+
elementSorter: g => g.Order));
2703+
26912704
#endregion
26922705

26932706
#region GroupBySelectFirst
@@ -2788,81 +2801,73 @@ public virtual Task GroupBy_select_grouping_composed_list_2(bool async)
27882801

27892802
#region GroupByEntityType
27902803

2791-
[ConditionalTheory]
2792-
[MemberData(nameof(IsAsyncData))]
2793-
public virtual Task Select_GroupBy_SelectMany(bool async)
2794-
// Entity equality. Issue #15938.
2795-
=> AssertTranslationFailed(
2796-
() => AssertQuery(
2797-
async,
2798-
ss => ss.Set<Order>().Select(
2799-
o => new ProjectedType { Order = o.OrderID, Customer = o.CustomerID })
2800-
.GroupBy(p => p.Customer)
2801-
.SelectMany(g => g),
2802-
elementSorter: g => g.Order));
2803-
28042804
[ConditionalTheory]
28052805
[MemberData(nameof(IsAsyncData))]
28062806
public virtual Task GroupBy_with_group_key_being_navigation(bool async)
2807-
// Entity equality. Issue #15938.
2808-
=> AssertTranslationFailed(
2809-
() => AssertQuery(
2810-
async,
2811-
ss => ss.Set<OrderDetail>()
2812-
.GroupBy(od => od.Order)
2813-
.Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }),
2814-
elementSorter: e => e.Key));
2807+
=> AssertQuery(
2808+
async,
2809+
ss => ss.Set<OrderDetail>()
2810+
.GroupBy(od => od.Order)
2811+
.Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }),
2812+
elementSorter: e => e.Key.OrderID,
2813+
elementAsserter: (e, a) =>
2814+
{
2815+
AssertEqual(e.Key, a.Key);
2816+
AssertEqual(e.Aggregate, a.Aggregate);
2817+
},
2818+
entryCount: 830);
28152819

28162820
[ConditionalTheory]
28172821
[MemberData(nameof(IsAsyncData))]
28182822
public virtual Task GroupBy_with_group_key_being_nested_navigation(bool async)
2819-
// Entity equality. Issue #15938.
2820-
=> AssertTranslationFailed(
2821-
() => AssertQuery(
2822-
async,
2823-
ss => ss.Set<OrderDetail>()
2824-
.GroupBy(od => od.Order.Customer)
2825-
.Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }),
2826-
elementSorter: e => e.Key));
2823+
=> AssertQuery(
2824+
async,
2825+
ss => ss.Set<OrderDetail>()
2826+
.GroupBy(od => od.Order.Customer)
2827+
.Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }),
2828+
elementSorter: e => e.Key.CustomerID,
2829+
elementAsserter: (e, a) =>
2830+
{
2831+
AssertEqual(e.Key, a.Key);
2832+
AssertEqual(e.Aggregate, a.Aggregate);
2833+
},
2834+
entryCount: 89);
28272835

28282836
[ConditionalTheory]
28292837
[MemberData(nameof(IsAsyncData))]
28302838
public virtual Task GroupBy_with_group_key_being_navigation_with_entity_key_projection(bool async)
2831-
// Entity equality. Issue #15938.
2832-
=> AssertTranslationFailed(
2833-
() => AssertQuery(
2834-
async,
2835-
ss => ss.Set<OrderDetail>()
2836-
.GroupBy(od => od.Order)
2837-
.Select(g => g.Key)));
2839+
=> AssertQuery(
2840+
async,
2841+
ss => ss.Set<OrderDetail>()
2842+
.GroupBy(od => od.Order)
2843+
.Select(g => g.Key),
2844+
entryCount: 830);
28382845

2839-
[ConditionalTheory]
2846+
[ConditionalTheory(Skip = "Issue#29014")]
28402847
[MemberData(nameof(IsAsyncData))]
28412848
public virtual Task GroupBy_with_group_key_being_navigation_with_complex_projection(bool async)
2842-
// Entity equality. Issue #15938.
2843-
=> AssertTranslationFailed(
2844-
() => AssertQuery(
2845-
async,
2846-
ss => ss.Set<OrderDetail>()
2847-
.GroupBy(od => od.Order)
2848-
.Select(
2849-
g => new
2850-
{
2851-
g.Key,
2852-
Id1 = g.Key.CustomerID,
2853-
Id2 = g.Key.Customer.CustomerID,
2854-
Id3 = g.Key.OrderID,
2855-
Aggregate = g.Sum(od => od.OrderID)
2856-
}),
2857-
elementSorter: e => e.Id3,
2858-
elementAsserter: (e, a) =>
2859-
{
2860-
AssertEqual(e.Key, a.Key);
2861-
Assert.Equal(e.Id1, a.Id1);
2862-
Assert.Equal(e.Id2, a.Id2);
2863-
Assert.Equal(e.Id3, a.Id3);
2864-
Assert.Equal(e.Aggregate, a.Aggregate);
2865-
}));
2849+
=> AssertQuery(
2850+
async,
2851+
ss => ss.Set<OrderDetail>()
2852+
.GroupBy(od => od.Order)
2853+
.Select(
2854+
g => new
2855+
{
2856+
g.Key,
2857+
Id1 = g.Key.CustomerID,
2858+
Id2 = g.Key.Customer.CustomerID,
2859+
Id3 = g.Key.OrderID,
2860+
Aggregate = g.Sum(od => od.OrderID)
2861+
}),
2862+
elementSorter: e => e.Id3,
2863+
elementAsserter: (e, a) =>
2864+
{
2865+
AssertEqual(e.Key, a.Key);
2866+
Assert.Equal(e.Id1, a.Id1);
2867+
Assert.Equal(e.Id2, a.Id2);
2868+
Assert.Equal(e.Id3, a.Id3);
2869+
Assert.Equal(e.Aggregate, a.Aggregate);
2870+
});
28662871

28672872
#endregion
28682873

test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2464,21 +2464,34 @@ public override async Task GroupBy_with_group_key_being_navigation(bool async)
24642464
{
24652465
await base.GroupBy_with_group_key_being_navigation(async);
24662466

2467-
AssertSql();
2467+
AssertSql(
2468+
@"SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], COALESCE(SUM([o].[OrderID]), 0) AS [Aggregate]
2469+
FROM [Order Details] AS [o]
2470+
INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID]
2471+
GROUP BY [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]");
24682472
}
24692473

24702474
public override async Task GroupBy_with_group_key_being_nested_navigation(bool async)
24712475
{
24722476
await base.GroupBy_with_group_key_being_nested_navigation(async);
24732477

2474-
AssertSql();
2478+
AssertSql(
2479+
@"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], COALESCE(SUM([o].[OrderID]), 0) AS [Aggregate]
2480+
FROM [Order Details] AS [o]
2481+
INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID]
2482+
LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID]
2483+
GROUP BY [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]");
24752484
}
24762485

24772486
public override async Task GroupBy_with_group_key_being_navigation_with_entity_key_projection(bool async)
24782487
{
24792488
await base.GroupBy_with_group_key_being_navigation_with_entity_key_projection(async);
24802489

2481-
AssertSql();
2490+
AssertSql(
2491+
@"SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]
2492+
FROM [Order Details] AS [o]
2493+
INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID]
2494+
GROUP BY [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]");
24822495
}
24832496

24842497
public override async Task GroupBy_with_group_key_being_navigation_with_complex_projection(bool async)

0 commit comments

Comments
 (0)