`await` does not block a thread. It registers the rest of the method as a continuation, returns control to the caller, and resumes when the awaited task completes. The thread is free to do other work in the meantime — that is the whole point of async I/O.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add snoodleboot-io/prompticorn --skill verbose --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Verbose?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/snoodleboot-io-verbose-e18d29a3)More formats (shields.io, HTML) on the badges page.
# C# Async And LINQ (Verbose)
## Core Patterns
### async/await And The Continuation Model
`await` does not block a thread. It registers the rest of the method as a
continuation, returns control to the caller, and resumes when the awaited task
completes. The thread is free to do other work in the meantime — that is the whole
point of async I/O.
```csharp
public async Task<Order> LoadAsync(int id)
{
var order = await _db.GetOrderAsync(id); // thread released while waiting
order.Lines = await _db.GetLinesAsync(id); // resumes, awaits again
return order;
}
```
Make async cascade up the call chain. A single blocking call in the middle
defeats the benefit for everything above it.
### Sync-Over-Async Deadlocks
Blocking on a task with `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` is
the most common async bug. On a captured synchronization context (classic ASP.NET,
WPF, WinForms), the continuation needs the very thread that is now blocked waiting
for it — a deadlock.
```csharp
// ❌ deadlocks under a sync context
public ActionResult Index()
{
var data = GetDataAsync().Result; // continuation can't get the UI/request thread
return View(data);
}
// ✅ await all the way up
public async Task<ActionResult> Index()
{
var data = await GetDataAsync();
return View(data);
}
```
### ConfigureAwait(false)
By default a continuation resumes on the captured context. Library code almost
never needs that context, and capturing it invites the deadlock above. Use
`ConfigureAwait(false)` in libraries so the continuation runs on a thread-pool
thread.
```csharp
public async Task<string> FetchAsync(Uri url)
{
using var resp = await _http.GetAsync(url).ConfigureAwait(false);
return await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
}
```
Application code on ASP.NET Core has no synchronization context, so it matters
less there; libraries should stay defensive and always add it.
### async void Is A Trap
`async void` cannot be awaited and its exceptions cannot be caught by the caller —
they tear down the process. Use it only for event handlers, which the framework
signature requires; everything else returns `Task`.
```csharp
// ✅ the only acceptable async void
private async void OnClick(object sender, EventArgs e) => await SaveAsync();
```
### Task vs ValueTask
`Task` is a reference type: awaiting one that is already complete still allocates.
`ValueTask` can wrap an already-available result with no allocation — valuable on
hot paths that usually complete synchronously (cache hits). The cost is stricter
rules: await it exactly once, never block on it, never store it in a field.
```csharp
public ValueTask<byte[]> ReadAsync(string key)
{
if (_cache.TryGetValue(key, out var cached))
return new ValueTask<byte[]>(cached); // synchronous, zero allocation
return new ValueTask<byte[]>(LoadFromDiskAsync(key));
}
```
Default to `Task`; reach for `ValueTask` only when profiling shows the allocation
matters.
### LINQ Deferred Execution
Most LINQ operators (`Where`, `Select`, `OrderBy`, `Take`) are lazy: they build a
pipeline and execute only when enumerated. Enumeration happens on `foreach`, or on
a materializing operator (`ToList`, `ToArray`, `Count`, `First`, `Any`, `Sum`).
```csharp
var query = orders.Where(o => o.Total > 100); // no work done here
foreach (var o in query) { /* ... */ } // executed now
var count = query.Count(); // executed AGAIN — second pass
```
### The Multiple-Enumeration Trap
Because the query is re-evaluated each time it is enumerated, holding an
`IEnumerable` and iterating it twice runs the pipeline twice — and against
`IQueryable`, that is two database round-trips.
```csharp
// ❌ two full evaluations
IEnumerable<Order> big = db.Orders.Where(o => o.Total > 100);
if (big.Any()) // round-trip 1
Process(big.First()); // round-trip 2
// ✅ materialize once, reuse
var list = db.Orders.Where(o => o.Total > 100).ToList();
if (list.Count > 0)
Process(list[0]);
```
### IEnumerable vs IQueryable
`IQueryable<T>` captures the query as an expression tree that the provider (EF
Core) translates to SQL, so filtering and paging run in the database. The moment
you switch to `IEnumerable<T>` — via `AsEnumerable()`, `ToList()`, or a method the
provider cannot translate — subsequent operators run in memory over rows already
fetched.
```csharp
// ✅ WHERE + ORDER BY + TOP run in SQL; one small result set returns
var top = db.Orders
.Where(o => o.Total > 100)
.OrderByDescending(o => o.Total)
.Take(10)
.ToList();
// ❌ pulls the entire table into memory, then filters client-side
var bad = db.Orders.AsEnumerable()
.Where(o => o.Total > 100)
.ToList();
```
## Common Anti-Patterns
❌ **Blocking on async**
```csharp
var user = _svc.GetUserAsync(id).Result; // deadlock risk, thread starvation
```
✅ **Await it**
```csharp
var user = await _svc.GetUserAsync(id);
```
❌ **async void beyond event handlers**
```csharp
public async void ProcessQueue() => await DrainAsync(); // unobservable exceptions
```
✅ **Return Task**
```csharp
public async Task ProcessQueueAsync() => await DrainAsync();
```
❌ **Enumerating a deferred query repeatedly**
```csharp
var q = db.Orders.Where(o => o.Open);
var n = q.Count(); var first = q.First(); // two queries
```
✅ **Materialize once**
```csharp
var open = db.Orders.Where(o => o.Open).ToList();
```
❌ **Filtering after leaving IQueryable**
```csharp
db.Orders.ToList().Where(o => o.Total > 100); // whole table loaded first
```
✅ **Filter in the query**
```csharp
db.Orders.Where(o => o.Total > 100).ToList();
```
## Async And LINQ Checklist
- [ ] async cascades up the chain; no `.Result` / `.Wait()` on async code
- [ ] `ConfigureAwait(false)` on awaits in library code
- [ ] `async void` only on event handlers
- [ ] `Task` the default; `ValueTask` only where profiling justifies it, awaited once
- [ ] Deferred queries materialized once before multiple use
- [ ] Filtering/paging kept in `IQueryable` so it runs in SQL
- [ ] `AsEnumerable()` / `ToList()` placed deliberately, not accidentally early
- [ ] `CancellationToken` accepted and passed through async APIs
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!