Isaac.

C# Async Patterns

Master async/await and asynchronous programming patterns.

By EMEPublished: February 20, 2025
csharpasyncawaitasynchronouspatterns

A Simple Analogy

Async is like ordering food at a restaurant. Instead of waiting at the counter, you get a number and do other things. When your food is ready (the Task completes), you pick it up.


Why Use Async?

  • Responsiveness: UI doesn't freeze during I/O
  • Scalability: Handle more concurrent requests
  • Efficiency: Don't block threads while waiting
  • Better UX: Apps feel snappier
  • Resource savings: Fewer threads = lower memory

Basic Async/Await

// Without async: Blocks thread
public string FetchData()
{
    var data = new WebClient().DownloadString("https://api.example.com/data");
    return data;
}

// With async: Non-blocking
public async Task<string> FetchDataAsync()
{
    var client = new HttpClient();
    var data = await client.GetStringAsync("https://api.example.com/data");
    return data;
}

// Usage
var result = await FetchDataAsync();

Async Patterns

// Pattern 1: Fire and forget (bad practice)
_ = LoadDataAsync(); // Ignores exceptions

// Pattern 2: Async all the way
public async Task ProcessOrderAsync(Order order)
{
    await ValidateOrderAsync(order);
    await SaveOrderAsync(order);
    await SendEmailAsync(order.Email);
}

// Pattern 3: Parallel async operations
public async Task<(Product, Reviews)> GetProductDetailsAsync(int id)
{
    var productTask = GetProductAsync(id);
    var reviewsTask = GetReviewsAsync(id);
    
    await Task.WhenAll(productTask, reviewsTask);
    
    return (productTask.Result, reviewsTask.Result);
}

// Pattern 4: Cancellation
public async Task<Data> FetchWithCancellationAsync(CancellationToken ct)
{
    var response = await httpClient.GetAsync("https://api.example.com/data", ct);
    return await response.Content.ReadAsAsync<Data>();
}

Exception Handling

// Try-catch works with async
public async Task<Order> GetOrderAsync(int id)
{
    try
    {
        return await orderService.GetAsync(id);
    }
    catch (HttpRequestException ex)
    {
        logger.LogError("API error: {0}", ex.Message);
        throw;
    }
}

// Multiple async operations with error handling
public async Task ProcessMultipleAsync(List<int> ids)
{
    var tasks = ids.Select(id => ProcessOneAsync(id)).ToList();
    
    try
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex)
    {
        var failed = tasks.Where(t => t.IsFaulted).ToList();
        logger.LogError("Failed tasks: {0}", failed.Count);
    }
}

ValueTask for Performance

// Use Task for I/O operations
public async Task<string> FetchFromApiAsync()
{
    var response = await httpClient.GetAsync("https://api.example.com");
    return await response.Content.ReadAsStringAsync();
}

// Use ValueTask when result is often synchronous
public async ValueTask<int> GetCachedValueAsync(string key)
{
    // Often returns synchronously from cache
    if (cache.TryGetValue(key, out var value))
        return value;
    
    // Occasionally awaits database
    return await database.GetValueAsync(key);
}

Best Practices

  1. Async all the way: Make callers async too
  2. Avoid blocking: Never use .Result or .Wait()
  3. Configure await: Use ConfigureAwait(false) in libraries
  4. Handle cancellation: Accept CancellationToken
  5. Use ValueTask: When result is often synchronous

Related Concepts

  • Task vs. ValueTask performance
  • Async iterators
  • Task schedulers
  • Synchronization context
  • Async streams

Summary

C# async patterns enable responsive, scalable applications by preventing thread blocking. Master async/await to build modern, efficient applications.