Using TempData and Creating Custom Exception Filter in ASP.NET Core 3.1

The exception handling in Web application is the most important part. Exception Handling must be implemented so that if the the HTTP request processing is failed on the server and exception occurred then the error page with required error must be shown to the user.   

Filters in ASP.NET Core
In ASP.NET Core, Filters allows code to before or after specific stages in the request pipeline are executed e.g. OnActionExecuting and OnActionExecuted().  We have standard filters like Authorization and ResponseCaching. We can add custom filters in the request pipeline for additional operations e.g. Exception Handling, Custom Authorization, etc. The following figure will explain  filter execution in ASP.NET Core 3,1 application 


Figure 1: The Filter Execution

As shown in the above figure, filters are executed in MVC application pipeline. This means that when MVC controllers are requested the filters will be loaded in the pipeline and they are executed as per the configuration. For example, the exception filter will be executed when the exception occurred during the request is being processed.
The following figure explains the execution of various action filters in pipeline


Figure 2: The execution of various filters   
The above figure explains the execution of action filters pipeline. The diagram clearly explains the execution of each action filter. Once the action is executed successfully the result filter will be executed and HTTP Response with model binding (Resource Filter) will be executed. If an exception occurred while executing the action then the exception filter will be executed and HTTP Response with error page will be returned.

Implementing Custom exception filter in ASP.NET Core 3.1    
In this article we will implement the Custom Exception Filter in ASP.NET Core 3.1. Custom exception filter must be registered in the request processing in ConfigureServices() method of the Startup class in Startup.cs file.

Note: The code for this article targets to .NET Core SDK 3.1. You can download it from this link.

Step 1: Open Visual Studio 2019 and create a new ASP.NET Core application  as shown in the following figure

Figure 3: The ASP.NET Core Web Application
Name it as AspNetCore_Tempdata_ExceptionFilter as shown in the following figure

Figure 4: The Project name
Click on Create button and select the Model-View-Controller application as shown in the following figure

Figure 5: The MVC application
Note that here we are selecting .NET Core and ASP.NET Core 3.1 framework.
Step 2: In the project, in Models folder add the two C# class files and name them as Category.cs and Product.cs. Add the code in these files as shown in the following listings

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace AspNetCore_Tempdata_ExceptionFilter.Models
{
public class Category
{
[Required(ErrorMessage ="CategoryId is required")]
public int CategoryId { get; set; }
[Required(ErrorMessage = "Category Name required")]
public string CategoryName { get; set; }
[Required(ErrorMessage = "Base Price is required")]
public int BasePrice { get; set; }
}

public class Categories : List<Category>
{
public Categories()
{
Add(new Category() { CategoryId = 101, CategoryName = "Electronics", BasePrice = 20000 });
Add(new Category() { CategoryId = 102, CategoryName = "Electrical", BasePrice = 100 });
Add(new Category() { CategoryId = 103, CategoryName = "Food", BasePrice = 20 });
}
}
}


Listing 1: Category.cs
The above code contains Category entity class and Categories class that contains list of Categories.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace AspNetCore_Tempdata_ExceptionFilter.Models
{
public class Product
{
[Required(ErrorMessage = "ProductId is required")]
public int ProductId { get; set; }
[Required(ErrorMessage = "Product Name is required")]
public string ProductName { get; set; }
[Required(ErrorMessage = "Manufacturer is required")]
public string Manufacturer { get; set; }
[Required(ErrorMessage = "CategoryId is required")]
public int CategoryId { get; set; }
[Required(ErrorMessage = "Price is required")]
public int Price { get; set; }
}
public class Products : List<Product>
{
public Products()
{
Add(new Product() { ProductId = 10001, ProductName = "Laptop", Manufacturer = "HP", Price = 90300, CategoryId = 101 });
Add(new Product() { ProductId = 10002, ProductName = "Iron", Manufacturer = "Bajaj", Price = 2300, CategoryId = 102 });
Add(new Product() { ProductId = 10003, ProductName = "Biscuts", Manufacturer = "Parle", Price = 20, CategoryId = 103 });
Add(new Product() { ProductId = 10004, ProductName = "Desktop", Manufacturer = "IBM", Price = 60300, CategoryId = 101 });
Add(new Product() { ProductId = 10005, ProductName = "Mixer", Manufacturer = "Godrej", Price = 4500, CategoryId = 102 });
Add(new Product() { ProductId = 10006, ProductName = "Maggie", Manufacturer = "Nestle", Price = 40, CategoryId = 103 });
Add(new Product() { ProductId = 10007, ProductName = "RAM", Manufacturer = "Asus", Price = 6000, CategoryId = 101 });
Add(new Product() { ProductId = 10008, ProductName = "Fan", Manufacturer = "Crompton", Price = 5000, CategoryId = 102 });
Add(new Product() { ProductId = 10009, ProductName = "Pasta", Manufacturer = "Britinia", Price = 90, CategoryId = 103 });
}
}
}

Listing 2: Product.cs
The above code contains Product entity class and Product list.

(Note: I have just ignored data-access code here and instead had the default pre-initialized data for Category and Product. You can have database and the data access layer implemented using EntityFramework Core.)

Step 3: In the project add a new folder and name it as CustomFilter. In this folder add a new class file and name it as MyExceptionFilter.cs. Add the code in this class file as shown in the following listing

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace AspNetCore_Tempdata_ExceptionFilter.CustomFilter
{
public class MyExceptionFilter : ExceptionFilterAttribute
{
private readonly IModelMetadataProvider metadataProvider;
/// <summary>
/// The provider that supplies in instance of the ModelMotadata
///
/// </summary>
/// <param name="metadataProvider"></param>
public MyExceptionFilter(IModelMetadataProvider metadataProvider)
{
this.metadataProvider = metadataProvider;
}

/// <summary>
/// Method for handling Exception. This method should Handle exception and complete request
/// </summary>
/// <param name="context"></param>
public override void OnException(ExceptionContext context)
{
// read exception message
string message = context.Exception.Message;
// handle Exception
context.ExceptionHandled = true;
// go to view to display error messages
var result = new ViewResult();
// defining VeiwDataDictionary for controller/action/errormessage
var ViewData = new ViewDataDictionary(metadataProvider, context.ModelState);
ViewData["controller"] = context.RouteData.Values["controller"].ToString();
ViewData["action"] = context.RouteData.Values["action"].ToString();
ViewData["errormessage"] = message;
// ViewName
result.ViewName = "CustomError";
// ViewData
result.ViewData = ViewData;
// setting result in HttpResponse
context.Result = result;
}
}
}

Listing 3: The Custom Exception Filter class
The MyExceptionFilter class is derived from ExceptionFilterAttribute class. The ExceptionFilterAttribute class contains the OnException() and  OnExceptionAsync() virtual methods. In the  MyExceptionFilter class we are overriding OnException() method. This method accepts ExceptionContext as input parameter. The ExceptionContext class will listen to the exception raised during the action execution. Since the OnceException() method  will be returning to error view as HttpResponse to show then exception details, we need ViewDataDictionary object to pass key/value pairs to view.
The ViewDataDictionary  instance needs IModelMetadataProvider and ModelState as constructor parameter to define an instance of the ViewDataDictionary  class. The IModelMetadataProvider is constructor injected in the MyExceptionFilter class. The OnException() method reads exception message and handles exception. It defines an instance of ViewDataDictionary class. This instance is used to set Key/Value pairs for the controller, action and errormessage.
The ViewDataDictionary instance is pass to the ViewData property of the ViewResult instance. The ViewName property of the ViewResult class contains CustomError as value. This will be the custom view which we will be adding in the next step. The OnException() method implementation will add the CustomError View in the HTTP Response when an exception is handled by this exception filter.

Step 4: In the Shared sub-folder of the Views folder add a new empty view and name it as CustomError.cshtml. Add the following markup in this file

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

<div class="container">
    <h2>
        Error Occured in Controller @ViewData["controller"]
    </h2>

    <h2>
        Error Occured in Action Method @ViewData["action"]
    </h2>
    <h2>
        Error message is @ViewData["errormessage"]
    </h2>
</div>

<br />
<a asp-action="@ViewData["action"]" asp-controller="@ViewData["controller"]">Go Back</a>

Listing 4: The CustomError.cshtml          
The above view uses ViewData key/value pair to show error details.

Step 5: Modify the ComnfigureServices() method of the StartUp class in StartUp.cs to register custom exception filter as shown in the following listing

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options => {
options.Filters.Add(typeof(MyExceptionFilter));
});
}

Listing 5: The registration of the Custom Exception filter in the HTTP Pipeline 
Here we are adding the custom exception filter using the AddControllersWithViews() method. This method represents the MVC pipeline for execution of MVC controllers and action methods. The MyExceptionFilter will be applied when any exception occurred in the action method of the MVC controller.

Step 6: We will be using the the ITempDataProvider  to share data across controllers along with this we will be using ITempDataProvider to maintain the model object that causes exception across the action methods in the controller. The TempData object provided by ITempDataProvider is used to maintain state of the data across HTTP requests across controllers. The following figure will provide an idea of the TempData.


Figure 6: The TempData 

The above figure explains two scenarios of the using TempData in the ASP.NET Core application. The first scenario explain the TempData uses across controllers. The action method from the controller 1 stores data in the TempData and redirects to controller 2. The action method from the controller 2 can retrieve data from TempData and use it for the processing. Using the TempData approach we can maintain the data across two controllers. The scenario 2 uses two action methods from the same controllers. This approach is typically used in the scenario when an exception occurs in Action method 1 and it redirects to ErrorPage. When we want to redirect back from the ErrorPage to the Action 2 of the same controller with model data that causes error then we need to store the data from Action 1 to TempData and retrieve in Action 2. But in this scenario to store the complex object and maintain its state across the Actions and ErrorPage, it is necessary to customize the TempDataProvider.

Step 7: In the project add a new folder and name it as CustomProviders. In this folder add a new class file and name it as TempDataProviderExtensions.cs. Add the code in this file as shown in the following listing
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Newtonsoft.Json;

namespace AspNetCore_Tempdata_ExceptionFilter.CustomProviders
{
    public static class TempDataProviderExtensions
{
        public static void SetData<T>(this ITempDataDictionary temp, string key, T value) where T : class
        {
            temp[key] = JsonConvert.SerializeObject(value);
        }

        public static T GetData<T>(this ITempDataDictionary temp, string key) where T : class
        {
            object obj;
            temp.TryGetValue(key, out obj);
            return obj == null ? null : JsonConvert.DeserializeObject<T>((string)obj);
        }
    }

}


Listing 6: The Custom TempData provider       
The above is an extension class for ITempDataDictionary interface. The class have defined SetData<T>() and GetData<T>() methods. These methods will be used to serialize and deserialize the complex object T in JSON format. The TempData object will use these methods to maintain state of model object across.

Step 8: In the Controllers folder add two Empty MVC Controllers and name them as CategoryComtroller.cs Add the code in these controllers as shown in the following listing. Scaffold Index view from an Index action method of the Category Controller..

CategoryController.cs

using AspNetCore_Tempdata_ExceptionFilter.Models;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCore_Tempdata_ExceptionFilter.Controllers
{
    public class CategoryController : Controller
    {
        Categories cats;
        public CategoryController()
        {
            cats = new Categories();
        }
        public IActionResult Index()
        {
         
            return View(cats);
        }

        public IActionResult Details(int id)
        {
            TempData["CatId"] = id;
            return RedirectToAction("Index", "Product");
        }

    }
}

Listing 7: The Category Controller 

The CategoryController class contains action method of name details. This method stores the CategoryId in TempData object and it redirects to an Index action method of the Product controller.
The Index.cshtml view of the Index action method of the CategoryController will have markup as shown in the following listing.

@model IEnumerable<AspNetCore_Tempdata_ExceptionFilter.Models.Category>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.CategoryId)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.CategoryName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.BasePrice)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.CategoryId)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.CategoryName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.BasePrice)
            </td>
            <td>
                @Html.ActionLink("Details", "Details", new { id=item.CategoryId })
            </td>
        </tr>
}
    </tbody>
</table>

Listing 8: Index.cshtml for the Category Controller
The Details action link will make the request to the Details action method of the CategoryController.

Step 9: Add a new Empty MVC Controller in Controllers folder and name it as ProductController. Add Index and Create Action methods in this controller. Scaffold Index and Create views from Index and Create action methods of the ProductController.  Add the following code in  the ProductController.cs

ProductController.cs

using AspNetCore_Tempdata_ExceptionFilter.CustomProviders;
using AspNetCore_Tempdata_ExceptionFilter.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;

namespace AspNetCore_Tempdata_ExceptionFilter.Controllers
{
    public class ProductController : Controller
    {
        Products prds;
        Categories cats;
        public ProductController()
        {
            prds = new Products();
            cats = new Categories();
        }
        public IActionResult Index()
        {
            List<Product> products = new List<Product>();
            if (TempData.Values.Count > 0)
            {
                var catid = Convert.ToInt32(TempData["CatId"]);
                products = (from p in prds
                           where p.CategoryId == catid
                            select p).ToList();
               return View(products);
            }
            return View(prds);
        }

        public IActionResult Create()
        {
            ViewBag.CategoryId = new SelectList(cats, "CategoryId", "CategoryName");
            if (TempData.Values.Count > 0)
            {
                Product prdData = TempData.GetData<Product>("Prd") as Product;
                return View(prdData);
            }
            var prd = new Product();
            return View(prd);
        }

        [HttpPost]
        public IActionResult Create(Product prd)
        {
            if (ModelState.IsValid)
            {
                if (prd.Price < 0)
                {
                    TempData.SetData<Product>("Prd", prd); the TempDataP
                    throw new Exception("Price can not be -ve");
                }

                prds.Add(prd);
                return RedirectToAction("Index"); 
            }
            return View(prd);
        }

    }
}


Listing 9: The ProductController   
The Index action method of the ProductController class reads CategoryId data from TempData and display Products based on the CategoryId. The Create HttpPost method throws an exception if the posted Product model contains negative value for the price. The Product model that contains the invalid data will be stored in the TempData. TempData uses SetData<T>() method that we have added in TempDataProviderExtension class in step 7. Since we have already added the custom exception filter, the exception will be caught and CustomError page will be displayed. Once we navigate back to the HttpGet Create action method of the ProductController, it will read the Product data from the TempData using GetData<T>() extension method of the TempDataProviderExtension class. Hence the Create View will show the the Product data that caused exception to be thrown.

The following listing shows Index view and Create view for the Index and Create action method of the ProductController class

Index.cshtml

@model IEnumerable<AspNetCore_Tempdata_ExceptionFilter.Models.Product>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.ProductId)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ProductName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Manufacturer)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.CategoryId)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.ProductId)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ProductName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Manufacturer)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.CategoryId)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
        </tr>
}
    </tbody>
</table>


Create.cshtml

@model AspNetCore_Tempdata_ExceptionFilter.Models.Product

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Product</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="ProductId" class="control-label"></label>
                <input asp-for="ProductId" class="form-control" />
                <span asp-validation-for="ProductId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ProductName" class="control-label"></label>
                <input asp-for="ProductName" class="form-control" />
                <span asp-validation-for="ProductName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Manufacturer" class="control-label"></label>
                <input asp-for="Manufacturer" class="form-control" />
                <span asp-validation-for="Manufacturer" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="CategoryId" class="control-label"></label>
                <select asp-for="CategoryId" class="form-control" asp-items="@ViewBag.CategoryId"></select>
                <span asp-validation-for="CategoryId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}


Listing 10: Index and Create views

Step 10: Modify the _Layout.cshtml in Shared subfolder of Views folder by adding links for Category and Product controllers as shown in the following listing.

........
       <a class="nav-link text-dark" asp-area="" asp-controller="Category" asp-action="Index">Category</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Product" asp-action="Index">Product</a>
                        </li>
.........
Listing 11: The modified _Layout.cshtml 
     
Run the application, the view will be loaded in the browser as shown in the following figure

Figure 7: The Home page loaded in browser will all links
Click on the Category link the Category Index view will be displayed as shown in the following figure

Figure 8: The Category Index View
Click on the Details link of any of the Category Row, the page will be navigated to the Index view of the Product Controller and Products will be displayed based on the selected CategoryId e.g. for CategoryId=101 Products will be displayed as shown in the following figure

Figure 9: List of products for the CategoryId =101

Click on the Create New link, the Create View will be displayed. In the create view enter Product data and set the Price value as -ve as shown in the following figure

Figure 10: The Create view with error data
We have entered -ve value for the Price. Click on Create button, the application will crash as shown in following figure

Figure 11: The exception
The exception is thrown.  Continue the execution by pressing F5 button, error page will be shown as displayed in following figure

Figure 12: The Custom Error Page
Since we have handled the exception using Custom Exception filter and it is registered in the MVC Filter pipeline in Startup.cs the errorpage is returned. Click on Go Back link, the Create View will be displayed with error data in it. The data is still maintained because of the custom TempDataProviderExtension class added in Step 7.


The Code foe the article can be downloaded from this link.

Conclusion: The TempData is the most useful feature of ASP.NET Core applications to maintain data across HTTP requests to the controllers. The Exception Filter in MVC Request pipeline prevents the application crash and helps to handle the exceptions and redirect to error page. These features adds value  to the ASP.NET Core applications.  





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