ASP.NET Core Using Tasks and Cancellation Tokens Step by step Implementation and Top 10 Questions and Answers
 Last Update: April 01, 2025      12 mins read      Difficulty-Level: beginner

Understanding ASP.NET Core Using Tasks and Cancellation Tokens: A Comprehensive Guide for Beginners

When developing high-performance applications in ASP.NET Core, managing asynchronous operations and resource usage efficiently is crucial. Tasks and cancellation tokens are powerful tools provided by .NET Core to handle concurrency, perform long-running operations, and provide a way to cancel these operations gracefully. In this step-by-step guide, we will dive deep into understanding how to leverage these features to build robust, scalable, and responsive applications.

What are Tasks?

In .NET, a Task represents a single operation that might run asynchronously. Tasks are part of the Task-based Asynchronous Pattern (TAP) introduced in .NET Framework 4.0 and further enhanced in subsequent versions. Tasks enable developers to perform background operations, improving application responsiveness by not blocking the main thread.

Step 1: Creating and Starting a Task

To create and start a task, you can use the Task.Run method, which queues the task to the thread pool and returns a Task object that represents the operation. Here's a simple example:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        Task task = Task.Run(() =>
        {
            // Simulate a long-running operation
            Task.Delay(5000).Wait();
            Console.WriteLine("Task completed.");
        });

        Console.WriteLine("Main thread is free to do other work.");

        await task; // Await the task completion
        Console.WriteLine("Task is finished and main thread can continue.");
    }
}

In this example, a long-running operation is simulated using Task.Delay. The main thread doesn't block and is free to perform other tasks while the task is running.

Step 2: Returning Values from Tasks

If you need to perform a task that returns a result, use Task<TResult>. The result can be accessed by awaiting the task.

static async Task Main(string[] args)
{
    Task<int> task = Task.Run(() =>
    {
        Task.Delay(5000).Wait();
        return 42; // Return a result
    });

    Console.WriteLine("Main thread is free to do other work.");

    int result = await task; // Await the task and get the result
    Console.WriteLine($"Task completed with result: {result}");
}

Step 3: Handling Exceptions

When working with tasks, exceptions can occur. It's crucial to handle these exceptions properly to avoid unexpected errors.

static async Task Main(string[] args)
{
    Task<int> task = Task.Run(() =>
    {
        Task.Delay(5000).Wait();
        throw new InvalidOperationException("An error occurred in the task.");
    });

    Console.WriteLine("Main thread is free to do other work.");

    try
    {
        int result = await task; // Await the task and handle exceptions
        Console.WriteLine($"Task completed with result: {result}");
    }
    catch (AggregateException ex)
    {
        // Handle exception from the task
        Console.WriteLine($"Exception occurred: {ex.Message}");
    }
}

// Note: In modern .NET, AggregateException is flattened by 'await',
// and you can catch the inner exception directly.
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Exception occurred: {ex.Message}");
}

Step 4: Parallel Tasks

You may want to run multiple tasks in parallel to improve performance. Use Task.WhenAll to wait for all tasks to complete.

static async Task Main(string[] args)
{
    Task task1 = Task.Run(() =>
    {
        Task.Delay(3000).Wait();
        Console.WriteLine("Task 1 completed.");
    });

    Task task2 = Task.Run(() =>
    {
        Task.Delay(6000).Wait();
        Console.WriteLine("Task 2 completed.");
    });

    Console.WriteLine("Main thread is free to do other work.");

    await Task.WhenAll(task1, task2); // Wait for all tasks to complete
    Console.WriteLine("All tasks are finished and main thread can continue.");
}

Cancellation Tokens

A cancellation token is a mechanism to communicate a cancellation request from the main thread to long-running tasks. It helps in canceling operations gracefully without abrupt terminations.

Step 5: Setting Up a Cancellation Token

To use a cancellation token, you need to create a CancellationTokenSource and pass its Token to the task.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        Task task = Task.Run(() =>
        {
            for (int i = 0; i < 10; i++)
            {
                // Check if cancellation has been requested
                if (cts.IsCancellationRequested)
                {
                    Console.WriteLine("Cancellation requested. Stopping the task...");
                    cts.Token.ThrowIfCancellationRequested();
                }

                // Simulate work
                Task.Delay(500).Wait();
                Console.WriteLine($"Working... {i + 1}");
            }
        }, cts.Token);

        Console.WriteLine("Main thread is free to do other work. Press 'Enter' to cancel the task...");

        // Wait for user input to cancel the task
        Console.ReadLine();
        cts.Cancel();

        try
        {
            await task; // Await the task and handle exceptions
            Console.WriteLine("Task completed successfully.");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Task was canceled.");
        }
        finally
        {
            // Dispose the CancellationTokenSource
            cts.Dispose();
        }
    }
}

In this example, the CancellationTokenSource (cts) is used to create a cancellation token. The task checks for cancellation requests periodically using cts.IsCancellationRequested and throws an OperationCanceledException if cancellation is requested.

Step 6: Propagating Cancellation Tokens

Tasks can propagate cancellation tokens to other tasks, allowing you to cancel multiple tasks simultaneously. Here's an example:

static async Task Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();

    Task task1 = Task.Run(() =>
    {
        for (int i = 0; i < 10; i++)
        {
            if (cts.IsCancellationRequested)
            {
                Console.WriteLine("Task 1: Cancellation requested. Stopping...");
                cts.Token.ThrowIfCancellationRequested();
            }

            Task.Delay(500).Wait();
            Console.WriteLine($"Task 1 is working... {i + 1}");
        }
    }, cts.Token);

    Task task2 = Task.Run(() =>
    {
        for (int i = 0; i < 5; i++)
        {
            if (cts.IsCancellationRequested)
            {
                Console.WriteLine("Task 2: Cancellation requested. Stopping...");
                cts.Token.ThrowIfCancellationRequested();
            }

            Task.Delay(1000).Wait();
            Console.WriteLine($"Task 2 is working... {i + 1}");
        }
    }, cts.Token);

    Console.WriteLine("Main thread is free to do other work. Press 'Enter' to cancel the tasks...");

    Console.ReadLine();
    cts.Cancel();

    try
    {
        await Task.WhenAll(task1, task2); // Wait for all tasks to complete
        Console.WriteLine("All tasks completed successfully.");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Tasks were canceled.");
    }
    finally
    {
        cts.Dispose();
    }
}

In this example, both task1 and task2 receive the same cancellation token, allowing them to be canceled simultaneously.

Best Practices

  1. Use Cancellation Tokens: Always provide cancellation tokens in asynchronous methods that perform long operations, enabling users to cancel them gracefully.

  2. Avoid Blocking Calls: Never use .Wait() or .Result on tasks in ASP.NET Core, as they can lead to deadlocks. Always use await.

  3. Proper Exception Handling: Handle exceptions in tasks properly to avoid unhandled exceptions leading to application crashes.

  4. Resource Management: Always dispose of CancellationTokenSource objects to release resources.

  5. Concurrency Control: Use Task.WhenAll and Task.WhenAny to manage multiple tasks efficiently.

Real-world Example in ASP.NET Core

Let's see how to apply these concepts in a real-world ASP.NET Core application scenario.

Scenario: Downloading multiple files simultaneously with a timeout and cancellation capability.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class FileDownloader
{
    private readonly HttpClient _httpClient;

    public FileDownloader(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task DownloadFilesAsync(List<string> urls, string downloadFolder, CancellationToken cancellationToken)
    {
        List<Task> tasks = new List<Task>();
        foreach (var url in urls)
        {
            Task task = DownloadFileAsync(url, downloadFolder, cancellationToken);
            tasks.Add(task);
        }

        try
        {
            // Set a timeout of 30 seconds
            await Task.WhenAll(tasks).TimeoutAfter(30000, cancellationToken);
            Console.WriteLine("All files downloaded successfully.");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("File download canceled.");
        }
        catch (TimeoutException)
        {
            Console.WriteLine("File download timed out.");
        }
    }

    private async Task DownloadFileAsync(string url, string downloadFolder, CancellationToken cancellationToken)
    {
        try
        {
            using (var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
            {
                response.EnsureSuccessStatusCode();
                string fileName = Path.GetFileName(url);
                string filePath = Path.Combine(downloadFolder, fileName);

                using (var contentStream = await response.Content.ReadAsStreamAsync())
                using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
                {
                    await contentStream.CopyToAsync(fileStream, cancellationToken);
                }

                Console.WriteLine($"File {fileName} downloaded successfully.");
            }
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine($"File download for {url} canceled.");
            throw;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error downloading file {url}: {ex.Message}");
            throw;
        }
    }
}

public static class TaskExtensions
{
    public static Task TimeoutAfter(this Task task, int millisecondsTimeout, CancellationToken cancellationToken)
    {
        TaskDelay delay = Task.Delay(millisecondsTimeout, cancellationToken);
        Task completedTask = Task.WhenAny(task, delay);

        return completedTask.ContinueWith(taskResult =>
        {
            if (taskResult != task || task.IsFaulted || task.IsCanceled)
                return;

            // Throw a TimeoutException if the delay completed first
            throw new TimeoutException();
        });
    }
}

class Program
{
    static async Task Main(string[] args)
    {
        using (HttpClient client = new HttpClient())
        {
            FileDownloader downloader = new FileDownloader(client);
            List<string> urls = new List<string>
            {
                "https://example.com/file1.pdf",
                "https://example.com/file2.jpg",
                "https://example.com/file3.zip"
            };

            CancellationTokenSource cts = new CancellationTokenSource();

            Task downloadTask = downloader.DownloadFilesAsync(urls, ".", cts.Token);

            Console.WriteLine("Downloading files... Press 'Enter' to cancel.");
            Console.ReadLine();
            cts.Cancel();

            await downloadTask;

            cts.Dispose();
        }
    }
}

Explanation:

  • HttpClient: Used to download files from URLs.
  • FileDownloader: A class that handles file downloads using cancellation tokens.
  • DownloadFilesAsync: Downloads multiple files simultaneously and handles cancellation.
  • DownloadFileAsync: Downloads a single file and handles cancellation.
  • TaskExtensions.TimeoutAfter: Extension method to add a timeout to a task.

This example demonstrates the practical application of tasks and cancellation tokens in a web application, ensuring efficient and responsive operations.

Conclusion

Tasks and cancellation tokens are essential tools in ASP.NET Core for handling asynchronous operations and managing resources effectively. By understanding how to create, manage, and cancel tasks using cancellation tokens, you can build scalable and resilient applications. Remember to follow best practices to ensure your applications remain robust and efficient. Happy coding!