- Lab
-
Libraries: If you want this lab, consider one of these libraries.
Guided: C# 14 Async Coordination
Develop a solid understanding of asynchronous programming with this hands-on Guided: C# 14 Async Coordination lab. Through practical exercises, you’ll explore synchronous and asynchronous execution, coordinate multiple tasks, stream results as they become available, and apply cancellation to control long-running operations. By working with real-world scenarios such as UI responsiveness and multi-URL data streaming, you’ll gain the skills needed to build efficient, non-blocking, and responsive C# applications.
Lab Info
Table of Contents
-
Challenge
Introduction
Welcome to the Guided: C# 14 Async Coordination Challenge lab. This hands-on Code Lab is designed for developers who want to build responsive and efficient applications by mastering modern asynchronous programming concepts in C# 14. Throughout this lab, you’ll work through structured, practical exercises to explore synchronous and asynchronous execution, coordinate multiple tasks using
Task.WhenAll,Task.WhenAny, andTask.WhenEach, stream results withIAsyncEnumerableand handle cancellations in long-running operations.By the end of this Code Lab, you’ll be confident in designing real-world asynchronous workflows, including parallel task execution, streaming data from multiple sources, and cancellation handling. You’ll gain a strong understanding of how to apply C# async patterns to build non-blocking, responsive applications using best practices. ### Key Takeaways:
- Understand the difference between synchronous and asynchronous execution in C#.
- Coordinate multiple asynchronous tasks using
Task.WhenAll,Task.WhenAny, andTask.WhenEach. - Stream results over time with
IAsyncEnumerablefor responsive workflows. - Apply cancellation to safely control long-running and streaming operations. ### Prerequisites
Basic C# Knowledge
- Learners should be familiar with core C# concepts such as classes, methods, variables, and basic control flow.
Introductory Asynchronous Concepts
- A basic understanding of asynchronous programming concepts, such as
asyncandawait, is helpful but not required.
Text Editor, Terminal & Browser Experience
-
Comfort using a text editor or IDE.
-
Experience with basic command-line operations, such as navigating directories and running C# applications.
-
Ability to run, test, and observe application behaviour in the terminal and interact with browser-based demos used in the lab. ### Movie Listing App
A simple Movie Listing app has been provided for this lab. The app is built using Blazor, but no prior Blazor experience is required, as you will primarily work with C# files.
Five API endpoints have been created to fetch movie listings by genre: action, comedy, drama, horror, and sci-fi. Artificial delays simulate long-running processes; in real-world scenarios, these would correspond to operations such as API requests, database queries, or file read/write operations.
Throughout the lab, you will enhance the application by implementing both synchronous and asynchronous workflows and observing their impact on UI responsiveness. You will also build a multi-URL downloader using the provided API endpoints to stream movie data into the listing page and add support for cancelling in-progress streams. ###
__solutionfolder:-
codes: The final code for each step is stored in the__solution/codefolder. For instance, the final code for Step 2 is available in the__solution/code/Step02directory. -
images: This folder contains images showing the lab’s final state for relevant steps. For example,__solution/images/Step02shows the final state of Step 2.
-
Challenge
Create Synchronous Work (Blocking)
In this step, you will create synchronous work using
Thread.Sleepand test it in the browser to observe how blocking operations affect application responsiveness and UI behaviour.Explanation -
Thread.Sleep(TimeSpan.FromSeconds(delaySeconds))introduces a synchronous, blocking delay by pausing the current thread for the specified number of seconds. -
TimeSpan.FromSeconds(delaySeconds)converts thedelaySecondsvalue into aTimeSpanobject that represents the delay duration. -
During this sleep period, the executing thread cannot perform any other work, leaving it unavailable to handle additional tasks.
-
This is useful for demonstrating blocking behaviour in synchronous code, especially when comparing it later with asynchronous alternatives such as
Task.Delay. -
Because the method is fully synchronous, the caller will not regain control until the delay completes and the method finishes execution.
-
In real-world applications, using
Thread.Sleepin server-side or UI code is discouraged, as it can reduce responsiveness and scalability. It is used here strictly for learning and comparison purposes.
cd ~/workspace/MovieListing/Services/Tasks && dotnet run SynchronousWorkDemo.csYou should see a response in your terminal as shown in the example response below.
20:48:20.289 Start 20:48:20.314 Task A started 20:48:23.315 Task A finished Task A executed in 3 seconds 20:48:23.316 Task B started 20:48:25.316 Task B finished Task B executed in 2 seconds 20:48:25.316 All tasks completed Total execution time: 5027 msTypically,
dotnet runis used to build and run an entire .NET project from the project root. For this lab, we run it from the Tasks folder to keep examples simple and focused on the core concepts being taught. Navigate to the second terminal and execute the following command to start the development server.cd ~/workspace/MovieListing && dotnet watch --restart ``` Now, navigate to the URL {{localhost}}/sync-work-demo in your browser. Note: If you see `An unhandled error has occurred. Reload` in your browser, hard refresh the page using `Ctrl/Cmd+Shift+R`. - Click `+1` a few times to confirm that the counter is responsive. - Click the button to start the synchronous work (this calls the `DoWorkSync` method you updated). - Immediately try clicking `+1` again while the synchronous work is running. You should notice that the UI freezes: the counter won’t increment after you click the button, and the page won’t respond until the synchronous work finishes. Once the work is completed, the UI becomes responsive again, and the counter starts working normally. Congratulations! In this step, you created synchronous work using `Thread.Sleep` that blocks the executing thread and freezes the UI. This demonstrates why long-running synchronous operations are problematic in interactive applications and prepares you to explore asynchronous, non-blocking alternatives next. -
-
Challenge
Create Asynchronous Work (Non-Blocking)
In this step, you will create asynchronous work using
asyncandawait, and observe how non-blocking operations keep the UI responsive during long-running tasks. In this step, the work has been simulated usingTask.Delay, but in real-world scenarios, it would involve operations such as fetching data from an API endpoint, querying a database, or reading from and writing to a file.Explanation -
await Task.Delay(TimeSpan.FromSeconds(delaySeconds))introduces a non-blocking delay by asynchronously waiting for the specified number of seconds. While waiting, control is returned to the caller instead of blocking the current thread. -
TimeSpan.FromSeconds(delaySeconds)converts thedelaySecondsvalue into aTimeSpanthat represents the delay duration. -
The
asynckeyword allows the method to perform asynchronous operations and return aTask<T>instead of blocking the calling thread. -
The
awaitkeyword pauses execution of the method until the delay completes, but does so asynchronously, allowing other work (such as UI updates or request handling) to continue. -
When
awaitis used, the runtime can reuse the thread for other operations while the asynchronous work is in progress, improving responsiveness and scalability. -
The
asyncmodifier andawaitkeyword must be used together—addingasyncwithoutawaitprovides no benefit, and usingawaitrequires the method to be marked asasync. -
This pattern improves responsiveness and scalability compared to synchronous delays like
Thread.Sleep, which blocks the thread. -
Marking a method as
asyncdoesn’t automatically make it non-blocking. If you still useThread.Sleep, the thread will block, and the app can freeze; to make the functionality truly asynchronous, useawait Task.Delayor similar asynchronous functionality instead.
cd ~/workspace/MovieListing/Services/Tasks && dotnet run AsynchronousWorkDemo.csYou should see a response in your terminal as shown in the example response below.
06:06:31.293 Start 06:06:31.318 Task A started 06:06:34.321 Task A finished Task A executed in 3 seconds 06:06:34.322 Task B started 06:06:36.322 Task B finished Task B executed in 2 seconds 06:06:36.322 All tasks completed Total execution time: 5029 msNotice that the total execution time is similar to the synchronous version. This is because the tasks are still being awaited one after the other. The key difference is that the thread is no longer blocked during the delay, keeping the UI responsive. To reduce the total execution time, the tasks need to be run in parallel, which you will explore in the next step. Now, navigate to the URL {{localhost}}/async-work-demo in your browser.
- Click
+1a few times to confirm that the counter is responsive. - Click the button to start the asynchronous work (this calls the
DoWorkAsyncmethod you updated). - While the asynchronous work is running, immediately try clicking
+1again.
You should notice that the UI remains responsive. The counter continues to increment because the asynchronous work does not block the executing thread. Congratulations! In this step, you implemented truly asynchronous work using
Task.Delayand observed how it keeps the UI responsive. -
-
Challenge
Handle Multiple Asynchronous Tasks in Parallel
In this step, you will run multiple asynchronous tasks in parallel and learn how to coordinate their execution using
Task.WhenAll,Task.WhenAny, andTask.WhenEach.Explanation -
Calling
DoWorkAsyncfortaskA,taskB, andtaskCstarts all three asynchronous operations immediately and in parallel. -
Task.WhenAll(taskA, taskB, taskC)creates a single task that completes only when all the provided tasks have finished. -
await Task.WhenAll(...)asynchronously waits for every task to complete without blocking the executing thread. -
The total execution time is determined by the longest-running task in the group, since the method completes only after all tasks have finished.
-
Executing tasks in parallel using
Task.WhenAllsaves time compared to running each asynchronous task one after another, making this approach more efficient for independent operations. -
Task.WhenAllreturns aTask<string[]>when all provided tasks are of typeTask<string>, meaning the results of all completed tasks are collected into an array in the order they were passed in.
cd ~/workspace/MovieListing/Services/Tasks && dotnet run TaskWhenAll.csYou should see a response in your terminal as shown in the example response below.
12:29:22 Start 12:29:22 Task A started 12:29:22 Task B started 12:29:22 Task C started 12:29:24 Task B finished 12:29:25 Task A finished 12:29:26 Task C finished Task A executed in 3 seconds Task B executed in 2 seconds Task C executed in 4 seconds 12:29:26 All tasks completed Total execution time1: 4041 ms ``` <details> <summary>Explanation</summary> - `Task.WhenAny(tasks)` returns a task that completes when any one of the tasks in the list finishes. - `await Task.WhenAny(tasks)` gives you the first completed task as a `Task<string>` (not the string result yet), which is why `completedTask` is typed as `Task<string>`. - After a task completes, you remove it from the list using `tasks.Remove(completedTask)` . So, the loop can continue waiting for the remaining tasks. - You then `await completedTask` to retrieve the actual string result produced by `DoWorkAsync`. - This loop repeats until the list is empty, allowing you to process results as soon as they are ready and in the order the tasks are completed, which is the main benefit of `Task.WhenAny`. - The `while` loop combined with `Task.WhenAny` is a recognised C# async pattern for processing tasks as they complete. It allows results to be handled in completion order rather than the order in which the tasks were started. </details> Navigate to the first terminal and execute the following command to test the above changes. ```bash cd ~/workspace/MovieListing/Services/Tasks && dotnet run TaskWhenAny.csYou should see a response in your terminal as shown in the example response below.
07:54:13.724 Start 07:54:13.754 Task A started 07:54:13.758 Task B started 07:54:13.758 Task C started 07:54:15.762 Task B finished 07:54:15.764 Task completed: Task B executed in 2 seconds 07:54:16.757 Task A finished 07:54:16.757 Task completed: Task A executed in 3 seconds 07:54:17.758 Task C finished 07:54:17.758 Task completed: Task C executed in 4 seconds 07:54:17.759 All tasks completed (in completion order): Task B executed in 2 seconds Task A executed in 3 seconds Task C executed in 4 seconds Elapsed time: 4035 ms ``` <details> <summary>Explanation</summary> - `Task.WhenEach(tasks)` returns an `IAsyncEnumerable<Task<string>>` that yields each task as it completes, in completion order. - Unlike `Task.WhenAny`, you do not need to manually manage a loop or remove completed tasks from a list. `Task.WhenEach` handles that internally. - `await foreach` iterates over the async stream, waiting for the next completed task to become available. - Each `completedTask` represents a finished task, but you still `await completedTask` to retrieve the actual `string` result (and correctly handle exceptions). - The `message` is added to the `results` list, so results are collected in the order tasks finish, not the order they were started. - This continues automatically until all tasks have completed. - `Task.WhenEach` (introduced in .NET 9) provides a built-in async streaming abstraction over the common `while + Task.WhenAny + Remove` pattern, allowing tasks to be processed in completion order without manually managing the task list. </details> Navigate to the first terminal and execute the following command to test the above changes. ```bash cd ~/workspace/MovieListing/Services/Tasks && dotnet run TaskWhenEach.csYou should see a response in your terminal as shown in the example response below.
18:59:29.656 Start 18:59:29.702 Task A started 18:59:29.704 Task B started 18:59:29.704 Task C started 18:59:31.707 Task B finished 18:59:32.703 Task A finished 18:59:33.704 Task C finished 18:59:33.704 All tasks completed (in completion order): Task B executed in 2 seconds Task A executed in 3 seconds Task C executed in 4 seconds Elapsed time: 4048 ms ``` Congratulations! In this step, you used `Task.WhenAll`, `Task.WhenAny`, and `Task.WhenEach` to run multiple asynchronous tasks in parallel and coordinate their completion effectively. You learned when to wait for all tasks to finish, when to respond as soon as the first task completes, and how to process tasks as they complete one by one. -
-
Challenge
Create Streaming Results
In this step, you will create asynchronous streams using
IAsyncEnumerableto emit results over time and explore how streaming operations can be cancelled in a controlled way.Explanation -
Adding the return type of
IAsyncEnumerable<string>allows the method to stream results asynchronously rather than returning all results at once. -
The
asynckeyword enables the use ofawait, whileyield returnallows values to be produced incrementally as they become available. -
Each
await Task.Delay(...)simulates asynchronous work without blocking the executing thread. -
When
yield returnis executed, the current value is sent to the caller immediately, and the method pauses until the next iteration is requested. -
This means the caller can start processing results as soon as they arrive, rather than waiting for the entire method to finish.
-
The total elapsed time is cumulative: the first message appears after 2 seconds, the second after 5 seconds, and the third after 9 seconds.
-
This pattern is useful for scenarios such as streaming data, progress updates, or handling long-running operations where partial results are valuable.
-
In the
Mainfunction, the caller consumes the stream usingawait foreach, which asynchronously iterates over each value returned byRunAsStreamAsyncas it becomes available, printing each message to the console without waiting for all values to be returned at once. -
yield breakimmediately terminates an iterator method and signals that no more elements will be produced, similar to howreturnexits a regular method.
cd ~/workspace/MovieListing/Services/Tasks && dotnet run SimpleAsyncStream.csYou should see a response in your terminal as shown in the example response below.
22:59:59.987 Start 23:00:02.017 STREAM: Task A executed 23:00:05.018 STREAM: Task B executed 23:00:09.018 STREAM: Task C executed 23:00:09.019 Done Elapsed time: 9032 ms ``` <details> <summary>Explanation</summary> - `CancellationTokenSource` is used to create and control a cancellation signal that can be shared across asynchronous operations. - The `using` keyword ensures that the `CancellationTokenSource` is automatically disposed of when it goes out of scope, releasing any resources it holds. - Passing `TimeSpan.FromSeconds(cancelAfterSeconds)` to the `CancellationTokenSource` constructor automatically schedules a cancellation request after the specified duration. The token is then passed to `RunAsStreamAsync` via `.WithCancellation(cts.Token)`, allowing the asynchronous stream to observe cancellation requests. For this task, cancellation is triggered after 6 seconds. - The `await foreach` loop processes each streamed value as it becomes available. - When cancellation is triggered, the asynchronous stream throws an `OperationCanceledException`, which is caught by the `try/catch` block and handled gracefully. - The `CancellationToken ct = default` parameter allows the stream method to accept an optional cancellation token. If no token is provided, it defaults to `CancellationToken.None`, meaning no cancellation will occur. - The `[EnumeratorCancellation]` attribute is required in an `async IAsyncEnumerable<T>` method to ensure that a cancellation token passed via `.WithCancellation()` in an `await foreach` loop is correctly forwarded to the method’s `ct` parameter. - The cancellation token `ct` is passed to each `Task.Delay` call, so when cancellation is triggered, the delay throws an `OperationCanceledException` immediately instead of waiting for the full duration. </details> Navigate to the first terminal and execute the following command to test the above changes. ```bash cd ~/workspace/MovieListing/Services/Tasks && dotnet run SimpleAsyncStreamWithCancellation.csYou should see a response in your terminal as shown in the example response below.
119:25:31.799 Start 19:25:33.844 STREAM: Task A executed 19:25:36.846 STREAM: Task B executed Stream cancelled after 6 seconds 19:25:37.846 Stream completed Elapsed time: 6046 ms ``` Congratulations! In this step, you created an asynchronous stream using `IAsyncEnumerable` and learned how to produce results over time. You also added cancellation support to safely stop streaming operations, reinforcing how streaming and cancellation work together in real-world asynchronous workflows. -
-
Challenge
Build a Multi-URL Downloader
In this step, you will build a multi-URL asynchronous streaming downloader and explore key async programming concepts, including task coordination, streaming results, and cancellation, to create responsive applications.
An API endpoint has been created for this lab to fetch movies by genre and simulate a delay. Five URLs are provided to retrieve data for five different genres.
Navigate to the URL {{localhost}}/api/genres/comedy?delay=1 in your browser to see an example response:
{ "genre": "comedy", "movies": [{ "title": "Superbad", "year": 2007 }, { "title": "The Mask", "year": 1994 }] }In real-world applications, API calls and similar operations such as file input and output or database queries can take significant time to complete. Asynchronous programming allows an application to continue processing other work while waiting for these operations to finish, improving responsiveness and supporting better scalability.
Explanation -
The
foreachloop iterates over the collection of URLs and creates a download task for each one by callingDownloadOneAsync, starting all download operations immediately. -
Adding each task to the
taskslist starts all download operations in parallel, allowing multiple URLs to be fetched simultaneously. -
await foreachwithTask.WhenEach(tasks)iterates over each task as they are completed, streaming results in completion order without manually managing the task list. -
Checking
ct.IsCancellationRequestedat the start of each iteration allows the streaming process to stop early by callingyield breakif a cancellation request has been made. -
yield return await finishedawaits the completed task to retrieve the actualUrlDownloadResultvalue and streams it to the caller immediately. -
This pattern combines parallel execution, streaming results, and cancellation support, making it well-suited for downloading data from multiple URLs simultaneously.
Navigate to the second terminal and press `Ctrl + C` to stop the current process. Then execute the following command to build and run the application.Explanation
-
The
Urlsarray defines five movie-genre API endpoints, each with a simulated delay, enabling movie data to be downloaded from multiple sources. -
In
DownloadMovies, the existing_ctsis disposed to clean up any previous download operation before starting a new one. -
A new
CancellationTokenSourceis created and assigned to the_ctsfield, providing a fresh cancellation signal for each download session. -
Passing
_cts.TokentoDownloadAsStreamAsyncallows the streaming process to observe and respond to cancellation requests. -
The
Cancelmethod invokes_cts?.Cancel(), signalling all in-progress download operations to stop gracefully. The null-conditional operator (?.) ensures no error is thrown if_ctshas not been initialised. -
This approach provides controlled, cancellable data streaming from multiple URLs, which is essential for responsive, UI-driven applications.
cd ~/workspace/MovieListing && dotnet run ``` In the Movie Listing App, the Movies page displays streamed results from five genres and allows users to cancel the stream. This page uses the `DownloadMovies` method to start streaming and the `Cancel` method to stop it. In this step, you added the respective functionality to these methods. Now, navigate to the URL {{localhost}}/movies in your browser. - Click the `Load` button to start loading the movies. - Click the `Cancel` button to stop the movie stream. Congratulations! You’ve completed the lab by building a multi-URL asynchronous streaming downloader with parallel execution and cancellation support. You now have hands-on experience with synchronous and asynchronous execution, coordinating multiple tasks using `Task.WhenAll` , `Task.WhenAny` and `Task.WhenEach`, streaming results with `IAsyncEnumerable`and handling cancellations in long-running operations. -
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.