Global Error Handler in ASP.NET Core

Error handling is crucial for any application. In Line-of-Business (LOB) ASP.NET applications, error handling can be implemented in multiple ways. Generally, when an application needs to perform data validations on data submitted by end-users, we implement data validations using annotations which is also known as Model validation

Model validation is limited with the Model class (aka entity class), but our application must be capable to validate code while executing - this is known as exception handling.

Traditionally, most of the programming languages provide try…catch…finally block for exception handling. There is nothing wrong in having a try…catch…finally block for every method in each class of the application, but it is always better to implement a global error or exception handler. By having a centralized exception handling, we can make our code more readable and maintainable.

In ASP.NET MVC applications we have exception action filters. Using this action filter, it is easier for the developer to implement global exception handler. I have already posted on MVC Exception Handling at this link

In the current article we will implement error handling in ASP.NET Core applications for WEB API. It is important to to have a global exception handler in WEB API so that the API action methods can remain clean, readable and maintainable.

ASP.NET Core provides a built-In middleware for global error handling, but we can also implement custom middleware or even we can write try...catch block for each action method. We will implement error handling with all these methods.

Middleware in ASP.NET Core



Middlewares are software code, that are used to configure additional features for the HTTP Request Pipeline. These Middlewares are configured using IApplicationBuilder interface.

Middleware uses RequestDelegate to handle Http Request under the current request HttpContext. In the Http pipeline there can be multiple Middlewares, the RequestDelegate for each component is responsible to for invoking next Middleware component in pipeline. There are Middlewares for Static Files, Authorization, Sessions, etc.  In ASP.NET Core, the Middlewares are configured in the Configure() method of the StartUp class.

Figure 1 explains available middleware in ASP.NET Core applications:



Figure 1: The ASP.NET Core Middleware Blocks

The following application is implemented using Visual Studio 2017 and .NET Core 2.2.

Step 1: Open Visual Studio 2017 and create a new ASP.NET Core Web Application. Name this application as Core_ErrorHandler. In the Models folder, add a new class file of name Employee.cs. This file contains Employee class as shown in the following code.

using System.ComponentModel.DataAnnotations;
namespace Core_ErrorHandler.Models
{
    public class Employee
    {
        [Key]
        public int EmpId { get; set; }
        [Required(ErrorMessage ="EmpNo is Must")]
        public string EmpNo { get; set; }
        [Required(ErrorMessage = "EmpName is Must")]
        public string EmpName { get; set; }
        [Required(ErrorMessage = "Salary is Must")]
        public int Salary { get; set; }

    }
}
The above class defines Employee entity class with data annotations on each property. Since we need to create a database in SQL Server, modify the appsettings.json file as shown in the following code

"ConnectionStrings": {
    "ApplicationConnection": "Server=.;Database=MyAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  }

Step 2: Since the application uses EntityFramework Core, add NuGet packages in the application as shown in Figure 2.


Figure 2: List NuGet packages to be installed

Step 3: Add a new class file in the Models folder and name it AppDbContext.cs. Add the following code in this file. This code will connect to the database and define mapping with Employees table

using Microsoft.EntityFrameworkCore;

namespace Core_ErrorHandler.Models
{
    public class AppDbContext : DbContext
    {
        public DbSet Employees { get; set; }
        public AppDbContext(DbContextOptions options):base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }

}
Build the application and make sure that it is error free.

Step 4: To generate Sql Server database, we need to run database migration commands. To do so, open the .NET Command prompt and navigate to the project application folder and run commands as shown in Figure 3 (red marked commands).


Figure 3: EF Core Migrations

Step 5: In the project, add a new folder of name Services. Add a new class file in the project and name it as DataService.cs. Add the following code in it:

using Core_ErrorHandler.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Core_ErrorHandler.Services
{
    public class DataService
    {
        private  readonly AppDbContext context;
        public DataService(AppDbContext context)
        {
            this.context = context;
        }

        public async Task> ReadEmployeesAsync()
        {
            var emps = await context.Employees.ToListAsync();
            return emps;
        }
        public async Task WriteEmployeeAsync(Employee employee)
        {
            await context.Employees.AddAsync(employee);
            await context.SaveChangesAsync();
            return employee;
        }
    }
}

The above class uses AppDbContext class to perform Read/Write operation on Employees table of MyAppDb Database.

Step 6: Modify the code of ConfigurationServices() method to Startup class in Startup.cs to register AddDbContext and DataService class in DI Container

 // Registering the AppDbContext class in DI Container
            services.AddDbContext(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("ApplicationConnection")));
            // Registering the DataService class in DI Container
            services.AddScoped();

Implementing Error Handling using Try…Catch block

Step 7: In the Controllers folder, add a new Empty WEB API and name it as Employee Controller.

using Core_ErrorHandler.Models;
using Core_ErrorHandler.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;

namespace Core_ErrorHandler.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeeController : ControllerBase
    {
        DataService ds;
        public EmployeeController(DataService ds)
        {
            this.ds = ds;
        }

        public IEnumerable Get()
        {
            var emps = ds.ReadEmployeesAsync().Result;
            return emps;
        }


        [HttpPost]
        public IActionResult Post(Employee emp)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    if (emp.Salary < 2000)
                    {
                        // If the Salary Validation Failed then throw the exception
                        throw new Exception("Salary Validation Failed");
                    }

                    emp = ds.WriteEmployeeAsync(emp).Result;
                }

            }
            catch (Exception ex)
            {
                return StatusCode(500, ex.Message);
            }
            return Ok(emp);

        }
    }
}

The above code contains the Get() and Post() methods. The Post() method validates the Employee model using ModelState property. This method validates the Salary values posted, if it is less than 2000, then the “Salary Validation Failed” exception is thrown, this exception is caught by catch block and the method returns Http status code as 500 and the exception message. We can test execution using Fiddler or Postman. I have used fiddler here.

Step 8: Run the WEB API application. Start Fiddler and enter address of the API and posted data as shown in Figure 4



Figure 4: The Fiddler for Http Post Request’

Execute this request, the WEB API will return the exception as shown in Figure 5.


Figure 5: The error response from API

The WEB API returns HTTP error status code 500 which means an Internal Server Error. This also shows the raised exception message in the WEB API method as Salary Validation Failed. Adding try…catch block in the action method makes sure that the method will execute the business logic correctly.

A challenge in this approach is that the developer must add try…catch block to each method. This may increase the development and maintenance cost. We must find a way out to handle errors globally to increase maintainability of the code.

This is where the ASP.NET Core middleware comes into picture.

Using Built-In Middleware for Global Exception Handling

The ASP.NET Core pipeline is configured by IApplicationBuilder interface. This interface provides several middleware methods. These middleware methods will be added in the request pipeline to manage the request processing. One of the inbuilt middleware method is UseExceptionHandler(). This method will catch exceptions while request processing.

Step 9: In the project add a new folder. Name this folder as ErrorExtensions. In this folder add a new class file and name it as ErrorClass.cs. Add the following code in this file

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

namespace Core_ErrorHandler.ErrorExtensions
{
    public class ErrorClass
    {
        public int ErrorCode { get; set; }
        public string ErrorMessage { get; set; }
    }


    public static class ErrorExtensionsMiddleware
    {
        public static void ErrorHandler(this IApplicationBuilder appBuilder)
        {
            // 1. Invoke UserExceptionHandler() method and pass IApplicationBuilder to it.
            appBuilder.UseExceptionHandler((error) =>
            {
                // 2. Invoke the Run() method. This method accepts RequestDelegate parameter 
                error.Run(async ctx => {
                    // 3. send the status code based on the error occured
                    ctx.Response.StatusCode = ctx.Response.StatusCode;
                    ctx.Response.ContentType = "application/json";
 
                    // 4. Get the error thrown while processing request
                    string errorMessage = ctx.Features.Get()
                            .Error.GetBaseException().Message;

                    // 5. Create the response message. This will contain error message 
                    string ResponseMessage = JsonConvert.SerializeObject(new ErrorClass()
                    {
                        ErrorCode = ctx.Response.StatusCode,
                        ErrorMessage = errorMessage
                    });
                    // 6. Write the response message
                    await ctx.Response.WriteAsync(ResponseMessage);
                });
            });
        }
    }
}

The above code has the following specifications.

The ErrorClass class contains ErrorCode and ErrorMessage properties.

These properties will be used to write Error Status Error Code and Error Message in the Http response message. The ErrorExtensionsMiddleware class has  ErrorHandler() method. This method accepts IApplicationBuilder as an input parameter. This method does the following. (Note: Following numbering matches with the comment numbers applied on the code) 
  1. The UseExceptionHandler() method accepts the Action Delegate of type IApplicationBuilder. This is used to invoke Run() method.
  2. The Run() method accepts the RequestDelegate object. This object is used to handle process current Http request that provides HttpContext object.
  3. This step listen to the Http Status code for the Response from the current Http Request and also set the response message content type.
  4. This step is used to receive the exception message generated while processing request. The Features property is the FeatureCollection object of the HttpContext class. This is a collection of features provided by the Http Server and Middleware. These features may be the exceptions generated while processing request. The IExceptionHandlerFeature interface represents  an exception handler with the Http Request. This is used to read exception message thrown by the server while processing request.  
  5. This step creates a Http Response message.
  6. This step write the Http Response.
Step 10: Modify the Startup.cs  use the the ErrorExtensions namespace in it as shown in the following code:

using Core_ErrorHandler.ErrorExtensions;

Modify the Configure() method of the Startup class and call the ErrorHandler() extension method which we have created in ErrrorExtensionsMiddleware class as shown in the following code:

app.ErrorHandler();

 
Step 11: Remove the try—catch block from the Post() method of the EmployeeController web api class and run the application. Open Fiddler/Postman to call Post method of the WEB API, the result will be displayed as shown in the following image:


Figure 6: The Error Response

This shows the error message thrown by the exception while executing the Http Request.

Implementing Global Handler for writing Custom Middleware


The best part of using ASP.NET Core is that, it allows developers to define extensions for Middlewares that provides a great control over the application and makes it maintainable.
In this step we will implement a custom middleware to develop a global error handler.  As discussed above, in ASP.NET Core, the RequestDelegate object processes Http Request, which means that it holds and monitors the current Http Context. When the WEB API is requested, the RequestDelegate will listen to any error that is thrown while processing the request over the current HttpContext.

When we create a custom middleware class, we need to inject RequestDelegate in its constructor. This class must have InvokeAsync() method. This method accepts HttpContext object and the method body uses try..catch block. If an error occurs, the catch block will handle the error and generate the error response. In the following steps, we will implement code for custom middleware for error handling.

Step 12: In the project add a new folder and name it as ErrorMiddleware. In this folder add a new class file and name it as ErrorMiddleware.cs. Add the following code in this class file.


using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

namespace Core_ErrorHandler.ErrorMiddleware
{

    public class ErrorClass
    {
        public int ErrorCode { get; set; }
        public string ErrorMessage { get; set; }
    }

    /// 
    /// This is the Custom Middleware
    /// 
    public class ErrorMiddleware
    {
        /// 
        /// We need to inject the "RequestDelegate" class.
        /// This will be used to process httprequest
        /// 
        RequestDelegate requestDelegate;
        public ErrorMiddleware(RequestDelegate requestDelegate)
        {
            this.requestDelegate = requestDelegate;
        }
        /// 
        /// We need to add the InvokeAsync() method. 
        /// We need this method so that RequestDelegate can process the HttpRequest
        /// 
        /// 
        /// 
        public async Task InvokeAsync(HttpContext httpContext)
        {
            try
            {
                await requestDelegate(httpContext);
            }
            catch (Exception ex)
            {
                await HandleErrorAsync(httpContext, ex);
            }
        }

        /// 
        /// Method to handle Exceptions occures while Request processing
        /// 
        /// 
        /// 
        /// 
        public static Task HandleErrorAsync(HttpContext httpContext, Exception exception)
        {
            // send the status code based on the error occured
            httpContext.Response.StatusCode = 500;
            httpContext.Response.ContentType = "application/json";

            // Get the thrown error
            string errorMessage = exception.Message;

            // create the response
            string ResponseMessage = JsonConvert.SerializeObject(new ErrorClass()
            {
                ErrorCode = httpContext.Response.StatusCode,
                ErrorMessage = errorMessage
            });
            // get the response message
            return httpContext.Response.WriteAsync(ResponseMessage);
        }
    }
    /// 
    /// The class that register the Middleware
    /// 
    public static class CustomErrorExtensionsMiddleware
    {
        /// 
        /// The following method will use ErrorMiddleware class
        /// 
        /// 
        public static void CustomExceptionHandlerMiddleware(this IApplicationBuilder app)
        {
            app.UseMiddleware();
        }
    }
}

The above code has the following specifications
  • The ErrorClass contains properties for ErrorCode and ErrorMessage
  • The ErrorMiddleware class is injected with RequestDelegate object. The HandleErrorAsync() method accepts HttpContext and Exception objects as input parameter. This method is responsible for generating error response for current Http request along with the Exception raised during processing current Http request. The InvokeAsync() method accepts HttpContext object, this is a standard method of Custom Middleware, this method is used to process Http request. If an exception is thrown while processing the Http request, this method will catch it. In the catch block, the InvokeAsync() method calls HandleErrorAsync() method. This method is used to write the error response for the current Http request.
  • The CustomErrorExtensionsMiddleware class contains CustomExceptionHandlerMiddleware() extension method for the IApplicationBuilder. This method registers our ErrorMiddleware class in the application request pipeline using UseMiddleware() method of the IApplicationBuilder interface.

Step 13: Registering the Custom Middleware in Configure() methodModify the Startup.cs and importing Core_ErrorHandler.ErrorMiddleware namespace in it and modify the Configure() method as to use the Custom Middleware as as shown in the following code
       
app.CustomExceptionHandlerMiddleware();


Run the application and test it in fiddler, the result will be as shown in Figure 7.

Pass the data as shown in Figure 7.



Figure 7: The Request using Fiddler

Execute the request, it will display the following result.



Figure 8: The Error response

Conclusion: It is a good practice to have a Global Error handler in ASP.NET Core applications. This will make our code clean, readable and maintainable. The advantage it offers is to have a central control on our application by defining a custom middleware.




About The Author

Mahesh Sabnis is a Microsoft MVP having over 18 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). He also blogs regularly at DotNetCurry.com. Follow him on twitter @maheshdotnet

No comments: