Analyzer rule
LC051: ToAsyncEnumerable() runs an EF Core query synchronously
LC051 flags ToAsyncEnumerable() on EF Core queries, which enumerates them synchronously and blocks a thread per row, and switches to AsAsyncEnumerable().
- Default severity
- Warning
- Code fix
- Available
- Category
- Performance
- Domain
- Execution & Async
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC051.severity = error
LC051: ToAsyncEnumerable() runs an EF Core query synchronously
In Plain Terms
You ask for a delivery service, but the courier walks every parcel over one at a time while you stand at the door. It looks asynchronous from the outside. Underneath, someone is waiting the whole time.
Goal
Detect ToAsyncEnumerable() called directly on an EF Core query.
The Problem
.NET 10 ships System.Linq.AsyncEnumerable in the box (earlier versions get it from the System.Linq.Async or System.Linq.AsyncEnumerable packages). Its ToAsyncEnumerable() accepts any IEnumerable<T>, and an EF Core query is one. So this compiles:
// Violation: looks async, runs the query with synchronous I/O.
await foreach (var blog in db.Blogs.Where(b => b.Rating > 3).ToAsyncEnumerable())
{
...
}
ToAsyncEnumerable() only wraps the query’s synchronous enumerator. EF Core opens the connection and reads every row synchronously, blocking a thread-pool thread for the whole query while the code looks asynchronous. Under load this starves the thread pool just like .Result on a task.
The Fix
Use EF Core’s AsAsyncEnumerable(), which returns the same IAsyncEnumerable<T> and streams rows with async I/O:
await foreach (var blog in db.Blogs.Where(b => b.Rating > 3).AsAsyncEnumerable())
{
...
}
Analyzer Logic
ID: LC051
Category: Performance
Severity: Warning
- Find
System.Linq.AsyncEnumerable.ToAsyncEnumerable(IEnumerable<T>), fluent or static. - Walk its source back through
Queryableoperators, EF Core query operators (AsNoTracking,Include,TagWith, …), and query syntax. - Report when the walk reaches a
DbSet<T>orDbContext.Set<T>().
EF Core 11 ships its own check for this (EF1004), so LC051 stays quiet in projects that reference EF Core 11 or later.
When it stays quiet (non-goals)
- In-memory sources: lists, arrays, and
AsQueryable()over them.AsAsyncEnumerable()throws on a queryable EF Core does not back, so the rule only reports what it can prove is an EF query. - Queries held in a variable, parameter, or returned by a helper. The rule cannot prove what backs them.
- Queries that already switched to LINQ to Objects with
AsEnumerable(). That is an explicit choice to run the rest in memory. ToAsyncEnumerable()overloads for tasks and observables.
Code Fix
The fix renames the call to AsAsyncEnumerable() and adds using Microsoft.EntityFrameworkCore; when the method is not already in scope. The static form (AsyncEnumerable.ToAsyncEnumerable(query)) is reported without a fix.
Test Cases
Violations
db.Orders.ToAsyncEnumerable();
db.Orders.AsNoTracking().Where(o => o.Total > 0).Select(o => o.Id).ToAsyncEnumerable();
(from o in db.Orders where o.Total > 0 select o.Id).ToAsyncEnumerable();
Valid
db.Orders.Where(o => o.Total > 0).AsAsyncEnumerable();
cachedOrders.AsQueryable().Where(o => o.Total > 0).ToAsyncEnumerable();
db.Orders.AsEnumerable().Where(o => o.Total > 0).ToAsyncEnumerable();