October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk5 min

Async LINQ in EF Core: Why ToListAsync Exists (And Why WhereAsync Doesn’t)

In EF Core, Where only builds a query expression, so it has no async version. ToListAsync is the point where the query runs and the rows are awaited and buffered into a list.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Where never talks to the database, so there is nothing for it to await. It only adds a predicate to the query expression that an IQueryable<T> carries. ToListAsync exists because it is the step that executes the composed query, asynchronously pulls the rows back, and returns them as a List<T>. The working rule in EF Core is simple: compose the query first, and execute it at a terminal operation.

Why there is no WhereAsync

An asynchronous method earns its place where a program waits on I/O. Where does not wait on anything. When you call it on an EF Core IQueryable<T>, the operator records a predicate inside an expression tree and returns a new queryable. That tree can keep growing with OrderBy, Select, and other operators, and EF Core’s provider translates the finished tree into the database’s query language.

As an Amazon Associate I earn from qualifying purchases.

Microsoft Learn’s EF Core asynchronous programming guidance states the design directly:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“Note that there are no async versions of some LINQ operators such as Where or OrderBy, because these only build up the LINQ expression tree and don’t cause the query to be executed in the database.”

This is a documentation statement from Microsoft Learn, not a quotation from a named engineer. The same logic explains why EF Core’s async surface is built around operations such as ToListAsync, FirstOrDefaultAsync, CountAsync, and SaveChangesAsync: each of those is the point where the database is actually contacted.

Do not carry this reasoning over to in-memory LINQ. Over an IEnumerable<T>, Where runs its predicate as the sequence is enumerated, and there is no database round trip to await. The question here concerns EF Core’s provider-backed query path, where the same operator is only a description of work.

What ToListAsync does

ToListAsync is a terminal operation. It is an extension method on IQueryable<TSource>, defined in the Microsoft.EntityFrameworkCore namespace, and its shape is Task<List<TSource>>. It accepts an optional CancellationToken. The EF Core 10.0 API reference describes it as asynchronously creating a List<T> from an IQueryable<T> by enumerating it asynchronously.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Building a list in memory is cheap. What costs time is fetching the rows, and that fetch is I/O. Calling ToListAsync lets the calling code await the database instead of blocking a thread while the results arrive. The synchronous counterpart, ToList, performs the same enumeration on the calling thread.

When the query actually runs

Execution happens when results are consumed, not when a LINQ operator is called. In practice, consumption means enumeration or one of the terminal operators such as ToList, Single, or Count, or their async counterparts. The sequence looks like this:

  1. Start from the DbSet, for example context.Blogs. No query has been sent yet.
  2. Add translatable filters and projections with Where, OrderBy, and Select. Each call returns a new IQueryable<T> and still sends nothing.
  3. Call a terminal operation such as ToListAsync. EF Core translates the expression tree, sends the command, and awaits the result.
  4. Use the returned List<T>. The rows are now in application memory, and further LINQ over the list is ordinary in-memory LINQ.

A representative query looks like this:

var blogs = await context.Blogs
    .Where(b => b.Rating > 3)
    .ToListAsync();

Because the predicate is part of the query, the database filters the rows. Only the matching blogs cross the network.

ToListAsync or AsAsyncEnumerable

Both operations execute the query asynchronously. They differ in how much data sits in memory at once, and that difference should drive the choice.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Aspect ToListAsync AsAsyncEnumerable with await foreach
Return type Task<List<T>> IAsyncEnumerable<T>
Result handling Buffers the full result set in memory Streams rows one at a time
Memory growth Grows with the total number of results Does not need to hold the whole result set
Best fit Small results, results that must be a list, or results enumerated more than once Large result sets processed row by row
Client-side LINQ Ordinary LINQ over the materialized list Operators over IAsyncEnumerable<T>, which depend on the target framework (see below)

Microsoft’s efficient-querying guidance points out that buffering is not free. For a large result, prefer streaming, and also consider limiting the number of rows or paging through them. Avoid calling ToListAsync only to run a single extra operation that could have been composed into the query or applied to a stream.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Filtering on the server before the execution boundary

Put every translatable predicate and projection before AsAsyncEnumerable. Once rows have left the database, a filter written in C# runs against data that has already been transferred. For logic that EF Core cannot translate, the usual pattern is to compose the translatable part on the query, cross the boundary with AsAsyncEnumerable, and then apply the local .NET logic to the streamed rows:

await foreach (var blog in context.Blogs
    .Where(b => b.Rating > 3)
    .AsAsyncEnumerable())
{
    if (IsEligible(blog)) // local .NET method, not translated to SQL
    {
        Process(blog);
    }
}

EF Core’s client-evaluation guidance says that untranslatable expressions in the top-level projection can be evaluated on the client, and that other client-side work should be made explicit with AsAsyncEnumerable or ToListAsync. Making the boundary explicit keeps the database-bound and client-bound parts easy to read.

Operators over IAsyncEnumerable<T> are a separate question. Microsoft Learn notes that these LINQ operators are introduced in .NET 10. On earlier target frameworks, the guidance points to the System.Linq.Async package. Do not assume that every ordinary Where is available on an IAsyncEnumerable<T> in every framework; check the target before writing the pipeline.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Caveats that break async EF Core code

  • Overlapping operations on one context. EF Core does not support multiple parallel operations on a single DbContext. Await each async query before starting the next operation on the same context.
  • Cancellation depends on the provider. The token passed to ToListAsync is observed while the task is awaited, and EF Core forwards it to the underlying provider. The provider may or may not honor it, so do not treat cancellation as an immediate database stop.
  • Provider-specific async issues. EF Core’s async documentation notes known issues in the async implementation of Microsoft.Data.SqlClient. If you see unexpected performance problems, especially with large text or binary values, the documentation advises investigating synchronous command execution as a comparison. This is a provider-level caveat, not a reason to avoid async EF Core.
  • Accidental early execution. Calling ToListAsync before a filter that could have been part of the query sends more data than necessary. Check where the list is created in the pipeline.

Practical reading of the pattern

Read a query chain from the bottom up. The terminal call is the only point where a database command is sent. Everything above it is a description of that command, which is why Where has no async version and ToListAsync does.

“

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.