Section 10 of 10

Asynchronous Programming with Async/Await

🎯 What You'll Learn

  • What asynchronous programming is and why it matters
  • The difference between synchronous and asynchronous code
  • Understanding async and await keywords
  • Working with Task and Task<T>
  • Common async patterns and best practices
  • Async methods in ASP.NET Core
  • Error handling in async code
  • When to use async/await (and when not to)
  • Building responsive InvenTrack features

The Problem: Blocking Operations

Imagine your application needs to fetch data from a database, call an external API, or read a large file. These operations take time—sometimes seconds. What happens while your code waits?

SynchronousCode.cs C#
// Synchronous (blocking) code
public void ProcessOrder()
{
    Console.WriteLine("Starting order processing...");
    
    // This blocks for 3 seconds - nothing else can happen!
    Thread.Sleep(3000);
    
    Console.WriteLine("Order processed!");
}

// During those 3 seconds:
// - Desktop app: UI freezes, can't click anything
// - Web app: Thread is blocked, can't handle other requests
// - Console app: Just sits there waiting
⚠️ The Blocking Problem

Desktop apps: UI freezes, users think it crashed
Web apps: Threads are wasted waiting, limiting scalability
Mobile apps: Battery drain, poor responsiveness
Result: Bad user experience and poor resource utilization

The Solution: Asynchronous Programming

Asynchronous programming lets your code start a long-running operation and continue doing other work while waiting for it to complete. When the operation finishes, your code picks up where it left off.

💡 Key Concept

Think of async like ordering food at a restaurant. Synchronous: You stand at the counter until your food is ready (blocking). Asynchronous: You order, get a buzzer, sit down and chat with friends, and come back when it buzzes (non-blocking). The kitchen still takes the same time, but you're free to do other things!

Async/Await Basics

AsyncBasics.cs C#
// Async method returns Task
public async Task ProcessOrderAsync()
{
    Console.WriteLine("Starting order processing...");
    
    // await = "wait for this, but don't block"
    await Task.Delay(3000);
    
    Console.WriteLine("Order processed!");
}

// Calling async method
await ProcessOrderAsync();

// During those 3 seconds:
// - Thread is free to do other work
// - UI remains responsive
// - Web server can handle other requests

The async and await Keywords

Keyword Purpose Where Used
async Marks a method as asynchronous Method signature
await Waits for async operation without blocking Inside async methods
ℹ️ Important Rules

1. Methods marked async must return Task, Task<T>, or void (avoid void except for event handlers)
2. You can only use await inside async methods
3. Async methods should be named with "Async" suffix by convention
4. await doesn't create a new thread—it yields control

Task and Task<T>

Task represents an asynchronous operation. Task<T> represents an async operation that returns a value of type T.

TaskExamples.cs C#
// Task - no return value
public async Task SaveDataAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("Data saved");
}

// Task<T> - returns a value
public async Task<string> FetchDataAsync()
{
    await Task.Delay(1000);
    return "Data from server";
}

// Task<int> - returns an integer
public async Task<int> CalculateAsync()
{
    await Task.Delay(500);
    return 42;
}

// Using these methods
await SaveDataAsync();
string data = await FetchDataAsync();
int result = await CalculateAsync();

Real-World Async Operations

File I/O

FileAsync.cs C#
public async Task<string> ReadFileAsync(string path)
{
    return await File.ReadAllTextAsync(path);
}

public async Task WriteFileAsync(string path, string content)
{
    await File.WriteAllTextAsync(path, content);
}

HTTP Requests

HttpAsync.cs C#
public async Task<string> GetWebPageAsync(string url)
{
    using HttpClient client = new();
    return await client.GetStringAsync(url);
}

public async Task<Product> GetProductFromApiAsync(string sku)
{
    using HttpClient client = new();
    string json = await client.GetStringAsync($"https://api.example.com/products/{sku}");
    return JsonSerializer.Deserialize<Product>(json);
}

Database Operations

DatabaseAsync.cs C#
public async Task<List<Product>> GetProductsAsync()
{
    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();
    
    using var command = new SqlCommand("SELECT * FROM Products", connection);
    using var reader = await command.ExecuteReaderAsync();
    
    List<Product> products = new();
    while (await reader.ReadAsync())
    {
        // Map data to Product objects
    }
    
    return products;
}

Running Multiple Async Operations

Sequential (One After Another)

Sequential.cs C#
// Takes 3 seconds total (1 + 2)
await Task1Async();  // 1 second
await Task2Async();  // 2 seconds

Concurrent (At the Same Time)

Concurrent.cs C#
// Start both tasks
Task task1 = Task1Async();
Task task2 = Task2Async();

// Wait for both to complete
await Task.WhenAll(task1, task2);

// Takes 2 seconds total (max of 1 and 2)

Task.WhenAll and Task.WhenAny

TaskCombinators.cs C#
// WhenAll - wait for all tasks to complete
Task<string> task1 = FetchData1Async();
Task<string> task2 = FetchData2Async();
Task<string> task3 = FetchData3Async();

string[] results = await Task.WhenAll(task1, task2, task3);

// WhenAny - wait for first task to complete
Task<string> completedTask = await Task.WhenAny(task1, task2, task3);
string firstResult = await completedTask;
💡 Performance Tip

Use Task.WhenAll when you have multiple independent async operations. Instead of waiting 5 seconds for 5 sequential 1-second operations, they all run concurrently and complete in just 1 second!

Error Handling in Async Code

AsyncErrorHandling.cs C#
public async Task<Product> GetProductAsync(string sku)
{
    try
    {
        using HttpClient client = new();
        string json = await client.GetStringAsync($"https://api.example.com/products/{sku}");
        return JsonSerializer.Deserialize<Product>(json);
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"Network error: {ex.Message}");
        return null;
    }
    catch (JsonException ex)
    {
        Console.WriteLine($"Invalid JSON: {ex.Message}");
        return null;
    }
}

// try-catch works the same way with async/await!

Handling Multiple Task Errors

WhenAllErrors.cs C#
try
{
    await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
    // Only the first exception is caught
    // To see all exceptions, check task.Exception
    Console.WriteLine(ex.Message);
}

InvenTrack: Async in Action

InvenTrackAsync.cs C#
class ProductService
{
    private readonly HttpClient httpClient;
    private readonly string apiBaseUrl;
    
    public ProductService()
    {
        httpClient = new HttpClient();
        apiBaseUrl = "https://api.inventrackdemo.com";
    }
    
    // Fetch single product
    public async Task<Product> GetProductAsync(string sku)
    {
        string url = $"{apiBaseUrl}/products/{sku}";
        string json = await httpClient.GetStringAsync(url);
        return JsonSerializer.Deserialize<Product>(json);
    }
    
    // Fetch all products
    public async Task<List<Product>> GetAllProductsAsync()
    {
        string url = $"{apiBaseUrl}/products";
        string json = await httpClient.GetStringAsync(url);
        return JsonSerializer.Deserialize<List<Product>>(json);
    }
    
    // Update product stock
    public async Task UpdateStockAsync(string sku, int newQuantity)
    {
        string url = $"{apiBaseUrl}/products/{sku}/stock";
        var content = new StringContent(
            JsonSerializer.Serialize(new { quantity = newQuantity }),
            Encoding.UTF8,
            "application/json"
        );
        
        await httpClient.PutAsync(url, content);
    }
    
    // Fetch multiple products concurrently
    public async Task<List<Product>> GetProductsBatchAsync(List<string> skus)
    {
        // Create tasks for all SKUs
        var tasks = skus.Select(sku => GetProductAsync(sku)).ToList();
        
        // Wait for all to complete
        Product[] products = await Task.WhenAll(tasks);
        
        return products.ToList();
    }
    
    // Generate daily report (multiple async operations)
    public async Task GenerateDailyReportAsync()
    {
        Console.WriteLine("Generating daily report...");
        
        // Fetch data concurrently
        var productsTask = GetAllProductsAsync();
        var ordersTask = GetTodaysOrdersAsync();
        var customersTask = GetActiveCustomersAsync();
        
        await Task.WhenAll(productsTask, ordersTask, customersTask);
        
        // Process results
        var products = await productsTask;
        var orders = await ordersTask;
        var customers = await customersTask;
        
        // Generate and save report
        string report = BuildReport(products, orders, customers);
        await File.WriteAllTextAsync("daily-report.txt", report);
        
        Console.WriteLine("Report generated!");
    }
}
🚀 Real-World Pattern

Notice how GenerateDailyReportAsync fetches products, orders, and customers concurrently with Task.WhenAll. If each takes 2 seconds, sequential would take 6 seconds, but concurrent takes just 2 seconds!

Best Practices

✅ Do's

✅ Use async/await for I/O operations (file, network, database)
✅ Name async methods with "Async" suffix
✅ Return Task or Task<T>, not void
✅ Use ConfigureAwait(false) in libraries (not UI apps)
✅ Use Task.WhenAll for concurrent operations
✅ Propagate async all the way up (don't block with .Result)

❌ Don'ts

❌ Don't use async for CPU-bound work (use Task.Run instead)
❌ Don't block on async code with .Result or .Wait() (deadlock risk)
❌ Don't use async void except for event handlers
❌ Don't forget to await your tasks
❌ Don't create unnecessary tasks for synchronous work

When to Use Async/Await

Use Async For Don't Use Async For
File I/O operations Simple calculations
Network/HTTP requests In-memory operations
Database queries Property getters/setters
External API calls Constructors
Long-running operations CPU-intensive work (use Task.Run)

Key Takeaways

  • Async/await enables non-blocking I/O operations
  • async keyword marks methods as asynchronous
  • await keyword waits without blocking the thread
  • Task represents an async operation; Task<T> returns a value
  • Task.WhenAll runs multiple operations concurrently
  • Error handling works the same with try-catch
  • Use async for I/O-bound work, not CPU-bound
  • Never block on async code with .Result or .Wait()
  • Async methods should end with "Async" suffix
  • Essential for scalable, responsive applications
🎉 Part I Complete!

Congratulations! You've completed Part I: Foundations. You now understand C# fundamentals, OOP, collections, LINQ, and asynchronous programming. You're ready to build real applications with ASP.NET Core!

Next up: Part II will dive into ASP.NET Core fundamentals—building web APIs, handling HTTP requests, dependency injection, middleware, and more. The real fun begins! 🚀