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

 In this article, we will implement a custom Error Handler Middleware in ASP.NET Core 6. Middleware in ASP.NET Core Eco-System is a custom object that we integrate into the HTTP Request Response pipeline. This is a replacement for HttpModule and HttpHandler that was provided in ASP.NET and in MVC on .NET Framework. The HttpHandler and HttpModule were provided with IIS on Windows OS. Since the ASP.NET Core Eco-System is supported on Cross-Platform, the HttpHandler and HttpModule are replaced by the Middlewares.

Implementation of the Custom Middleware

The implementation of the custom middleware is done by adding  a new class in ASP.NET Core project. This class mus be constructor injected using the RequestDelegate delegate. This delegate is used to build the HTTP pipeline. Every HTTP request is handled by the RequestDelegate. This delegate invokes a method InvokeAsync() that accepts the HttpContext as input parameter. The current HTTP request is represented using the HttpContext class. Once the custom middleware is registered into the pipeline, the InvokeAsync() method will be invoked and it is executed. The figure 1 will provide an idea of an execution of the middleware in ASP.NET Core request Pipeline


Figure 1: The ASP.NET Core middleware pipeline

The Figure 1 provide fair idea of the Middleware Pipeline. There are several built-in middlewares provided by the ASP.NET Core to hanlde the request. The custom middlewaer is invoked after the authorization. The logic of the custom  middleware is executed when a specific condition is matched. 

To implement the project discussed in this article, the Visual Studio 2022 and .NET 6 SDK is mandatory.  The Visual Studio 2022 can be downloaded from this link. The .NET 6 SDK can be downloaded from this link.

Step 1: Open Visual Stusio 2022 and create a new ASP.NET Core WEB API project targetted to .NET 6. Name this project as Net6_Middlewares. Since, we will be logging error messages in SQL Server, we need to us EntityFramework Core. In this project add follwing packages

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Relational
  • Microsoft.EntityFrameworkCore.Tools
Step 2: In the project add a new folder and name it as Models. In this folder, add a class file and name it as Employee.cs. In this class file add an Employee class as shown in listing 1

 
public class Employee
{
   [Required(ErrorMessage ="EmpNo is Required")]
   public int EmpNo { get; set; } = 0;
   [Required(ErrorMessage = "EmpName is Required")]
   public string EmpName { get; set; } = string.Empty;
}
Listing 1: The Employee class

We will use this class  to accept Employee data from the enduser using the Web API. In the Models folder add a new class file and name it as ErrorLogger.cs. In this class file add an ErrorLogger class as shown in listing 2

public class ErrorLogger
{
  [Key]
  public int LoggerId { get; set; }
  [Required]
  public string ErrorDetails { get; set; } = String.Empty; 
  public DateTime LogDate { get; set; }= DateTime.Now;
}


Listing 2: The ErrorLogger class  

We will use the ErrorLogger class to log Error Messages in the SQL Server database. To generate database from the ErrorLogger class we will be using the EntityFramework Core Code-First approach. To use the Code-First approach, we need the DbContext class and the connectionstring in the application. Modify the appsettings.json file by adding a connsection string in it as shown in listing 3


"ConnectionStrings": {
    "LoggerConn": "Data Source=.;Initial Catalog=LoggerDb;Integrated Security=SSPI"
}

Listing 3: The databse connection string   

In the Models folder add a new class file and name it as LoggerDbContext.cs. In this file add a code to define DbContext class and DbSet property in it so that LoggerDb database and ErrorLogger table in this database is created. The code of the LoggerDbContext is provided in listing 4

public class LoggerDbContext : DbContext
{
  public DbSet<ErrorLogger> ErrorLogger { get; set; }
  public LoggerDbContext(DbContextOptions<LoggerDbContext> dbContext) :base(dbContext)
  {
  }
  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
  }
}

Listing 4: The LoggerDbContext class

Step 3: Once we have the Model classes and the DbContext class, its time for us to register it in dependency container of the ASP.NET Core. To register the DbContext class in dependency container, modify the Program.cs file by adding the code shown in listing 5 in it  


builder.Services.AddDbContext<LoggerDbContext>(options => {
    options.UseSqlServer(builder.Configuration.GetConnectionString("LoggerConn"));
});

Listing 5: The Dependency registration of DbContext

Step 4: Open the Command Prompt and navigate to the project folder and run the following commands to generate Migrations and the database from the DbContext class to generate a SQL Server database of name LoggerDb and ErrorLogger table in it

dotnet ef migrations add firstMigration -c Net6_Middlewares.Models.LoggerDbContext

dotnet ef database update -c Net6_Middlewares.Models.LoggerDbContext

Step 5: We have the initial setup for the implementation ready. Now, it is time for use to create a custom middleware for the API project. This Custom Middleware will handle exception thrown while executing the HTTP request to API and log error messag in the table. In the project add a new folder and name it as AppMiddlewares. In this folder add a new class file name it as CustomMiddleware.cs. In this class file add code as shwon in listing 6


using Net6_Middlewares.Models;
namespace Net6_Middlewares.AppMiddlewares
{
    public class ErrorInfo
    {
        public int StatusCode { get; set; } = 0;
        public string ErrorMessage { get; set; } = string.Empty;
    }


    public class ErrorHandler
    {
        private readonly RequestDelegate requestDelegate;
       
        public ErrorHandler(RequestDelegate requestDelegate)
        {
            this.requestDelegate = requestDelegate;
          
        }

        public async Task InvokeAsync(HttpContext context, LoggerDbContext loggerContext)
        {
            try
            {
                await requestDelegate(context);
            }
            catch (Exception ex)
            {
                context.Response.StatusCode = 500;

                var errorInfo = new ErrorInfo()
                { 
                    StatusCode = context.Response.StatusCode,
                    ErrorMessage = ex.Message
                };

                var errorLog = new ErrorLogger()
                { 
                  ErrorDetails = errorInfo.ErrorMessage,
                  LogDate = DateTime.Now
                };
                await loggerContext.ErrorLogger.AddAsync(errorLog);
                await loggerContext.SaveChangesAsync();

                await context.Response.WriteAsJsonAsync<ErrorInfo>(errorInfo);
            }
        }
    }



    public static class MiddlewareRegistrationExtension
    {
        public static void UseAppException(this IApplicationBuilder builder)
        {
            builder.UseMiddleware<ErrorHandler>();
        }
    }
}

Listing 6: The Custom Middleware Logic    

The code in Listing 6, has the following specifications

  • The ErroInfo class, defines properties like StatusCode and ErrorMessage, these propeties are used to define Error Code and the Error Message throwsn by the API action method while execuion.
  • The ErroHandler class is constructor injected using RequestDelegate. As explained in the beginning of this article, the RequestDelegate will be used to handle the HTTP request and execute the middleware by invoking an IncokeAsync() method. The InvokeAsync() method accepts HttpContext and the LoggerDbContext objects as input parameters. The HttpContext represents the current HTTP request and the LoggerDbContext object is passed to this method so that, error information is logged in the database. The InvokeAsync() method contains try...catch.. block. If there is no execption thrown while executing the HTTP request then the HTTP pipeline will continue execution and successful response will be generated. If any exception is thrown while executing the HTTP request then the catch block will be executed and StatusCode and Error message of the ErroInfo class will be set. The ErrorInfo class instance will be responded using the WriteAsJsonAsync<T> method of the Response property of the HttpContext class. The error information will also be logged in the database using the loggerContext parameter of the type LoggerDbContext. 
  • The MiddlewareRegistrationExtension class will be used to register the ErrorHandler class as a custom middleware in the HTTP pipeline using the UseMiddleware<T>() extension method of the IApplicationBuilder inside the UseAppException() method
Step 6: Once the Middleware is created, we need to use it in the HTTP Pipeline. Modify the Program.cs call the UseAppException() method after the UseAuthorization() middleware as shown in listing 7
........
app.UseAppException();
.......

Listing 7: Using the Middleware         

Step 7: In the Controllers folder, add a new Empty API controller and name it as EmployeeController. In this controller add the code as shown in lisitng 8



using Microsoft.AspNetCore.Mvc;
using Net6_Middlewares.Models;
namespace Net6_Middlewares.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeeController : ControllerBase
    {
        List<Employee> Employees;

        public EmployeeController()
        {
            Employees = new List<Employee>();
        }

        [HttpPost]
        public IActionResult Post(Employee employee)
        {
            if (ModelState.IsValid)
            {
                if (employee.EmpNo < 0) throw new Exception("EmpNo cannot be -ve");
                Employees.Add(employee);
                return Ok(Employees);
            }
            else
            {
                return BadRequest(ModelState);
            }
        }

    }
}

Listing 8: The Employee Controller 
The Post action method accepts Employee object, this method throws an exception if the EmpNo is negative.

Run the application, the Swagger UI will be shown in Browser where we can test the Post method as shown in figure 2






Figure 2: Testing the Post method 
The Error message will be logged into the table as shown in the figure 3



Figure 3: Error Message Logged into the Database
So when an exception is thrown by the Post method the Custom Middleware is invoked and the error response will be send to the caller and the error message will be logged into the database. This avoids the try...catch..block in each action method of the Controller's action methods.

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

Conclusion: The Custom Middleware provide a better control on the HTTP Pipeline in ASP.NET Core execution. It is easy for us to define a custom global logic which we want to execute across all controllers in ASP.NET Core application.

Comments

Popular posts from this blog

gRPC Services using .NET 5: Using Entity Framework Core with gRPC for Performing Database Operations

Passing Data Across Blazor Components