ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Unknown
    • 10.0.3, 9.1.3, 8.4.3
    • Affects Version/s: None
    • Component/s: None
    • None
    • Dotnet Drivers
    • Not Needed
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      Two chained Join calls onto the same target entity type are never flattened, so the driver's second Join re-wraps the document under a second _outer while the shaper is committed to one level of nesting. The query throws.

      Established by measurement during a spike on 2026-08-05 (branch NativeQueryOngoing, EF10, real atlas-local container). Present on upstream/main (58e05a0e) verbatim; unreleased (absent from v10.0.2, v9.1.2, v8.4.2).

      Repro

      db.Roots.Join(db.Mids, r => r.MidId, m => m.Id, (r, m) => r)
              .Join(db.Mids, r => r.MidId, m => m.Id, (r, m) => r)
      

      Model shape: Root has an FK MidId to Mid (no navigation required to trigger this — a bare key-equality Join is enough). Expected: 1 row, the joined Root. Observed: throws.

      Result: InvalidOperationException: Document element is missing for required non-nullable property 'Id'.

      Emitted pipeline:

      aggregate([{ "$project" : { "_outer" : "$$ROOT", "_id" : 0 } },
      { "$lookup" : { "from" : "M", "localField" : "_outer.MidId", "foreignField" : "_id", "as" : "_inner" } },
      { "$unwind" : "$_inner" }, { "$project" : { "_outer" : "$_outer", "_inner" : "$_inner", "_id" : 0 } },
      { "$project" : { "_outer" : "$$ROOT", "_id" : 0 } },
      { "$lookup" : { "from" : "M", "localField" : "_outer._outer.MidId", "foreignField" : "_id", "as" : "_inner" } },
      { "$unwind" : "$_inner" }, { "$project" : { "_outer" : "$_outer", "_inner" : "$_inner", "_id" : 0 } }])
      

      Scope of the failure, measured:

      • (1) Same target type, whole-root-entity result — fails (above).
      • (2) Same target type, inner-entity result ((x, m) => m) — correct rows.
      • (3) Same target type, scalar result — correct rows.
      • (4) Different target types — flattens correctly, 3 correct rows. This is the control that must stay green.

      Mode(s) this reproduces under — added by audit, 2026-08-07

      Not stated in the original write-up; added here rather than left implicit. The root cause (InnerCollections.Count > 1 at MongoQueryableMethodTranslatingExpressionVisitor.cs:1740, keyed by IEntityType) sits in MongoQueryableMethodTranslatingExpressionVisitor, the visitor that builds the MongoQueryExpression once, before the Native/DriverLinq fork. The file's own comments state this explicitly for the sibling EF-379 defect at the same site: a wrong flatten decision there "hard-failed a query that works at this branch's base in BOTH Native and DriverLinq" and propagates "in EVERY MongoQueryMode alike — Native, DriverLinq, and NativeOnly — since MongoQueryMode is only consulted later, by the compile-time gate." By the same mechanism, this ticket's throw and the two silent symptom classes below are expected to reproduce under default Native and explicit DriverLinq alike; nothing in the code path is mode-gated before the shaper crashes or misreads. This has not been independently re-executed for this audit pass — flagged so it is verified rather than assumed before being relied on further.

      Root cause

      src/MongoDB.EntityFrameworkCore/Query/Visitors/MongoQueryableMethodTranslatingExpressionVisitor.cs:1675

      isSecondOrLaterJoin = InnerCollections.Count > 1
      

      innerCollections is a dictionary _keyed by IEntityType (Query/Expressions/MongoQueryExpression.Lookup.cs:32, populated at :184). Two joins onto the same target type collapse into one entry, so Count stays 1, flattening never fires, and UsesDriverJoinFields (:176) stays true.

      This is the same entity-type-keyed blind spot recorded for the EF-368 candidate/confirmed counter.

      Chosen fix shape

      Flatten properly (decided 2026-08-05, in preference to a clean decline): key the collection by something finer than IEntityType so the flatten trigger fires per join rather than per target type. Note that AddLookup dedups on As, and both joins derive the same _lookup_Mid alias — the alias collision has to be resolved as part of this.

      Corrections to earlier notes

      An internal spike note described this bug as a "malformed second $lookup.localField" and attributed it to RewriteLeftJoins / StripOuterSelectForJoin in MongoEFToLinqTranslatingExpressionVisitor. Both are wrong:

      • (1) "_outer._outer.MidId" is the correct address for the driver's doubly-nested document. The MQL is well-formed and semantically right.
      • (2) Those rewriters faithfully rewrite a chain that the flattening decision in the QMTEV had already declined to flatten.

      The same note called this a sibling of EF-372 / EF-373. Measured: it shares no site with either. EF-372 is a missing localField prefix decided ~20 lines earlier at :1656; EF-373 is a stage-position problem in LeftJoin.cs:694. Three independent defects.

      Comment (2026-08-05) — three symptom classes, not one, from three different sites; frequency is higher than filed; EF-378 is a duplicate

      Measured in an isolated worktree at 6a7a5f3c, Debug EF10, with instrumentation on the decision sites. Line numbers in the original description are from 34a02067 and are stale: isSecondOrLaterJoin = InnerCollections.Count > 1 is at 1740 at the current tip.

      The precondition is "same target entity type", not "chained Join". Sibling reference {{Include}}s onto different target types work correctly (measured, 2- and 3-navigation cases). The defect requires two joins onto the same entity type. That makes EF-378 a duplicate of this ticket.

      There are three symptom classes, not one, from three different sites. The original description covers only the first.

      • (1) Throw (loud) — bare same-typed pair, e.g. Root.Include(A).Include(B) where both target the same type. _innerCollections collapses, so isSecondOrLaterJoin is false at both joins, no ForceUnwind lookup is registered, UsesDriverJoinFields stays true, and the driver LeftJoin shape is emitted twice nested.
      • (2) Silent null navigation — add any third, distinct-typed join and flattening does fire, moving the failure to a different site: the retroactive flattening registration picks a navigation by target type alone. Measured: .Include(A).Include(B).Include(C) returns B = [null] where TARGET-B is correct, and the emitted pipeline contains a $lookup for navigation A that the query never mentions.
      • (3) Silent wrong values — a fifth site not named in any ticket. MongoProjectionBindingRemovingExpressionVisitor's UsesDriverJoinFields ? "_inner" : accessExpression.Name discards the navigation entirely, so two entity projections both read _inner, which after the second $lookup holds the second target. Measured: a double join projecting both entities returns TARGET-B | TARGET-B where TARGET-A | TARGET-B is correct (silent wrong data — the MQL is well-formed and the driver reads it correctly, so this is a shaper-side defect, not a pipeline one).

      Fixing this ticket must address all three sites, or the fix converts a throw into silent wrong data.

      Frequency is higher than the description implies: Employee.Include(Manager).Include(Mentor) — a self-referencing entity with two reference navigations — throws the identical Document element is missing for required non-nullable property 'Id'. Same-typed siblings are guaranteed by construction on any self-referencing model, an ordinary modelling pattern. A lone self-reference Include is fine.

      Already partly documented in-tree: NativeReferenceIncludeTests has a "same-target sibling Includes" row excluded from the Native/DriverLinq parity half via HasNoDriverLinqParityOracle, and Two_joins_onto_the_same_target_stay_declined names the _outer._outer.[FK] mechanism verbatim, calling it pre-existing and out of scope.

      Interaction with EF-372: a fix here should delete the agreement check in TryResolveIntermediateLookupPrefix — that check exists solely to compensate for the retroactive-registration imprecision described above, and carries a TODO(EF-375) saying so. Consequence: the pinned test Two_same_typed_navigations_second_branch_only_declines_cleanly currently pins a decline for a query that should return correct rows, and will need re-baselining to a passing assertion as part of this work.

      Sequencing: recommended second, after EF-379 (which is smaller, is silent wrong data, and increases traffic through the code path whose imprecision the agreement check is papering over — so doing EF-379 first surfaces, as clean declines, exactly the shapes this ticket must convert to correct rows).

      Suggested tests

      • (1) Same-target chained Join with whole-root-entity result returns correct rows. Mutation: revert the flatten trigger, get InvalidOperationException ... 'Id'.
      • (2) Pipeline contains no "_outer._outer" substring. Mutation: revert, substring present.
      • (3) Control: different-target chained Join still emits _lookup_Other + _lookup_Mid with unchanged MQL. Mutation: a fix that widens the trigger too far changes this.
      • (4) Orders.Include(o => o.Buyer).Join(db.Buyers, ...) whole-entity — same entity-type-keyed root cause, assert not silently wrong.

      See also docs/superpowers/specs/2026-08-07-native-query-merge-plan-design.md §4 and docs/native-query-status-EF-322.md §9.5.

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

              Created:
              Updated:
              Resolved: