A second operation was started on this context instance before a previous operation completed.
— two queries ran at the same time on one DbContext, and EF Core allows one at a
time. Await each query before starting the next, or give each concurrent query its own
context from IDbContextFactory.
Blazor Server is where most people meet it. AddDbContext registers the context
as scoped, and in server-side Blazor a scope lasts as long as the user's circuit, so every
component on the page shares one context. Two components loading data at once, or a click
that starts a query while another is still running, is two operations on that one context.
Microsoft's guidance for Blazor is a factory and a new context per operation.
The error
fail: Microsoft.EntityFrameworkCore.Query[10100]
An exception occurred while iterating over the results of a query for context type 'DiLab.ClinicDb'.
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Infrastructure.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
Why it happens
A DbContext holds one connection and one change tracker and is not thread-safe.
EF Core guards it with a concurrency detector: each query enters a critical section, and a
second query that arrives before the first has left it throws. This endpoint starts the
second query while the first is still waiting on the database:
app.MapGet("/dashboard", async (ClinicDb db) =>
{
var patients = db.Patients.ToListAsync();
var visits = db.Visits.ToListAsync();
await Task.WhenAll(patients, visits);
return $"{patients.Result.Count} patients, {visits.Result.Count} visits";
});
One surprise from the lab: on plain SQLite this code worked. Microsoft.Data.Sqlite runs its async methods synchronously, because SQLite has no asynchronous I/O, so the first query had finished before the second began. The code is still wrong; nothing overlapped. To make the lab behave like a database reached over the network, I added an interceptor that waits 50 ms before each query, and the error appeared on the first request.
The fix
The simple fix is to await each query before starting the next:
app.MapGet("/dashboard", async (ClinicDb db) =>
{
var patients = await db.Patients.ToListAsync();
var visits = await db.Visits.ToListAsync();
return $"{patients.Count} patients, {visits.Count} visits";
});
If the queries should run in parallel, give each one its own context from a factory:
builder.Services.AddDbContextFactory<ClinicDb>(options => options
.UseSqlite("Data Source=clinic.db")
.AddInterceptors(new NetworkDelayInterceptor()));
app.MapGet("/dashboard", async (IDbContextFactory<ClinicDb> factory) =>
{
await using var patientsDb = await factory.CreateDbContextAsync();
await using var visitsDb = await factory.CreateDbContextAsync();
var patients = patientsDb.Patients.ToListAsync();
var visits = visitsDb.Visits.ToListAsync();
await Task.WhenAll(patients, visits);
return $"{patients.Result.Count} patients, {visits.Result.Count} visits";
});
The AddInterceptors line is the lab's 50 ms stand-in for network latency; leave it
out of your app. With it in place, the sequential version answered warm requests in 142 to
183 ms and the factory version in 52 to 68 ms, because its two queries overlap. In a Blazor
component, inject IDbContextFactory<ClinicDb> and create a context inside
each method that touches the database, disposed when the method ends, instead of injecting
the context itself.
How it was reproduced
A minimal API from dotnet new web -o DiLab on .NET SDK 10.0.401 (ASP.NET Core
runtime 10.0.12) with Microsoft.EntityFrameworkCore.Sqlite 10.0.12: a ClinicDb
with Patients and Visits tables, two fictional rows each, registered
with AddDbContext, and the GET /dashboard endpoint above. Without the
interceptor it returned 200; with it, the first request returned 500 with the error above.
Both fixes returned "2 patients, 2 visits" on three requests in a row.
Frequently asked
- Can I run two EF Core queries in parallel on the same DbContext?
- No. A DbContext supports one operation at a time. Await each query before starting the next, or create one context per query with IDbContextFactory and run those in parallel.
- How do I fix 'A second operation was started on this context instance' in Blazor Server?
- Register the context with AddDbContextFactory, inject IDbContextFactory into the component, and create a new context inside each method that uses the database, disposing it when the method ends. A scoped DbContext lives for the whole circuit and is shared by every component.
- Why doesn't this EF Core error happen with SQLite?
- Microsoft.Data.Sqlite runs async methods synchronously because SQLite has no asynchronous I/O, so each query finishes before the next one starts and nothing overlaps. The code is still wrong and fails as soon as a query really waits on I/O.
More decoded errors in the Fixes category. Why a scoped service lives for the whole circuit in Blazor Server is covered in Services and Dependency Injection in Blazor.