ExportXMLWordPrintableJSON

    • 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:

      Unable to find source-code formatter for language: csharp. Available languages are: actionscript, ada, applescript, bash, c, c#, c++, cpp, css, erlang, go, groovy, haskell, html, java, javascript, js, json, lua, none, nyan, objc, perl, php, python, r, rainbow, ruby, scala, sh, sql, swift, visualbasic, xml, yaml
      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.

            Assignee:
            Arthur Vickers
            Reporter:
            Arthur Vickers
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: