Sub-case of #22517. Prior report: #26873 (closed as duplicate). This one is filed to track the specific plain-inner variant, which — unlike its siblings — cannot be fixed with the narrow relational marker and should be tackled as part of the navigation-expansion rework (#32957 / #37859).
Repro shape
A whole non-entity object projected from the nullable side of a LeftJoin / GroupJoin+DefaultIfEmpty, where the inner is a plain (non-aggregated) Select:
var categories = context.Requests.Select(r => new { r.PickupStatusId, Count = 1 });
var query = context.Statuses
.LeftJoin(categories, s => s.PickupStatusId, c => c.PickupStatusId, (s, countInfo) => new { s.PickupStatusId, countInfo });
On a no-match row this throws Nullable object must have a value instead of yielding a null countInfo. Same shape via GroupJoin + DefaultIfEmpty.
Root cause
Navigation expansion folds the trivial inner Select into the pending selector and moves the projection forward. So at AddJoin the inner shaper is the raw entity, not a New/MemberInit. The #30915 marker gate is conditioned on the inner shaper being New/MemberInit, so the marker is never injected. Nothing gates the object at materialization.
Why this cannot be fixed in the relational layer
Two relational-layer approaches were tried and rejected:
- Inject the marker at
AddJoin. The code path never runs — the inner shaper is already a bare entity by the time AddJoin sees it.
- Gate at projection-binding time instead. This false-positives on ordinary optional-navigation projections (
Select(x => new { x.OptionalNav.Prop })): it nulls the whole object instead of the member, defeating optional-navigation type compensation. Confirmed regression on 8 GearsOfWarQuery tests.
After folding, the whole-DefaultIfEmpty'd-element case is structurally identical to an optional-navigation member access. The distinguishing signal is PendingSelector, which is a core-only concept, invisible to providers, and erased before translation runs. No relational-layer rule can recover it.
Conclusion
The marker gate needs the inner projection to still be a New/MemberInit at translation time; folding removes that precondition. Removing/reworking navigation expansion dissolves the fold — the projection reaches translation as written, the marker fires, and the optional-navigation case stays distinct. This variant belongs in that work, not a narrow relational fix.
The other #22517 sub-cases (struct/record-struct whole object #30915, GroupBy-after-join #28119, value-type left-join default #38555) do not fold and remain fixable narrowly; they are unaffected.
Sub-case of #22517. Prior report: #26873 (closed as duplicate). This one is filed to track the specific plain-inner variant, which — unlike its siblings — cannot be fixed with the narrow relational marker and should be tackled as part of the navigation-expansion rework (#32957 / #37859).
Repro shape
A whole non-entity object projected from the nullable side of a
LeftJoin/GroupJoin+DefaultIfEmpty, where the inner is a plain (non-aggregated)Select:On a no-match row this throws
Nullable object must have a valueinstead of yielding a nullcountInfo. Same shape viaGroupJoin+DefaultIfEmpty.Root cause
Navigation expansion folds the trivial inner
Selectinto the pending selector and moves the projection forward. So atAddJointhe inner shaper is the raw entity, not aNew/MemberInit. The #30915 marker gate is conditioned on the inner shaper beingNew/MemberInit, so the marker is never injected. Nothing gates the object at materialization.Why this cannot be fixed in the relational layer
Two relational-layer approaches were tried and rejected:
AddJoin. The code path never runs — the inner shaper is already a bare entity by the timeAddJoinsees it.Select(x => new { x.OptionalNav.Prop })): it nulls the whole object instead of the member, defeating optional-navigation type compensation. Confirmed regression on 8GearsOfWarQuerytests.After folding, the whole-DefaultIfEmpty'd-element case is structurally identical to an optional-navigation member access. The distinguishing signal is
PendingSelector, which is a core-only concept, invisible to providers, and erased before translation runs. No relational-layer rule can recover it.Conclusion
The marker gate needs the inner projection to still be a
New/MemberInitat translation time; folding removes that precondition. Removing/reworking navigation expansion dissolves the fold — the projection reaches translation as written, the marker fires, and the optional-navigation case stays distinct. This variant belongs in that work, not a narrow relational fix.The other #22517 sub-cases (struct/record-struct whole object #30915, GroupBy-after-join #28119, value-type left-join default #38555) do not fold and remain fixable narrowly; they are unaffected.