.NET 8: Task Chaining and Exception Handling while using Tasks

This is a go-back article for .NET Developers. The Task Parallel Library (TPL) was introduced long back in .NET Framework 4.0. The TPL have simplified the Threading. Several developers those who have faced challenges while using Threading for concurrent as well as Asynchronous programming, the TPL have made the programming easy and maintainable. The TPL provides several classes but very frequently used classes are Task and Parallel classes.

The Task class in C# is part of the System.Threading.Tasks namespace and is used for handling asynchronous programming. Task class represents an operation that executes asynchronously and returns a result in the future.  The Task class have following features:

Asynchronous Execution: Helps to run operations asynchronously without blocking the main thread.

Parallel Processing: Supports concurrent execution.

Chaining Tasks: Enables execution of dependent tasks using the ContinueWith method.

Exception Handling: Provides built-in error management using Try, Catch, and Finally.

Cancellation Support: This helps to cancel the Task using CancellationToken.

Let's implement the application as shown in Figure 1



Figure 1: The Task Chaining

As shown in Figure 1, the Main Thread will use the Task 1 to read data from the File1.txt, once the data is read from this file, it will be returned from the Task 1, further the execution will be continued to Task 2, where the contents from File1.txt will be passed to Task 2 and Task 2 will write these contents to the File2.txt. In this case, there may be an issue occur when the Filr1.txt will be no accessible to Task 1, in this case we can either handle an exception that will put the Task1 in the Faulted state. We can pass this faulted state to Task2 and handle error in Task 2. 

Let's create a .NET 8 targeted application named CS_TaskChanining. In this project, add a new class file named FileOperations.cs. In this file, we will create class named FileOperations. In this class, we will add methods named ReadFile() and WriteFile() to read data from first file and write to another file respectively.  Listing 1, shows the code for FileOperations class.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CS_TaskContinue_With
{
    internal class FileOperations
    {
        public string ReadFile(string fileName)
        {
            try
            {
                FileStream Fs = File.OpenRead(fileName);
                StreamReader sr = new StreamReader(Fs);
                string fileContent = sr.ReadToEnd();
                sr.Close();
                Fs.Close();
                return fileContent;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        public bool WriteFile(string fileName, string content)
        {
            FileStream fs = File.Create(fileName);
            StreamWriter sw = new StreamWriter(fs);
            sw.WriteLine(content);
            sw.Close();
            fs.Close();
            return true;
        }
    }
}

Listing 1: The FileOperations class

Have a careful look to the ReadFile() method. This method throws an exception is file read is failed. The WriteFile() method will write data into the file. Modify the Main() method in the Program.cs where we will add code to create Tasks to perform read and write operations as shown in Listing 2.


namespace CS_TaskContinue_With
{
    internal class Program
    {
        static void Main(string[] args)
        {
            FileOperations operations = new FileOperations();
            Console.WriteLine("DEMO Task Continue With");
            
            try
            {
                Task task = Task.Factory.StartNew<string>(() =>
                {
                    try
                    {
                        string fileContent = operations.ReadFileOne(@"c:\file1.txt");
                        Console.WriteLine(fileContent);
                        return fileContent;
                    }
                    catch (Exception ex)
                    {
                          
                        throw ex;
                    }
                } ).ContinueWith(t =>
                {
                    try
                    {
                        if (t.Status == TaskStatus.Faulted)
                        {
                            Console.WriteLine("Task is at fault status");
throw t.Exception; } else { var result = operations.WriteFile(@"c:\file2.txt", t.Result); if (result) { Console.WriteLine("Data is written in the file"); } else { Console.WriteLine("Write File Failed.."); } } } catch (Exception ex) { Console.WriteLine($"Error Occurred in Task {ex.Message}"); } }); } catch (Exception ex) { Console.WriteLine($"Error {ex.Message}"); } Console.ReadLine(); } } }

Listing 2: The Main() method

Carefully have a look to the code. We are handling exception in each of the Task. If the file read is successful in Task 1, the execution will be continued by invoking the ContinueWith() method on the Task object. This methos accepts an input parameter as a Task object that contains the Status of the Task execution. If the previous Task is executed successfully, then the data returned from Task 1 will be used by the next Task and then it will be processed otherwise the Task2 can check for the status of the previous Task and if the previous task has the Status as Faulted then the Task 2 will throw and exception and then it will be handled.    

Create a text file named File.txt on C-drive and add some text data in it. Run the application, the data will be read from File1.txt and will be written in File2.txt. This is successful execution. Now remove the File1.txt and run the application, this will throw an exception in Task 1 and the faulted status as shown in Figure 2.



Figure 2: The Fault State 

If the Task 1 fails, then instead of throwing an exception we can also pass the cancellation token across Tasks as shown in modified code of Main() method as shown in Listing 2.


namespace CS_TaskContinue_With
{
    internal class Program
    {
        static void Main(string[] args)
        {
            FileOperations operations = new FileOperations();
            Console.WriteLine("DEMO Task Continue With");
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;
            try
            {
                Task task = Task.Factory.StartNew<string>(() =>
                {
                    try
                    {
                        string fileContent = operations.ReadFileOne(@"c:\file11.txt");
                        Console.WriteLine(fileContent);
                        return fileContent;
                    }
                    catch (Exception ex)
                    {
                        //Cancel the Task 
                      cts.Cancel();
                        Console.WriteLine("The Task is cancelled");
                       throw ex;
                    }
                }, token).ContinueWith(t =>
                {
                    try
                    {
                        if (t.Status == TaskStatus.Faulted)
                        {
                            Console.WriteLine("Task is at fault status");
                            throw t.Exception;
                        }
                        else if (!token.IsCancellationRequested)
                        {
                            var result = operations.WriteFile(@"c:\file2.txt", t.Result);
                            if (result)
                            {
                                Console.WriteLine("Data is written in the file");
                            }
                            else
                            {
                                Console.WriteLine("Write File Failed..");
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Error Occurred in Task {ex.Message}");
                    }
                },token);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error {ex.Message}");
            }
            Console.ReadLine(); 
        }
    }
}

Listing 2: The Cancellation Token

In the Listing 2, we have used the cancellation token in the Task so that if there is an error occurred, we can cancel the task and hence prevent the further execution. Run the application. Since the File11.txt is not available the  ReadFile() method will throw an exception, and Task will be cancelled with message as shown in Figure 3.



Figure 3: The Cancelled Task

That's it. The code for this article can be downloaded from this link.

Conclusion: The Task is one of the best classes in .NET programming that helps to perform multiple operations on separate threads and helps to manage them effectively. 


      

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach