Featured resource
2025 Tech Upskilling Playbook
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Check it out
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
Labs

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 platform
Lab Info
Last updated
Mar 04, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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, and Task.WhenEach, stream results with IAsyncEnumerableand 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, and Task.WhenEach.
    • Stream results over time with IAsyncEnumerable for 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 async and await, 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. ### __solution folder:

    • codes: The final code for each step is stored in the __solution/code folder. For instance, the final code for Step 2 is available in the __solution/code/Step02 directory.

    • images: This folder contains images showing the lab’s final state for relevant steps. For example, __solution/images/Step02 shows the final state of Step 2.

  2. Challenge

    Create Synchronous Work (Blocking)

    In this step, you will create synchronous work using Thread.Sleep and 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 the delaySeconds value into a TimeSpan object 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.Sleep in server-side or UI code is discouraged, as it can reduce responsiveness and scalability. It is used here strictly for learning and comparison purposes.

    Observe the `Main` function in the `SynchronousWorkDemo.cs` file. Navigate to the first terminal and execute the following command to run the demo.
    cd ~/workspace/MovieListing/Services/Tasks && dotnet run SynchronousWorkDemo.cs
    

    You 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 ms
    

    Typically, dotnet run is 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.
  3. Challenge

    Create Asynchronous Work (Non-Blocking)

    In this step, you will create asynchronous work using async and await, and observe how non-blocking operations keep the UI responsive during long-running tasks. In this step, the work has been simulated using Task.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 the delaySeconds value into a TimeSpan that represents the delay duration.

    • The async keyword allows the method to perform asynchronous operations and return a Task<T> instead of blocking the calling thread.

    • The await keyword 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 await is used, the runtime can reuse the thread for other operations while the asynchronous work is in progress, improving responsiveness and scalability.

    • The async modifier and await keyword must be used together—adding async without await provides no benefit, and using await requires the method to be marked as async.

    • This pattern improves responsiveness and scalability compared to synchronous delays like Thread.Sleep, which blocks the thread.

    • Marking a method as async doesn’t automatically make it non-blocking. If you still use Thread.Sleep, the thread will block, and the app can freeze; to make the functionality truly asynchronous, use await Task.Delay or similar asynchronous functionality instead.

    Navigate to the first terminal and execute the following command to test the above changes.
    cd ~/workspace/MovieListing/Services/Tasks && dotnet run AsynchronousWorkDemo.cs
    

    You 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 ms
    

    Notice 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 +1 a few times to confirm that the counter is responsive.
    • Click the button to start the asynchronous work (this calls the DoWorkAsync method you updated).
    • While the asynchronous work is running, immediately try clicking +1 again.

    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.Delay and observed how it keeps the UI responsive.

  4. 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, and Task.WhenEach.

    Explanation
    • Calling DoWorkAsync for taskA, taskB, and taskC starts 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.WhenAll saves time compared to running each asynchronous task one after another, making this approach more efficient for independent operations.

    • Task.WhenAll returns a Task<string[]> when all provided tasks are of type Task<string>, meaning the results of all completed tasks are collected into an array in the order they were passed in.

    Navigate to the first terminal and execute the following command to test the above changes.
    cd ~/workspace/MovieListing/Services/Tasks && dotnet run TaskWhenAll.cs
    

    You 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.cs
    

    You 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.cs
    

    You 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.
  5. Challenge

    Create Streaming Results

    In this step, you will create asynchronous streams using IAsyncEnumerable to 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 async keyword enables the use of await, while yield return allows values to be produced incrementally as they become available.

    • Each await Task.Delay(...) simulates asynchronous work without blocking the executing thread.

    • When yield return is 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 Main function, the caller consumes the stream using await foreach, which asynchronously iterates over each value returned by RunAsStreamAsync as it becomes available, printing each message to the console without waiting for all values to be returned at once.

    • yield break immediately terminates an iterator method and signals that no more elements will be produced, similar to how return exits a regular method.

    Navigate to the first terminal and execute the following command to test the above changes.
    cd ~/workspace/MovieListing/Services/Tasks && dotnet run SimpleAsyncStream.cs
    

    You 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.cs
    

    You 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.
  6. 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 foreach loop iterates over the collection of URLs and creates a download task for each one by calling DownloadOneAsync, starting all download operations immediately.

    • Adding each task to the tasks list starts all download operations in parallel, allowing multiple URLs to be fetched simultaneously.

    • await foreach with Task.WhenEach(tasks) iterates over each task as they are completed, streaming results in completion order without manually managing the task list.

    • Checking ct.IsCancellationRequested at the start of each iteration allows the streaming process to stop early by calling yield break if a cancellation request has been made.

    • yield return await finished awaits the completed task to retrieve the actual UrlDownloadResult value 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.

    Explanation
    • The Urls array defines five movie-genre API endpoints, each with a simulated delay, enabling movie data to be downloaded from multiple sources.

    • In DownloadMovies, the existing _cts is disposed to clean up any previous download operation before starting a new one.

    • A new CancellationTokenSource is created and assigned to the _cts field, providing a fresh cancellation signal for each download session.

    • Passing _cts.Token to DownloadAsStreamAsync allows the streaming process to observe and respond to cancellation requests.

    • The Cancel method invokes _cts?.Cancel(), signalling all in-progress download operations to stop gracefully. The null-conditional operator (?.) ensures no error is thrown if _cts has not been initialised.

    • This approach provides controlled, cancellable data streaming from multiple URLs, which is essential for responsive, UI-driven applications.

    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.
    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

Asmin Bhandari is a full stack developer with years of experience in designing, developing and testing many applications and web based systems.

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.

Get started with Pluralsight