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
asyncandawaitkeywords - Working with
TaskandTask<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?
// 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
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.
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
// 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 |
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.
// 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
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
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
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)
// Takes 3 seconds total (1 + 2)
await Task1Async(); // 1 second
await Task2Async(); // 2 seconds
Concurrent (At the Same Time)
// 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
// 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;
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
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
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
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!");
}
}
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
✅ 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'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
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! 🚀