-
Type:
Bug
-
Resolution: Duplicate
-
Priority:
Unknown
-
None
-
Affects Version/s: None
-
Component/s: None
-
None
-
None
-
Dotnet Drivers
-
Not Needed
-
None
-
None
-
None
-
None
-
None
-
None
Summary
On main, a query combining a multi-join Include (two reference Includes, or one Include(...).ThenInclude(...)) with an operator composed after the Include silently loses that operator. A Where is dropped entirely, so the query returns every row instead of the filtered set.
Minimal reproduction — returns every order, not the matching ones:
db.Orders.Include(o => o.Customer).ThenInclude(c => c.Region)
.Where(o => o.Customer.Region.RegionName == "West")
.ToList();
This is silent wrong data, not an exception, and it occurs in the default query mode.
Scope: unreleased, no released version affected
Introduced by efb5f25 ("EF-117: Cross-collection Include / navigations / joins", PR #309, 2026-06-11), together with the cross-collection Include feature itself. Verified: git merge-base --is-ancestor says that commit is not an ancestor of v10.0.2, and StripJoinForLookup / IsJoinRelatedMethod are absent from v8.4.2, v9.1.2 and v10.0.2 alike. So the affected code has never shipped, no upgrading consumer can observe a regression, and this is not a breaking change under the versioning rubric.
It does need fixing before the next release, since the feature that carries it is on main and the wrong data is silent.
An earlier diagnosis of this defect claimed it was a shipped defect in released v8/v9/v10. That was wrong — it rested on comparing against upstream/main, which is ahead of the release tags. Recorded here so the incorrect framing is not propagated.
Root cause
MongoEFToLinqTranslatingExpressionVisitor.LeftJoin.cs. StripJoinForLookup exists to remove the join chain from an explicit-join query, because the $lookup stages appended separately perform the join. It finds the base source by walking down through the chain, and FindBaseSourceThroughJoin recurses through "Select", "Where", "SelectMany" and the three join methods. IsJoinRelatedMethod likewise treats "Where" and "Select" as join-related.
So when the outermost call is a user Where, the method returns the bare base source and the entire composed chain above the join, including that Where, is discarded rather than reattached. The stages that survive are only the $lookup and $unwind; no $match is emitted at all.
A predicate composed before the Include is unaffected, because nav-expansion hoists it ahead of the join, where it is recorded normally. The determinant is whether the operator lands outside the join chain.
Measurements
A throwaway functional test over six shapes, in both Native and DriverLinq modes, gave identical results on EF8, EF9 and EF10: 4 passed, 8 failed.
| Shape | Expected | Actual |
|---|---|---|
| Two Includes + Where on a navigation | 1 row | 3 rows |
| Two Includes + Where mixing a navigation and a root property | 1 row | 3 rows (even the root-property conjunct is lost) |
| Include(...).ThenInclude(...) + Where on the nested navigation | 1 row | 3 rows |
| Two Includes + OrderBy on a navigation, Skip(1).Take(2) | 2 rows | throws, "Document element is missing for required non-nullable property 'Id'" |
| Control: two Includes + a root-only Where | 1 row | 1 row, correct |
| Control: a single Include + Where on a navigation | 1 row | 1 row, correct |
The two controls locate the trigger precisely: it needs more than one join (so the flat-lookup path is selected) and the composed operator must sit outside the join chain. Captured MQL confirms the mechanism directly — the emitted pipeline for the failing Where case is byte-identical to the same query with no predicate at all.
Fails in Native and DriverLinq alike, because both reach the same bridge. Reproduced in a detached worktree at upstream/main (58e05a0), where the same four shapes fail, confirming it is not specific to the in-flight native-query branch.
Relationship to EF-368
EF-368 (native single-level reference Include) prototyped registering a reference lookup for a single join, which routes lone reference Includes through this same flat path. That measured 116 data failures — it widened this existing defect from multi-join to single-join rather than introducing a new one. Fixing this is therefore likely a prerequisite for one of EF-368's candidate designs, not an alternative to it.
Suggested fix direction
Reattach the composed operators to the stripped base source rather than discarding them. StripJoinForLookup already has a rebuild path for the case where the outer call is not itself join-related (it rewrites argument 0 and reconstructs the call); the defect is the early return that bypasses that path when the outer call is a Where or Select. Distinguishing an EF-synthesized join-chain Select from a user-composed Where/Select is the substance of the fix.
Gating tests should cover the six shapes above, including both controls, and assert row counts rather than only MQL — the MQL for the failing case is indistinguishable from a legitimately unfiltered query.
Not investigated
- Which currently-green specification tests mask this. The suite is green on main, so either no spec test composes an operator after a multi-join Include, or one does and asserts the wrong expectation.
- Whether the EF9-plus bulk ExecuteUpdate / ExecuteDelete plan builders, which also construct this bridge visitor, can reach the same strip and lose a filter. That would be materially more serious than the read path, since a dropped filter on a bulk delete widens what gets deleted.
- is duplicated by
-
EF-370 A required reference navigation emits a left-outer $unwind instead of an inner join, returning extra rows (unreleased, main only)
-
- Closed
-