-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Fix null check optimization for IQueryable/DbSet types in LINQ queries #36986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
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
roji
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
Added in commit 2ce7698. The two new tests are:
Both tests use
All 30 Where_Queryable tests pass. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot You forgot to add overrides in |
Added in commit 0b73951. The overrides for both Each override handles both
|
|
@copilot For 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] |
Fixed in commit b4598c9. When |
ad825f6 to
6f2643b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Fixes #35598 Co-authored-by: cincuranet <[email protected]>
81a391a to
8de8a2a
Compare
249ae47 to
6b86657
Compare
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.