dotnet/efcore · error · DbQueryConcurrencyException
SplitQueryConcurrentModification
SplitQueryConcurrentModification
Error message
The results of a split query could not be correlated because the data was modified concurrently while the query was executing. Re-execute the query, or execute it within a serializable or snapshot transaction to prevent concurrent modifications.
What it means
Thrown by SplitQueryResultCoordinator.VerifyNoOrphanedChildRows (SplitQueryResultCoordinator.cs:104) as a DbQueryConcurrencyException. Split queries run the parent query and each related collection as separate SQL statements, correlating child rows to parents by key order. After the last parent is processed, if a child reader still has rows (HasNext == true), those rows belong to no parent, meaning the data was modified concurrently between the parent and child queries. EF throws rather than silently drop orphaned rows.
Source
Thrown at src/EFCore.Relational/Query/Internal/SplitQueryResultCoordinator.cs:104
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public void VerifyNoOrphanedChildRows()
{
foreach (var dataReaderContext in DataReaders)
{
// Split collection queries correlate child rows to parents by consuming, for each parent (in order), the leading child
// rows whose parent key matches. HasNext == true here means the split reader is parked on a child row that didn't match
// the last parent - and since every parent has now been processed, that row (and any after it) belongs to no parent in
// the parent query's results. This can only happen if the data was modified concurrently between the execution of the
// parent query and the child query, leaving orphan child rows that would otherwise be silently dropped (see #33826).
if (dataReaderContext?.HasNext == true)
{
throw new DbQueryConcurrencyException(RelationalStrings.SplitQueryConcurrentModification);
}
}
}
}
View on GitHub (pinned to dbf9771522)
Solutions
- Re-execute the query (transient races often resolve on retry).
- Wrap the split query in a serializable or snapshot transaction so the read sees a stable point-in-time view.
- Switch the query to AsSingleQuery() (a single JOINed statement) which has no parent/child correlation window.
- Reduce the window between parent and child reads (fewer includes, smaller result sets).
- Apply an optimistic-retry policy around the read for concurrency-induced occurrences.
Example fix
// before - split query races against concurrent writes var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList(); // may throw SplitQueryConcurrentModification if items are inserted concurrently // after - snapshot transaction gives a stable view using var tx = ctx.Database.BeginTransaction(System.Data.IsolationLevel.Snapshot); var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList(); tx.Commit(); // or fall back to a single query (no correlation window) var data = ctx.Orders.AsSingleQuery().Include(o => o.Items).ToList();
Defensive patterns
Strategy: retry
Validate before calling
// Run split queries under a snapshot/serializable transaction to avoid concurrent-modification races. using var tx = ctx.Database.BeginTransaction(IsolationLevel.Snapshot); var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList(); tx.Commit(); // Or prefer AsSingleQuery() for writes-prone environments.
Try / catch
// Retry transient split-query concurrent-modification failures.
for (int attempt = 0; ; attempt++) {
try {
using var tx = ctx.Database.BeginTransaction(IsolationLevel.Snapshot);
var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList();
tx.Commit();
return data;
} catch (DbQueryConcurrencyException ex) when (attempt < 3) {
// SplitQueryConcurrentModification: data changed concurrently; retry
await Task.Delay(100 * (attempt + 1));
}
} Prevention
- Run split queries inside a snapshot/serializable transaction for a stable read view.
- Use AsSingleQuery() when related collections are small or writes are frequent.
- Retry transient occurrences; treat persistent ones as an isolation-config problem.
- Avoid long-running read transactions that widen the modification window.
When it happens
Trigger: Running an AsSplitQuery() with related collections while another transaction inserts child rows whose parent key is not among the (already-fetched) parents, or deletes parents, between the parent and child query executions. Fires at the end of enumeration when VerifyNoOrphanedChildRows is called (GroupBySplitQueryingEnumerable / SplitQueryingEnumerable end-of-enumeration paths).
Common situations: High-concurrency apps using split queries without transaction isolation; background jobs inserting children during a read; deleting parents mid-query; race between read and write under default (read committed / no snapshot) isolation.
Related errors
- The entity type '{entityType}' has property '{property}' con
- The type of the etag property '{property}' on '{entityType}'
- The entity type '{entityType}' is mapped to the stored proce
- Entity type '{entityType}' doesn't contain a property mapped
- This enumerator cannot be reset.
AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06).
Data as JSON: /api/errors/e639080e51bb02e6.
Report an issue: GitHub.