LINQ and async/await

Query collections and databases with one syntax, then do I/O without blocking the thread that is serving other requests.

LINQ

LINQ is a set of extension methods over IEnumerable<T>, and over IQueryable<T> when a provider such as EF Core can translate it. The same operators therefore work on an array, a list or a database table.

using System.Linq;

var people = new[]
{
    new { Name = "Ada",   Age = 36, City = "London" },
    new { Name = "Grace", Age = 45, City = "New York" },
    new { Name = "Alan",  Age = 41, City = "London" },
};

var byCity = people
    .Where(p => p.City == "London")
    .OrderByDescending(p => p.Age)
    .Select(p => p.Name)
    .ToList();

var grouped = people
    .GroupBy(p => p.City)
    .Select(g => new { City = g.Key, Count = g.Count(), Oldest = g.Max(p => p.Age) });

var average = people.Average(p => p.Age);

// query syntax expresses the same operators in a different shape
var query = from p in people
            where p.Age > 40
            orderby p.Name
            select p.Name;

// one pass, then repeated lookups that do not re-run the query
var byCityLookup = people.ToLookup(p => p.City);
OperatorDoesDeferred?
WhereFiltersYes
SelectProjects every elementYes
OrderBy / ThenBySorts, stablyYes
GroupByBuckets elements by keyYes
JoinMatches two sequences on a keyYes
Take / SkipPages through a sequenceYes
ToList / ToArray / CountForces the query to runNo — it executes now
Any / First / SingleTests or takes an elementNo

Deferred execution

Most operators build a description rather than a result. The query runs when it is enumerated, so it sees whatever the source contains at that moment, and it runs again on each enumeration.

var numbers = new List<int> { 1, 2, 3 };

var even = numbers.Where(n => n % 2 == 0);   // nothing has run yet
numbers.Add(4);
var list = even.ToList();                    // runs here: 2 and 4

var query = numbers.Where(n => n > 1);
Console.WriteLine(query.Count());            // executes
Console.WriteLine(query.Count());            // executes again

var snapshot = query.ToList();               // materialise when the source is volatile
⚠️
Do not enumerate the same IQueryable twice in one request: each enumeration is another database round trip, and calling a deferred query inside a loop is where N+1 queries come from. Materialise once, or shape the query so the database returns everything in a single result.

async and await

public async Task<Report> BuildAsync(int id, CancellationToken ct)
{
    var order = await _orders.GetAsync(id, ct);        // frees the thread while waiting
    var customer = await _customers.GetAsync(order.CustomerId, ct);
    return new Report(order, customer);
}

// start independent operations first, then wait for all of them
var tasks = ids.Select(id => _orders.GetAsync(id, ct));
var orders = await Task.WhenAll(tasks);

// an async stream for results that arrive over time
public async IAsyncEnumerable<Order> StreamAsync(
    [EnumeratorCancellation] CancellationToken ct)
{
    await foreach (var order in _source.ReadAllAsync(ct))
    {
        if (order.Total > 0) yield return order;
    }
}
  • async methods return Task, Task<T> or ValueTask<T>; async void is only for event handlers.
  • await releases the thread while I/O is pending. It does not create a thread, and it does not make CPU work faster.
  • Pass a CancellationToken through every layer; a timeout that cannot cancel the work underneath it is a resource leak.
  • Blocking on async code with .Result or .Wait() can deadlock. Make the whole call chain async instead.
  • Await inside a loop only when each step depends on the previous one; otherwise start the tasks and await them together.

FAQ

Should every method be async?
Only methods that do I/O or call other async work. Pure computation should stay synchronous: wrapping it in Task.Run moves the work to another thread without making it cheaper, and it adds scheduling overhead.
IEnumerable or IQueryable?
Declare IEnumerable<T> for in-memory collections and in method signatures. Keep IQueryable<T> at the data layer so a provider can translate the expression tree into SQL, and be aware that only operators the provider understands can be translated.

Classes and interfaces Syntax and types

Last refreshed 2026-09-18.