Query: whole non-entity object from a plain (non-aggregated) LeftJoin/DefaultIfEmpty inner is not nulled on no-match

Aperta
#38,608 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 2 giorni

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
30/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Tranquilla
Stack tecnologico
csharp
Ambito
databases

Direzione di ricerca

Inizia con la revisione dell’espansione della navigazione in #32957/#37859 e traccia come PendingSelector viene ripiegato prima di AddJoin. Riproduci il Select semplice non aggregato con LeftJoin o GroupJoin più DefaultIfEmpty, quindi confrontalo con la navigazione opzionale e con gli altri casi di #22517. Il lavoro è completato quando l’intero oggetto è null in assenza di corrispondenze, senza introdurre regressioni nella compensazione della navigazione opzionale.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

area-query

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:

  1. Inject the marker at AddJoin. The code path never runs — the inner shaper is already a bare entity by the time AddJoin sees it.
  2. 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.

Lingua principale
C#
Stelle
14.8k
Fork
3.4k
Merge medio
3g 18h
PR unite (30g)
159

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/efcore

Tutte le issue di dotnet/efcore

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.