C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
53: Handling Multiple Async Operations with Task.WhenAll
A few years ago, I was reviewing a PR for a teammate who was building a user dashboard. The page had to pull data from three different sources: the user's profile, their recent order history, and their notification settings. When I ran the page, it felt sluggish—almost a three-second delay before anything appeared. Looking at the code, I saw exactly why:
var profile = await _profileService.GetProfileAsync(userId);
var orders = await _orderService.GetRecentOrdersAsync(userId);
var settings = await _settingsService.GetSettingsAsync(userId);
They were awaiting each call sequentially. The code would start the profile request, stop everything and wait for it to finish, then start the orders request, wait again, and so on. Since these three calls didn't depend on each other, we were wasting a massive amount of time. I showed them how to use Task.WhenAll, and we got that load time down to the duration of the single slowest request. It's a simple change, but it's one of the most impactful performance wins you can implement in an async C# application.
Stopping the Sequential Wait
When you await a task immediately, you are telling the runtime, "Don't move to the next line of code until this specific operation is done." That's perfectly fine if the second operation needs a value from the first. But if you're just gathering data from a few independent sources, you're creating a bottleneck.
The trick is to start the tasks without awaiting them immediately. When you call an async method, it returns a Task object that represents the ongoing work. You can store these tasks in variables or a list, and they will all begin executing in the background. Once you have all your "promises" started, you use Task.WhenAll to pause execution until every single one of them has completed.
// Start all three tasks. They are now running concurrently.
Task<UserProfile> profileTask = _profileService.GetProfileAsync(userId);
Task<List<Order>> ordersTask = _orderService.GetRecentOrdersAsync(userId);
Task<UserSettings> settingsTask = _settingsService.GetSettingsAsync(userId);
// Now, wait for all of them to finish.
await Task.WhenAll(profileTask, ordersTask, settingsTask);
// Now it's safe to access the .Result or use await again (which will return immediately)
var profile = profileTask.Result;
var orders = ordersTask.Result;
var settings = settingsTask.Result;
Handling Results from Collections
In the example above, we had a fixed number of tasks. But what happens if you have a list of 50 IDs and you need to fetch data for all of them? You can't write 50 separate variables. Instead, you can project your list of IDs into a list of Tasks using LINQ.
I've seen developers try to use a foreach loop with an await inside it to solve this. Please, don't do that. That's just sequential execution in disguise. Instead, create a collection of tasks and pass that collection to WhenAll.
List<int> userIds = new() { 1, 2, 3, 4, 5 };
// Create a list of tasks. Note: we aren't awaiting inside the Select!
IEnumerable<Task<User>> downloadTasks = userIds.Select(id => _userService.GetUserAsync(id));
// Await the entire group.
User[] users = await Task.WhenAll(downloadTasks);
One detail I want you to notice here: Task.WhenAll is smart. If you give it a collection of Task<T>, it returns a single task that, when awaited, yields an array of T[] containing the results in the same order as the tasks you provided. It handles the bookkeeping for you.
The Nuance of Exceptions
Here is where things get a bit tricky. If one or more of the tasks passed to Task.WhenAll fails, the resulting task will be faulted. However, if you simply await Task.WhenAll(...), the runtime will only throw the exception from the first task that failed. The others are essentially swallowed by the await keyword.
If you need to know about every single failure—for instance, if you're logging errors for several different API calls—you'll need to examine the Exception property of the task returned by WhenAll. That property contains an AggregateException, which wraps all the individual failures into one object. I usually recommend a try-catch block around the WhenAll, and if you need the full details, casting the caught exception or checking the original tasks individually.
📋 Practical Task
Exercise: Concurrent Stock Price Aggregator
You are building a financial dashboard that needs to fetch the current price of several different stocks. Currently, the code fetches them one by one, which is too slow for a real-time dashboard.
Your Goal: Refactor the PriceAggregator class to fetch all stock prices concurrently using Task.WhenAll.
public class StockPriceService
{
// Simulates an API call that takes time
public async Task<decimal> GetPriceAsync(string symbol)
{
await Task.Delay(500); // Simulate network lag
return new Random().Next(10, 500);
}
}
public class PriceAggregator
{
private readonly StockPriceService _service = new();
public async Task<Dictionary<string, decimal>> GetPortfolioPricesAsync(List<string> symbols)
{
var results = new Dictionary<string, decimal>();
// TODO: Refactor this loop to use Task.WhenAll
// Currently, this is sequential and slow!
foreach (var symbol in symbols)
{
decimal price = await _service.GetPriceAsync(symbol);
results.Add(symbol, price);
}
return results;
}
}
Requirements:
- Modify
GetPortfolioPricesAsyncso that allGetPriceAsynccalls start simultaneously. - Ensure the final dictionary is populated correctly with the symbols and their corresponding prices.
- The total execution time for the method should be roughly 500ms, regardless of how many symbols are in the list.
There are no comments for now.