A missing or explicitly-null embedded array materializes as null instead of an empty collection

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Unknown
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Dotnet Drivers
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      For a document whose embedded (owned-collection) array field is missing or explicitly BSON null, the two materialization paths disagree:

      • *Whole-entity materialization* normalizes it to an *empty list* — blog.Posts.Count == 0.
      • *The projection path* (CollectionShaperExpression) materializes *null*.

      Measured on the same three documents (empty array / missing field / explicit null), EF10, isolated mongodb-atlas-local container:

      empty-array   whole-entity: Posts is count=0     bare-count projection: OK -> 0
      missing       whole-entity: Posts is count=0     bare-count projection: ArgumentNullException (Parameter 'source')
      explicit-null whole-entity: Posts is count=0     bare-count projection: ArgumentNullException (Parameter 'source')
      

      ArgumentNullException: Value cannot be null. (Parameter 'source') is Enumerable.Count(null) — the fold receives null where LINQ semantics say empty.

      Why this is filed separately

      This is the residual that keeps EF-357 only partially resolved. EF-357's translation-time ArgumentException for Select(b => b.Posts.Count) is fixed on branch EF-322-owned-collection-count-projection (commit 0cb1b1b), but the projection now reaches materialization and throws this ArgumentNullException for exactly those rows.

      It is not a count problem. It is a general property of the projection path and will be the first thing an array-projection slice (Select(b => b.Posts)) hits.

      Why it was not folded into the count-projection slice

      Fixing it changes observable results for collection projections that work today: Select(b => b.Posts) currently returns null for those rows, and normalizing to an empty list changes that. It therefore needs its own verification pass over the array-projection shapes, rather than riding along with a count fix.

      Note on the native path

      The native $project path does not have this problem: an owned count leaf renders as {{{ $size:

      { $ifNull: [ "$Posts", [] ] }

      }}} and correctly yields 0 for both states. The asymmetry is between whole-entity/native and the driver-LINQ fallback projection path.

      A practical consequence for testing: a Native == DriverLinq parity assertion over a seed containing missing/null arrays fails by design for these shapes, because native is more correct than the fallback. Differential tests for such shapes must use an in-memory LINQ oracle built from whole-entity materialization, not driver parity.

      Source of the measurement

      Spike findings document committed on branch EF-322-owned-collection-count-projection:
      docs/superpowers/specs/2026-07-28-native-owned-collection-count-projections-spike-findings.md (question Q4).

      Pinned in the test suite by
      NativeOwnedCollectionCountTests.Bare_embedded_collection_Count_projection_still_throws_for_a_missing_or_null_array.

      Also documented in src/MongoDB.EntityFrameworkCore/Query/AGENTS.md, in the "Owned-collection .Count as a PROJECTION LEAF" note.

      Suggested fix

      Have the projection path's CollectionShaperExpression produce an empty collection instance rather than null when the underlying BSON element is missing or null, matching whole-entity materialization. Verify against the full set of array-projection shapes before landing, since it changes today's observable results.

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

              Created:
              Updated: