Translate List<T>.Exists to Queryable.Any#38226
Conversation
Rewrites List<T>.Exists(predicate) to AsQueryable().Any(predicate) in QueryableMethodNormalizingExpressionVisitor, mirroring the existing ICollection<T>.Contains conversion. The Predicate<T> lambda is rebuilt as Func<T, bool> 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 dotnet#17762
- Wrap Cosmos overrides for the 4 Where_Join_*Exists* tests in AssertTranslationFailed, matching the existing Cosmos pattern for navigation-collection Any-style queries (Issue dotnet#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.
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\`.
There was a problem hiding this comment.
Pull request overview
This PR extends EF Core’s query normalization to translate List<T>.Exists(predicate) into Queryable.Any(predicate) (via AsQueryable()), enabling server translation for scenarios like Northwind navigation collections. It also updates Northwind tests and SQL Server baselines to validate the new translation behavior, while keeping Cosmos behavior as translation-failing per existing limitations.
Changes:
- Add
List<T>.Exists(...)normalization toQueryable.Any(...)inQueryableMethodNormalizingExpressionVisitor, including rebuilding the predicate delegate type toFunc<T, bool>. - Flip several Northwind “Issue #17762” tests from translation-failure placeholders to actual
AssertQueryexecution (with SQL Server baselines). - Keep Cosmos overrides asserting translation failure for these tests in line with existing client-evaluation limitations.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs | Adds normalization rewriting List<T>.Exists to Queryable.Any with a rewritten predicate delegate type. |
| test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs | Converts Exists tests from AssertTranslationFailed to AssertQuery and adjusts expectations for empty-result cases. |
| test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs | Adds SQL baselines validating translation output for the updated Exists tests. |
| test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs | Wraps the updated base tests in AssertTranslationFailed to preserve Cosmos behavior. |
| if (methodCallExpression.Object is MemberInitExpression or NewExpression) | ||
| { | ||
| return base.VisitMethodCall(methodCallExpression); | ||
| } |
|
Thanks for the review. I dug into this and inline-list The literal-receiver bailout ( Verified against SQL Server ( // new List<int> { 1, 2, 3 }.Exists(x => x == b.Id)
SELECT [b].[Id], [b].[Name] FROM [Blogs] AS [b]
WHERE [b].[Id] IN (1, 2, 3)
// new List<int> { 1, 2, 3 }.Exists(x => x > b.Id) (non-IN-able predicate)
SELECT [b].[Id], [b].[Name] FROM [Blogs] AS [b]
WHERE EXISTS (
SELECT 1
FROM (VALUES (CAST(1 AS int)), (2), (3)) AS [v]([Value])
WHERE [v].[Value] > [b].[Id])Both translate, identically to the explicit I've added a regression test ( |
Rewrites
List<T>.Exists(predicate)toAsQueryable().Any(predicate)inQueryableMethodNormalizingExpressionVisitor, mirroring the existingICollection<T>.Containsconversion. ThePredicate<T>lambda is rebuilt asFunc<T, bool>so the resultingQueryable.Anycall type-checks.TryConvertListExistsToQueryableAnyhelper alongsideTryConvertCollectionContainsToQueryableContainsList<T>(other types likeArray.Existsare out of scope)new List<T>/new {...}) or the predicate is a non-lambda delegateWhere_Join_Exists,_Inequality,_Constant,Where_Join_Not_Exists) tagged with// Issue #17762fromAssertTranslationFailedtoAssertQueryfalsepredicates)AssertTranslationFailedper Cosmos's existingIssue #17246pattern for navigation-collectionAnytranslationsFixes #17762