Global Error Handling in ASP.NET Core Web API using NLog

Introduction

In this article, we will learn how to handle exception at global level using NLog. Exception handling is one of most important functionalities of any application. Here we will learn how to configure NLog to log Info, Error, Warning, Debug, Trace information and we will learn how to create middleware to handle the error at global level. Here I will try to explain it step by step.

Background

NLog is a flexible and free logging platform for various .NET platforms, including .NET standard. NLog makes it easy to write to several targets (database, file, console) and change the logging configuration on-the-fly.

Similiar to other Logging Frameworks, NLog has the following log levels.

  1. Trace – The entire trace of the codebase.
  2. Debug – useful while developing the application.
  3. Info – A general Message.
  4. Warn – Used for unexpected events.
  5. Error – When something breaks.
  6. Fatal – When something very crucial breaks.

To know more about NLog, please click on the link https://nlog-project.org/

Project Architecture

Implementation

Let’s create the application step by step.

Step 1. Create an ASP .NET Core 3.1 Web API and give the application name as “CustomExceptionHandlerPOC”.

Step 2. Install NLog (NLog.Web.AspNetCore(5.0.0)) by using Nuget Package Manager.

Step 3. Now let’s configure NLog. To configure NLog add a config file “nlog.config”. Set your folder path where you want to log error.  As in below code I have set path as “C:/Learning/.NET Core/ExceptionHandling/logs”. You can set the path as per your choice.

<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Trace"
      internalLogFile="C:/Learning/.NET Core/ExceptionHandling/internal_logs/internallog.txt">

  <targets>
    <target name="logfile" xsi:type="File"
            fileName="C:/Learning/.NET Core/ExceptionHandling/logs/${shortdate}_logfile.txt"
            layout="${longdate} ${level:uppercase=true} ${message}"/>
  </targets>

  <rules>
    <logger name="*" minlevel="Debug" writeTo="logfile" />
  </rules>
</nlog>

Step 4. Create a “Logger” folder in the application. Inside the Logger folder, create an Interface “ILoggerService”.  

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

namespace CustomExceptionHandlerPOC.Logger
{
    public interface ILoggerService
    {
        void LogInfo(string message);
        void LogWarn(string message);
        void LogDebug(string message);
        void LogError(string message);
        void LogTrace(string message);
    }
}

Step 5. Inside the Logger folder, create a class “LoggerService” and implement the “ILoggerService” interface. The GetCurrentClassLogger() method provides the logger named after the currently-being-initialized class.

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

namespace CustomExceptionHandlerPOC.Logger
{
    public class LoggerService : ILoggerService
    {
        private static ILogger logger = LogManager.GetCurrentClassLogger();
        public LoggerService()
        {
        }
        public void LogDebug(string message)
        {
            logger.Debug(message);
        }

        public void LogError(string message)
        {
            logger.Error(message);
        }

        public void LogInfo(string message)
        {
            logger.Info(message);
        }

        public void LogWarn(string message)
        {
            logger.Warn(message);
        }
        public void LogTrace(string message)
        {
            logger.Trace(message);
        }
    }
}

Step 6. Create a “Models” folder in the application. Inside the Logger folder, create two classes. One is “ErrorInfo”, which will be used for logging errors and other is “PurchaseDetails” which will be used for creating Purchase order List.

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

namespace CustomExceptionHandlerPOC.Models
{
    public class ErrorInfo
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }
        public override string ToString()
        {
            return JsonSerializer.Serialize(this);
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CustomExceptionHandlerPOC.Models
{
    public class PurchaseDetails
    {
        public string ProductName { get; set; }
        public int Rate { get; set; }
        public int Qty { get; set; }
        public int Amount { get; set; }
    }
}

Step 7. Create a “CustomDB” folder in the application. Inside the CustomDB folder, create a “PurchaseOrderDB” classes. This class has a method “GetPurchaseOrders”, which will provide list of order. Basically, here I am not using any Database.  So, for the demo purpose I have created list of purchase orders locally.

using CustomExceptionHandlerPOC.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CustomExceptionHandlerPOC.CustomDB
{
    public class PurchaseOrderDB
    {
        public static List<PurchaseDetails> GetPurchaseOrders()
        {
            return new List<PurchaseDetails>
            {
               new PurchaseDetails { ProductName="Laptop", Rate=80000, Qty=2, Amount=160000},
               new PurchaseDetails { ProductName="Dekstop", Rate=40000, Qty=1, Amount=40000},
               new PurchaseDetails { ProductName="Hard Disk", Rate=4000, Qty=10, Amount=40000},
               new PurchaseDetails { ProductName="Pen Drive", Rate=600, Qty=10, Amount=6000},
            };
        }
    }
} 

Step 8. Now let’s create middleware. Create a “ExceptionHandlerMiddleware” folder in the application. Inside “ExceptionHandlerMiddleware” folder create a class “ExceptionMiddleware”.

using CustomExceptionHandlerPOC.Logger;
using CustomExceptionHandlerPOC.Models;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace CustomExceptionHandlerPOC.ExceptionHandlerMiddleware
{
    public class ExceptionMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILoggerService _logger;
        public ExceptionMiddleware(RequestDelegate next, ILoggerService logger)
        {
            _logger = logger;
            _next = next;
        }
        public async Task Invoke(HttpContext httpContext)
        {
            try
            {
                await _next(httpContext);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong: {ex}");
                await HandleException(httpContext, ex);
            }
        }
        private async Task HandleException(HttpContext context, Exception exception)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            await context.Response.WriteAsync(new ErrorInfo()
            {
                StatusCode = context.Response.StatusCode,
                Message = "Internal Server Error from the custom middleware."
            }.ToString());
        }
    }
}

Step 9. Now let’s register the middleware and configure the “nlog.config”.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using System.IO;
using CustomExceptionHandlerPOC.Logger;
using CustomExceptionHandlerPOC.ExceptionHandlerMiddleware;
namespace CustomExceptionHandlerPOC
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            LogManager.LoadConfiguration(string.Concat(Directory.GetCurrentDirectory(), "/nlog.config"));
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();

            // Configure Logger service. 
            services.AddSingleton<ILoggerService, LoggerService >();

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            //Configure Exception Middelware
            app.UseMiddleware<ExceptionMiddleware>();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Step 10. Now let’s create a PurchaseOrder Controller inside Controllers folder.

using CustomExceptionHandlerPOC.CustomDB;
using CustomExceptionHandlerPOC.Logger;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CustomExceptionHandlerPOC.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PurchaseOrderController : ControllerBase
    {
        private ILoggerService _logger;
        public PurchaseOrderController(ILoggerService logger)
        {
            _logger = logger;
        }
        [HttpGet("GetPurchaseOrder")]
        public IActionResult GetPurchaseOrder()
        {
            _logger.LogInfo("Fetching All Purchase orders");
            // Get Purchase data from  PurchaseOrder DB
            var purchaseOrders = PurchaseOrderDB.GetPurchaseOrders();

         // Uncomment below two lines code to get exception.
            //var num1 = 100;
            //dynamic num = num1 / 0;

            _logger.LogInfo($"Total purchase order count: {purchaseOrders.Count}");
            return Ok(purchaseOrders);
        }
    }
}

Step 11. Now run the application. Open the Postman tool and test the API.  The API will return the Purchase order data. Please see the below scree shot.

Now open the logs folder. You will get a file name (DDDD-MM-YY_logfile.txt) which logs details.

Step 12. Now let’s break the code and try to generate error. For this uncomment the two lines of code in GetPurchaseOrder() endpoint.

var num1 = 100;

dynamic num = num1 / 0;

Open the Postman tool and test the API.  You will get error in response as below.

Now check the logfile, the error info will be written in the logfile as below.

Summary

We learned how to handle expectation globally using NLog in ASP.NET core web API.  I hope this article will help you. Happy learning!!