Guides expert-level C# async programming: async/await pitfalls, ValueTask vs Task decision tree, CancellationToken patterns, IAsyncEnumerable, and ConfigureAwait considerations. Use when the user asks about C# async, await, ValueTask, Task, CancellationToken, IAsyncEnumerable, ConfigureAwait, async patterns. Do NOT use when the user asks about C# modern idioms (use `csharp-modern-idioms`), C# performance (use `csharp-performance`), C# ASP.NET (use `csharp-aspnet-patterns`).
Scanned 9/2/2026
Install to Claude Code
npx -y skills add FerroxLabs/wayland --skill csharp-async-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Csharp Async Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ferroxlabs-csharp-async-patterns)More formats (shields.io, HTML) on the badges page.
---
name: csharp-async-patterns
description: |
Guides expert-level C# async programming: async/await pitfalls, ValueTask vs Task decision tree, CancellationToken patterns, IAsyncEnumerable, and ConfigureAwait considerations.
Use when the user asks about C# async, await, ValueTask, Task, CancellationToken, IAsyncEnumerable, ConfigureAwait, async patterns.
Do NOT use when the user asks about C# modern idioms (use `csharp-modern-idioms`), C# performance (use `csharp-performance`), C# ASP.NET (use `csharp-aspnet-patterns`).
license: Apache-2.0
metadata:
author: foundry-skills
version: "1.0.0"
tags: "csharp backend optimization"
category: "software-engineering"
subcategory: "languages-runtimes"
depends: ""
disclaimer: "none"
difficulty: "intermediate"
---
# C# Async Patterns
## When to Use
**Use this skill when the user asks about:**
- `async`/`await` usage, pitfalls, or anti-patterns in C# (deadlocks, `.Result`/`.Wait()` blocking, fire-and-forget)
- `Task<T>` vs `ValueTask<T>` selection -- when to use each, allocation costs, misuse risks
- `CancellationToken` propagation, linked token sources, timeout patterns, cooperative cancellation design
- `IAsyncEnumerable<T>` and `await foreach` -- streaming data, backpressure, producer design
- `ConfigureAwait(false)` -- when it matters, when it is irrelevant, and why library authors must use it
- `SynchronizationContext` capture behavior and how it interacts with UI frameworks or ASP.NET Classic
- `TaskCompletionSource<T>` for bridging callback-based or event-based APIs to async
- Thread pool starvation in heavily async systems, async-over-sync anti-pattern
- `async void` contracts -- event handlers, fire-and-forget, exception surface
- Parallel async work patterns -- `Task.WhenAll`, `Task.WhenAny`, bounded parallelism with `SemaphoreSlim`
**Do NOT use this skill when:**
- The user asks about modern C# language idioms unrelated to async (records, pattern matching, file-scoped namespaces) -- use `csharp-modern-idioms`
- The user asks about CPU-bound parallel processing with `Parallel.For`, PLINQ, or `System.Threading.Channels` architecture -- use `csharp-performance`
- The user asks about ASP.NET Core middleware, Minimal API async endpoints, or Blazor async lifecycles in an architectural sense -- use `csharp-aspnet-patterns`
- The user is asking a pure .NET threading question about `Monitor`, `Mutex`, `ReaderWriterLockSlim`, or low-level `Interlocked` operations -- this is threading, not async
- The user is asking about Rx.NET (Reactive Extensions) observables and subscriptions -- that is a separate reactive programming paradigm
---
## Process
### Step 1 -- Classify the Async Problem
Identify which category the user's problem falls into before suggesting any code. Async problems in C# break into four families:
- **I/O-bound async** -- database queries, HTTP calls, file reads: use `async`/`await` with `Task<T>`. The thread is returned to the pool during the wait.
- **Streaming async data** -- reading large result sets, event streams, paginated APIs: use `IAsyncEnumerable<T>` and `await foreach`.
- **Parallelism over async work** -- fan-out HTTP calls, parallel database batches: use `Task.WhenAll` with bounded concurrency via `SemaphoreSlim` rather than raw parallelism.
- **Bridging legacy or callback APIs** -- `BeginXxx`/`EndXxx` APM, event-based EAP, P/Invoke callbacks: use `TaskCompletionSource<T>`.
Ask these diagnostic questions:
- Is this library code or application code? Library code must use `ConfigureAwait(false)` on every await.
- Does the operation complete synchronously in the common/hot path? If yes, consider `ValueTask<T>`.
- Is cancellation required? Yes unless this is a truly fire-and-forget operation (rare).
- Does the caller need results? If no, the return type is `Task` (not `void`), except for event handlers.
---
### Step 2 -- Apply the `Task<T>` vs `ValueTask<T>` Decision Tree
This is the single most misunderstood decision in C# async. Use the following criteria:
**Use `Task<T>` (the default) when:**
- The operation almost always genuinely awaits (completes asynchronously most of the time)
- The result will be awaited multiple times (multiple `await` on same task, `.Result` access after completion)
- The task will be stored in a field and awaited later
- You are writing a public API and cannot predict caller behavior
- The method has multiple `await` points (the allocation cost is amortized over the work)
**Use `ValueTask<T>` when ALL of these are true:**
- The operation completes synchronously in the hot/common path (e.g., a cache hit before an async cache miss)
- The result is awaited exactly once and immediately
- You have profiling evidence that `Task<T>` allocation is a measurable bottleneck (thousands of calls per second)
- The ValueTask is NOT stored, re-awaited, or converted to a Task without calling `.AsTask()`
**Hard rules about `ValueTask<T>` misuse:**
- Never `await` a `ValueTask<T>` more than once -- it is undefined behavior after the first await
- Never call `.Result` on a `ValueTask<T>` that is not yet complete
- Never store `ValueTask<T>` in a variable and use it after the producing method has been called again (especially from pooled `IValueTaskSource<T>`)
- The overhead reduction only matters above ~1M calls/second on hot paths -- measure before optimizing
---
### Step 3 -- Design CancellationToken Propagation
CancellationToken is not optional in production code. Every method that performs I/O or long-running work must accept a `CancellationToken`. Apply these rules:
**Parameter position:** Always the last parameter with default `default` for public APIs:
```csharp
public async Task<Order> GetOrderAsync(int orderId, CancellationToken cancellationToken = default)
```
**Propagation rules:**
- Pass the token to EVERY awaited async call downstream. Missing even one link breaks the cancellation chain.
- Pass the token to `HttpClient.SendAsync`, `DbCommand.ExecuteReaderAsync`, `Stream.ReadAsync`, `Task.Delay`, `SemaphoreSlim.WaitAsync` -- all accept it.
- If you use `Task.Run` for CPU-bound work, pass the token to `Task.Run` AND check it inside the lambda.
**Creating linked sources:**
- Use `CancellationTokenSource.CreateLinkedTokenSource(externalToken, internalToken)` when you need to impose a local timeout on top of an external cancellation:
```csharp
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(30));
await DoWorkAsync(cts.Token);
```
- Always `Dispose` the `CancellationTokenSource` -- it holds unmanaged resources. Use `using` or `await using`.
**Handling cancellation in loops:**
```csharp
while (!cancellationToken.IsCancellationRequested)
{
// work...
}
// OR prefer:
cancellationToken.ThrowIfCancellationRequested();
```
- Prefer `ThrowIfCancellationRequested()` for methods that should surface `OperationCanceledException`; use `IsCancellationRequested` only when you need to do cleanup before returning.
---
### Step 4 -- Apply ConfigureAwait Correctly
`ConfigureAwait(false)` tells the runtime not to capture the current `SynchronizationContext` or `TaskScheduler` when resuming after an await. This is one of the most commonly misapplied patterns.
**When `ConfigureAwait(false)` IS required:**
- In any library/NuGet package code -- you do not know what context the caller runs in, and failing to use it can deadlock in WinForms, WPF, or classic ASP.NET callers that block on `.Result` or `.Wait()`
- In any code that will run in a UI application's library layer but does not need to return to the UI thread after the await
**When `ConfigureAwait(false)` is NOT needed:**
- In ASP.NET Core application code -- ASP.NET Core has no `SynchronizationContext`, so `ConfigureAwait(false)` has no behavioral effect (though it is not harmful)
- In code that explicitly needs to resume on the UI thread (e.g., updating a WPF control after an awaited database call)
- In the outermost application entry points (top-level `await` in `Main`)
**The classic deadlock pattern to diagnose and explain:**
```csharp
// DEADLOCK in WinForms/WPF/classic ASP.NET:
public string GetData()
{
return GetDataAsync().Result; // blocks the UI/request thread
}
private async Task<string> GetDataAsync()
{
await Task.Delay(100); // tries to resume on captured SynchronizationContext
return "data"; // but that context is blocked by .Result above
}
// FIX: ConfigureAwait(false) in the library method:
private async Task<string> GetDataAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return "data"; // resumes on thread pool, no deadlock
}
```
---
### Step 5 -- Design IAsyncEnumerable Streaming
Use `IAsyncEnumerable<T>` when data is produced incrementally -- do not buffer everything into a `List<T>` before returning it.
**When to use `IAsyncEnumerable<T>`:**
- Database result sets with thousands of rows (use with EF Core's `ToAsyncEnumerable()` or Dapper with `QueryUnbufferedAsync`)
- Paginated API responses where each page requires a separate HTTP call
- Real-time event streams, log tailing, SSE (Server-Sent Events) bridging
- Any producer where results become available before all work is complete
**Implementation rules:**
```csharp
public async IAsyncEnumerable<Product> GetProductsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var batch in FetchBatchesAsync(cancellationToken))
{
foreach (var item in batch)
{
cancellationToken.ThrowIfCancellationRequested();
yield return item;
}
}
}
```
- Always use `[EnumeratorCancellation]` attribute on the `CancellationToken` parameter of an async iterator -- this allows `WithCancellation()` calls from the consumer to propagate correctly
- The `[EnumeratorCancellation]` attribute is on the parameter, not the method
- The compiler generates a state machine; `yield return` inside `async` methods with `IAsyncEnumerable<T>` return type is valid C# 8+
**Consumer pattern:**
```csharp
await foreach (var product in repo.GetProductsAsync()
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
await ProcessProductAsync(product, cancellationToken).ConfigureAwait(false);
}
```
---
### Step 6 -- Handle Parallel Async with Bounded Concurrency
`Task.WhenAll` with unbounded parallelism is a common mistake. When fanning out 10,000 tasks simultaneously, you exhaust connection pools, hit rate limits, and cause cascading failures.
**Safe fan-out pattern using `SemaphoreSlim`:**
```csharp
private static async Task<IEnumerable<Result>> ProcessInParallelAsync<T>(
IEnumerable<T> items,
Func<T, CancellationToken, Task<Result>> processor,
int maxConcurrency,
CancellationToken cancellationToken)
{
using var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = items.Select(async item =>
{
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
return await processor(item, cancellationToken).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
});
return await Task.WhenAll(tasks).ConfigureAwait(false);
}
```
**Concurrency limits by resource type:**
- HTTP calls to an external API: 4--16 concurrent (respect rate limits; default `HttpClient` connection limit is 10 per host)
- SQL database queries: equal to connection pool size / 2 (default pool max is 100, so ~50 concurrent is safe)
- File I/O: 1--4 (disk seeks dominate above this)
- CPU-bound work launched via `Task.Run`: `Environment.ProcessorCount` or `Environment.ProcessorCount - 1`
**`Task.WhenAny` for timeouts and racing:**
```csharp
var workTask = DoLongWorkAsync(cancellationToken);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
var completed = await Task.WhenAny(workTask, timeoutTask).ConfigureAwait(false);
if (completed == timeoutTask)
throw new TimeoutException("Operation exceeded 5 second limit.");
return await workTask.ConfigureAwait(false);
```
---
### Step 7 -- Handle `async void` and Fire-and-Forget Safely
`async void` is a trap. Exceptions thrown inside `async void` methods crash the process (in most runtimes) because there is no `Task` for the caller to observe.
**Legitimate uses of `async void`:**
- Top-level event handlers in WinForms/WPF where the signature is mandated:
```csharp
private async void Button_Click(object sender, EventArgs e)
{
try
{
await LoadDataAsync(_cts.Token);
}
catch (OperationCanceledException) { /* user cancelled, safe to ignore */ }
catch (Exception ex)
{
_logger.LogError(ex, "Load failed");
ShowErrorDialog(ex.Message);
}
}
```
- Always wrap in try/catch -- the event handler is the last line of defense
**Fire-and-forget with error handling (never use `async void` for this):**
```csharp
// BAD: exceptions are unobserved
_ = DoBackgroundWorkAsync(); // suppresses warning but exception is lost
// BETTER: use a safe fire-and-forget wrapper
public static void FireAndForget(
this Task task,
ILogger logger,
CancellationToken cancellationToken = default)
{
task.ContinueWith(
t => logger.LogError(t.Exception, "Background task failed"),
cancellationToken,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default);
}
// Usage:
DoBackgroundWorkAsync(cancellationToken).FireAndForget(logger, cancellationToken);
```
---
### Step 8 -- Diagnose Common Async Bugs
Guide the user to the root cause of async bugs using this checklist:
**Deadlock diagnosis:**
1. Is `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` called anywhere on the call stack? (blocking on async)
2. Is the code running in a context with a single-threaded `SynchronizationContext` (WinForms, WPF, classic ASP.NET)?
3. Does the awaited method lack `ConfigureAwait(false)`?
If all three are true: classic deadlock. Fix by adding `ConfigureAwait(false)` throughout the library chain, or by making the call stack fully async.
**Thread pool starvation diagnosis:**
1. Are there many synchronous `.Wait()` or `.Result` calls in hot paths?
2. Is thread pool usage consistently near `Environment.ProcessorCount * 2` active threads?
3. Are async operations queued but not starting for seconds?
Fix: make the entire call chain async. Never block thread pool threads.
**Unobserved exception diagnosis:**
1. Is a `Task` returned from an async method discarded with `_` or no assignment?
2. Is `async void` used outside event handlers?
3. Is `TaskScheduler.UnobservedTaskException` firing in logs?
Fix: always `await` tasks or use the fire-and-forget wrapper above. Subscribe to `TaskScheduler.UnobservedTaskException` as a last-resort global handler.
---
## Output Format
When answering a C# async question, structure your response as follows:
```
## Problem Classification
[I/O-bound async | Streaming | Parallel fan-out | Legacy bridge | Cancellation design]
Root cause identified: [specific issue, e.g., deadlock from missing ConfigureAwait, ValueTask misuse, unbounded parallelism]
## Recommendation
### Primary Pattern
[Task<T> / ValueTask<T> / IAsyncEnumerable<T> / TaskCompletionSource<T>]
Rationale: [1-2 sentences explaining why this fits]
### Implementation
\```csharp
// [Annotated, production-quality code example]
// Comments explain non-obvious choices
\```
### Decision Matrix (include when comparing two approaches)
| Criterion | Task<T> | ValueTask<T> | Recommendation |
|------------------------|--------------|---------------|-------------------------|
| Allocation cost | 1 heap alloc | 0 on sync path| ValueTask if sync common |
| Multi-await safe | Yes | No | Task if awaited >1 time |
| Public API surface | Preferred | Advanced use | Task for public APIs |
| Hot path call rate | Any | >1M/sec | ValueTask if measured |
### Pitfalls to Avoid
- [Specific pitfall 1 with code showing what NOT to do]
- [Specific pitfall 2]
### Testing Guidance
- [How to test this async pattern -- fake delays, CancellationTokenSource, etc.]
```
---
## Rules
1. **Never recommend `.Result` or `.Wait()` as a solution** -- always guide the user to propagate `async` up the call stack. The only acceptable exception is in synchronous `Main` methods prior to C# 7.1, and even then, use `.GetAwaiter().GetResult()` which does not wrap exceptions in `AggregateException`.
2. **Never return `void` from async methods except in event handlers** -- `async void` silently swallows exceptions in most contexts. The return type must be `Task` (no result), `Task<T>` (with result), `ValueTask`, `ValueTask<T>`, or `IAsyncEnumerable<T>`.
3. **Always add `ConfigureAwait(false)` in library code** -- if the code being written is not a UI event handler or an ASP.NET Core controller action, every `await` must use `.ConfigureAwait(false)`. Set up a Roslyn analyzer (Microsoft.VisualStudio.Threading.Analyzers or Roslynator) to enforce this automatically.
4. **Never use `ValueTask<T>` in a public API without explicit documentation** -- callers must know they cannot `await` it more than once, store it, or call `.Result` before completion. The misuse surface is too large for undocumented public APIs.
5. **Always use `[EnumeratorCancellation]` on `CancellationToken` parameters in async iterators** -- without it, calling `.WithCancellation()` on the returned `IAsyncEnumerable<T>` has no effect and the token is silently ignored.
6. **Never create a `SemaphoreSlim` with an initial count of 0 as a concurrency limiter** -- use `new SemaphoreSlim(maxConcurrency, maxConcurrency)` with the same value for both initial count and max count. The first argument is the initial permits available, not the maximum.
7. **Always `Dispose` `CancellationTokenSource` instances** -- `CancellationTokenSource` registers callbacks and holds a `WaitHandle`. Failing to dispose it leaks memory, especially in loops that create thousands of linked token sources.
8. **Never `await` inside a `lock` statement** -- `await` inside `lock` is a compiler error, but developers sometimes work around it with `.Result` inside a lock (deadlock risk) or with `SemaphoreSlim(1,1)` incorrectly. The correct pattern for async mutual exclusion is `await semaphore.WaitAsync()` with a `try/finally` release.
9. **Always propagate `OperationCanceledException` up the stack when a `CancellationToken` fires** -- do not catch and swallow `OperationCanceledException` in intermediate methods unless you are the top-level handler or need to do cleanup before rethrowing. `catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)` is the safe pattern.
10. **Never use `Task.Run` as a substitute for proper async I/O** -- `Task.Run(() => File.ReadAllText(path))` moves the blocking call to a thread pool thread but still blocks that thread. Use `await File.ReadAllTextAsync(path)` instead. Reserve `Task.Run` for genuinely CPU-bound work (parsing, hashing, compression) that would otherwise block the calling thread.
---
## Edge Cases
### Legacy Codebase with Synchronous Interfaces
When a codebase has interfaces that return `string` or `IEnumerable<T>` and you cannot change the interface, do not add `async` to the implementing method and call `.GetAwaiter().GetResult()` inside it -- this recreates all the deadlock risk. Instead:
- Create a parallel `Async` interface (`IOrderRepositoryAsync`) and implement it alongside the synchronous version
- Migrate callers to the async interface incrementally
- If you absolutely must call async code from a synchronous context, use a dedicated thread: `Task.Run(() => GetDataAsync(token)).GetAwaiter().GetResult()` -- this prevents the deadlock because `Task.Run` runs on a thread pool thread without a `SynchronizationContext`. Still, document why this is unavoidable.
### EF Core and IAsyncEnumerable
EF Core's `ToAsyncEnumerable()` returns `IAsyncEnumerable<T>`, but the DbContext must remain open for the duration of the enumeration. If the DbContext is scoped (common in ASP.NET Core), and you yield items out to a higher layer that outlives the scope, you will get a `DbContext disposed` exception mid-stream.
- Do not return raw `IAsyncEnumerable<T>` from repositories that use scoped DbContexts across tier boundaries
- Either materialize the results within the repository scope, or ensure the consumer awaits the full enumeration within the same scope
- Use `await using var context = _factory.CreateDbContext()` pattern with `IDbContextFactory<T>` when you need the DbContext lifetime tied to the streaming operation
### HttpClient and CancellationToken
`HttpClient.SendAsync` with a `CancellationToken` will throw `OperationCanceledException` when the token is cancelled, but it also throws `OperationCanceledException` (not `TaskCanceledException`) when the `HttpClient.Timeout` is exceeded in older .NET versions. In .NET 5+, timeout throws `TaskCanceledException` with `InnerException` of `TimeoutException`. Always check the exception chain:
```csharp
catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
{
// Caller cancelled -- propagate
throw;
}
catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient timeout -- convert to a meaningful exception
throw new ServiceUnavailableException("Request timed out", ex);
}
```
### async/await in Constructors
C# constructors cannot be `async`. Developers sometimes call `.GetAwaiter().GetResult()` in a constructor, which introduces blocking. The correct patterns are:
- **Factory method pattern:** `public static async Task<MyService> CreateAsync(...)` that awaits initialization and returns the fully constructed instance
- **Lazy initialization:** Defer async initialization to the first use with a `Lazy<Task<T>>` or a private `_initialized` flag and an `EnsureInitializedAsync()` method called at the start of each public method
- **`IHostedService.StartAsync`:** In ASP.NET Core, async startup work belongs in `StartAsync`, not the constructor
### ValueTask Pooling via IValueTaskSource
Advanced libraries (like `System.IO.Pipelines` and `System.Net.Sockets`) pool `IValueTaskSource<T>` instances to achieve zero-allocation async paths. If you implement `IValueTaskSource<T>`:
- The source can only be in one of three states: pending, succeeded, faulted -- never recycle it back to the pool before the consumer has completed its await
- `GetResult()` must only be called once; calling it twice corrupts state
- Use `ManualResetValueTaskSourceCore<T>` (in `System.Threading.Tasks.Sources`) as the backing implementation -- do not implement the interface manually from scratch
- This level of optimization is only appropriate for hot-path server code handling millions of connections. For normal business logic, use `Task<T>`.
### Async Streams with Exceptions Mid-Sequence
When an `IAsyncEnumerable<T>` producer throws mid-sequence, the consumer's `await foreach` will surface the exception at the next `MoveNextAsync()` call. The producer's `finally` and `using` blocks ARE executed -- the compiler-generated state machine ensures cleanup on `DisposeAsync()`. However:
- If the consumer disposes the enumerator early (breaks out of `await foreach`), `DisposeAsync()` is called -- ensure the producer disposes its resources in `finally` blocks, not after the final `yield return`
- Never `yield return` inside a `try/catch` block that catches `Exception` broadly -- it prevents the compiler from generating correct state machine transitions and produces a compiler error in C# prior to C# 8.0 iterator semantics updates
### Thread Pool Starvation in Azure Functions / AWS Lambda
Serverless runtimes impose tight thread pool limits. A common failure mode: sync-over-async calls during cold starts stall the thread pool, causing subsequent requests to queue and timeout.
- In Azure Functions, enable `FUNCTIONS_WORKER_PROCESS_COUNT` and ensure the host is fully async
- Never use `.Result` or `.Wait()` in Azure Functions middleware or DI factory methods
- Monitor thread pool starvation with EventSource / `ThreadPool.GetAvailableThreads()` -- if available worker threads drop below `ProcessorCount * 2`, starvation is occurring
---
## Example
**Input:** "We have a C# service that calls a third-party payment API and we're seeing intermittent deadlocks and occasional lost exceptions. The service is a WPF desktop app with a synchronous call chain that eventually hits `GetPaymentStatusAsync().Result`. There are also background retry tasks being started but we never hear when they fail. How do we fix this?"
---
**Output:**
## Problem Classification
**I/O-bound async + deadlock from blocking + unobserved exceptions (fire-and-forget)**
Root causes identified:
1. `GetPaymentStatusAsync().Result` on a WPF UI thread blocks the SynchronizationContext, which deadlocks when the async method tries to resume on the same context
2. Background retry tasks are discarded (fire-and-forget without error observation), causing exceptions to be swallowed
---
## Recommendation
### Fix 1 -- Eliminate the Deadlock
The call chain must become fully async from the button click handler down to the HTTP call. There is no shortcut.
**Before (deadlocks):**
```csharp
// WPF ViewModel -- DEADLOCKS
public void RefreshStatus()
{
var status = _paymentService.GetPaymentStatusAsync().Result; // blocks UI thread
StatusText = status.Description;
}
```
**After (correct):**
```csharp
// WPF ViewModel -- correct async command pattern
public ICommand RefreshCommand => new AsyncRelayCommand(RefreshStatusAsync);
private async Task RefreshStatusAsync(CancellationToken cancellationToken = default)
{
IsLoading = true;
try
{
var status = await _paymentService.GetPaymentStatusAsync(cancellationToken);
// After await, we ARE back on the UI thread (WPF SynchronizationContext captured)
StatusText = status.Description;
}
catch (OperationCanceledException)
{
StatusText = "Cancelled.";
}
catch (PaymentApiException ex)
{
StatusText = $"Error: {ex.Message}";
_logger.LogError(ex, "Payment status refresh failed");
}
finally
{
IsLoading = false;
}
}
```
**In the payment service (library code -- must use ConfigureAwait(false)):**
```csharp
public class PaymentService : IPaymentService
{
private readonly HttpClient _httpClient;
private readonly ILogger<PaymentService> _logger;
public PaymentService(HttpClient httpClient, ILogger<PaymentService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<PaymentStatus> GetPaymentStatusAsync(
CancellationToken cancellationToken = default)
{
// ConfigureAwait(false) here -- this is library code, not UI code
// We do NOT need to return to the WPF SynchronizationContext after this await
using var response = await _httpClient
.GetAsync("/api/payments/status", cancellationToken)
.ConfigureAwait(false);
// HttpClient timeout vs caller cancellation -- differentiate them
response.EnsureSuccessStatusCode();
var json = await response.Content
.ReadAsStringAsync(cancellationToken)
.ConfigureAwait(false);
return JsonSerializer.Deserialize<PaymentStatus>(json)
?? throw new PaymentApiException("Empty response from payment API");
}
}
```
---
### Fix 2 -- Replace Fire-and-Forget with Observable Background Tasks
**Before (exceptions silently lost):**
```csharp
// Nobody awaits this -- if it throws, the exception disappears
_ = RetryPaymentAsync(paymentId);
```
**After (exceptions are logged, task is tracked):**
```csharp
// Extension method for safe fire-and-forget
public static class TaskExtensions
{
public static void FireAndForget(
this Task task,
ILogger logger,
string operationName,
CancellationToken cancellationToken = default)
{
if (task == null) throw new ArgumentNullException(nameof(task));
task.ContinueWith(
completedTask =>
{
if (completedTask.IsFaulted)
{
logger.LogError(
completedTask.Exception?.GetBaseException(),
"Background operation '{OperationName}' failed",
operationName);
}
},
CancellationToken.None, // continuation always runs
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default); // always thread pool, never UI context
}
}
// Usage in the ViewModel or service:
private CancellationTokenSource _backgroundCts = new CancellationTokenSource();
private void ScheduleRetry(Guid paymentId)
{
RetryPaymentAsync(paymentId, _backgroundCts.Token)
.FireAndForget(_logger, $"RetryPayment:{paymentId}", _backgroundCts.Token);
}
// Clean up when the window closes
protected override void OnClosed(EventArgs e)
{
_backgroundCts.Cancel();
_backgroundCts.Dispose();
base.OnClosed(e);
}
```
---
### Fix 3 -- Add CancellationToken with Timeout to the Retry Logic
**Retry with timeout and cancellation:**
```csharp
private async Task RetryPaymentAsync(
Guid paymentId,
CancellationToken cancellationToken)
{
const int maxAttempts = 3;
var delay = TimeSpan.FromSeconds(2);
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
// Each individual attempt gets its own timeout, linked to the external token
using var attemptCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
attemptCts.CancelAfter(TimeSpan.FromSeconds(10));
try
{
await _paymentService
.SubmitPaymentRetryAsync(paymentId, attemptCts.Token)
.ConfigureAwait(false);
_logger.LogInformation("Payment {PaymentId} retry succeeded on attempt {Attempt}",
paymentId, attempt);
return;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Caller (app shutdown) cancelled -- stop retrying, do not log as error
_logger.LogInformation("Payment retry cancelled for {PaymentId}", paymentId);
throw;
}
catch (OperationCanceledException ex) when (attemptCts.IsCancellationRequested)
{
// This specific attempt timed out -- log and retry if attempts remain
_logger.LogWarning("Payment retry attempt {Attempt} timed out for {PaymentId}",
attempt, paymentId);
}
catch (PaymentApiException ex) when (attempt < maxAttempts)
{
_logger.LogWarning(ex,
"Payment retry attempt {Attempt} failed for {PaymentId}, retrying in {Delay}",
attempt, paymentId, delay);
}
// Exponential backoff -- does not block the thread
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
delay *= 2; // 2s, 4s, 8s
}
throw new PaymentRetryExhaustedException(
$"All {maxAttempts} retry attempts failed for payment {paymentId}");
}
```
---
### Decision Matrix
| Concern | Old Approach | Fixed Approach | Why It Matters |
|---|---|---|---|
| UI thread blocking | `.Result` in ViewModel | `async Task` command pattern | Prevents SynchronizationContext deadlock |
| Library awaits | No ConfigureAwait | `ConfigureAwait(false)` on all awaits | Removes context capture; safe for any caller |
| Background exceptions | Discarded `_` task | `FireAndForget` with ContinueWith | All failures logged; no silent data loss |
| Per-request timeout | None | Linked CTS with `CancelAfter` | Prevents retry storms hanging indefinitely |
| Cancellation at shutdown | None | `CancellationTokenSource` on window close | Clean shutdown without lingering background work |
---
### Pitfalls Still to Avoid
```csharp
// DO NOT do this -- still deadlocks even with ConfigureAwait(false) added to service
// if there is any intermediate await that does NOT have ConfigureAwait(false)
var status = _paymentService.GetPaymentStatusAsync().GetAwaiter().GetResult();
// Every single await in the call chain must have ConfigureAwait(false) for this to be safe.
// One missing ConfigureAwait(false) can restore the deadlock.
// DO NOT do this -- ValueTask is not the right fit here
// GetPaymentStatusAsync always awaits the HTTP call (no synchronous hot path)
public async ValueTask<PaymentStatus> GetPaymentStatusAsync(...) // wrong type
// Use Task<T> -- the HTTP call always genuinely awaits
// DO NOT discard the CancellationTokenSource without disposing
var cts = CancellationTokenSource.CreateLinkedTokenSource(token); // no using -- LEAK
await DoWorkAsync(cts.Token);
// Fix: always wrap in using
```
---
### Testing Guidance
```csharp
[Fact]
public async Task GetPaymentStatusAsync_WhenCancelled_ThrowsOperationCancelledException()
{
var cts = new CancellationTokenSource();
var httpHandler = new Mock<HttpMessageHandler>();
httpHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.Returns(async (HttpRequestMessage req, CancellationToken ct) =>
{
await Task.Delay(500, ct); // simulate slow response
return new HttpResponseMessage(HttpStatusCode.OK);
});
var client = new HttpClient(httpHandler.Object) { BaseAddress = new Uri("https://api.example.com") };
var service = new PaymentService(client, NullLogger<PaymentService>.Instance);
cts.CancelAfter(50); // cancel before the 500ms response
await Assert.ThrowsAsync<OperationCanceledException>(
() => service.GetPaymentStatusAsync(cts.Token));
}
[Fact]
public async Task RetryPaymentAsync_LogsError_WhenAllAttemptsExhausted()
{
// Use a real CancellationTokenSource with a generous timeout (test shouldn't hang)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var logger = new TestLogger<PaymentService>();
// inject a mock service that always throws PaymentApiException
// assert that logger received 3 Warning entries and 1 Error entry
// assert that PaymentRetryExhaustedException is thrown
}
```
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!