Entity Framework Core: Fix 'Second Operation Started' Error
Learn why Entity Framework Core throws 'A second operation was started...' and how to fix it: scope DbContext, await queries, or use IDbContextFactory.
Entity Framework Core: “A second operation was started on this context instance before a previous operation completed” error when querying top challenges by distinct user milestone counts
I am developing an API endpoint to retrieve the top 3 most won challenges in a gamification system, ranked by the number of distinct users who achieved milestones for each challenge.
Initial Query (Causes Error)
var topChallenges = await ChallengeRepository.GetAllAsQueryable()
.Where(c =>
c.ChallengeStatus != ChallengeStatusEnum.Draft &&
(!request.Data.MainDate.From.HasValue || c.StartDate >= request.Data.MainDate.From) &&
(!request.Data.MainDate.To.HasValue || c.EndDate <= request.Data.MainDate.To) &&
(!request.Data.IsDeleted.HasValue || c.IsDeleted == request.Data.IsDeleted) &&
(request.Data.ChallengeStatus == null || request.Data.ChallengeStatus.Contains(c.ChallengeStatus)) &&
(request.Data.Segments == null || request.Data.Segments.Count == 0 ||
c.ChallengeFilters.Any(s => request.Data.Segments.Select(x => x.Value).Contains(s.Value)))
)
.OrderByDescending(c => c.Milestones.Select(m => m.UserId).Distinct().Count())
.Take(3)
.Select(c => new
{
c,
DistinctUserCount = c.Milestones.Select(m => m.UserId).Distinct().Count(),
Name = c.ChallengeResources
.Where(r => r.SupportedLanguageId == Language.Id)
.Select(r => r.Name)
.FirstOrDefault(),
ImagePath = c.ChallengeImages
.OrderBy(ci => ci.Id)
.Select(ci => ci.Image)
.FirstOrDefault()
})
.AsNoTracking()
.ToListAsync();
First Refactored Approach (Still Fails)
var filteredChallenges = await ChallengeRepository.GetAllAsQueryable()
.Where(c =>
c.ChallengeStatus != ChallengeStatusEnum.Draft &&
(!request.Data.MainDate.From.HasValue || c.StartDate >= request.Data.MainDate.From) &&
(!request.Data.MainDate.To.HasValue || c.EndDate <= request.Data.MainDate.To) &&
(!request.Data.IsDeleted.HasValue || c.IsDeleted == request.Data.IsDeleted) &&
(request.Data.ChallengeStatus == null || request.Data.ChallengeStatus.Contains(c.ChallengeStatus)) &&
(request.Data.Segments == null || request.Data.Segments.Count == 0 ||
c.ChallengeFilters.Any(s => request.Data.Segments.Select(x => x.Value).Contains(s.Value)))
)
.Select(c => new
{
c.Id,
c.ChallengeStatus,
c.StartDate,
c.EndDate,
DistinctUserCount = c.Milestones.Select(m => m.UserId).Distinct().Count(),
Name = c.ChallengeResources
.Where(r => r.SupportedLanguageId == Language.Id)
.Select(r => r.Name)
.FirstOrDefault(),
ImagePath = c.ChallengeImages
.OrderBy(ci => ci.Id)
.Select(ci => ci.Image)
.FirstOrDefault()
})
.AsNoTracking()
.ToListAsync();
var topChallenges = filteredChallenges
.OrderByDescending(c => c.DistinctUserCount)
.Take(3)
.ToList();
Second Refactored Approach (Separate Queries, Still Fails)
var challengeIdsWithCounts = await ChallengeRepository.GetAllAsQueryable()
.Where(c =>
c.ChallengeStatus != ChallengeStatusEnum.Draft &&
(!request.Data.MainDate.From.HasValue || c.StartDate >= request.Data.MainDate.From) &&
(!request.Data.MainDate.To.HasValue || c.EndDate <= request.Data.MainDate.To) &&
(!request.Data.IsDeleted.HasValue || c.IsDeleted == request.Data.IsDeleted) &&
(request.Data.ChallengeStatus == null || request.Data.ChallengeStatus.Contains(c.ChallengeStatus)) &&
(request.Data.Segments == null || request.Data.Segments.Count == 0 ||
c.ChallengeFilters.Any(s => request.Data.Segments.Select(x => x.Value).Contains(s.Value)))
)
.Select(c => new
{
ChallengeId = c.Id,
DistinctUserCount = c.Milestones.Select(m => m.UserId).Distinct().Count()
})
.AsNoTracking()
.ToListAsync();
var topChallengeIds = challengeIdsWithCounts
.OrderByDescending(c => c.DistinctUserCount)
.Take(3)
.Select(c => c.ChallengeId)
.ToList();
var topChallenges = await ChallengeRepository.GetAllAsQueryable()
.Where(c => topChallengeIds.Contains(c.Id))
.Select(c => new
{
c.Id,
c.ChallengeStatus,
c.StartDate,
c.EndDate,
DistinctUserCount = c.Milestones.Select(m => m.UserId).Distinct().Count(),
Name = c.ChallengeResources
.Where(r => r.SupportedLanguageId == Language.Id)
.Select(r => r.Name)
.FirstOrDefault(),
ImagePath = c.ChallengeImages
.OrderBy(ci => ci.Id)
.Select(ci => ci.Image)
.FirstOrDefault()
})
.AsNoTracking()
.ToListAsync();
topChallenges = topChallenges
.OrderByDescending(c => c.DistinctUserCount)
.ToList();
Error Message
Something went wrong. 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.
My DbContext is registered as scoped (not singleton). I have reviewed Microsoft documentation on avoiding threading issues with DbContext, but cannot identify the cause. How can I fix this EF Core concurrency error?
Entity Framework Core throws that exception whenever a second EF Core database operation overlaps an in-flight operation on the same DbContext instance — common causes are per-row nested queries (projections that force additional queries while a reader is open), not awaiting async calls, or accidentally sharing a scoped DbContext (for example via a singleton service). Fix it by pushing the distinct-user aggregation into a single DB-side query (group Milestones), or by materializing intermediate results before running further EF calls; if you must run queries in parallel, create a new DbContext per task with AddDbContextFactory or an explicit scope. These approaches stop concurrent use of one DbContext and resolve the “A second operation was started…” error in entity framework core and net core entity framework.
Contents
- Why Entity Framework Core throws this exception
- Root causes that match your three refactors
- Entity Framework Core fixes: safe patterns (summary)
- Code: recommended single-query / server-side aggregation
- Code: safe two-step pattern (materialize counts then fetch details)
- Code: running parallel work safely with IDbContextFactory
- Diagnostics checklist — how to find the real overlap
- Sources
- Conclusion
Entity Framework Core: Why “A second operation was started…” happens
That error is EF Core’s safety mechanism: EF Core does not allow another database operation to start on the same DbContext while a previous one is still executing. The single-threaded rule applies even when your code runs on the same thread — what matters is overlapping async database activity or nested queries executed during result materialization. See Microsoft’s DbContext configuration guidance for the core reasoning and lifetime guidance: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/.
Two common technical reasons you hit this with your LINQ:
- Correlated / per-row nested queries during projection. If EF can’t translate a projection fully to one SQL statement it may execute extra queries while it streams the top-level results; those nested queries run before the original reader is finished, and that counts as a second operation.
- Lifetime / concurrency issues. A scoped DbContext can still be shared accidentally — for example by storing it in a singleton, returning an IQueryable from a long-lived service, or by launching parallel queries (Task.WhenAll) that reuse the same DbContext.
Damir’s independent reproduction shows EF Core will throw this even with MARS enabled — EF Core still prohibits overlapping operations on one context: https://www.damirscorner.com/blog/posts/20241101-MultipleConcurrentQueriesInEfCore.html. That matches the exception text and the stack traces you’ll see.
Root causes that match your three refactors
Looking at your three attempts, here’s how those causes map:
-
Your initial single-query projection used nested expressions (Distinct().Count(), FirstOrDefault() on nav properties) inside OrderBy and Select. EF Core will often translate correlated subqueries to SQL, but sometimes it can’t and will execute per-row queries during materialization — that produces overlapping operations.
-
The first refactor (selecting into an anonymous type and ToListAsync then ordering in memory) normally avoids streaming overlap. If you still saw the exception there, the overlap is likely coming from elsewhere: a repository registered with the wrong lifetime or some other code performing parallel EF calls on the same scoped context.
-
The second refactor (get ids/counts, then fetch details) is the usual safe pattern, but problems still happen if any of these are executed concurrently or if navigation-property access triggers additional queries while a reader is open (e.g., lazy loading during JSON serialization). Also check that the repository isn’t promoted to a singleton (StackOverflow example of the singleton→scoped leak): https://stackoverflow.com/questions/48767910/entity-framework-core-a-second-operation-started-on-this-context-before-a-previ.
In short: either EF produced nested queries during materialization, or the DbContext is being reused concurrently (DI lifetime bug, background tasks, or accidental shared instance).
Entity Framework Core fixes: push aggregation or create separate DbContext instances
Which approach to pick? Two safe paths:
-
Push work into the database (preferred). Compute the distinct-user counts in SQL (group by Milestones or correlated subquery) so EF runs a single server-side command that returns top 3 rows — no per-row nested queries, no overlap.
-
If you need concurrency or multiple simultaneous queries, create a fresh DbContext per operation (use AddDbContextFactory or create a scope with IServiceScopeFactory). Don’t share a single scoped context across parallel tasks.
Other important fixes:
- Make sure your repository and services have correct DI lifetimes (repositories that use DbContext must be scoped, not singleton).
- Avoid lazy-loading or navigation access during JSON serialization of entities; use DTO projections or AsNoTracking projections instead.
- AsNoTracking is useful for performance but it does not make a DbContext thread-safe.
See Microsoft’s DbContext lifetime guidance for full patterns and IDbContextFactory mention: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/.
Code: recommended single-query / server-side aggregation
This pattern computes counts on the Milestones table (server-side), then selects the top 3 challenges and projects the fields you need in one or two SQL-friendly queries.
Example (two-step but all DB-side work is translated to SQL):
// 1) get IDs of challenges that match your filters
var filteredChallengeIds = await _db.Challenges
.Where(c =>
c.ChallengeStatus != ChallengeStatusEnum.Draft &&
(!request.Data.MainDate.From.HasValue || c.StartDate >= request.Data.MainDate.From) &&
(!request.Data.MainDate.To.HasValue || c.EndDate <= request.Data.MainDate.To) &&
(!request.Data.IsDeleted.HasValue || c.IsDeleted == request.Data.IsDeleted) &&
(request.Data.ChallengeStatus == null || request.Data.ChallengeStatus.Contains(c.ChallengeStatus)) &&
(request.Data.Segments == null || request.Data.Segments.Count == 0 ||
c.ChallengeFilters.Any(s => request.Data.Segments.Select(x => x.Value).Contains(s.Value)))
)
.Select(c => c.Id)
.ToListAsync();
// 2) aggregate on Milestones and pick top 3 by distinct user count
var topCounts = await _db.Milestones
.Where(m => filteredChallengeIds.Contains(m.ChallengeId))
.GroupBy(m => m.ChallengeId)
.Select(g => new {
ChallengeId = g.Key,
DistinctUserCount = g.Select(x => x.UserId).Distinct().Count()
})
.OrderByDescending(x => x.DistinctUserCount)
.Take(3)
.ToListAsync();
var topIds = topCounts.Select(x => x.ChallengeId).ToList();
// 3) fetch challenge details for those ids (single query)
var topChallenges = await _db.Challenges
.Where(c => topIds.Contains(c.Id))
.Select(c => new {
c.Id,
Name = c.ChallengeResources.Where(r => r.SupportedLanguageId == Language.Id).Select(r => r.Name).FirstOrDefault(),
ImagePath = c.ChallengeImages.OrderBy(ci => ci.Id).Select(ci => ci.Image).FirstOrDefault()
})
.AsNoTracking()
.ToListAsync();
// 4) join counts in memory (safe, no concurrent DbContext use)
var countsDict = topCounts.ToDictionary(x => x.ChallengeId, x => x.DistinctUserCount);
var result = topChallenges
.OrderByDescending(c => countsDict[c.Id])
.Select(c => new {
c.Id,
DistinctUserCount = countsDict[c.Id],
c.Name,
c.ImagePath
})
.ToList();
Why this is safe: each ToListAsync completes before the next query starts, and the heavy DISTINCT/COUNT work runs inside the database. That removes per-row nested queries and avoids streaming overlap.
Code: safe two-step pattern (materialize counts then fetch details)
If you prefer to compute counts directly from Challenges (correlated subqueries), make sure EF translates the query to a single SQL statement. If translation fails, use the two-step Milestones grouping above.
You can validate translation with ToQueryString() (inspect SQL) or EF logging. If EF would stream and then execute nested queries per-row, materialize the intermediate results (ToListAsync) before accessing navigation properties.
Example of a correlated subquery that often is translated into SQL (verify with ToQueryString()):
var top = await _db.Challenges
.Where(c => /* same filters as above */)
.Select(c => new {
c.Id,
DistinctUserCount = _db.Milestones.Where(m => m.ChallengeId == c.Id).Select(m => m.UserId).Distinct().Count(),
Name = c.ChallengeResources.Where(r => r.SupportedLanguageId == Language.Id).Select(r => r.Name).FirstOrDefault(),
ImagePath = c.ChallengeImages.OrderBy(ci => ci.Id).Select(ci => ci.Image).FirstOrDefault()
})
.OrderByDescending(x => x.DistinctUserCount)
.Take(3)
.AsNoTracking()
.ToListAsync();
If that query executes as one SQL command, it’s the simplest fix. If EF emits multiple queries per row, switch to the Milestones grouping approach above.
Code: running parallel work safely with IDbContextFactory
If you need parallel database calls (e.g., Task.WhenAll with independent queries), create a new DbContext per task. Register a factory at startup:
// Program.cs / Startup.cs
services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
Use it:
// injected: IDbContextFactory<AppDbContext> _dbFactory
var tasks = someList.Select(async id => {
using var ctx = _dbFactory.CreateDbContext();
return await ctx.Challenges
.Where(c => c.Id == id)
.Select(c => new {
c.Id,
DistinctUserCount = c.Milestones.Select(m => m.UserId).Distinct().Count()
})
.AsNoTracking()
.FirstOrDefaultAsync();
});
var results = await Task.WhenAll(tasks);
This ensures each task has its own context and avoids the exception described in Microsoft docs: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/. The same pattern works inside background services — create a scope per unit of work (IServiceScopeFactory.CreateScope()).
Diagnostics checklist — how to find the real overlap
Try these steps, they find the usual culprits quickly:
- Inspect the exception stack trace to see which LINQ/ToListAsync line started the second operation.
- Turn on EF logging and/or call query.ToQueryString() to see whether EF translates everything to a single SQL statement or emits nested queries.
- Search code for Task.WhenAll, Parallel.ForEach, Task.Run or any parallelization that may call repository methods concurrently.
- Check DI registrations: confirm services.AddScoped<IChallengeRepository, ChallengeRepository>() and not AddSingleton. A singleton repository that depends on DbContext will effectively make the DbContext long-lived and shared.
- Verify you don’t keep DbContext in static fields, or store IQueryable for later execution across requests.
- Disable lazy-loading proxies if enabled (they trigger queries during serialization): use explicit projection to DTOs instead.
- Reproduce a minimal test that triggers the exception: two concurrent ToListAsync() calls on the same AppDbContext will throw — that’s a quick proof of shared-context concurrency.
- If the problem occurs only in production, check middlewares, custom logging, or JSON serialization that might access navigation properties while the query is still materializing.
If after these checks you still can’t find the overlap, capture a full exception stack trace and the SQL emitted by EF and compare the two — that usually points to a nested per-row query or a concurrent caller.
Sources
- DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Learn
- Multiple concurrent queries in EF Core | Damir’s Corner
- Microsoft Q&A — “A second operation was started on this context instance…” (community thread)
- StackOverflow — Entity Framework Core: A second operation started on this context
- Why ‘A Second Operation Was Started’ Happens in EF Core — and How to Truly Fix It (Medium)
- The Importance of Multiple Active Result Sets in EF Core Data Models - DevExpress Documentation
- GitHub issue: EF Core concurrency error with shared context (npgsql/efcore.pg)
Conclusion
You’re seeing a concurrency protection built into entity framework core: something starts a second EF operation before the first completes. The most reliable fixes are: (A) move the distinct-user aggregation into a DB-side GroupBy on Milestones (or a single correlated subquery that EF can translate), or (B) stop sharing a DbContext for parallel work — use AddDbContextFactory or create a scope per operation and ensure your repository lifetimes are scoped. Do those and the “A second operation was started…” exception will go away; if it persists, inspect EF’s SQL and your DI lifetimes — the stack trace will point to the overlap.