CodeOath
← All posts
.NET Core / Web API70 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Contents — Part 17 of 19: Async/Await Throughout the Pipeline
Part 17 of 19 · ~1 min

Async/Await Throughout the Pipeline

Every layer of this reference — actions, EF Core calls, middleware — has used async/await, and it's worth being explicit about why that's not just a style convention in a web API specifically. Kestrel handles concurrent requests using a limited thread pool; a synchronous, blocking call (a blocking database query, a blocking HTTP call to another service) ties up one of those threads for the entire duration of the wait, doing nothing useful. An awaited asynchronous call releases the thread back to the pool while the I/O is in flight, letting it serve other requests in the meantime, and resumes on a (possibly different) pool thread once the awaited operation completes.

// Blocks a thread pool thread for the whole database round trip — scales poorly under load
public Order GetByIdSync(int id) => _db.Orders.First(o => o.Id == id);

// Frees the thread during the I/O wait — the same thread pool serves far more concurrent
// requests under load, because threads aren't sitting idle waiting on the database
public async Task<Order> GetByIdAsync(int id) => await _db.Orders.FirstAsync(o => o.Id == id);

A specific, common mistake: mixing sync and async incorrectly by calling .Result or .Wait() on a Task instead of awaiting it. This blocks the calling thread and, in certain contexts (classic ASP.NET's SynchronizationContext, less common but not impossible in some hosting scenarios), can deadlock entirely, because the blocked thread is the same thread the awaited task needs to resume on. The fix is simply: await all the way up the call stack, rather than "synchronously waiting" for an async operation partway through.