AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Async Concurrency Review

skill-tunahanaliozturk-secure-dotnet-skills-async-concurrency-review · by tunahanaliozturk

Use when reviewing asynchronous and concurrent .NET code — async/await correctness, deadlocks, cancellation propagation, and thread safety of shared state.

No reviews yet
0 installs
19 views
0.0% view→install

Install

$ agentstack add skill-tunahanaliozturk-secure-dotnet-skills-async-concurrency-review

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution Used

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-tunahanaliozturk-secure-dotnet-skills-async-concurrency-review)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Async Concurrency Review? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Async / Concurrency Review

Directs the agent to walk .NET / ASP.NET Core async and concurrent code through concrete correctness lenses — sync-over-async deadlock risk, cancellation propagation, shared-state safety, fire-and-forget hazards, and parallel-execution patterns — surfacing defects with precise fixes.

When to use

  • A PR adds or modifies async/await code, background work, or concurrent operations in a .NET service.
  • Code uses Task.WhenAll, Parallel.ForEachAsync, or any form of parallelism over shared resources.
  • A service is deadlocking, hanging under load, or losing exceptions silently.
  • A DbContext, static field, or other non-thread-safe resource is accessed from multiple threads or concurrent tasks.

Process

  1. Map the async call chains and shared mutable state. Trace each async method from its entry point (controller action, IHostedService, message handler) to its leaves. Note any non-async call sites that block on Task results, any shared objects accessed across concurrent paths, and any fire-and-forget launches.
  2. Check for sync-over-async and deadlock risk. Find every .Result, .Wait(), and GetAwaiter().GetResult() call. For each, determine whether a synchronization context is present (classic ASP.NET, WinForms, WPF, MAUI all have one; ASP.NET Core does not by default). Even where deadlock is not imminent, blocking wastes a thread-pool thread for the full I/O duration — identify and convert to await.
  3. Check cancellation propagation. Confirm that every method doing I/O (EF Core queries, HttpClient calls, file I/O, Task.Delay) accepts a CancellationToken parameter, passes it downstream, and — where the work is a loop — checks ct.ThrowIfCancellationRequested() (or equivalent) at each iteration boundary.
  4. Check thread safety of shared state. For each object that could be reached from two concurrent tasks or threads, verify it is thread-safe. Flag DbContext instances captured across Task.WhenAll branches (throws InvalidOperationException: "A second operation was started on this context before a previous operation completed"), static mutable fields, and unsynchronized shared collections. Where a lock is present, verify it does not span an await — that is a compile error in C# but the intention (mutual exclusion around async work) must be redirected to SemaphoreSlim.
  5. Check fire-and-forget patterns. Find _ = DoAsync(), unawaited method calls, and Task.Run(() => ...) whose result is discarded. Determine how exceptions are observed. A fire-and-forget task whose exception is never observed will be silently swallowed (.NET 4.5+); the work may also outlive the request or host shutdown. Additionally, fire-and-forget work that captures scoped services (e.g. a scoped DbContext) frequently FAILS with ObjectDisposedException when the request scope is disposed — so the failure mode is often a crash, not just an untracked success. Replace with a safe background pattern: channel + IHostedService consumer, or IBackgroundTaskQueue.
  6. Recommend fixes with correct .NET APIs. For each finding, name the concrete replacement: await over .Result; SemaphoreSlim.WaitAsync over lock+await; IDbContextFactory for per-operation scoped contexts; Channel or IHostedService for safe background work; Parallel.ForEachAsync with a ParallelOptions.MaxDegreeOfParallelism for throttled parallel I/O.

.NET / Azure checks

  • Sync-over-async: .Result / .Wait() / GetAwaiter().GetResult(). All three block the calling thread until the Task completes. In environments with a SynchronizationContext — classic ASP.NET, WinForms, WPF — this causes a deadlock: await captures the context and tries to resume on it, but .Result/.Wait() is holding the context's single permitted thread, so the continuation can never run. ASP.NET Core does not install a single-threaded SynchronizationContext, so the classic deadlock does not occur there; however, blocking still wastes a thread-pool thread for the full I/O duration, reducing throughput under load. The fix in all environments is to go async all the way to the entry point.
  • async void outside event handlers. async void methods are only legitimate for event handlers (Button.Click +=, ICommand.Execute implementations) where the delegate signature requires void. For all other methods, async void has two defects: (1) exceptions thrown after the first await are raised on the thread-pool synchronization context and are unobserved — in .NET 6+ they crash the process via UnhandledException; (2) callers cannot await the method, so they have no way to know when it completes or whether it succeeded. Replace with async Task.
  • ConfigureAwait(false) in library code. In a library (a NuGet package or shared class library consumed by multiple app types), await someTask without .ConfigureAwait(false) captures the caller's SynchronizationContext and resumes on it. In a classic ASP.NET or UI host, this can cause deadlock when combined with .Result upstream, and always incurs a context-switch overhead. Call .ConfigureAwait(false) on every await in library code that does not need to resume on the original context. In ASP.NET Core application code (controllers, Razor pages, minimal API handlers), .ConfigureAwait(false) is not required because ASP.NET Core does not install a blocking single-threaded context — omitting it is fine and reduces noise.
  • CancellationToken accepted and propagated through the chain. Every method that performs I/O — DbContext queries, HttpClient calls, Task.Delay, file reads — must accept a CancellationToken parameter and pass it to every downstream async call. A token that is accepted but not passed to ToListAsync(ct), SendAsync(request, ct), or Task.Delay(ms, ct) provides no cancellation benefit and misleads callers. For long-running loops, call ct.ThrowIfCancellationRequested() at the top of each iteration, or use ct.IsCancellationRequested with a graceful break, so that cancellation is honored promptly rather than only between I/O calls.
  • Fire-and-forget swallowing exceptions. A discarded Task (_ = DoAsync(), an un-awaited call, or Task.Run(...) whose result is not stored and awaited) means any exception thrown after the first await is silently lost — it is placed on the task and never observed. The work also continues past request completion, past IApplicationLifetime.ApplicationStopping, and past host shutdown. The safe pattern is a Channel (unbounded or bounded) written to by the request handler and drained by a BackgroundService (IHostedService) consumer that observes exceptions and respects CancellationToken on shutdown.
  • Shared mutable state without synchronization. Objects accessed from multiple concurrent tasks without synchronization produce data races. Dictionary is not thread-safe — concurrent reads during a write can corrupt its internal state; use ConcurrentDictionary. static mutable fields (counters, caches, configuration that mutates) must be protected with Interlocked, lock, or a thread-safe type. Non-thread-safe state machines or domain objects must be confined to a single task at a time.
  • DbContext not thread-safe across Task.WhenAll. DbContext is explicitly documented as not thread-safe; concurrent operations on the same instance throw InvalidOperationException: "A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext." A common mistake is capturing a single injected DbContext in a closure and then fanning it out across Task.WhenAll. The correct pattern is IDbContextFactory (registered via AddDbContextFactory): call await factory.CreateDbContextAsync(ct) inside each parallel branch, await its work, and dispose it — each branch owns a fully independent context and connection.
  • lock cannot wrap await. C# prohibits await inside a lock block at the compiler level (CS1996). The intent — mutual exclusion around an async critical section — must be fulfilled by SemaphoreSlim instead: await semaphore.WaitAsync(ct) before the critical section and semaphore.Release() in a finally block after it. SemaphoreSlim initialized to (1, 1) provides the same mutual-exclusion semantics as lock for async code.
  • Task.WhenAll / Parallel.ForEachAsync with throttling. Unbounded parallelism — Task.WhenAll over a large sequence without a concurrency cap — can exhaust the thread pool, open too many database connections, or overwhelm a downstream service. Use Parallel.ForEachAsync (introduced in .NET 6) with ParallelOptions { MaxDegreeOfParallelism = N, CancellationToken = ct } to process a sequence with bounded concurrency. For a batch of known tasks, use SemaphoreSlim as a gate: acquire before launching each task, release inside the task body. Always pair parallelism with a CancellationToken so the fan-out can be aborted on shutdown or timeout.

Red flags

| Signal | Why it matters | |--------|----------------| | .Result, .Wait(), or GetAwaiter().GetResult() in a request handler or service method | Blocks a thread-pool thread for the full I/O duration; deadlocks in any host with a single-threaded SynchronizationContext (classic ASP.NET, WinForms, WPF). Go async all the way. | | async void on a method that is not an event handler | Exceptions after the first await are unobserved and crash the process (UnhandledException) or silently disappear. Callers cannot await it. Return async Task. | | lock block containing an await expression | Does not compile (CS1996); the intent (async mutual exclusion) requires SemaphoreSlim.WaitAsync + Release in finally instead. | | A single DbContext instance captured across Task.WhenAll branches | Concurrent operations on DbContext throw InvalidOperationException: "A second operation was started…". Use IDbContextFactory to create one context per parallel branch. | | _ = DoAsync() or an un-awaited Task-returning call | Exceptions are silently swallowed; the work outlives the request and ignores host-shutdown signals. Replace with a Channel + IHostedService consumer. | | A method performing I/O with no CancellationToken parameter | Cancellation signals from the HTTP request or host shutdown are not honored — the operation runs to completion even after the caller has given up, wasting resources. Accept and propagate CancellationToken. | | static mutable field written from multiple tasks or threads | Data races on non-atomic types corrupt state silently. Protect with Interlocked, lock, or replace with ConcurrentDictionary / IMemoryCache. | | Task.WhenAll over an unbounded sequence without a concurrency cap | Can open hundreds of database connections or HTTP connections simultaneously, overwhelming the downstream resource. Use Parallel.ForEachAsync with MaxDegreeOfParallelism or a SemaphoreSlim gate. | | CancellationToken accepted by a method but not forwarded to ToListAsync, SendAsync, or Task.Delay | The token is accepted but ignored — cancellation has no effect on the I/O. Pass the token to every async call in the chain. | | ConfigureAwait(false) absent in a shared library that also has .Result callers upstream | The missing .ConfigureAwait(false) captures the caller's SynchronizationContext; combined with a .Result upstream, this creates a deadlock in classic ASP.NET or UI hosts. |

Example

See [examples/async-concurrency-review/](../../examples/async-concurrency-review/).

Related skills

  • [dotnet-performance-review](../dotnet-performance-review/SKILL.md) — use for broader performance review covering allocations, LINQ, caching, and serialization.
  • [resilience-review](../resilience-review/SKILL.md) — use to review timeout and cancellation handling on downstream calls.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.