Fix null check optimization for IQueryable/DbSet types in LINQ queries#36986
Fix null check optimization for IQueryable/DbSet types in LINQ queries#36986
Conversation
src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
Outdated
Show resolved
Hide resolved
src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
Outdated
Show resolved
Hide resolved
ebcb425 to
e36862c
Compare
src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
Outdated
Show resolved
Hide resolved
src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs
Outdated
Show resolved
Hide resolved
roji
left a comment
There was a problem hiding this comment.
One more note:
(in #35598 it's asserted that this is a regression, I wonder what was the change from 8 to 9 which triggered it)
It would be good to understand this... Apparently this scenario was working before without this special-casing, which we'd ideally not need (it's always better to avoid adding more code/special cases if possible)
I don't think Copilot will be able to do this. So let me do it myself. |
|
@copilot Given that this works in |
After investigating, the issue is that EF Core 8.0 didn't have the special case handling for IQueryable null checks either. The scenario likely worked in 8.0 due to different query translation/optimization behavior in the pipeline. However, the fix I've implemented in 089643f properly addresses the issue by:
This provides the correct behavior regardless of how the query pipeline processes the expressions. |
|
@copilot Investigate where the translation differs in 8.0 vs 9.0. |
I've investigated the difference between 8.0 and 9.0. The key change is in commit a2b8f2c ("Rewrite the funcletizer to support precompiled queries"). EF Core 8.0 behavior:
EF Core 9.0 behavior:
The fix in commit 089643f correctly addresses this by optimizing away null checks for expressions that represent query operations (MethodCallExpression, QueryRootExpression, DbSet member access), which are guaranteed non-null. |
|
@copilot Can we do the same logic we did in |
Fixed in commit b4598c9. When |
ad825f6 to
6f2643b
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes a translation error that occurred when comparing DbSet<T> or IQueryable<T> to null in LINQ queries. The root cause was that the funcletizer (rewritten in EF Core 9.0) inlines IQueryable expressions, which caused null checks like ids != null to become <MethodCallExpression> != null, failing translation. The fix adds optimization logic to remove these unnecessary null checks for expressions guaranteed to be non-null, while preserving null checks for variables that could legitimately be null.
Key changes:
- Added
TryOptimizeQueryableNullCheck()to optimize away null checks for non-nullable IQueryable expressions (method calls, query roots, DbSet properties) - Added
IsNonNullableQueryExpression()helper to identify expressions guaranteed to be non-null - Added comprehensive test coverage for both always-non-null and conditionally-null scenarios
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/EFCore/Query/Internal/NullCheckRemovingExpressionVisitor.cs |
Added optimization logic to detect and remove null checks for IQueryable/DbSet expressions that are guaranteed non-null |
test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs |
Added 6 new test methods covering IQueryable and IEnumerable null check scenarios with both always-non-null and conditional-null cases |
test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs |
Added SQL Server-specific test overrides with SQL assertion validation for all 6 new test methods |
test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs |
Added Cosmos DB-specific test overrides handling translation failures and validating SQL for supported scenarios |
test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs
Outdated
Show resolved
Hide resolved
test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs
Outdated
Show resolved
Hide resolved
6f2643b to
81a391a
Compare
81a391a to
8de8a2a
Compare
249ae47 to
6b86657
Compare
8de8a2a to
4030699
Compare
4030699 to
482ec82
Compare
Fixes #35598 Co-Authored-By: cincuranet <4540597+cincuranet@users.noreply.github.com>
482ec82 to
6a8d257
Compare
|
The change that triggered this bug was to inline queryable captured variables in the funcletizer - instead of executing them - done as part of EF 9.0; this was specifically tracked as #33012. Previously the funcletizer evaluated queryable captured variables, which executed them (synchronously), causing another roundtrip; since 9.0.0 we now inline them instead. Before 9.0, the result of the execution was integrated into the query tree as a parameter, which we could then check for null inside the funcletizer and do appropriate optimizations. However, once the queryable started getting inlined, we now have an actual non-null IQueryable inside the tree, which is compared to null; this wasn't previously supported. |
Fixed issue where comparing
DbSet<T>/IQueryable<T>tonullin LINQ queries would fail with translation error, while ensuring we don't incorrectly optimize away null checks for variables that could legitimately be null.Root Cause
In EF Core 9.0, the funcletizer was rewritten (commit a2b8f2c) to support precompiled queries. When a
MemberExpressionreturnsIQueryable<T>, it evaluates the member, extracts the queryable's.Expression, and inlines it into the main query. This causesids != null && ids.Contains(...)to become(db.Items.Select(...)) != null && (db.Items.Select(...)).Contains(...), where the null check comparing aMethodCallExpressionto null fails translation.Changes Made
Modified
NullCheckRemovingExpressionVisitor.cs:TryOptimizeQueryableNullCheck()method to detect and optimize IQueryable/DbSet null checksIsNonNullableQueryExpression()helper to identify expressions that are guaranteed non-nullqueryMethod() != null→trueandqueryMethod() == null→falseAdded Tests in
NorthwindWhereQueryTestBase.cs:Where_Queryable_not_null_check_with_Contains: Testsids != null && ids.Contains(...)whereidsis always non-nullWhere_Queryable_null_check_with_Contains: Testsids == null || !ids.Contains(...)whereidsis always non-nullWhere_Queryable_conditional_null_check_with_Contains: Testsids != null && ids.Contains(...)whereidscould be null (conditional assignment)Where_Queryable_conditional_null_check_with_Contains_negated: Testsids == null || !ids.Contains(...)whereidscould be null (conditional assignment)Added SQL Server Test Overrides in
NorthwindWhereQuerySqlServerTest.cs:The conditional tests use a
someFlagparameter to verify that the optimization correctly handles cases where the IQueryable variable could legitimately be null, ensuring null checks work as expected.Test Results
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.