RESTful Day #6: Request Logging And Exception Handing/Logging In Web APIs Using Action Filters, Exception Filters and NLog

The article is also written and compiled by Sachin Verma as co-author.

Table of Contents

Table of Contents
Introduction
Roadmap
Request Logging

  • Setup NLog in WebAPI

      Step 1: Download NLog Package
      Step 2: Configuring NLog

  • NLogger Class
  • Adding Action Filter

      Step 1: Adding LoggingFilterAttribute class
      Step 2: Registering Action Filter (LoggingFilterAttribute)

Running the application
Exception Logging

  • Implementing Exception logging

      Step 1: Exception Filter Attribute
      Step 2: Modify NLogger Class
      Step 3: Modify Controller for Exceptions
      Step 4: Run the application

Custom Exception logging

  • JSon Serializers
  • Modify NLogger Class
  • Modify GlobalExceptionAttribute
  • Modify Product Controller
  • Run the application
  • Update the controller for new Exception Handling

      Product Controller

Conclusion
Other Series

Introduction


We have been learning a lot about WebAPI, its uses, implementations, and security aspects since last five articles of the series. The article is also written and compiled by my co-author Sachin Verma. This article of the series will explain how we can handle requests and log them for tracking and for the sake of debugging, how we can handle exceptions and log them. We’ll follow centralized way of handling exceptions in WebAPI and write our custom classes to be mapped to the type of exception that we encounter and log accordingly. I’ll use NLog to log requests and exceptions as well. We’ll leverage the capabilities of Exception Filters and Action Filters to centralize request logging and exception handling in WebAPI.

Roadmap

The following is the roadmap I have setup to learn WebAPI step by step,

Roadmap

 
 
 

I’ll purposely use Visual Studio 2010 and .NET Framework 4.0 because there are few implementations that are very hard to find in .NET Framework 4.0, but I’ll make it easy by showing how we can do it.

Request Logging

Since we are writing web services, we are exposing our end points. We must know where the requests are coming from and what requests are coming to our server. Logging could be very beneficial and helps us in a lot of ways such as debugging, tracing, monitoring and analytics.

Request Logging

We already have an existing design. If you open the solution, you’ll get to see the structure as mentioned below or one can also implement this approach in their existing solution as well,

Setup NLog in WebAPI

NLog serves various purposes but primarily logging. We’ll use NLog for logging into files and windows event as well. You can read more about NLog.

One can use the sample application that we used in Day#5 or can have any other application as well. I am using the existing sample application that we were following throughout all the parts of this series. Our application structure looks something like,

web api

Step 1: Download NLog Package

Right click WebAPI project and select manage Nuget Packages from the list. When the Nuget Package Manager appears, search for NLog. You’ll get Nlog like in the following image, install it to our project,

search for NLog

install

After adding this you will find the following NLog dll referenced in your application –

NLog

Step 2: Configuring NLog

To configure NLog with application add the following settings in our existing WebAPI web.config file,

ConfigSection –

ConfigSection

Configuration Section - I have added the <NLog> section to configuration and defined the path and format dynamic target log file name, also added the eventlog source to API Services.

dynamic log file name

As mentioned in the above target path, I have also created “APILog” folder in the base directory of application –

APILog image

Now we have configured the NLog in our application, and it is ready to start work for request logging. Note that in the rules section we have defined rules for logging in files as well as in windows events log as well, you can choose both of them or can opt for one too. Let’s start with logging request in application, with action filters.

NLogger Class

Add a folder “Helpers” in the API, which will segregate the application code for readability, better understanding and maintainability.

Helpers
To start add our main class “NLogger”, which will be responsible for all types of errors and info logging, to same Helper folder. Here NLogger class implements ITraceWriter interface, which provides “Trace” method for the service request,

  1. #region Using namespaces.  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.Linq;  
  5. using System.Web;  
  6. using System.Web.Http.Tracing;  
  7. using NLog;  
  8. using System.Net.Http;  
  9. using System.Text;  
  10. using WebApi.ErrorHelper;  
  11. #endregion  
  12.   
  13. namespace WebApi.Helpers  
  14. {  
  15.     /// <summary>  
  16.     /// Public class to log Error/info messages to the access log file  
  17.     /// </summary>  
  18.     public sealed class NLogger : ITraceWriter  
  19.     {  
  20.         #region Private member variables.  
  21.         private static readonly Logger ClassLogger = LogManager.GetCurrentClassLogger();  
  22.   
  23.         private static readonly Lazy<Dictionary<TraceLevel, Action<string>>> LoggingMap = new Lazy<Dictionary<TraceLevel, Action<string>>>(() => new Dictionary<TraceLevel, Action<string>> { { TraceLevel.Info, ClassLogger.Info }, { TraceLevel.Debug, ClassLogger.Debug }, { TraceLevel.Error, ClassLogger.Error }, { TraceLevel.Fatal, ClassLogger.Fatal }, { TraceLevel.Warn, ClassLogger.Warn } });  
  24.         #endregion  
  25.  
  26.         #region Private properties.  
  27.         /// <summary>  
  28.         /// Get property for Logger  
  29.         /// </summary>  
  30.         private Dictionary<TraceLevel, Action<string>> Logger  
  31.         {  
  32.             get { return LoggingMap.Value; }  
  33.         }  
  34.         #endregion  
  35.  
  36.         #region Public member methods.  
  37.         /// <summary>  
  38.         /// Implementation of TraceWriter to trace the logs.  
  39.         /// </summary>  
  40.         /// <param name="request"></param>  
  41.         /// <param name="category"></param>  
  42.         /// <param name="level"></param>  
  43.         /// <param name="traceAction"></param>  
  44.         public void Trace(HttpRequestMessage request, string category, TraceLevel level, Action<TraceRecord> traceAction)  
  45.         {  
  46.             if (level != TraceLevel.Off)  
  47.             {  
  48.                 if (traceAction != null && traceAction.Target != null)  
  49.                 {  
  50.                     category = category + Environment.NewLine + "Action Parameters : " + traceAction.Target.ToJSON();  
  51.                 }  
  52.                 var record = new TraceRecord(request, category, level);  
  53.                 if (traceAction != null) traceAction(record);  
  54.                 Log(record);  
  55.             }  
  56.         }  
  57.         #endregion  
  58.  
  59.         #region Private member methods.  
  60.         /// <summary>  
  61.         /// Logs info/Error to Log file  
  62.         /// </summary>  
  63.         /// <param name="record"></param>  
  64.         private void Log(TraceRecord record)  
  65.         {  
  66.             var message = new StringBuilder();  
  67.   
  68.             if (!string.IsNullOrWhiteSpace(record.Message))  
  69.                 message.Append("").Append(record.Message + Environment.NewLine);  
  70.   
  71.             if (record.Request != null)  
  72.             {  
  73.                 if (record.Request.Method != null)  
  74.                     message.Append("Method: " + record.Request.Method + Environment.NewLine);  
  75.   
  76.                 if (record.Request.RequestUri != null)  
  77.                     message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine);  
  78.   
  79.                 if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null)  
  80.                     message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine);  
  81.             }  
  82.   
  83.             if (!string.IsNullOrWhiteSpace(record.Category))  
  84.                 message.Append("").Append(record.Category);  
  85.   
  86.             if (!string.IsNullOrWhiteSpace(record.Operator))  
  87.                 message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation);  
  88.   
  89.               
  90.             Logger[record.Level](Convert.ToString(message) + Environment.NewLine);  
  91.         }  
  92.         #endregion  
  93.     }  
  94. }  
Adding Action Filter

Action filter will be responsible for handling all the incoming requests to our APIs and logging them using NLogger class. We have “OnActionExecuting” method that is implicitly called if we mark our controllers or global application to use that particular filter. So each time any action of any controller will be hit, our “OnActionExecuting” method will execute to log the request.

Step 1: Adding LoggingFilterAttribute class

Create a class LoggingFilterAttribute to “ActionFilters” folder and add the following code:
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Web;  
  5. using System.Web.Http.Filters;  
  6. using System.Web.Http.Controllers;  
  7. using System.Web.Http.Tracing;  
  8. using System.Web.Http;  
  9. using WebApi.Helpers;  
  10.   
  11. namespace WebApi.ActionFilters  
  12. {  
  13.     public class LoggingFilterAttribute : ActionFilterAttribute  
  14.     {  
  15.         public override void OnActionExecuting(HttpActionContext filterContext)  
  16.         {  
  17.             GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());  
  18.             var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter();  
  19.             trace.Info(filterContext.Request, "Controller : " + filterContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + filterContext.ActionDescriptor.ActionName, "JSON", filterContext.ActionArguments);  
  20.         }  
  21.     }  
  22. }  
The LoggingFilterAttribute class derived from ActionFilterAttribute, which is under System.Web.Http.Filters and overriding the OnActionExecuting method.
Here I have replaced the default “ITraceWriter” service with our NLogger class instance in the controllers service container. Now GetTraceWriter() method will return our instance (instance NLogger class) and Info() will call trace() method of our NLogger class.

Note that the following code,
  1. GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());  
Is used to resolve dependency between ITaceWriter and NLogger class. Thereafter we use a variable named trace to get the instance and trace.Info() is used to log the request and whatever text we want to add along with that request.

Step 2: Registering Action Filter (LoggingFilterAttribute).

In order to register the created action filter to applications filters, just add a new instance of your action filter to config.Filters in WebApiConfig class.
  1. using System.Web.Http;  
  2. using WebApi.ActionFilters;  
  3.   
  4. namespace WebApi.App_Start  
  5. {  
  6.     public static class WebApiConfig  
  7.     {  
  8.         public static void Register(HttpConfiguration config)  
  9.         {  
  10.             config.Filters.Add(new LoggingFilterAttribute());  
  11.         }  
  12.     }  
  13. }  
Now this action filter is applicable to all the controllers and actions in our project. You may not believe but request logging is done. It’s time to run the application and validate our homework.

Registering
Image credit: https://pixabay.com/en/social-media-network-media-54536/

Running the application

Let’s run the application and try to make a call, using token based authorization, we have already covered authorization in day#5. You first need to authenticate your request using login service, and then that service will return a token for making calls to other services. Use that token to make calls to other services, for more details you can read day5 of this series.

Just run the application, we get the following,

Just run the application

We already have our test client added, but for new readers, just go to Manage Nuget Packages, by right clicking WebAPI project and type WebAPITestClient in searchbox in online packages,

Nuget Packages

You’ll get “A simple Test Client for ASP.NET Web API”, just add it. You’ll get a help controller in Areas-> HelpPage like the following screenshot,

HelpPage

I have already provided the database scripts and data in my previous article, you can use the same.
Append “/help” in the application url, and you’ll get the test client,

GET:

GET

POST:

POST

PUT:

PUT

DELETE:

DELETE

You can test each service by clicking on it. Once you click on the service link, you'll be redirected to test the service page of that particular service.On that page there is a button Test API in the right bottom corner, just press that button to test your service,

test API

Service for Get All products,

Service for Get All products

In the following case, I have already generated the token and now using it to make call to fetch all the products from products table in the database.

Token API

Here I have called allproducts API, Add the value for parameter Id and “Token” header with its current value and click to get the result -

Add the value for parameter

Now let’s see what happens to our APILog folder in application. Here you can find that the API log has been created, with the same name we have configured in NLog configuration in web.config file. The log file contains all the supplied details such as Timestamp, Method type, URL, Header information (Token), Controller name, action and action parameters. You can also add more details to this log that you deem important for your application.

APILog folder

Logging Done!

Logging Done

Exception Logging

Our logging setup is completed, now we’ll focus on centralizing exception logging as well, so that none of the exception escapes without logging itself. Logging exception is of very high importance, it keeps track of all the exceptions. No matter business or application or system exceptions, but all of them have to be logged.

Implementing Exception logging

Step 1: Exception Filter Attribute.

Adding an action filter in our application for logging the exceptions, for this create a class GlobalExceptionAttribute to “ActionFilter” folder and add the following code, the class is derived from ExceptionFilterAttribute, which is under System.Web.Http.Filters.

I have overriden OnException() method, and replaced the default “ITraceWriter” service with our NLogger class instance in the controllers service container, same as we have done in Action logging in the above section. Now GetTraceWriter() method will return our instance (instance NLogger class) and Info() will call trace() method of NLogger class.
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Web;  
  5. using System.Web.Http.Filters;  
  6. using System.Web.Http;  
  7. using System.Web.Http.Tracing;  
  8. using WebApi.Helpers;  
  9. using System.ComponentModel.DataAnnotations;  
  10. using System.Net.Http;  
  11. using System.Net;  
  12.   
  13. namespace WebApi.ActionFilters  
  14. {  
  15.     /// <summary>  
  16.     /// Action filter to handle for Global application errors.  
  17.     /// </summary>  
  18.     public class GlobalExceptionAttribute : ExceptionFilterAttribute  
  19.     {  
  20.         public override void OnException(HttpActionExecutedContext context)  
  21.         {  
  22.             GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());  
  23.             var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter();  
  24.             trace.Error(context.Request, "Controller : " + context.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + context.ActionContext.ActionDescriptor.ActionName, context.Exception);  
  25.   
  26.             var exceptionType = context.Exception.GetType();  
  27.   
  28.             if (exceptionType == typeof(ValidationException))  
  29.             {  
  30.                 var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "ValidationException", };  
  31.                 throw new HttpResponseException(resp);  
  32.   
  33.             }  
  34.             else if (exceptionType == typeof(UnauthorizedAccessException))  
  35.             {  
  36.                 throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.Unauthorized));  
  37.             }  
  38.             else  
  39.             {  
  40.                 throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.InternalServerError));  
  41.             }  
  42.         }  
  43.     }  
  44. }  
Step 2: Modify NLogger Class.

Our NLogger class is capable to log all info and events, I have done some changes in private method Log() to handle the exceptions,
  1. # region Private member methods.  
  2.     /// <summary>  
  3.     /// Logs info/Error to Log file  
  4.     /// </summary>  
  5.     /// <param name="record"></param>  
  6. private void Log(TraceRecord record) 
  7. {  
  8.     var message = new StringBuilder();  
  9.   
  10.     if (!string.IsNullOrWhiteSpace(record.Message))  
  11.         message.Append("").Append(record.Message + Environment.NewLine);  
  12.   
  13.     if (record.Request != null) {  
  14.         if (record.Request.Method != null)  
  15.             message.Append("Method: " + record.Request.Method + Environment.NewLine);  
  16.   
  17.         if (record.Request.RequestUri != null)  
  18.             message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine);  
  19.   
  20.         if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null)  
  21.             message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine);  
  22.     }  
  23.   
  24.     if (!string.IsNullOrWhiteSpace(record.Category))  
  25.         message.Append("").Append(record.Category);  
  26.   
  27.     if (!string.IsNullOrWhiteSpace(record.Operator))  
  28.         message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation);  
  29.   
  30.     if (record.Exception != null && !string.IsNullOrWhiteSpace(record.Exception.GetBaseException().Message)) {  
  31.         var exceptionType = record.Exception.GetType();  
  32.         message.Append(Environment.NewLine);  
  33.         message.Append("").Append("Error: " + record.Exception.GetBaseException().Message + Environment.NewLine);  
  34.     }  
  35.   
  36.     Logger[record.Level](Convert.ToString(message) + Environment.NewLine);  
  37. }  
Step 3: Modify Controller for Exceptions.

Our application is now ready to run, but there is no exception in our code, so I added a throw exception code in ProductController, just the Get(int id) method so that it can throw exception for testing our exception logging mechanism, It will throw an exception if the product is not there in the database with the provided id.
  1.   // GET api/product/5  
  2.  [GET("productid/{id?}")]  
  3.  [GET("particularproduct/{id?}")]  
  4.  [GET("myproduct/{id:range(1, 3)}")]  
  5.  public HttpResponseMessage Get(int id)  
  6.  {  
  7. var product = _productServices.GetProductById(id);  
  8.       if (product != null)  
  9.         return Request.CreateResponse(HttpStatusCode.OK, product);  
  10.   
  11.  throw new Exception("No product found for this id");  
  12.       //return Request.CreateErrorResponse(HttpStatusCode.NotFound,   "No product found for this id");  
  13.  }  
Step 4: Run the application,

Run the application and click on Product/all API

click on Product

all API

Add the parameter id value to 1 and header Token with it’s current value, click on send button to get the result,

click on send button

Now we can see that the Status is 200/OK, and we also get a product with the provided id in the response body. Let’s see the API log now,

API log

The log has captured the call of Product API, now provide a new product id as parameter, which is not there in the database, I am using 12345 as product id and the result is:

product id

We can see there is an 500/Internal Server Error now in response status, let's check the API Log-

response status

Well, now the log has captured both the event and error of same call on the server, you can see call log details and the error with provided error message in the log.

error

Custom Exception logging

In the above section we have implemented exception logging, but there is a default system response and status ( i. e. 500/Internal Server Error), It will be always good to have your own custom response and exceptions for your API. That will be easier for client to consume and understand the API responses.

Step 1: Add Custom Exception Classes.

Add “Error Helper” folder to application to maintain our custom exception classes separately and add “IApiExceptions” interface to newly created “ErrorHelper” folder -
IApiExceptions

Add the following code in the IApiExceptions interface, this will serve as a template for all exception classes, I have added four common properties for our custom classes to maintain Error Code, ErrorDescription, HttpStatus (Contains the values of status codes defined for HTTP) and ReasonPhrase.
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Net;  
  6.   
  7. namespace WebApi.ErrorHelper  
  8. {  
  9.     /// <summary>  
  10.     /// IApiExceptions Interface  
  11.     /// </summary>  
  12.     public interface IApiExceptions  
  13.     {  
  14.         /// <summary>  
  15.         /// ErrorCode  
  16.         /// </summary>  
  17.         int ErrorCode { getset; }  
  18.         /// <summary>  
  19.         /// ErrorDescription  
  20.         /// </summary>  
  21.         string ErrorDescription { getset; }  
  22.         /// <summary>  
  23.         /// HttpStatus  
  24.         /// </summary>  
  25.         HttpStatusCode HttpStatus { getset; }  
  26.         /// <summary>  
  27.         /// ReasonPhrase  
  28.         /// </summary>  
  29.         string ReasonPhrase { getset; }  
  30.     }  
  31. }  
Here, I divided our exceptions in the following three categories:
  1. API Exceptions – for API level exceptions.
  2. Business Exceptions – for exceptions at business logic level.
  3. Data Exceptions – Data related exceptions.

To implement this create three new classes ApiException.cs, ApiDataException.cs and ApiBusinessException classes to same folder which implements IApiExceptions interface with the following code to the classes,

  1. #region Using namespaces.  
  2. using System;  
  3. using System.Net;  
  4. using System.Runtime.Serialization;  
  5. #endregion  
  6.   
  7.   
  8. namespace WebApi.ErrorHelper  
  9. {  
  10.     /// <summary>  
  11.     /// Api Exception  
  12.     /// </summary>  
  13.     [Serializable]  
  14.     [DataContract]  
  15.     public class ApiException : Exception, IApiExceptions  
  16.     {  
  17.         #region Public Serializable properties.  
  18.         [DataMember]  
  19.         public int ErrorCode { getset; }  
  20.         [DataMember]  
  21.         public string ErrorDescription { getset; }  
  22.         [DataMember]  
  23.         public HttpStatusCode HttpStatus { getset; }  
  24.           
  25.         string reasonPhrase = "ApiException";  
  26.   
  27.         [DataMember]  
  28.         public string ReasonPhrase  
  29.         {  
  30.             get { return this.reasonPhrase; }  
  31.   
  32.             set { this.reasonPhrase = value; }  
  33.         }  
  34.         #endregion  
  35.     }  
  36. }  
I have initialized ReasonPhrase property with different default values in these classes to differentiate the implementation, you can implement your custom classes as per your application needs.

The directives applied on class as Serializable and DataContract to make sure that the class defines or implements a data contract serializable and can be serialized by a serializer.
 
Note – Add reference of “System.Runtime.Serialization.dll” dll if you facing any assembly issue.

In the same way add “ApiBusinessException” and “ApiDataException” classes into the same folder with the following code,
  1. #region Using namespaces.  
  2. using System;  
  3. using System.Net;  
  4. using System.Runtime.Serialization;   
  5. #endregion  
  6.   
  7. namespace WebApi.ErrorHelper  
  8. {  
  9.     /// <summary>  
  10.     /// Api Business Exception  
  11.     /// </summary>  
  12.     [Serializable]  
  13.     [DataContract]  
  14.     public class ApiBusinessException : Exception, IApiExceptions  
  15.     {  
  16.         #region Public Serializable properties.  
  17.         [DataMember]  
  18.         public int ErrorCode { getset; }  
  19.         [DataMember]  
  20.         public string ErrorDescription { getset; }  
  21.         [DataMember]  
  22.         public HttpStatusCode HttpStatus { getset; }  
  23.   
  24.         string reasonPhrase = "ApiBusinessException";  
  25.   
  26.         [DataMember]  
  27.         public string ReasonPhrase  
  28.         {  
  29.             get { return this.reasonPhrase; }  
  30.   
  31.             set { this.reasonPhrase = value; }  
  32.         }  
  33.         #endregion  
  34.  
  35.         #region Public Constructor.  
  36.         /// <summary>  
  37.         /// Public constructor for Api Business Exception  
  38.         /// </summary>  
  39.         /// <param name="errorCode"></param>  
  40.         /// <param name="errorDescription"></param>  
  41.         /// <param name="httpStatus"></param>  
  42.         public ApiBusinessException(int errorCode, string errorDescription, HttpStatusCode httpStatus)  
  43.         {  
  44.             ErrorCode = errorCode;  
  45.             ErrorDescription = errorDescription;  
  46.             HttpStatus = httpStatus;  
  47.         }   
  48.         #endregion  
  49.   
  50.     }  
  51. }  
  1. #region Using namespaces.  
  2. using System;  
  3. using System.Net;  
  4. using System.Runtime.Serialization;  
  5. #endregion  
  6.   
  7. namespace WebApi.ErrorHelper  
  8. {  
  9.     /// <summary>  
  10.     /// Api Data Exception  
  11.     /// </summary>  
  12.     [Serializable]  
  13.     [DataContract]  
  14.     public class ApiDataException : Exception, IApiExceptions  
  15.     {  
  16.         #region Public Serializable properties.  
  17.         [DataMember]  
  18.         public int ErrorCode { getset; }  
  19.         [DataMember]  
  20.         public string ErrorDescription { getset; }  
  21.         [DataMember]  
  22.         public HttpStatusCode HttpStatus { getset; }  
  23.   
  24.         string reasonPhrase = "ApiDataException";  
  25.   
  26.         [DataMember]  
  27.         public string ReasonPhrase  
  28.         {  
  29.             get { return this.reasonPhrase; }  
  30.   
  31.             set { this.reasonPhrase = value; }  
  32.         }  
  33.  
  34.         #endregion  
  35.  
  36.         #region Public Constructor.  
  37.         /// <summary>  
  38.         /// Public constructor for Api Data Exception  
  39.         /// </summary>  
  40.         /// <param name="errorCode"></param>  
  41.         /// <param name="errorDescription"></param>  
  42.         /// <param name="httpStatus"></param>  
  43.         public ApiDataException(int errorCode, string errorDescription, HttpStatusCode httpStatus)  
  44.         {  
  45.             ErrorCode = errorCode;  
  46.             ErrorDescription = errorDescription;  
  47.             HttpStatus = httpStatus;  
  48.         }  
  49.         #endregion  
  50.     }  
  51. }  
JSON Serializers

There are some objects need to be serialized in json, to log and to transfer through the modules, for this I have added some extension methods to Object class.

For that add “System.Web.Extensions.dll” reference to project and add “JSONHelper” class to Helpers folder with the following cod,
  1. #region Using namespaces.  
  2. using System.Web.Script.Serialization;  
  3. using System.Data;  
  4. using System.Collections.Generic;  
  5. using System;  
  6.  
  7. #endregion  
  8.   
  9. namespace WebApi.Helpers  
  10. {  
  11.     public static class JSONHelper  
  12.     {  
  13.          #region Public extension methods.  
  14.         /// <summary>  
  15.         /// Extened method of object class, Converts an object to a json string.  
  16.         /// </summary>  
  17.         /// <param name="obj"></param>  
  18.         /// <returns></returns>  
  19.         public static string ToJSON(this object obj)  
  20.         {  
  21.             var serializer = new JavaScriptSerializer();  
  22.             try  
  23.             {  
  24.                 return serializer.Serialize(obj);  
  25.             }  
  26.             catch(Exception ex)  
  27.             {  
  28.                 return "";  
  29.             }  
  30.         }  
  31.          #endregion  
  32.     }  
  33. }  
In the above code “ToJSON()” method is an extension of base Object class, which serializes the object to a JSON string. The method using “JavaScriptSerializer” class which exist in “System.Web.Script.Serialization”.

Modify NLogger Class

For exception handling, I have modified the Log() method of NLogger, which will now handle different API exceptions.
  1. /// <summary>  
  2. /// Logs info/Error to Log file  
  3. /// </summary>  
  4. /// <param name="record"></param>  
  5. private void Log(TraceRecord record)  
  6. {  
  7.    var message = new StringBuilder();  
  8.   
  9.    if (!string.IsNullOrWhiteSpace(record.Message))  
  10.              message.Append("").Append(record.Message + Environment.NewLine);  
  11.   
  12.          if (record.Request != null)  
  13.          {  
  14.              if (record.Request.Method != null)  
  15.                  message.Append("Method: " + record.Request.Method + Environment.NewLine);  
  16.   
  17.              if (record.Request.RequestUri != null)  
  18.                  message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine);  
  19.   
  20.              if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null)  
  21.                  message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine);  
  22.          }  
  23.   
  24.          if (!string.IsNullOrWhiteSpace(record.Category))  
  25.              message.Append("").Append(record.Category);  
  26.   
  27.          if (!string.IsNullOrWhiteSpace(record.Operator))  
  28.              message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation);  
  29.   
  30.          if (record.Exception != null && !string.IsNullOrWhiteSpace(record.Exception.GetBaseException().Message))  
  31.          {  
  32.              var exceptionType = record.Exception.GetType();  
  33.              message.Append(Environment.NewLine);  
  34.              if (exceptionType == typeof(ApiException))  
  35.              {  
  36.                  var exception = record.Exception as ApiException;  
  37.                  if (exception != null)  
  38.                  {  
  39.                      message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine);  
  40.                      message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine);  
  41.                  }  
  42.              }  
  43.              else if (exceptionType == typeof(ApiBusinessException))  
  44.              {  
  45.                  var exception = record.Exception as ApiBusinessException;  
  46.                  if (exception != null)  
  47.                  {  
  48.                      message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine);  
  49.                      message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine);  
  50.                  }  
  51.              }  
  52.              else if (exceptionType == typeof(ApiDataException))  
  53.              {  
  54.                  var exception = record.Exception as ApiDataException;  
  55.                  if (exception != null)  
  56.                  {  
  57.                      message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine);  
  58.                      message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine);  
  59.                  }  
  60.              }  
  61.              else  
  62.                  message.Append("").Append("Error: " + record.Exception.GetBaseException().Message + Environment.NewLine);  
  63.          }  
  64.   
  65.          Logger[record.Level](Convert.ToString(message) + Environment.NewLine);  
  66.      }  
The code above checks the exception object of TraceRecord and updates the logger as per the exception type.

Modify GlobalExceptionAttribute

As we have created GlobalExceptionAttribute to handle al theexceptions and create response in case of any exception. Now I have added some new code to this in order to enable the GlobalExceptionAttribute class to handle custom exceptions. I am adding only modified method here for your reference.
  1. public override void OnException(HttpActionExecutedContext context)  
  2. {  
  3.     GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());  
  4.     var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter();  
  5.     trace.Error(context.Request, "Controller : " + context.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + context.ActionContext.ActionDescriptor.ActionName, context.Exception);  
  6.   
  7.     var exceptionType = context.Exception.GetType();  
  8.   
  9.     if (exceptionType == typeof(ValidationException))  
  10.     {  
  11.         var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "ValidationException", };  
  12.         throw new HttpResponseException(resp);  
  13.   
  14.     }  
  15.     else if (exceptionType == typeof(UnauthorizedAccessException))  
  16.     {  
  17.         throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.Unauthorized, new ServiceStatus() { StatusCode = (int)HttpStatusCode.Unauthorized, StatusMessage = "UnAuthorized", ReasonPhrase = "UnAuthorized Access" }));  
  18.     }  
  19.     else if (exceptionType == typeof(ApiException))  
  20.     {  
  21.         var webapiException = context.Exception as ApiException;  
  22.         if (webapiException != null)  
  23.             throw new HttpResponseException(context.Request.CreateResponse(webapiException.HttpStatus, new ServiceStatus() { StatusCode = webapiException.ErrorCode, StatusMessage = webapiException.ErrorDescription, ReasonPhrase = webapiException.ReasonPhrase }));  
  24.     }  
  25.     else if (exceptionType == typeof(ApiBusinessException))  
  26.     {  
  27.         var businessException = context.Exception as ApiBusinessException;  
  28.         if (businessException != null)  
  29.             throw new HttpResponseException(context.Request.CreateResponse(businessException.HttpStatus, new ServiceStatus() { StatusCode = businessException.ErrorCode, StatusMessage = businessException.ErrorDescription, ReasonPhrase = businessException.ReasonPhrase }));  
  30.     }  
  31.     else if (exceptionType == typeof(ApiDataException))  
  32.     {  
  33.         var dataException = context.Exception as ApiDataException;  
  34.         if (dataException != null)  
  35.             throw new HttpResponseException(context.Request.CreateResponse(dataException.HttpStatus, new ServiceStatus() { StatusCode = dataException.ErrorCode, StatusMessage = dataException.ErrorDescription, ReasonPhrase = dataException.ReasonPhrase }));  
  36.     }  
  37.     else  
  38.     {  
  39.         throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.InternalServerError));  
  40.     }  
  41. }  
In the above code I have modified the overrided method OnExeption() and created new Http response exception based on the different exception types.

Modify Product Controller

Now modify the Product controller to throw our custom exception form, please look into the Get method I have modified to throw the APIDataException in case if data is not found and APIException in any other kind of error.
  1. // GET api/product/5  
  2. [GET("productid/{id?}")]  
  3. [GET("particularproduct/{id?}")]  
  4. [GET("myproduct/{id:range(1, 3)}")]  
  5. public HttpResponseMessage Get(int id)  
  6. {  
  7. if (id != null)  
  8.       {  
  9.         var product = _productServices.GetProductById(id);  
  10.             if (product != null)  
  11.                 return Request.CreateResponse(HttpStatusCode.OK, product);  
  12.   
  13. throw new ApiDataException(1001, "No product found for this id.", HttpStatusCode.NotFound);  
  14.       }  
  15.       throw new ApiException() { ErrorCode = (int)HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..." };  
  16. }  
Run the application

Run the application and click on Product/all API -

Run the application

Product

Add the parameter id value to 1 and header Token with its current value, click on send button to get the result,

Token

Now we can see that the Status is 200/OK, and we also get a product with the provided id in the response body. Lets see the API log now,

response body

The log has captured the call of Product API, now provide a new product id as parameter, which is not there in the database, I am using 12345 as product id and the result is,

result

We can see now there is a custom error status code “1001” and messages “No product found for this id.” And the generic status code “500/Internal Server Error” is now replaced with our supplied code “404/ Not Found”, which is more meaningful for the client or consumer.

Let's see the APILog now,

APILog

Well, now the log has captured both the event and error of same call on the server, you can see call log details and the error with provided error message in the log with our custom error code, I have only captured error description and error code, but you can add more details in the log as per your application needs.

Update the controller for new Exception Handling

The following is the code for controllers with implementation of custom exception handling and logging –

Product Controller
  1. using System.Collections.Generic;  
  2. using System.Linq;  
  3. using System.Net;  
  4. using System.Net.Http;  
  5. using System.Web.Http;  
  6. using AttributeRouting;  
  7. using AttributeRouting.Web.Http;  
  8. using BusinessEntities;  
  9. using BusinessServices;  
  10. using WebApi.ActionFilters;  
  11. using WebApi.Filters;  
  12. using System;  
  13. using WebApi.ErrorHelper;  
  14.   
  15. namespace WebApi.Controllers  
  16. {  
  17.     [AuthorizationRequired]  
  18.     [RoutePrefix("v1/Products/Product")]  
  19.     public class ProductController : ApiController  
  20.     {  
  21.         #region Private variable.  
  22.   
  23.         private readonly IProductServices _productServices;  
  24.  
  25.         #endregion  
  26.  
  27.         #region Public Constructor  
  28.   
  29.         /// <summary>  
  30.         /// Public constructor to initialize product service instance  
  31.         /// </summary>  
  32.         public ProductController(IProductServices productServices)  
  33.         {  
  34.             _productServices = productServices;  
  35.         }  
  36.  
  37.         #endregion  
  38.   
  39.         // GET api/product  
  40.         [GET("allproducts")]  
  41.         [GET("all")]  
  42.         public HttpResponseMessage Get()  
  43.         {  
  44.             var products = _productServices.GetAllProducts();  
  45.             var productEntities = products as List<ProductEntity> ?? products.ToList();  
  46.             if (productEntities.Any())  
  47.                 return Request.CreateResponse(HttpStatusCode.OK, productEntities);  
  48.             throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);  
  49.         }  
  50.   
  51.         // GET api/product/5  
  52.         [GET("productid/{id?}")]  
  53.         [GET("particularproduct/{id?}")]  
  54.         [GET("myproduct/{id:range(1, 3)}")]  
  55.         public HttpResponseMessage Get(int id)  
  56.         {  
  57.             if (id != null)  
  58.             {  
  59.                 var product = _productServices.GetProductById(id);  
  60.                 if (product != null)  
  61.                     return Request.CreateResponse(HttpStatusCode.OK, product);  
  62.   
  63.                 throw new ApiDataException(1001, "No product found for this id.", HttpStatusCode.NotFound);  
  64.             }  
  65.             throw new ApiException() { ErrorCode = (int)HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..." };  
  66.         }  
  67.   
  68.         // POST api/product  
  69.         [POST("Create")]  
  70.         [POST("Register")]  
  71.         public int Post([FromBody] ProductEntity productEntity)  
  72.         {  
  73.             return _productServices.CreateProduct(productEntity);  
  74.         }  
  75.   
  76.         // PUT api/product/5  
  77.         [PUT("Update/productid/{id}")]  
  78.         [PUT("Modify/productid/{id}")]  
  79.         public bool Put(int id, [FromBody] ProductEntity productEntity)  
  80.         {  
  81.             if (id > 0)  
  82.             {  
  83.                 return _productServices.UpdateProduct(id, productEntity);  
  84.             }  
  85.             return false;  
  86.         }  
  87.   
  88.         // DELETE api/product/5  
  89.         [DELETE("remove/productid/{id}")]  
  90.         [DELETE("clear/productid/{id}")]  
  91.         [PUT("delete/productid/{id}")]  
  92.         public bool Delete(int id)  
  93.         {  
  94.             if (id != null && id > 0)  
  95.             {  
  96.                 var isSuccess = _productServices.DeleteProduct(id);  
  97.                 if (isSuccess)  
  98.                 {  
  99.                     return isSuccess;  
  100.                 }  
  101.                 throw new ApiDataException(1002, "Product is already deleted or not exist in system.", HttpStatusCode.NoContent );  
  102.             }  
  103.             throw new ApiException() {ErrorCode = (int) HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..."};  
  104.         }  
  105.     }  
  106. }  
Now you can see, our application is so rich and scalable that none of the exception or transaction can escape logging. Once setup is inplaced, now you don’t have to worry about writing code each time for logging or requests and exceptions, but you can relax and focus on business logic only.

rich and scalable
Image credit: https://pixabay.com/en/kermit-frog-meadow-daisy-concerns-383370/

Conclusion

In this article we learned how to perform request logging and exception logging in WebPI. There could be numerous ways in which you can perform these operations but I tried to present this in as simple way as possible. My approach was to take our enterprise level to next level of development, where developers should not always be worried about exception handling and logging. Our solution provides a generic approach of centralizing the operations at one place; all the requests and exceptions are automatically taken care of. In my new articles, I’ll try to enhance the application by explaining unit testing in WebAPI and OData in WebAPI. You can download the complete source code of this article with packages from GitHub. Happy coding.

Read more:

For more technical articles you can reach out to CodeTeddy

My other series of articles:


Similar Articles