Skip to main content
Async Await Patterns (commonly referred to as async/await) is a C# language feature that enables developers to write asynchronous code that maintains the readability and structure of synchronous code. The core purpose is to simplify asynchronous programming by allowing non-blocking operations without the complexity of callback-based approaches.

ConfigureAwait

ConfigureAwait controls the context in which an asynchronous operation resumes after completion. The method accepts a boolean parameter (continueOnCapturedContext) that determines whether to marshal the continuation back to the original synchronization context (like UI thread) or execute it on any available thread pool thread.
public class DataService
{
    public async Task<string> GetDataAsync()
    {
        // In a non-UI context, ConfigureAwait(false) avoids unnecessary
        // context switching and can prevent deadlocks
        var data = await HttpClient.GetStringAsync("https://api.example.com/data")
            .ConfigureAwait(false);
        
        // Subsequent code runs on thread pool thread, not original context
        return ProcessData(data);
    }
    
    public async Task UpdateUIAsync()
    {
        // In UI context, omitting ConfigureAwait (or using true) ensures
        // UI updates happen on the correct thread
        var data = await LoadDataAsync(); // Implicit ConfigureAwait(true)
        
        // This runs on UI thread - safe for UI updates
        textBox.Text = data;
    }
}
Deadlock Prevention: Improper use of .Result or .Wait() can cause deadlocks. ConfigureAwait(false) helps prevent this by avoiding synchronization context capture.

Async Streams

Async streams (C# 8.0+) enable asynchronous enumeration of data using IAsyncEnumerable\<T\>. This pattern allows yielding elements one at a time while performing asynchronous operations between yields.
public class SensorService
{
    public async IAsyncEnumerable<SensorReading> GetSensorReadingsAsync()
    {
        for (int i = 0; i < 10; i++)
        {
            // Simulate async operation to get each reading
            var reading = await FetchReadingFromSensorAsync(i);
            
            // Yield each reading as it becomes available
            yield return reading;
        }
    }
    
    public async Task ProcessReadingsAsync()
    {
        // Consume async stream with await foreach
        await foreach (var reading in GetSensorReadingsAsync())
        {
            Console.WriteLine($"Received: {reading.Value} at {reading.Timestamp}");
            
            // Process each reading as it arrives
            await ProcessReadingAsync(reading);
        }
    }
    
    private async Task<SensorReading> FetchReadingFromSensorAsync(int sensorId)
    {
        await Task.Delay(100); // Simulate async I/O
        return new SensorReading(sensorId, DateTime.UtcNow, Random.Shared.NextDouble());
    }
}

ValueTask

ValueTask\<T\> is a value type alternative to Task\<T\> designed for performance optimization. It should be used when an asynchronous method frequently completes synchronously (cached results, validation failures) to avoid heap allocations.
public class CachedDataService
{
    private readonly ConcurrentDictionary<string, string> _cache = new();
    
    // Use ValueTask for methods that often complete synchronously
    public async ValueTask<string> GetCachedDataAsync(string key)
    {
        // Check cache synchronously - common case
        if (_cache.TryGetValue(key, out var cachedValue))
        {
            return cachedValue; // Returns completed ValueTask without allocation
        }
        
        // Fall back to async operation - less common
        var freshData = await FetchDataFromSourceAsync(key);
        _cache[key] = freshData;
        return freshData;
    }
    
    // Use regular Task for consistently asynchronous operations
    public async Task<string> AlwaysAsyncOperationAsync()
    {
        await Task.Delay(1000);
        return "This always runs asynchronously";
    }
}
ValueTask\<T\> should generally be awaited immediately and not stored or used in multiple await operations due to its non-thread-safe nature.

Why Async Await Patterns Matter

Enables efficient resource utilization through non-blocking I/O operations, allowing applications to handle more concurrent requests with fewer threads.
Reduces boilerplate code compared to callback-based approaches, making asynchronous code as readable as synchronous code.
Follows the Single Responsibility Principle by separating asynchronous operation management from business logic, keeping components focused and testable.

Advanced Nuances

ValueTask Consumption Patterns

public async Task ProcessValueTaskCorrectly()
{
    var valueTask = GetCachedDataAsync("key");
    
    // ✅ Correct - await immediately
    var result = await valueTask;
    
    // ❌ Incorrect - don't await multiple times
    // var result2 = await valueTask; // Potential exception
}

Async Stream Composition

Async streams can be composed and transformed using System.Linq.Async methods:
public async IAsyncEnumerable<ProcessedData> TransformStreamAsync()
{
    var sourceStream = GetSensorReadingsAsync();
    
    // Apply LINQ-like operations on async streams
    await foreach (var item in sourceStream
        .Where(x => x.Value > 0.5)
        .SelectAwait(async x => await TransformAsync(x)))
    {
        yield return item;
    }
}

Roadmap Context

Within the “Asynchronous Programming” section, Async Await Patterns serves as the fundamental building block that unlocks more advanced topics:
  • Advanced Async Patterns: Cancellation tokens, async factories, and async disposal patterns
  • Parallel Programming: How async/await interacts with Parallel LINQ (PLINQ) and Dataflow
  • Performance Optimization: Memory-efficient async programming and benchmarking async code
  • Testing Async Code: Writing effective unit tests for asynchronous methods

Build docs developers (and LLMs) love