Skip to content

Bug: EF Core LEFT JOIN with GroupBy subquery throws InvalidOperationException on NULL values from non-matching rows #3801

@lttruc1402

Description

@lttruc1402

Bug: EF Core LEFT JOIN with GroupBy subquery throws InvalidOperationException on NULL values from non-matching rows

Component: Query / EF Core
Severity: High
Reported: 2026-04-06


Environment

Platform .NET / EF Core
EF Core Provider Npgsql.EntityFrameworkCore.PostgreSQL 10.0.1
Database PostgreSQL
Schema ops.pickup_status / ops.pickup_request
Operation LEFT JOIN + GroupBy subquery

Description

When performing a LEFT JOIN between pickup_status and a GroupBy subquery on pickup_request, EF Core generates SQL with count(*)::int (non-nullable cast). For status records that have no matching pickup_request rows within the given date filter, the "Count" column returns NULL from the LEFT JOIN. EF Core then attempts to map NULL into a non-nullable int and throws an exception. Additionally, EF Core projects an extra column (p1."pickupStatusId") into the subquery that was not explicitly selected in C#.


Code to Reproduce

var statusWithCount = _queryService.AsQueryable(filterPickup, true)
    .GroupBy(
        o => o.PickupStatusId,
        (pickupstatusId, el) => new { pickupstatusId, Count = el.Count() });

var result = await _queryService.AsQueryable(filterStatus, true)
    .LeftJoin(
        statusWithCount,
        e => e.PickupStatusId,
        e => e.pickupstatusId,
        (status, debitNote) => new { status, debitNote }
    )
    .Select(e => new
    {
        e.status.PickupStatusId,
        C = e.debitNote == null ? 0 : e.debitNote.Count,
    })
    .ToListAsync(cancellationToken: cancellationToken);

Steps to Reproduce

  1. Build a GroupBy subquery on pickup_request using (pickupStatusId, el) => new { pickupstatusId, Count = el.Count() }
  2. LEFT JOIN the subquery against pickup_status on PickupStatusId
  3. Select PickupStatusId and Count with a null-check e.debitNote == null ? 0 : e.debitNote.Count
  4. Call ToListAsync()
  5. Ensure at least one status has no matching pickup_request rows within the filter range

Actual SQL Generated

SELECT p.pickup_status_id, p1."pickupStatusId", p1."Count"
FROM ops.pickup_status AS p
LEFT JOIN (
    SELECT p0.pickup_status_id AS "pickupStatusId", count(*)::int AS "Count"
    FROM ops.pickup_request AS p0
    WHERE p0.created_time > @fromDate AND p0.created_time <= @toDate
    GROUP BY p0.pickup_status_id
) AS p1 ON p.pickup_status_id = p1."pickupStatusId"

For pickup_status_id = 50, both p1."pickupStatusId" and p1."Count" are NULL. Note that p1."pickupStatusId" is an extra projected column not explicitly selected in C# — EF Core adds it internally as the JOIN key.


Exception

System.InvalidOperationException: Nullable object must have a value.
   at System.Nullable`1.get_Value()
   at lambda_method175(Closure, QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

Root Cause

EF Core translates el.Count() to count(*)::int — a non-nullable PostgreSQL cast. After the LEFT JOIN, this column is NULL for non-matching rows. EF Core's materializer calls .Value on a Nullable<int> that is null, causing the exception. The null-check in C# (e.debitNote == null ? 0 : ...) has no effect because the crash occurs during materialization, before the object is constructed.


Attempted Workarounds (Ineffective)

  • Casting (int?)el.Count() in the GroupBy selector — EF Core still generates count(*)::int
  • Adding null-check e.debitNote == null ? 0 : e.debitNote.Count in Select — crash happens before object mapping
  • Rewriting with GroupJoin + SelectMany — EF Core does not support filter conditions in the ON clause, producing incorrect results

Tags

ef-core left-join group-by nullable postgresql linq pickup-status

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions