Skip to main content

C# Async Patterns

Asynchronous programming in C# has evolved significantly over the years, providing developers with powerful tools to create responsive and efficient applications. This guide introduces you to the most common async patterns in C# and how to use them effectively in your projects.

Introduction

Asynchronous programming allows your application to perform work while waiting for other operations to complete, making your code more efficient and responsive. C# offers several patterns to implement asynchronous code, each with its own benefits and use cases.

As a beginner, understanding these patterns will help you write better code that doesn't freeze up when performing time-consuming operations like network requests, file I/O, or database queries.

Basic Async/Await Pattern

The async/await pattern is the cornerstone of modern asynchronous programming in C#. It allows you to write asynchronous code that looks and behaves like synchronous code.

How It Works

  1. Mark your method with the async keyword
  2. Use the await keyword when calling asynchronous methods
  3. Return Task or Task<T> from your async methods

Example

csharp
// Basic async/await example
public async Task<string> DownloadWebPageAsync(string url)
{
Console.WriteLine("Starting to download...");

// HttpClient should be instantiated once and reused in production code
using (HttpClient client = new HttpClient())
{
// The await keyword pauses execution until the task completes
// without blocking the thread
string content = await client.GetStringAsync(url);

Console.WriteLine("Download completed!");
return content;
}
}

// Calling the async method
public async Task DemoAsync()
{
Console.WriteLine("Before calling async method");
string content = await DownloadWebPageAsync("https://example.com");
Console.WriteLine($"Content length: {content.Length}");
Console.WriteLine("After calling async method");
}

Output:

Before calling async method
Starting to download...
Download completed!
Content length: 1256
After calling async method

Key Points

  • The async keyword doesn't make a method run asynchronously - it enables the await keyword inside the method.
  • When await is encountered, the method returns to the caller, allowing the application to remain responsive.
  • When the awaited task completes, execution continues from where it left off.

Task-Based Asynchronous Pattern (TAP)

TAP is the modern approach to asynchronous programming in .NET. It uses the Task and Task<T> classes to represent asynchronous operations.

Key Components

  • Task: Represents an asynchronous operation that doesn't return a value
  • Task<T>: Represents an asynchronous operation that returns a value of type T
  • TaskCompletionSource: Allows manual creation and control of a Task

Creating Tasks

csharp
// Creating and returning a completed task
public Task<int> GetCompletedTaskAsync()
{
return Task.FromResult(42);
}

// Creating a task that completes after a delay
public async Task<string> DelayedGreetingAsync(string name)
{
await Task.Delay(1000); // Wait for 1 second asynchronously
return $"Hello, {name}!";
}

Working with Multiple Tasks

csharp
// Run multiple tasks in parallel and wait for all to complete
public async Task ProcessDataInParallelAsync()
{
List<Task> tasks = new List<Task>
{
ProcessItemAsync(1),
ProcessItemAsync(2),
ProcessItemAsync(3)
};

// Wait for all tasks to complete
await Task.WhenAll(tasks);

Console.WriteLine("All items processed!");
}

private async Task ProcessItemAsync(int id)
{
Console.WriteLine($"Starting to process item {id}");
await Task.Delay(1000 * id); // Simulate work
Console.WriteLine($"Finished processing item {id}");
}

Output:

Starting to process item 1
Starting to process item 2
Starting to process item 3
Finished processing item 1
Finished processing item 2
Finished processing item 3
All items processed!

Async Void Pattern (Be Careful!)

The async void pattern is generally used for event handlers, but should be avoided in other scenarios.

Example

csharp
// This is acceptable for event handlers
private async void Button_Click(object sender, EventArgs e)
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
// Must handle exceptions in async void methods
MessageBox.Show($"Error: {ex.Message}");
}
}

// This is better - returns a Task instead of void
private async Task ProcessDataAsync()
{
await Task.Delay(1000);
// Process data here...
}

Why Avoid Async Void?

  1. Exceptions in async void methods can crash your application
  2. Cannot be awaited or easily tested
  3. Difficult to track when the operation completes

Progress Reporting Pattern

When performing long-running operations, it's often useful to report progress back to the caller.

Using IProgress<T>

csharp
public async Task ProcessFilesAsync(string[] filePaths, IProgress<int> progress)
{
for (int i = 0; i < filePaths.Length; i++)
{
await ProcessFileAsync(filePaths[i]);

// Report progress as percentage
int percentComplete = (i + 1) * 100 / filePaths.Length;
progress?.Report(percentComplete);
}
}

// Calling with progress reporting
public async Task DemoProgressAsync()
{
string[] files = new[] { "file1.txt", "file2.txt", "file3.txt" };

// Create progress reporter
var progressHandler = new Progress<int>(percent =>
{
Console.WriteLine($"Processing: {percent}% complete");
});

await ProcessFilesAsync(files, progressHandler);
Console.WriteLine("All files processed!");
}

Output:

Processing: 33% complete
Processing: 66% complete
Processing: 100% complete
All files processed!

Cancellation Pattern

Allowing operations to be cancelled is an important aspect of responsive applications.

Using CancellationToken

csharp
public async Task<string> LongRunningOperationAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// Check if cancellation was requested
cancellationToken.ThrowIfCancellationRequested();

await Task.Delay(500, cancellationToken);
Console.WriteLine($"Step {i + 1} completed");
}

return "Operation completed successfully";
}

// Using cancellation
public async Task DemoCancellationAsync()
{
// Create cancellation source with 3-second timeout
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
{
try
{
string result = await LongRunningOperationAsync(cts.Token);
Console.WriteLine(result);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
}
}

Output:

Step 1 completed
Step 2 completed
Step 3 completed
Step 4 completed
Step 5 completed
Step 6 completed
Operation was cancelled

Asynchronous Streams (C# 8.0+)

C# 8.0 introduced asynchronous streams, which allow you to asynchronously yield results as they become available.

Using IAsyncEnumerable<T>

csharp
public async IAsyncEnumerable<string> GetDataStreamAsync()
{
for (int i = 1; i <= 5; i++)
{
// Simulate fetching data from a service
await Task.Delay(1000);
yield return $"Data item {i}";
}
}

// Consuming the async stream
public async Task ConsumeStreamAsync()
{
await foreach (var item in GetDataStreamAsync())
{
Console.WriteLine($"Received: {item}");
}
}

Output:

Received: Data item 1
Received: Data item 2
Received: Data item 3
Received: Data item 4
Received: Data item 5

Real-World Example: Weather Forecast API

Let's look at a practical example combining several async patterns to create a weather forecast application:

csharp
public class WeatherService
{
private readonly HttpClient _httpClient;

public WeatherService()
{
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri("https://api.weather.example.com/");
}

public async Task<WeatherForecast> GetForecastAsync(
string city,
IProgress<string> progress,
CancellationToken cancellationToken)
{
try
{
progress?.Report("Connecting to weather service...");

// Check for cancellation
cancellationToken.ThrowIfCancellationRequested();

// Get current conditions
progress?.Report("Fetching current conditions...");
var currentConditions = await _httpClient.GetFromJsonAsync<CurrentConditions>(
$"current?city={city}",
cancellationToken);

// Get forecast data
progress?.Report("Fetching forecast data...");
var forecastData = await _httpClient.GetFromJsonAsync<ForecastData>(
$"forecast?city={city}&days=5",
cancellationToken);

// Combine the results
progress?.Report("Processing weather data...");
await Task.Delay(500, cancellationToken); // Simulated processing time

return new WeatherForecast
{
City = city,
CurrentTemperature = currentConditions.Temperature,
ForecastDays = forecastData.Days
};
}
catch (Exception) when (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException("Weather forecast was cancelled");
}
catch (Exception ex)
{
throw new ApplicationException($"Error getting weather forecast: {ex.Message}", ex);
}
}
}

// Using the service
public async Task ShowWeatherForecastAsync()
{
var weatherService = new WeatherService();
var progressReporter = new Progress<string>(status => Console.WriteLine(status));
var cts = new CancellationTokenSource();

// Allow cancellation after 10 seconds
cts.CancelAfter(TimeSpan.FromSeconds(10));

try
{
var forecast = await weatherService.GetForecastAsync("New York", progressReporter, cts.Token);
Console.WriteLine($"Current temperature in {forecast.City}: {forecast.CurrentTemperature}°C");

Console.WriteLine("5-day forecast:");
foreach (var day in forecast.ForecastDays)
{
Console.WriteLine($"{day.Date.ToShortDateString()}: {day.MinTemp}°C - {day.MaxTemp}°C");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Weather forecast request was cancelled");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}

This example demonstrates:

  • Async/await pattern for the overall operation
  • Progress reporting using IProgress<T>
  • Cancellation with CancellationToken
  • Exception handling in async methods
  • Combining multiple asynchronous operations

Summary

C# offers a rich set of asynchronous patterns that enable you to write efficient, responsive code:

  1. Basic Async/Await: The foundation of modern asynchronous programming in C#
  2. Task-based Asynchronous Pattern: Working with Task and Task<T> objects
  3. Async Void: Primarily for event handlers, should be avoided elsewhere
  4. Progress Reporting: Using IProgress<T> for feedback during long operations
  5. Cancellation: Enabling operations to be cancelled using CancellationToken
  6. Asynchronous Streams: Processing data as it becomes available with IAsyncEnumerable<T>

By mastering these patterns, you'll be able to create applications that remain responsive even when performing time-consuming operations like network requests, file I/O, or database access.

Additional Resources

Exercises

  1. Create a simple file downloading application that shows progress and allows cancellation.
  2. Implement an asynchronous method that processes multiple API requests in parallel using Task.WhenAll().
  3. Build a console application that reads data from a large file asynchronously and processes the data as it becomes available.
  4. Modify an existing synchronous method to be asynchronous, following the async/await pattern.
  5. Create an application that uses an asynchronous stream to simulate reading sensor data at regular intervals.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)