diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 12db4533633..6e209f8697c 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -1871,7 +1871,7 @@ private sealed class GroupingAggregateScanner(ParameterExpression groupingParame [nameof(Enumerable.Sum), nameof(Enumerable.Min), nameof(Enumerable.Max), nameof(Enumerable.Average)]; private static readonly string[] PredicateAggregateMethodNames = - [nameof(Enumerable.Count), nameof(Enumerable.LongCount)]; + [nameof(Enumerable.Any), nameof(Enumerable.All), nameof(Enumerable.Count), nameof(Enumerable.LongCount)]; public List Aggregates { get; } = []; public bool HasUnsupportedUsage { get; private set; } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index 32bcdb9b70f..231644ae840 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -386,6 +386,117 @@ public virtual Task GroupBy_Count_with_predicate_through_navigation_property(boo .Select(g => new { g.Key, Londons = g.Count(o => o.Customer!.City == "London") }), elementSorter: e => e.Key); + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_with_predicate_through_navigation_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select(g => new { g.Key, Londons = g.Any(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_All_with_predicate_through_navigation_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select(g => new { g.Key, Londons = g.All(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Queryable_Any_with_predicate_through_navigation_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select(g => new { g.Key, Londons = g.AsQueryable().Any(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Queryable_All_with_predicate_through_navigation_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select(g => new { g.Key, Londons = g.AsQueryable().All(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_and_aggregate_through_navigation_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select( + g => new + { + g.Key, + HasOrders = g.Any(), + Londons = g.Count(o => o.Customer!.City == "London") + }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_All_and_aggregate_through_navigation_property(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select( + g => new + { + g.Key, + AllLate = g.All(o => o.OrderID > 10250), + Londons = g.Count(o => o.Customer!.City == "London") + }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_multiple_aggregates_with_Any_and_All_sharing_same_navigation(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select( + g => new + { + g.Key, + Region = g.Max(o => o.Customer!.Region), + AnyLondon = g.Any(o => o.Customer!.City == "London"), + AllLondon = g.All(o => o.Customer!.City == "London"), + Count = g.Count() + }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_through_two_level_navigation(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(od => od.ProductID) + .Select(g => new { g.Key, Londons = g.Any(od => od.Order!.Customer!.City == "London") }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_key_and_Any_through_same_navigation(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.Customer!.City) + .Select(g => new { g.Key, Londons = g.Any(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_through_navigation_in_intermediate_projection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Select(o => new { o.EmployeeID, o.Customer!.City }) + .GroupBy(x => x.EmployeeID) + .Select(g => new { g.Key, Londons = g.Any(x => x.City == "London") }), + elementSorter: e => e.Key); + [Theory, MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_key_and_aggregate_through_same_navigation(bool async) => AssertQuery( diff --git a/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs index ff81684326b..fb9bf0ef05d 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindQueryFiltersQueryTestBase.cs @@ -162,6 +162,40 @@ public virtual Task GroupBy_aggregate_through_filtered_navigation_ignore_query_f .Select(g => new { g.Key, Londons = g.Count(o => o.Customer!.City == "London") }), elementSorter: e => e.Key.GetValueOrDefault()); + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_through_filtered_navigation(bool async) + => AssertFilteredQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select(g => new { g.Key, Londons = g.Any(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key.GetValueOrDefault()); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_through_filtered_navigation_with_total(bool async) + => AssertFilteredQuery( + async, + ss => ss.Set() + .GroupBy(o => o.EmployeeID) + .Select( + g => new + { + g.Key, + Total = g.Count(), + Londons = g.Any(o => o.Customer!.City == "London") + }), + elementSorter: e => e.Key.GetValueOrDefault()); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Any_through_filtered_navigation_ignore_query_filters(bool async) + => AssertQuery( + async, + ss => ss.Set() + .IgnoreQueryFilters() + .GroupBy(o => o.EmployeeID) + .Select(g => new { g.Key, Londons = g.Any(o => o.Customer!.City == "London") }), + elementSorter: e => e.Key.GetValueOrDefault()); + [Theory, MemberData(nameof(IsAsyncData))] public virtual Task Included_many_to_one_query2(bool async) => AssertFilteredQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 3d0c1a89f9e..145c473f00e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -2422,6 +2422,210 @@ GROUP BY [o].[EmployeeID] """); } + public override async Task GroupBy_Any_with_predicate_through_navigation_property(bool async) + { + await base.GroupBy_Any_with_predicate_through_navigation_property(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_All_with_predicate_through_navigation_property(bool async) + { + await base.GroupBy_All_with_predicate_through_navigation_property(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND ([c].[City] <> N'London' OR [c].[City] IS NULL)) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_Queryable_Any_with_predicate_through_navigation_property(bool async) + { + await base.GroupBy_Queryable_Any_with_predicate_through_navigation_property(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_Queryable_All_with_predicate_through_navigation_property(bool async) + { + await base.GroupBy_Queryable_All_with_predicate_through_navigation_property(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND ([c].[City] <> N'London' OR [c].[City] IS NULL)) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_Any_and_aggregate_through_navigation_property(bool async) + { + await base.GroupBy_Any_and_aggregate_through_navigation_property(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + WHERE [o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [HasOrders], COUNT(CASE + WHEN [c].[City] = N'London' THEN 1 +END) AS [Londons] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_All_and_aggregate_through_navigation_property(bool async) + { + await base.GroupBy_All_and_aggregate_through_navigation_property(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [o0].[OrderID] <= 10250) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [AllLate], COUNT(CASE + WHEN [c].[City] = N'London' THEN 1 +END) AS [Londons] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_multiple_aggregates_with_Any_and_All_sharing_same_navigation(bool async) + { + await base.GroupBy_multiple_aggregates_with_Any_and_All_sharing_same_navigation(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], MAX([c].[Region]) AS [Region], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c0].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [AnyLondon], CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM [Orders] AS [o1] + LEFT JOIN [Customers] AS [c1] ON [o1].[CustomerID] = [c1].[CustomerID] + WHERE ([o].[EmployeeID] = [o1].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o1].[EmployeeID] IS NULL)) AND ([c1].[City] <> N'London' OR [c1].[City] IS NULL)) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [AllLondon], COUNT(*) AS [Count] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_Any_through_two_level_navigation(bool async) + { + await base.GroupBy_Any_through_two_level_navigation(async); + + AssertSql( + """ +SELECT [o].[ProductID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Order Details] AS [o0] + INNER JOIN [Orders] AS [o1] ON [o0].[OrderID] = [o1].[OrderID] + LEFT JOIN [Customers] AS [c] ON [o1].[CustomerID] = [c].[CustomerID] + WHERE [o].[ProductID] = [o0].[ProductID] AND [c].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Order Details] AS [o] +GROUP BY [o].[ProductID] +"""); + } + + public override async Task GroupBy_key_and_Any_through_same_navigation(bool async) + { + await base.GroupBy_key_and_Any_through_same_navigation(async); + + AssertSql( + """ +SELECT [c].[City] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] + WHERE ([c].[City] = [c0].[City] OR ([c].[City] IS NULL AND [c0].[City] IS NULL)) AND [c0].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +GROUP BY [c].[City] +"""); + } + + public override async Task GroupBy_Any_through_navigation_in_intermediate_projection(bool async) + { + await base.GroupBy_Any_through_navigation_in_intermediate_projection(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +GROUP BY [o].[EmployeeID] +"""); + } + public override async Task GroupBy_key_and_aggregate_through_same_navigation(bool async) { await base.GroupBy_key_and_aggregate_through_same_navigation(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs index 7f75614eaf5..3bad3f4d7f6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindQueryFiltersQuerySqlServerTest.cs @@ -408,6 +408,87 @@ GROUP BY [o].[EmployeeID] """); } + public override async Task GroupBy_Any_through_filtered_navigation(bool async) + { + await base.GroupBy_Any_through_filtered_navigation(async); + + AssertSql( + """ +@ef_filter__TenantPrefix_startswith='B%' (Size = 40) + +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN ( + SELECT [c2].[CustomerID], [c2].[City], [c2].[CompanyName] + FROM [Customers] AS [c2] + WHERE [c2].[CompanyName] LIKE @ef_filter__TenantPrefix_startswith ESCAPE N'\' + ) AS [c1] ON [o0].[CustomerID] = [c1].[CustomerID] + WHERE [c1].[CustomerID] IS NOT NULL AND [c1].[CompanyName] IS NOT NULL AND ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c1].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +LEFT JOIN ( + SELECT [c].[CustomerID], [c].[CompanyName] + FROM [Customers] AS [c] + WHERE [c].[CompanyName] LIKE @ef_filter__TenantPrefix_startswith ESCAPE N'\' +) AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] +WHERE [c0].[CustomerID] IS NOT NULL AND [c0].[CompanyName] IS NOT NULL +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_Any_through_filtered_navigation_with_total(bool async) + { + await base.GroupBy_Any_through_filtered_navigation_with_total(async); + + AssertSql( + """ +@ef_filter__TenantPrefix_startswith='B%' (Size = 40) + +SELECT [o].[EmployeeID] AS [Key], COUNT(*) AS [Total], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN ( + SELECT [c2].[CustomerID], [c2].[City], [c2].[CompanyName] + FROM [Customers] AS [c2] + WHERE [c2].[CompanyName] LIKE @ef_filter__TenantPrefix_startswith ESCAPE N'\' + ) AS [c1] ON [o0].[CustomerID] = [c1].[CustomerID] + WHERE [c1].[CustomerID] IS NOT NULL AND [c1].[CompanyName] IS NOT NULL AND ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c1].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +LEFT JOIN ( + SELECT [c].[CustomerID], [c].[CompanyName] + FROM [Customers] AS [c] + WHERE [c].[CompanyName] LIKE @ef_filter__TenantPrefix_startswith ESCAPE N'\' +) AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] +WHERE [c0].[CustomerID] IS NOT NULL AND [c0].[CompanyName] IS NOT NULL +GROUP BY [o].[EmployeeID] +"""); + } + + public override async Task GroupBy_Any_through_filtered_navigation_ignore_query_filters(bool async) + { + await base.GroupBy_Any_through_filtered_navigation_ignore_query_filters(async); + + AssertSql( + """ +SELECT [o].[EmployeeID] AS [Key], CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AND [c].[City] = N'London') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Londons] +FROM [Orders] AS [o] +GROUP BY [o].[EmployeeID] +"""); + } + public override async Task Included_many_to_one_query2(bool async) { await base.Included_many_to_one_query2(async);