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
- Build a GroupBy subquery on
pickup_request using (pickupStatusId, el) => new { pickupstatusId, Count = el.Count() }
- LEFT JOIN the subquery against
pickup_status on PickupStatusId
- Select
PickupStatusId and Count with a null-check e.debitNote == null ? 0 : e.debitNote.Count
- Call
ToListAsync()
- 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
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
Description
When performing a LEFT JOIN between
pickup_statusand a GroupBy subquery onpickup_request, EF Core generates SQL withcount(*)::int(non-nullable cast). For status records that have no matchingpickup_requestrows within the given date filter, the"Count"column returnsNULLfrom the LEFT JOIN. EF Core then attempts to mapNULLinto a non-nullableintand 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
Steps to Reproduce
pickup_requestusing(pickupStatusId, el) => new { pickupstatusId, Count = el.Count() }pickup_statusonPickupStatusIdPickupStatusIdandCountwith a null-checke.debitNote == null ? 0 : e.debitNote.CountToListAsync()pickup_requestrows within the filter rangeActual SQL Generated
For
pickup_status_id = 50, bothp1."pickupStatusId"andp1."Count"areNULL. Note thatp1."pickupStatusId"is an extra projected column not explicitly selected in C# — EF Core adds it internally as the JOIN key.Exception
Root Cause
EF Core translates
el.Count()tocount(*)::int— a non-nullable PostgreSQL cast. After the LEFT JOIN, this column isNULLfor non-matching rows. EF Core's materializer calls.Valueon aNullable<int>that isnull, 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)
(int?)el.Count()in the GroupBy selector — EF Core still generatescount(*)::inte.debitNote == null ? 0 : e.debitNote.Countin Select — crash happens before object mappingGroupJoin + SelectMany— EF Core does not support filter conditions in theONclause, producing incorrect resultsTags
ef-coreleft-joingroup-bynullablepostgresqllinqpickup-status