diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs index 31b864eb51f..9c1692c45e6 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs @@ -870,6 +870,22 @@ private static Expression GetGroupingKey(Expression key, List groupi return memberInitExpression.Update(updatedNewExpression, memberBindings); + case EntityShaperExpression entityShaperExpression + when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression: + var entityProjectionExpression = (EntityProjectionExpression)((InMemoryQueryExpression)projectionBindingExpression.QueryExpression) + .GetProjection(projectionBindingExpression); + var readExpressions = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(entityProjectionExpression.EntityType)) + { + readExpressions[property] = (MethodCallExpression)GetGroupingKey( + entityProjectionExpression.BindProperty(property), + groupingExpressions, + groupingKeyAccessExpression); + } + + return entityShaperExpression.Update( + new EntityProjectionExpression(entityProjectionExpression.EntityType, readExpressions)); + default: var index = groupingExpressions.Count; groupingExpressions.Add(key); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index 5f9ff6cba25..ec5e2ebdbb8 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -455,6 +455,10 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy return memberInitExpression.Update(updatedNewExpression, newBindings); + case EntityShaperExpression entityShaperExpression + when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression: + return entityShaperExpression; + default: var translation = TranslateExpression(expression); if (translation == null) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 7809084f24e..f19ad57d426 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -450,7 +450,16 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent var translatedKey = TranslateGroupingKey(remappedKeySelector); if (translatedKey == null) { - return null; + // This could be group by entity type + if (remappedKeySelector is not EntityShaperExpression + { ValueBufferExpression : ProjectionBindingExpression }) + { + // ValueBufferExpression can be JsonQuery, ProjectionBindingExpression, EntityProjection + // We only allow ProjectionBindingExpression which represents a regular entity + return null; + } + + translatedKey = remappedKeySelector; } if (elementSelector != null) diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 174e54dfc6c..b5136d777c2 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1857,6 +1857,23 @@ private static void PopulateGroupByTerms( PopulateGroupByTerms(unaryExpression.Operand, groupByTerms, groupByAliases, name); break; + case EntityShaperExpression entityShaperExpression + when entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression: + var entityProjectionExpression = (EntityProjectionExpression)((SelectExpression)projectionBindingExpression.QueryExpression) + .GetProjection(projectionBindingExpression); + foreach (var property in GetAllPropertiesInHierarchy(entityProjectionExpression.EntityType)) + { + PopulateGroupByTerms(entityProjectionExpression.BindProperty(property), groupByTerms, groupByAliases, name: null); + } + + if (entityProjectionExpression.DiscriminatorExpression != null) + { + PopulateGroupByTerms( + entityProjectionExpression.DiscriminatorExpression, groupByTerms, groupByAliases, name: DiscriminatorColumnAlias); + } + + break; + default: throw new InvalidOperationException(RelationalStrings.InvalidKeySelectorForGroupBy(keySelector, keySelector.GetType())); } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index 965477edae0..072bcaca56f 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -2688,6 +2688,19 @@ public virtual Task GroupBy_selecting_grouping_key_list(bool async) AssertCollection(e.Data, a.Data); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_GroupBy_SelectMany(bool async) + // Entity equality. Issue #15938. + => AssertTranslationFailed( + () => AssertQuery( + async, + ss => ss.Set().Select( + o => new ProjectedType { Order = o.OrderID, Customer = o.CustomerID }) + .GroupBy(p => p.Customer) + .SelectMany(g => g), + elementSorter: g => g.Order)); + #endregion #region GroupBySelectFirst @@ -2788,81 +2801,73 @@ public virtual Task GroupBy_select_grouping_composed_list_2(bool async) #region GroupByEntityType - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Select_GroupBy_SelectMany(bool async) - // Entity equality. Issue #15938. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set().Select( - o => new ProjectedType { Order = o.OrderID, Customer = o.CustomerID }) - .GroupBy(p => p.Customer) - .SelectMany(g => g), - elementSorter: g => g.Order)); - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_with_group_key_being_navigation(bool async) - // Entity equality. Issue #15938. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set() - .GroupBy(od => od.Order) - .Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }), - elementSorter: e => e.Key)); + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(od => od.Order) + .Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }), + elementSorter: e => e.Key.OrderID, + elementAsserter: (e, a) => + { + AssertEqual(e.Key, a.Key); + AssertEqual(e.Aggregate, a.Aggregate); + }, + entryCount: 830); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_with_group_key_being_nested_navigation(bool async) - // Entity equality. Issue #15938. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set() - .GroupBy(od => od.Order.Customer) - .Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }), - elementSorter: e => e.Key)); + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(od => od.Order.Customer) + .Select(g => new { g.Key, Aggregate = g.Sum(od => od.OrderID) }), + elementSorter: e => e.Key.CustomerID, + elementAsserter: (e, a) => + { + AssertEqual(e.Key, a.Key); + AssertEqual(e.Aggregate, a.Aggregate); + }, + entryCount: 89); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_with_group_key_being_navigation_with_entity_key_projection(bool async) - // Entity equality. Issue #15938. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set() - .GroupBy(od => od.Order) - .Select(g => g.Key))); + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(od => od.Order) + .Select(g => g.Key), + entryCount: 830); - [ConditionalTheory] + [ConditionalTheory(Skip = "Issue#29014")] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_with_group_key_being_navigation_with_complex_projection(bool async) - // Entity equality. Issue #15938. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set() - .GroupBy(od => od.Order) - .Select( - g => new - { - g.Key, - Id1 = g.Key.CustomerID, - Id2 = g.Key.Customer.CustomerID, - Id3 = g.Key.OrderID, - Aggregate = g.Sum(od => od.OrderID) - }), - elementSorter: e => e.Id3, - elementAsserter: (e, a) => - { - AssertEqual(e.Key, a.Key); - Assert.Equal(e.Id1, a.Id1); - Assert.Equal(e.Id2, a.Id2); - Assert.Equal(e.Id3, a.Id3); - Assert.Equal(e.Aggregate, a.Aggregate); - })); + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(od => od.Order) + .Select( + g => new + { + g.Key, + Id1 = g.Key.CustomerID, + Id2 = g.Key.Customer.CustomerID, + Id3 = g.Key.OrderID, + Aggregate = g.Sum(od => od.OrderID) + }), + elementSorter: e => e.Id3, + elementAsserter: (e, a) => + { + AssertEqual(e.Key, a.Key); + Assert.Equal(e.Id1, a.Id1); + Assert.Equal(e.Id2, a.Id2); + Assert.Equal(e.Id3, a.Id3); + Assert.Equal(e.Aggregate, a.Aggregate); + }); #endregion diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index b38a1421882..bd45af8b62c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -2464,21 +2464,34 @@ public override async Task GroupBy_with_group_key_being_navigation(bool async) { await base.GroupBy_with_group_key_being_navigation(async); - AssertSql(); + AssertSql( + @"SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], COALESCE(SUM([o].[OrderID]), 0) AS [Aggregate] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +GROUP BY [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]"); } public override async Task GroupBy_with_group_key_being_nested_navigation(bool async) { await base.GroupBy_with_group_key_being_nested_navigation(async); - AssertSql(); + AssertSql( + @"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] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] +GROUP BY [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]"); } public override async Task GroupBy_with_group_key_being_navigation_with_entity_key_projection(bool async) { await base.GroupBy_with_group_key_being_navigation_with_entity_key_projection(async); - AssertSql(); + AssertSql( + @"SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +GROUP BY [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]"); } public override async Task GroupBy_with_group_key_being_navigation_with_complex_projection(bool async)