LINQ
async programming
C#
asynchronous
.NET

Is there a way to combine LINQ and async

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Yes, but the combination is slightly different from plain synchronous LINQ. Standard LINQ operators shape sequences immediately, while asynchronous work produces Task values or async streams, so the usual pattern is to let LINQ build the work and then await at the point where results are needed.

Using LINQ to Create a Set of Tasks

The most common case is an in-memory collection where each element requires asynchronous work, such as a web request, a cache lookup, or a file read. In that situation, Select does not magically become asynchronous. Instead, it returns a sequence of Task values, and you await them together with Task.WhenAll.

csharp
1using System;
2using System.Linq;
3using System.Threading.Tasks;
4
5static async Task<int> GetScoreAsync(int userId)
6{
7    await Task.Delay(50);
8    return userId * 10;
9}
10
11int[] userIds = { 1, 2, 3, 4, 5 };
12
13Task<int>[] scoreTasks = userIds
14    .Where(id => id % 2 == 1)
15    .Select(GetScoreAsync)
16    .ToArray();
17
18int[] scores = await Task.WhenAll(scoreTasks);
19
20Console.WriteLine(string.Join(", ", scores));

This pattern matters because the output of Select(GetScoreAsync) is IEnumerable<Task<int>>, not IEnumerable<int>. Until Task.WhenAll runs, you only have a description of the outstanding work.

There are two useful consequences of this approach. First, it keeps the projection readable. Second, it allows the asynchronous operations to run concurrently when that is safe for the data source. If each call is independent, this is often the cleanest way to combine LINQ with asynchronous code.

Keeping the Async Boundary in the Right Place

When the data source is remote, LINQ usually builds an expression tree that the provider translates into a database query or service call. The query operators such as Where, OrderBy, and Select are still written synchronously, but execution should happen with an async method supplied by the provider.

In Entity Framework Core, a typical query looks like this:

csharp
1using Microsoft.EntityFrameworkCore;
2
3var activeUsers = await db.Users
4    .Where(user => user.IsActive)
5    .OrderBy(user => user.LastName)
6    .Select(user => new
7    {
8        user.Id,
9        user.Email
10    })
11    .ToListAsync();

The important detail is that only the terminal operation is asynchronous. The earlier LINQ operators are just composing the query. Calling ToListAsync, FirstOrDefaultAsync, SingleAsync, or similar methods is what actually sends work to the provider without blocking the current thread.

This separation is a good mental model:

  • Use LINQ operators to describe filtering, projection, grouping, and ordering.
  • Use async terminal methods to execute against an async-aware provider.
  • Use Task.WhenAll when LINQ created many independent tasks.

Async Streams Need Async-Aware Operators

A third scenario is IAsyncEnumerable<T>, where values arrive over time instead of all at once. Plain LINQ operators work on IEnumerable<T>, so async streams need either await foreach or an async LINQ library such as System.Linq.Async.

Here is a simple example using await foreach, which works out of the box in modern .NET:

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5static async IAsyncEnumerable<int> ReadNumbersAsync()
6{
7    for (int i = 1; i <= 5; i++)
8    {
9        await Task.Delay(25);
10        yield return i;
11    }
12}
13
14await foreach (int value in ReadNumbersAsync())
15{
16    if (value % 2 == 0)
17    {
18        Console.WriteLine(value * 100);
19    }
20}

This is not traditional LINQ syntax, but it solves the same kind of transformation problem for an asynchronous data source. If you want chainable async operators, add a library designed for async streams instead of forcing synchronous LINQ onto the wrong abstraction.

Common Pitfalls

  • Forgetting that Select(async item => ...) returns tasks. If you skip Task.WhenAll, you will often print task objects or leave work unfinished.
  • Calling .Result or .Wait() on async work. That blocks threads and can introduce deadlocks in UI or ASP.NET applications.
  • Materializing too early. If you call ToList() before an async provider executes, you move work into memory and lose database-side optimization.
  • Assuming concurrency is always desirable. Launching hundreds of tasks at once can overload a downstream API, so add throttling when the external system has limits.
  • Mixing IEnumerable<T> and IAsyncEnumerable<T> without noticing the difference. They look similar, but their execution model is not the same.

Summary

  • LINQ itself is synchronous, but it works well with async when you await at the execution boundary.
  • For in-memory collections, project to tasks and await them with Task.WhenAll.
  • For providers such as EF Core, keep query composition in LINQ and use async terminal methods such as ToListAsync.
  • For async streams, use await foreach or an async LINQ library built for IAsyncEnumerable<T>.
  • Avoid blocking calls and early materialization, because both undermine the benefit of async code.

Course illustration
Course illustration

All Rights Reserved.