csharp-async
Skill2 starsChanged 42 days agoMITView on GitHub
Use when writing or reviewing C#/.NET async/await code — correctness (deadlocks, sync context, TaskScheduler, async void) and performance (allocations, ValueTask, ConfigureAwait, elision). Blocking on async is banned in product code but allowed in test code. Avoid WhenAll/WhenAny on a single task.
4 other projects use a version of this.
Adopted by
Most recently taken up by
- github/awesome-copilottheir own version, first seen Oct 25, 2026
- itsfatduck/optimizerDucktheir own version, first seen Oct 8, 2026
- RorroRojas3/maf-agentstheir own version, first seen Sep 10, 2026
- Adhamxon/claude-code-skillstheir own version, first seen Jul 30, 2026
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 4
- github/awesome-copilot★ 41ksame name
- itsfatduck/optimizerDuck★ 11ksame name
- Adhamxon/claude-code-skills★ 2same name
- RorroRojas3/maf-agents★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository56 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- benchmarkingSkill
- commit-messagesSkill
- csharp-15-syntaxSkill
- csharp-aot-trimmingSkill
- csharp-api-designSkill
- csharp-assembly-inspectionSkill
- csharp-collections-modernSkill
- csharp-concurrency-primitivesSkill
- csharp-data-layoutSkill
- csharp-debuggingSkill
- csharp-discardsSkill
- csharp-disposable-patternsSkill
- csharp-docsSkill
- csharp-error-handlingSkill
- csharp-extension-membersSkill
- csharp-generics-modernSkill
- csharp-guard-clausesSkill
- csharp-json-source-generationSkill
- csharp-language-versionsSkill
- csharp-logging-diagnosticsSkill
- csharp-lspSkill
- csharp-modern-typesSkill
- csharp-nullabilitySkill
- csharp-nullable-migrationSkill
- csharp-nunitSkill
- csharp-pattern-matchingSkill
- csharp-performanceSkill
- csharp-polyfillsSkill
- csharp-source-generatorsSkill
- csharp-static-lambdasSkill
- csharp-string-handlingSkill
- csharp-tunitSkill
- csharp-verificationSkill
- dispatching-subagentsSkill
- dotnet-11-librariesSkill
- dotnet-analyzersSkill
- dotnet-central-package-managementSkill
- dotnet-file-based-appsSkill
- dotnet-msbuild-usingsSkill
- dotnet-multi-targetingSkill
- dotnet-project-hygieneSkill
- dotnet-runSkill
- dotnet-runtime-asyncSkill
- dotnet-slnxSkill
- dotnet-test-platformsSkill
- executing-plansSkill
- performance-bounded-deduplicationSkill
- roslyn-analyzer-performanceSkill
- roslyn-discoverySkill
- roslyn-duplicate-detectionSkill
- roslyn-guard-rewritersSkill
- roslyn-rewritersSkill
- subagent-driven-developmentSkill
- tddSkill
The file
We split this skill into the rules it gives an agent
3 rules. 0 are common, 1 is distinctive, and 2 are very specific to this project.
0
Common
Most of the 4 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
In UI / app code that touches UI afterward: leave the default (capture) so you resume on the right thread.
- Very specific: line 31
Rule: use Task.Run for offloading — it targets TaskScheduler.Default, unwraps async lambdas, and denies child attach.
- Very specific: line 67
TaskCompletionSource: create with TaskCreationOptions.RunContinuationsAsynchronously — otherwise the thread that calls SetResult runs the …
“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Async/await — correct and fast
Async is about freeing threads, not magic speed. Most async bugs are about which context/scheduler the continuation resumes on; most async waste is needless allocations and state machines. Both matter.
The blocking rule
Never block on async in product code — no .Result, .Wait(), .GetAwaiter().GetResult(). It ties up a thread and, on any context that marshals continuations back (classic UI SynchronizationContext, older ASP.NET), it deadlocks: the blocked thread is the exact thread the continuation needs to resume on. Async all the way down.
Exception — test code only. In tests, blocking is acceptable when you have no async entry point and need a synchronous assertion path (e.g. a [Test] that can't be async, or asserting inside a non-async callback). task.GetAwaiter().GetResult() is preferred over .Result/.Wait() there because it unwraps the real exception instead of wrapping it in AggregateException. Keep it in test code; the moment it appears in src/, it's a bug. (TUnit tests should be async Task anyway — see csharp-tunit.)
SynchronizationContext & ConfigureAwait
By default await captures the current SynchronizationContext/TaskScheduler and resumes the continuation on it. That's what you want in UI code (resume on the UI thread); it's overhead you don't want in libraries.
- In library / non-UI code:
await foo.ConfigureAwait(false)on every await. It skips the context capture — faster, and immune to sync-over-async deadlocks if a caller misbehaves. - In UI / app code that touches UI afterward: leave the default (capture) so you resume on the right thread.
- Modern ASP.NET Core has no
SynchronizationContext, soConfigureAwait(false)is a no-op for correctness there — still worth it in shared libraries that might run under a context.
The TaskScheduler footgun (StartNew vs Task.Run)
Task.Factory.StartNew(action) schedules on TaskScheduler.Current, not .Default. Inside a continuation or a UI handler, .Current may be the UI scheduler or some nested scheduler — so your "background" work silently runs somewhere surprising (or serializes onto the UI). Two more StartNew traps:
- With an
asynclambda,StartNewreturnsTask<Task>— it completes when the outer delegate returns, not when your async work finishes. You must.Unwrap()(or just useTask.Run). StartNewdoes not default toDenyChildAttach, so child tasks can attach unexpectedly.
Rule: use Task.Run for offloading — it targets TaskScheduler.Default, unwraps async lambdas, and denies child attach. Only reach for StartNew when you deliberately pass an explicit TaskScheduler and TaskCreationOptions, and you know why.
async void
Banned except for top-level event handlers. async void can't be awaited, and an exception inside it is raised on the SynchronizationContext and usually crashes the process instead of being catchable by the caller. Use async Task. (Event handlers that must be async void should wrap their body in try/catch.)
WhenAll / WhenAny — only for multiple awaitables
Task.WhenAll(singleTask) and Task.WhenAny(singleTask) are pointless: they allocate an array/wrapper and an extra task to do what await singleTask already does. Just await the task directly. Reserve WhenAll/WhenAny for genuinely concurrent sets:
// ❌ noise + allocation, single target
await Task.WhenAll(DoThingAsync());
var done = await Task.WhenAny(FetchAsync());
// ✅ direct
await DoThingAsync();
// ✅ WhenAll for real concurrency — start all, then await
var a = FetchAAsync(); // start
var b = FetchBAsync(); // start
await Task.WhenAll(a, b);
var (ra, rb) = (await a, await b); // already complete — cheapNote the pattern: start the tasks (don't await each in turn — that serializes them), then WhenAll. WhenAny is for "first to finish wins" / timeout races — and remember the losers keep running, so observe or cancel them.
Performance
ValueTask/ValueTask<T>for hot paths that usually complete synchronously (cache hits, buffered reads) — avoids allocating aTaskper call. Rules: await it exactly once, never block on it, never await it twice, don't store it. If you need to await multiple times or fan out, call.AsTask()first. Don't reach forValueTaskby default —Taskis simpler and pooled-ish; useValueTaskwhere a profiler shows the allocation hurts.- Elide pointlessly-async methods. A method that only forwards can return the inner
Taskdirectly instead ofasync/await— no state machine, no extra allocation:
But don't elide when you have apublic Task<int> GetAsync() => _inner.GetAsync(); // no async/await neededusing/try/finally, aConfigureAwait, or post-processing — those require theawaitto work correctly. Correctness beats the micro-saving. - Fast-path completed work:
Task.CompletedTask,Task.FromResult(x),ValueTask.FromResultinstead ofTask.Run(() => x). TaskCompletionSource: create withTaskCreationOptions.RunContinuationsAsynchronously— otherwise the thread that callsSetResultruns the awaiter's continuations inline, which can deadlock or cause surprising reentrancy.Task.Runis for CPU-bound offload, not I/O. Wrapping an already-async I/O call inTask.Runjust burns a thread-pool thread to wait. Call the async API directly.- Cancellation is part of the contract. Take a
CancellationToken, thread it through every downstream async call, and honor it. Don't swallowOperationCanceledException. - Don't
async-ify CPU-bound libraries for no reason — a state machine around synchronous work is pure overhead. Offload at the call site withTask.Runif needed.
.NET 11: Runtime Async (net11.0+)
With runtime async, await compiles to a runtime-async method that the runtime suspends and resumes itself, instead of a classic compiler-generated state machine. The .NET 11 BCL is compiled this way. Your own code is not by default: a net11.0 project gets classic state machines unless it sets <Features>runtime-async=on</Features>. This can vary by SDK version — verify against yours before relying on either behaviour. Mono (Blazor WebAssembly) is not a supported runtime for it. See dotnet-runtime-async for enabling, detecting and library-author guidance. It doesn't change any of the rules above (still ConfigureAwait(false), still no blocking, still ValueTask on measured hot paths) — it changes the cost model underneath them:
- Stack traces through
awaitare cleaner — a runtime-async continuation resumes without the pile ofMoveNext/AsyncStateMachineframes, so exception stacks read like synchronous call stacks. Don't rewrite exception-handling code for this; it's a debugging quality-of-life win, not a correctness change. - Lower per-await overhead, and a continuation skips capturing
ExecutionContextwhen there's no ambient state (noAsyncLocal, no flowedSynchronizationContext) to flow — one of the costsConfigureAwait(false)and hand-rolled avoidance tricks existed to dodge. Keep writingConfigureAwait(false)in libraries anyway: it's still correct, still needed pre-net11.0 and on any TFM that doesn't runtime-async, and costs nothing when it's already a no-op. - Don't treat this as a reason to stop measuring. The state-machine-avoidance techniques here (eliding pointlessly-async wrappers,
ValueTaskon hot paths) are still valid; runtime async lowers the constant factor, it doesn't remove the case for them. Benchmark net11.0 separately from net10.0 before claiming a win — seebenchmarking.
Review checklist
- No
.Result/.Wait()/.GetAwaiter().GetResult()in product code (test code only, preferGetAwaiter().GetResult()) ConfigureAwait(false)on every await in library code- No
async voidexcept a guarded event handler Task.Run(notStartNew) for offloading; explicit scheduler only when deliberate- No
WhenAll/WhenAnywrapping a single task - Concurrent work is started then awaited together — not awaited one-by-one
ValueTaskawaited once and never blocked on;.AsTask()if reusedCancellationTokenaccepted and threaded throughTaskCompletionSourceusesRunContinuationsAsynchronously
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

