From 3605cbe4f7292ff3dc243f35a8a42b6370a993cc Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Tue, 5 May 2026 23:20:41 +0500 Subject: [PATCH 1/3] Translate List.Exists to Queryable.Any Rewrites List.Exists(predicate) to AsQueryable().Any(predicate) in QueryableMethodNormalizingExpressionVisitor, mirroring the existing ICollection.Contains conversion. The Predicate lambda is rebuilt as Func so the resulting Queryable.Any call type-checks. Flips four placeholder tests (Where_Join_Exists, _Inequality, _Constant, Where_Join_Not_Exists) from AssertTranslationFailed to AssertQuery, and populates the SqlServer SQL baselines with the EXISTS subquery output. Fixes #17762 --- ...yableMethodNormalizingExpressionVisitor.cs | 36 +++++++++++++++++ .../NorthwindMiscellaneousQueryTestBase.cs | 22 +++++----- ...orthwindMiscellaneousQuerySqlServerTest.cs | 40 +++++++++++++++++-- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 3589fcc6195..c8c35aca551 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -178,6 +178,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp visitedExpression = TryConvertCollectionContainsToQueryableContains(methodCallExpression); } + if (method.Name == nameof(List<>.Exists) + && method.DeclaringType is { IsGenericType: true } existsDeclaringType + && existsDeclaringType.GetGenericTypeDefinition() == typeof(List<>)) + { + visitedExpression = TryConvertListExistsToQueryableAny(methodCallExpression); + } + if (method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.Name is nameof(EntityFrameworkQueryableExtensions.Include) or nameof(EntityFrameworkQueryableExtensions.ThenInclude) @@ -506,6 +513,35 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa return methodCallExpression.Update(Visit(methodCallExpression.Object), arguments); } + private Expression TryConvertListExistsToQueryableAny(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Object is MemberInitExpression or NewExpression) + { + return base.VisitMethodCall(methodCallExpression); + } + + // List.Exists takes a Predicate; rewrite the lambda to Func so it matches + // Queryable.Any's Expression> parameter. + if (methodCallExpression.Arguments[0] is not LambdaExpression predicateLambda) + { + return base.VisitMethodCall(methodCallExpression); + } + + var sourceType = methodCallExpression.Method.DeclaringType!.GetGenericArguments()[0]; + var rewrittenPredicate = Expression.Lambda( + typeof(Func<,>).MakeGenericType(sourceType, typeof(bool)), + predicateLambda.Body, + predicateLambda.Parameters); + + return VisitMethodCall( + Expression.Call( + QueryableMethods.AnyWithPredicate.MakeGenericMethod(sourceType), + Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(sourceType), + methodCallExpression.Object!), + Expression.Quote(rewrittenPredicate))); + } + private Expression TryConvertCollectionContainsToQueryableContains(MethodCallExpression methodCallExpression) { if (methodCallExpression.Object is MemberInitExpression or NewExpression) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index bbff0e844c1..10509d303b0 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -1942,33 +1942,31 @@ public virtual Task Where_Join_Any(bool async) [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Where_Join_Exists(bool async) - // Translate List.Exists. Issue #17762. - => AssertTranslationFailed(() => AssertQuery( + => AssertQuery( async, ss => ss.Set() - .Where(c => c.CustomerID == "ALFKI" && c.Orders.Exists(o => o.OrderDate == new DateTime(2008, 10, 24))))); + .Where(c => c.CustomerID == "ALFKI" && c.Orders.Exists(o => o.OrderDate == new DateTime(2008, 10, 24))), + assertEmpty: true); [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Where_Join_Exists_Inequality(bool async) - // Translate List.Exists. Issue #17762. - => AssertTranslationFailed(() => AssertQuery( + => AssertQuery( async, ss => ss.Set() - .Where(c => c.CustomerID == "ALFKI" && c.Orders.Exists(o => o.OrderDate != new DateTime(2008, 10, 24))))); + .Where(c => c.CustomerID == "ALFKI" && c.Orders.Exists(o => o.OrderDate != new DateTime(2008, 10, 24)))); [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Where_Join_Exists_Constant(bool async) - // Translate List.Exists. Issue #17762. - => AssertTranslationFailed(() => AssertQuery( + => AssertQuery( async, - ss => ss.Set().Where(c => c.CustomerID == "ALFKI" && c.Orders.Exists(o => false)))); + ss => ss.Set().Where(c => c.CustomerID == "ALFKI" && c.Orders.Exists(o => false)), + assertEmpty: true); [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Where_Join_Not_Exists(bool async) - // Translate List.Exists. Issue #17762. - => AssertTranslationFailed(() => AssertQuery( + => AssertQuery( async, - ss => ss.Set().Where(c => c.CustomerID == "ALFKI" && !c.Orders.Exists(o => false)))); + ss => ss.Set().Where(c => c.CustomerID == "ALFKI" && !c.Orders.Exists(o => false))); [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Multiple_joins_Where_Order_Any(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 138214a588c..c652036a049 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -1964,28 +1964,60 @@ public override async Task Where_Join_Exists(bool async) { await base.Where_Join_Exists(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] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI' AND EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] AND [o].[OrderDate] = '2008-10-24T00:00:00.000') +"""); } public override async Task Where_Join_Exists_Inequality(bool async) { await base.Where_Join_Exists_Inequality(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] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI' AND EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] AND ([o].[OrderDate] <> '2008-10-24T00:00:00.000' OR [o].[OrderDate] IS NULL)) +"""); } public override async Task Where_Join_Exists_Constant(bool async) { await base.Where_Join_Exists_Constant(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] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI' AND EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] AND 0 = 1) +"""); } public override async Task Where_Join_Not_Exists(bool async) { await base.Where_Join_Not_Exists(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] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI' AND NOT EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] AND 0 = 1) +"""); } public override async Task Join_OrderBy_Count(bool async) From 65492e6a7567628a15a362c70b64b0c05376fb7a Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 6 May 2026 10:32:25 +0500 Subject: [PATCH 2/3] Fix CI failures on PR #38226 - Wrap Cosmos overrides for the 4 Where_Join_*Exists* tests in AssertTranslationFailed, matching the existing Cosmos pattern for navigation-collection Any-style queries (Issue #17246). The base test used to be wrapped in AssertTranslationFailed which covered Cosmos; after flipping the base to AssertQuery for the providers that can now translate, the failure-assertion has to live on Cosmos's override. - Correct the expected SqlServer SQL for Where_Join_Exists_Constant and Where_Join_Not_Exists. EF's optimizer simplifies Any(o => false) and !Any(o => false) at translation time, so the EXISTS subquery does not appear in the emitted SQL. Pattern matches the existing Not_Any_false baseline in NorthwindAggregateOperatorsQuerySqlServerTest. --- .../Query/NorthwindMiscellaneousQueryCosmosTest.cs | 12 ++++++++---- .../NorthwindMiscellaneousQuerySqlServerTest.cs | 10 ++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index f3e2da296dd..c19501d12b0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -1103,28 +1103,32 @@ public override async Task Where_Join_Any(bool async) public override async Task Where_Join_Exists(bool async) { - await base.Where_Join_Exists(async); + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_Join_Exists(async)); AssertSql(); } public override async Task Where_Join_Exists_Inequality(bool async) { - await base.Where_Join_Exists_Inequality(async); + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_Join_Exists_Inequality(async)); AssertSql(); } public override async Task Where_Join_Exists_Constant(bool async) { - await base.Where_Join_Exists_Constant(async); + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_Join_Exists_Constant(async)); AssertSql(); } public override async Task Where_Join_Not_Exists(bool async) { - await base.Where_Join_Not_Exists(async); + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_Join_Not_Exists(async)); AssertSql(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index c652036a049..c8d11481acd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -1998,10 +1998,7 @@ public override async Task Where_Join_Exists_Constant(bool async) """ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = N'ALFKI' AND EXISTS ( - SELECT 1 - FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] AND 0 = 1) +WHERE [c].[CustomerID] = N'ALFKI' AND 0 = 1 """); } @@ -2013,10 +2010,7 @@ public override async Task Where_Join_Not_Exists(bool async) """ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = N'ALFKI' AND NOT EXISTS ( - SELECT 1 - FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] AND 0 = 1) +WHERE [c].[CustomerID] = N'ALFKI' """); } From cdfb23d8798548dec2fe4ff2dc7039ebc6f1fb33 Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 6 May 2026 11:28:01 +0500 Subject: [PATCH 3/3] Correct Where_Join_Exists_Constant SqlServer baseline EF's optimizer recognizes that c.CustomerID == \"ALFKI\" && Exists(o => false) short-circuits to false and eliminates the entire predicate, including the customer-id filter. Actual emitted SQL is just \`WHERE 0 = 1\`, not \`WHERE [c].[CustomerID] = N'ALFKI' AND 0 = 1\`. --- .../Query/NorthwindMiscellaneousQuerySqlServerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index c8d11481acd..45cf929fbfd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -1998,7 +1998,7 @@ public override async Task Where_Join_Exists_Constant(bool async) """ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = N'ALFKI' AND 0 = 1 +WHERE 0 = 1 """); }