Async/Await Done Right
Web servers live or die by threads. Every request that sits waiting on SQL Server or an HTTP call
while holding a thread is a thread that can't serve anyone else. async/await releases
the thread during the wait β that's the whole point. It is about scalability, not speed: an
awaited query takes just as long, but the server survives load spikes.
Task, async, and "async all the way"
An async method returns Task (no result) or Task<T> and uses await at each
point where it would otherwise block. In MVC this composes cleanly top to bottom:
public async Task<IActionResult> Details(int id)
{
var order = await _orderService.GetOrderAsync(id);
return View(order);
}
The cardinal rule is async all the way down: if the data layer is async, the service and the
controller above it must be too. The moment someone bridges the gap with .Result or .Wait(),
you get sync-over-async β a thread parked doing nothing, thread-pool starvation under load,
and in legacy code, deadlocks. Never write async void except for event handlers; exceptions
from async void can't be caught by the caller and can take down the process.
Deadlocks and the ConfigureAwait story
You'll hear war stories about "the classic ASP.NET deadlock." Know the punchline: ASP.NET Core
has no SynchronizationContext. After an await, execution resumes on any thread-pool thread β
there is no per-request context for a blocked .Result to fight over, so that deadlock simply
doesn't exist here, and ConfigureAwait(false) in application code buys you nothing. Don't
sprinkle it through controllers and services. It's still a reasonable convention inside reusable
libraries (which may be consumed by UI frameworks that do have a context). Legacy note: the
deadlock is real if you ever touch an old System.Web app β there, never block on tasks, ever.
Either way, .Result in a controller remains a code-review flag: even without the deadlock,
it's sync-over-async and wastes the thread you were trying to free.
CancellationToken: stop work nobody wants
When a user closes the tab mid-request, why should your 20-second report query keep running?
Accept a CancellationToken in your async methods and pass it through to EF
(ToListAsync(ct)) and HttpClient. Add a CancellationToken parameter to the action and
model binding wires it to HttpContext.RequestAborted automatically β abandoned requests then
cancel the whole chain. The same token plumbing also serves timeouts you impose yourself via
CancellationTokenSource. Design your service signatures with the token from day one; adding it
later touches every layer.