ASP.NET Core 6: Taking Advantages of Task Parallel Library

 In this article, we will see the use of the Parallel class of Task Parallel Library in ASP.NET Core 6 MVC Applications. The reason for writing this article is that last week when I was discussing with the client we had a discussion regarding the use of Task Parallel Library in ASP.NET Core 6. There were a couple of scenarios we had discussed and then I thought to write my thoughts on them.

Task Parallel Library (TPL)

The TPL is introduced in .NET Framework 4.0 under the System.Threading and System.Threading.Task namespaces. The TPL has public types and APIs that simplify the process of writing parallel and concurrency code and hence improve the application's productivity.   The TPL scales the degree of concurrency dynamically to most efficiently use all the processors that are available. The TPL handlers perform the following work:

  1. Partitioning the work
  2. Schedule threads on ThreadPool  
  3. Cancellation support
  4. State Management
  5. Improvise the performance of the code and accomplish the designated work for the program
More information on the Task Parallel Library (TPL) can be read from this link

In this article, I have covered the following two scenarios

  1. Processing Collection Parallely
  2. Invoking APIs from ASP.NET Core MVC apps parallelly
Note: I have written this article using Visual Studio 2022 on MAC and you can use Visual Studio or Visual Studio Code
 
Scenario 1: Processing Collection Parallely

Step 1: Open Visual Studio and create a Blank Solution and name it as Core_Parallels. In this solution add a new ASP.NET Core MVC application and name it as Core_Parallels. In the Models folder, add a new class file and name it as ModelClasses.cs. In this file add code as shown in Listing 1


public class Employee
{
    public int EmpNo { get; set; }
    public string EmpName { get; set; } = String.Empty;
    public int Salary { get; set; }
    public decimal TDS { get; set; }
}

public static class ProcessTax
{
    public static Employee CalculateTax(Employee emp)
    {
        System.Threading.Thread.Sleep(100);
        emp.TDS = emp.Salary * Convert.ToDecimal(0.1);
        return emp;
    }
}

public class EmployeeList : List<Employee>
{
    public EmployeeList()
    {
        Add(new Employee() { EmpNo = 101, EmpName = "Abhay", Salary = 11000 });
        Add(new Employee() { EmpNo = 102, EmpName = "Baban", Salary = 22000 });
        Add(new Employee() { EmpNo = 103, EmpName = "Chaitanya", Salary = 33000 });
        Add(new Employee() { EmpNo = 104, EmpName = "Deepak", Salary = 44000 });
        Add(new Employee() { EmpNo = 105, EmpName = "Eshwar", Salary = 55000 });
        Add(new Employee() { EmpNo = 106, EmpName = "Falgun", Salary = 66000 });
        Add(new Employee() { EmpNo = 107, EmpName = "Ganpat", Salary = 77000 });
        Add(new Employee() { EmpNo = 108, EmpName = "Hitesh", Salary = 88000 });
        Add(new Employee() { EmpNo = 109, EmpName = "Ishan", Salary = 99000 });
        Add(new Employee() { EmpNo = 110, EmpName = "Jay", Salary = 31000 });
        Add(new Employee() { EmpNo = 111, EmpName = "Kaushubh", Salary = 21000 });
        Add(new Employee() { EmpNo = 112, EmpName = "Lakshman", Salary = 22000 });
        Add(new Employee() { EmpNo = 113, EmpName = "Mohan", Salary = 23000 });
        Add(new Employee() { EmpNo = 114, EmpName = "Naveen", Salary = 24000 });
        Add(new Employee() { EmpNo = 115, EmpName = "Omkar", Salary = 25000 });
        Add(new Employee() { EmpNo = 116, EmpName = "Prakash", Salary = 26000 });
        Add(new Employee() { EmpNo = 117, EmpName = "Qumars", Salary = 27000 });
        Add(new Employee() { EmpNo = 118, EmpName = "Ramesh", Salary = 28000 });
        Add(new Employee() { EmpNo = 119, EmpName = "Sachin", Salary = 29000 });
        Add(new Employee() { EmpNo = 120, EmpName = "Tushar", Salary = 30000 });
        Add(new Employee() { EmpNo = 121, EmpName = "Umesh", Salary = 31000 });
        Add(new Employee() { EmpNo = 122, EmpName = "Vivek", Salary = 32000 });
        Add(new Employee() { EmpNo = 123, EmpName = "Waman", Salary = 33000 });
        Add(new Employee() { EmpNo = 124, EmpName = "Xavier", Salary = 34000 });
        Add(new Employee() { EmpNo = 125, EmpName = "Yadav", Salary = 35000 });
        Add(new Employee() { EmpNo = 126, EmpName = "Zishan", Salary = 36000 });

    }
}
Listing 1: Employee, EmployeeList, and ProcessTax classes

In the code in Listing 1, we have EmployeeList class that contains the Employees collection that contains EmpNo, EmpName, and Salary. The class ProcessTax contains code to calculate the TDS of each employee based on Salary.

Step 2: In the Models folder, add a new class file and name it as DataViewModel.cs. In this class file, we will add a class that will be used to show data on the View. The code in Listing 2 shows DataViewModel class that contains properties to show data on View.


public class DataViewModel
    {
        public List<Employee>? NonParallelEmployeesResult { get; set; } 
        	= new List<Employee>();
        public double NonParallelTotalDuration { get; set; }
        public List<Employee>? ParallelEmployeesResult { get; set; }
        	= new List<Employee>();
        public double ParallelTotalDuration { get; set; }
    }

Listing 2: DataViewMolde class

Step 3: Modify Index() the HomeController to process each Employee record of the EmployeeList to calculate TDS in a Traditional Sequential way as well as parallelly as shown in Listing 3

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace Core_Parallels.Controllers;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    EmployeeList employees;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
        employees = new EmployeeList();
    }

    public IActionResult Index()
    {
        DataViewModel dataView = new DataViewModel();
        var timerNonParallel = Stopwatch.StartNew();
        for (int i = 0; i < employees.Count; i++)
        {
            employees[i] = ProcessTax.CalculateTax(employees[i]);
            dataView.NonParallelEmployeesResult.Add(employees[i]);
        }
        
        dataView.NonParallelTotalDuration = timerNonParallel.Elapsed.TotalSeconds;
        
        var timerParallel = Stopwatch.StartNew();
        Parallel.For(0, employees.Count, (int i) => {
            employees[i] = ProcessTax.CalculateTax(employees[i]);
            int j = i;
            i++;
            dataView.ParallelEmployeesResult.Add(employees[j]);
        });
      
         
        dataView.ParallelTotalDuration = timerParallel.Elapsed.TotalSeconds;

        return View(dataView);
    }

}


Listing 3:  Processing EmployeeCollection in Sequential and Parallel approach

Code in Listing 3 shows that we have used a sequential approach with for..loop to process the EmplopyeeList collection record-by-record and then we have used Parallel.For() method to process the same collection parallelly. The question here is what is the advantage of using the Parallel approach? The advantage of using TPL is that it uses the available threads optimally and hence improvises the performance of the code. Consider that in sequential processing of the records in the collection, if a first record takes time to process, then the next record has to wait for the first to complete, and hence by the time we complete the processing of all records the time required will be more. Instead, in Parallel processing based on the available threads records will be picked parallelly and processed, hence the time to process the collection is less as compared to sequential processing.     
As shown in Listing 3, we are saving the sequential and parallelly processed data along with the time taken to process these collections in the DataViewModel class object. We will show this data in the Index view.

Step 4: Modify the Index.cshtml view file in the Home sub-folder of the Views folder as shown in Listing 4


@model DataViewModel
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Taking Advantahes of  Parallel.For() in ASP.NET Core </h1>

    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th>
                    <h5>The Non-Parallel Execution</h5>
                </th>
                <th>
                    <h5>The Parellel Execution</h5>
                </th>
            </tr>
        </thead>
        <tbody>
            
            <tr>
                <td>
                    <table class="table table-bordered table-striped">
                        <thead>
                            <tr>
                                <th>
                                    EmpNo
                                </th>
                                <th>
                                    EmpName
                                </th>
                                <th>
                                    Salary
                                </th>
                                <th>
                                    TDS
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach (var emp in Model.NonParallelEmployeesResult)
                             {  
                                <tr>
                                    <td>
                                        @emp.EmpNo
                                    </td>
                                    <td>
                                        @emp.EmpName
                                    </td>
                                    <td>
                                        @emp.Salary
                                    </td>
                                    <td>
                                        @emp.TDS
                                    </td>
                                </tr>
                             }
                        </tbody>
                    </table>
                </td>
                <td>
                    <table class="table table-bordered table-striped">
                        <thead>
                            <tr>
                                <th>
                                    EmpNo
                                </th>
                                <th>
                                    EmpName
                                </th>
                                <th>
                                    Salary
                                </th>
                                <th>
                                    TDS
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach (var emp in Model.ParallelEmployeesResult)
                            {
                                <tr>
                                    <td>
                                        @emp.EmpNo
                                    </td>
                                    <td>
                                        @emp.EmpName
                                    </td>
                                    <td>
                                        @emp.Salary
                                    </td>
                                    <td>
                                        @emp.TDS
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </td>
            </tr>
        </tbody>
        <tfoot>
            <tr>
                <td>
                    Non-Parallel Time in Seconds: @Model.NonParallelTotalDuration
                </td>
                <td>
                    Parallel Time in Seconds: @Model.ParallelTotalDuration
                </td>
            </tr>
        </tfoot>
    </table>



</div>

Listing 4: Index.cshtml

Run the Project the Index View will show Employees' records processed with a sequential and parallel approach with the time takes for processing as shown in Figure 1



Figure 1: Sequential and Parallel Processing 
    
Figure 1 shows that Parallel processing takes lesser time than sequential processing. I must comment here that if using the large collections (millions of records in data) on the server-side, then using Parallel processing can be a reason for optimizing the application.

Scenario 2: Invoking APIs from ASP.NET Core MVC apps parallelly

The use case of fetching data from multiple APIs occurs frequently in Web applications. In such a case, if we use a Parallel class to invoke APIs parallelly then the advantage will that the MVC application will internally manage threads to access APIs and get data from them.

Step 5: In the same solution, add a new API project and name it as ProductCatelogService. In this project add a folder named Models. In this folder, add a new class file and name it ModelClasses. cs. In the class file create Product classes as shown in Listing 5

using System;
namespace ProductCatelogService.Models
{
    public class Product
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; } = String.Empty;
        public string CategoryName { get; set; } = String.Empty;
        public string Manufacturer { get; set; } = String.Empty;
        public int Price { get; set; }
    }

    public class Products : List<Product>
    {
        public Products()
        {
            Add(new Product() {ProductId=1,ProductName="Laptop",CategoryName="Electronics IT",Manufacturer="IBM",Price=345000 });
            Add(new Product() { ProductId = 2, ProductName = "Iron", CategoryName = "Electrical Home", Manufacturer = "Bajaj", Price = 5000 });
            Add(new Product() { ProductId = 3, ProductName = "Mobile", CategoryName = "Electronics Communication", Manufacturer = "Samsung", Price = 34000 });
            Add(new Product() { ProductId = 4, ProductName = "RAM", CategoryName = "Electronics IT", Manufacturer = "Samsung", Price = 345000 });
            Add(new Product() { ProductId = 5, ProductName = "Hard Disk", CategoryName = "Electronics IT", Manufacturer = "Western Digital", Price = 345000 });
            Add(new Product() { ProductId = 6, ProductName = "Power Adapter", CategoryName = "Electrical Power", Manufacturer = "HP", Price = 345000 });
            Add(new Product() { ProductId = 7, ProductName = "Mixer", CategoryName = "Electrical Home", Manufacturer = "Phillipse", Price = 345000 });
            Add(new Product() { ProductId = 8, ProductName = "TV", CategoryName = "Electronics Home", Manufacturer = "Samsung", Price = 345000 });
            Add(new Product() { ProductId = 9, ProductName = "Sofaset", CategoryName = "Furniture", Manufacturer = "Bajaj", Price = 345000 });
            Add(new Product() { ProductId = 10, ProductName = "LED Lights", CategoryName = "Electrical Home", Manufacturer = "Syska", Price = 345000 });
            Add(new Product() { ProductId = 11, ProductName = "Cycle", CategoryName = "Fitness", Manufacturer = "Bajaj", Price = 345000 });
            Add(new Product() { ProductId = 12, ProductName = "Keyboard", CategoryName = "Electronics IT", Manufacturer = "Microsoft", Price = 345000 });
            Add(new Product() { ProductId = 13, ProductName = "Mouse", CategoryName = "Electronics IT", Manufacturer = "Microsoft", Price = 345000 });
            Add(new Product() { ProductId = 14, ProductName = "Set Top Box", CategoryName = "Electrical Entertainment", Manufacturer = "D-link", Price = 345000 });
            Add(new Product() { ProductId = 15, ProductName = "Water Bottel", CategoryName = "Home Appliances", Manufacturer = "TATA", Price = 345000 });
            Add(new Product() { ProductId = 15, ProductName = "Monitor", CategoryName = "Electronics IT", Manufacturer = "Phillipse", Price = 345000 });
            Add(new Product() { ProductId = 16, ProductName = "Extension Box", CategoryName = "Electrical Entertainment", Manufacturer = "Bajaj", Price = 345000 });
            Add(new Product() { ProductId = 17, ProductName = "Headphone", CategoryName = "Electronics IT", Manufacturer = "Apple", Price = 345000 });
            Add(new Product() { ProductId = 18, ProductName = "Radio", CategoryName = "Electrical Entertainment", Manufacturer = "Apple", Price = 345000 });
            Add(new Product() { ProductId = 19, ProductName = "USB", CategoryName = "Electronics IT", Manufacturer = "Western Degital", Price = 345000 });
            Add(new Product() { ProductId = 20, ProductName = "Memory Card", CategoryName = "Electronics IT", Manufacturer = "Western Digital", Price = 345000 });
        }
    }
}

Listing 5: Product classes

In the Controllers folder, add a new empty API controller and name it as ProductController. In this controller add the HttpGet method that will returns Products as shown in Listing 6

 [Route("api/[controller]")]
    [ApiController]
    public class ProductController : ControllerBase
    {
        Products Products;

        public ProductController()
        {
            Products = new Products();
        }

        [HttpGet]
        public IActionResult Get()
        {
            return Ok(Products);
        }
    }
Listing 6: ProductController    

Step 6: Similarly, in the same solution add a new API project and name it as OrdersCatelogService. In this project add a new folder and name it Models. In this folder add a class file named ModelClasses.cs. In this file add Orders classes as shown in Listing 7

using System;
namespace OrdersCategologService.Models
{
    public class Order
    {
        public int OrderId { get; set; }
        public string CustomerName { get; set; }
        public string ProductName { get; set; }
        public int Quantity { get; set; }
    }

    public class Orders : List<Order>
    {
        public Orders()
        {
            Add(new Order() { OrderId = 20001, CustomerName = "Mahesh", ProductName = "Laptop", Quantity = 20 });
            Add(new Order() { OrderId = 20002, CustomerName = "Mukesh", ProductName = "Mobile", Quantity = 10 });
            Add(new Order() { OrderId = 20003, CustomerName = "Milind", ProductName = "RAM", Quantity = 8 });
            Add(new Order() { OrderId = 20004, CustomerName = "Mukul", ProductName = "Hard Disk", Quantity = 40 });
            Add(new Order() { OrderId = 20005, CustomerName = "Mukund", ProductName = "Mixer", Quantity = 2 });
            Add(new Order() { OrderId = 20006, CustomerName = "Mohan", ProductName = "Set Top Box", Quantity = 4 });
            Add(new Order() { OrderId = 20007, CustomerName = "Manohar", ProductName = "Extension Bopx", Quantity = 1 });
            Add(new Order() { OrderId = 20008, CustomerName = "Madhav", ProductName = "Laptop", Quantity = 30 });
            Add(new Order() { OrderId = 20009, CustomerName = "Mihir", ProductName = "Mobile", Quantity = 100 });
            Add(new Order() { OrderId = 20010, CustomerName = "Madhusudhan", ProductName = "TV", Quantity = 200 });
            Add(new Order() { OrderId = 20011, CustomerName = "Mrugank", ProductName = "Power Adapter", Quantity = 70 });
            Add(new Order() { OrderId = 20012, CustomerName = "Mrudul", ProductName = "Sofaset", Quantity = 2 });
            Add(new Order() { OrderId = 20013, CustomerName = "Manish", ProductName = "LED Lights", Quantity = 2000 });
            Add(new Order() { OrderId = 20014, CustomerName = "Manav", ProductName = "Water Bottle", Quantity = 300 });
            Add(new Order() { OrderId = 20015, CustomerName = "Ramesh", ProductName = "Monitor", Quantity = 10 });
            Add(new Order() { OrderId = 20016, CustomerName = "Ram", ProductName = "Radio", Quantity = 8 });
            Add(new Order() { OrderId = 20017, CustomerName = "Raghav", ProductName = "Monitor", Quantity = 9 });
            Add(new Order() { OrderId = 20018, CustomerName = "Ramdas", ProductName = "USB", Quantity = 2000 });
            Add(new Order() { OrderId = 20019, CustomerName = "Rajesh", ProductName = "Memory Catd", Quantity = 2000 });
            Add(new Order() { OrderId = 20020, CustomerName = "Radhey", ProductName = "Iron", Quantity = 2000 });
        }
    }

}


Listing 7: Orders classes 

In the Controllers folder, add a new empty API controller and name it as OrdersController. In this controller, add an HTTP GET method that returns Orders data as shown in Listing 8

[Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        Orders Orders;
        public OrdersController()
        {
            Orders = new Orders();
        }

        [HttpGet]
        public IActionResult Get()
        {
            return Ok(Orders);
        }
    }

Listing 8: OrdersController
 
Note: Make sure that both APIs are tested for an HTTP GET request.

Step 7: In Core_Parallel project, add a new empty MVC Controller and name it ParallelController. In this controller, we will add Index() action methods for the HTTP Get and Post requests. The Index() action method for Post request will invoke the APIs for ProductCatelog and OrdersCatelog parallelly using Invoke() method of the Parallel class as shown in Listing 9

public class ParallelController : Controller
    {
        public IActionResult Index()
        {
            var viewModel = new ParallelViewModel();
            return View(viewModel);
        }
        [HttpPost]
        public    IActionResult Index(ParallelViewModel data)
        {
            var viewModel = new ParallelViewModel();

            Parallel.Invoke(() =>
            {
                viewModel.Products =  GetProducts().Result;
                viewModel.Orders = GetOrders().Result;

            });
            return View("Index",viewModel);
        }


        private async Task<List<Product>> GetProducts()
        {
            List<Product> products = new List<Product>();
            HttpClient http = new HttpClient();

            products = await http.GetFromJsonAsync<List<Product>>
            ("https://localhost:7210/api/Product");

            return products;
        }

        private async Task<List<Order>> GetOrders()
        {
            List<Order> orders = new List<Order>();
            HttpClient http = new HttpClient();

            orders = await http.GetFromJsonAsync<List<Order>>
            ("https://localhost:7028/api/Orders");
            return orders;
        }

    }
Listing 9: The Parallel call to APIs
  
As shown in Listing 9, the Index() Post method uses the Invoke() method of the Parallel class to invoke GetProducts()  and GetOrders() methods. The Invoke() methods will be allocated threads implicitly to execute GetProducts() and GetOrders() on them.  Figure 2, will give you an idea of the execution.


Figure 2: Parallel.Invoke() execution.   

Run the Core_Parallel application after the ProductCatelogService and OrdersCatelogService projects. Request for the ParallelController. The Index Page will be loaded. On this page click on the Get Data button, and the Products and Orders data will be loaded on the view as shown in Figure 3


Figure 3: The Parallel.Invoke() results from API calls

The code for this article can be downloaded from this link.

Conclusion: Using Task Parallel Library (TPL) is a reason for improvising the performance of the application. The TPL APIs provide a mechanism to hide all the thread complexities and make the code clean and, maintainable.
 




 

Comments

Popular posts from this blog

ASP.NET Core 6: Adding Custom Middleware and Logging the Error Message in Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Role Based Security for ASP.NET Core 6 WEB API