Docs β€Ί .NET Development β€Ί Async/Await Programming

Async/Await Programming

Understanding asynchronous programming in C# with async and await

Async/Await Programming

Asynchronous programming is a technique that allows your program to perform multiple operations concurrently without blocking the main thread. This is critical for building responsive applications.

Why Async?

The Problem with Synchronous Code

// Synchronous β€” blocks the thread
public string GetDataSync()
{
    // Thread waits for 2 seconds
    var result = httpClient.GetStringAsync("https://api.example.com").Result;
    return result;
}

Problems:

  • The thread can’t do anything else
  • The application β€œfreezes”
  • Resources are wasted

The Async Solution

// Asynchronous β€” thread is free
public async Task<string> GetDataAsync()
{
    // Thread can do other work while waiting
    var result = await httpClient.GetStringAsync("https://api.example.com");
    return result;
}

Core Concepts

Task and Task

// Task = async operation with no return value
public async Task DoSomethingAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("Done!");
}

// Task<T> = async operation with a return value
public async Task<int> CalculateAsync()
{
    await Task.Delay(1000);
    return 42;
}

async and await Keywords

// async marks a method as asynchronous
// await waits for an async operation to complete
public async Task ProcessAsync()
{
    Console.WriteLine("Starting...");
    
    // await = wait until done, but the thread is free
    await Task.Delay(2000);
    
    Console.WriteLine("Done!");
}

Common Patterns

Sequential Execution

// Operations run one after another
public async Task SequentialAsync()
{
    var result1 = await GetData1Async();  // Wait for completion
    var result2 = await GetData2Async();  // Then run this
    var result3 = await GetData3Async();  // Then this
    
    // Total time = time1 + time2 + time3
}

Parallel Execution

// All operations run concurrently
public async Task ParallelAsync()
{
    // Start all tasks
    var task1 = GetData1Async();
    var task2 = GetData2Async();
    var task3 = GetData3Async();
    
    // Wait for all to complete
    await Task.WhenAll(task1, task2, task3);
    
    // Get results
    var result1 = task1.Result;
    var result2 = task2.Result;
    var result3 = task3.Result;
    
    // Total time = longest of the three
}

WhenAll vs WhenAny

// WhenAll β€” wait for ALL to complete
var results = await Task.WhenAll(task1, task2, task3);

// WhenAny β€” wait for ANY ONE to complete
var firstCompleted = await Task.WhenAny(task1, task2, task3);

Async in ASP.NET Core

Controller

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;
    
    [HttpGet]
    public async Task<ActionResult<List<Product>>> GetAll()
    {
        var products = await _service.GetAllAsync();
        return Ok(products);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetById(int id)
    {
        var product = await _service.GetByIdAsync(id);
        
        if (product == null)
            return NotFound();
            
        return Ok(product);
    }
}

Service

public class ProductService : IProductService
{
    private readonly HttpClient _httpClient;
    private readonly AppDbContext _context;
    
    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Id == id);
    }
    
    public async Task<List<Product>> GetFromExternalApiAsync()
    {
        var response = await _httpClient.GetAsync("https://api.example.com/products");
        response.EnsureSuccessStatusCode();
        
        var content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<List<Product>>(content);
    }
}

Error Handling

Try-Catch with Async

public async Task<Product?> SafeGetProductAsync(int id)
{
    try
    {
        return await _service.GetByIdAsync(id);
    }
    catch (HttpRequestException ex)
    {
        _logger.LogError(ex, "Failed to fetch product {Id}", id);
        return null;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unexpected error");
        throw; // re-throw for serious errors
    }
}

Exceptions in Task.WhenAll

try
{
    await Task.WhenAll(task1, task2, task3);
}
catch (Exception)
{
    // Only the first exception is caught
    // To get all exceptions:
    if (task1.IsFaulted) HandleError(task1.Exception);
    if (task2.IsFaulted) HandleError(task2.Exception);
    if (task3.IsFaulted) HandleError(task3.Exception);
}

Cancellation

CancellationToken

public async Task<Data> GetDataAsync(CancellationToken cancellationToken = default)
{
    // Check if cancelled
    cancellationToken.ThrowIfCancellationRequested();
    
    // Pass the token to async operations
    var response = await _httpClient.GetAsync(url, cancellationToken);
    
    return await response.Content.ReadFromJsonAsync<Data>(cancellationToken);
}

Using CancellationToken

// In a controller β€” ASP.NET Core auto-cancels when the request is aborted
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
    var data = await _service.GetDataAsync(cancellationToken);
    return Ok(data);
}

// Manual timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var result = await GetDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was cancelled or timed out");
}

Best Practices

1. Async All The Way

// ❌ DON'T β€” blocking on async code
public string GetData()
{
    return GetDataAsync().Result;  // Deadlock risk!
}

// βœ… DO β€” async all the way up
public async Task<string> GetDataAsync()
{
    return await FetchDataAsync();
}

2. ConfigureAwait

// In library code
public async Task<string> LibraryMethodAsync()
{
    // ConfigureAwait(false) for libraries
    var result = await SomeOperationAsync().ConfigureAwait(false);
    return result;
}

// In ASP.NET Core, you generally don't need ConfigureAwait

3. Avoid async void

// ❌ DON'T β€” exceptions can't be caught
public async void BadMethod()
{
    await SomethingAsync();
}

// βœ… DO β€” use Task
public async Task GoodMethod()
{
    await SomethingAsync();
}

// Exception: event handlers
button.Click += async (sender, e) =>
{
    await HandleClickAsync();
};

4. ValueTask for Hot Paths

// For operations that are often synchronous
public ValueTask<int> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out int value))
    {
        return new ValueTask<int>(value);  // No allocation
    }
    
    return new ValueTask<int>(FetchAndCacheAsync(key));
}

Common Mistakes

Deadlock

// ❌ Deadlock in ASP.NET Framework (not in Core)
public ActionResult Index()
{
    var data = GetDataAsync().Result;  // DEADLOCK!
    return View(data);
}

Fire and Forget

// ❌ Exceptions are silently lost
public void StartBackgroundWork()
{
    DoWorkAsync();  // Exception will never be seen!
}

// βœ… Better
public async Task StartBackgroundWork()
{
    try
    {
        await DoWorkAsync();
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Background work failed");
    }
}

Summary

ScenarioUse
I/O bound (database, HTTP, file)async/await
CPU boundTask.Run() or parallel
Sequential operationsawait one by one
Independent operationsTask.WhenAll()
Race conditionTask.WhenAny()

Async/await is one of the most important features in modern C#. Master this concept to build responsive and scalable applications!