Download Source Code from GitHub
Introduction
Roadmap
-
- RESTful Day #1: Enterprise level application architecture with Web APIs using Entity Framework, Generic Repository pattern and Unit of Work.
- RESTful Day #2: Inversion of control using dependency injection in Web APIs using Unity Container and Bootstrapper.
- RESTful Day #3: Resolve dependency of dependencies using Inversion of Control and dependency injection in Asp.net Web APIs with Unity Container and Managed Extensibility Framework (MEF).
- RESTful Day #4: Custom URL Re-Writing/Routing using Attribute Routes in MVC 4 Web APIs.
- RESTful Day #5: Basic Authentication and Token based custom Authorization in Web APIs using Action Filters.
- RESTful Day #6: Request logging and Exception handing/logging in Web APIs using Action Filters, Exception Filters and NLog.
- RESTful Day #7: Unit Testing and Integration Testing in WebAPI using NUnit and Moq framework (Part1).
- RESTful Day #8: Unit Testing and Integration Testing in WebAPI using NUnit and Moq framework (Part 2).
- RESTful Day #9: Extending OData support in ASP.NET Web APIs.
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 like, debugging, tracing, monitoring and analytics.
Setup NLog in WebAPI

Step 1: Download NLog Package
After adding this you will find following NLog dll referenced in your application.
Step 2: Configuring NLog
ConfigSection –
Configuration Section – I have added the section to configuration and defined the path and format dynamic target log file name, also added the eventlog source to Api Services.
As mentioned in above target path, I have also created to “APILog” folder in the base directory of application.
NLogger Class
Add a folder “Helpers” in the API, which will segregate the application code for readability, better understanding and maintainability.
NLogger
“, which will 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.#region Using namespaces. using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http.Tracing; using NLog; using System.Net.Http; using System.Text; using WebApi.ErrorHelper; #endregion namespace WebApi.Helpers { /// <summary> /// Public class to log Error/info messages to the access log file /// </summary> public sealed class NLogger : ITraceWriter { #region Private member variables. private static readonly Logger ClassLogger = LogManager.GetCurrentClassLogger(); 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 } }); #endregion #region Private properties. /// <summary> /// Get property for Logger /// </summary> private Dictionary<TraceLevel, Action<string>> Logger { get { return LoggingMap.Value; } } #endregion #region Public member methods. /// <summary> /// Implementation of TraceWriter to trace the logs. /// </summary> /// <param name="request"></param> /// <param name="category"></param> /// <param name="level"></param> /// <param name="traceAction"></param> public void Trace(HttpRequestMessage request, string category, TraceLevel level, Action traceAction) { if (level != TraceLevel.Off) { if (traceAction != null && traceAction.Target != null) { category = category + Environment.NewLine + "Action Parameters : " + traceAction.Target.ToJSON(); } var record = new TraceRecord(request, category, level); if (traceAction != null) traceAction(record); Log(record); } } #endregion #region Private member methods. /// <summary> /// Logs info/Error to Log file /// </summary> /// <param name="record"></param> private void Log(TraceRecord record) { var message = new StringBuilder(); if (!string.IsNullOrWhiteSpace(record.Message)) message.Append("").Append(record.Message + Environment.NewLine); if (record.Request != null) { if (record.Request.Method != null) message.Append("Method: " + record.Request.Method + Environment.NewLine); if (record.Request.RequestUri != null) message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine); if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null) message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine); } if (!string.IsNullOrWhiteSpace(record.Category)) message.Append("").Append(record.Category); if (!string.IsNullOrWhiteSpace(record.Operator)) message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation); Logger[record.Level](Convert.ToString(message) + Environment.NewLine); } #endregion } }
Adding Action Filter
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
LoggingFilterAttribute
to “ActionFilters” folder and add following code.using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http.Filters; using System.Web.Http.Controllers; using System.Web.Http.Tracing; using System.Web.Http; using WebApi.Helpers; namespace WebApi.ActionFilters { public class LoggingFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext filterContext) { GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger()); var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter(); trace.Info(filterContext.Request, "Controller : " + filterContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + filterContext.ActionDescriptor.ActionName, "JSON", filterContext.ActionArguments); } } }
LoggingFilterAttribute
class derived from ActionFilterAttribute
, which is underSystem.Web.Http.Filters
and overriding the OnActionExecuting
method.NLogger
class instance in the controller’s service container. Now GetTraceWriter()
method will return our instance (instance NLogger
class) and Info()
will call trace()
method of our NLogger
class.GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger());
ITaceWriter
and NLogger
class. Thereafter we use a variable namedtrace
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)
config.Filters
in WebApiConfig
class.using System.Web.Http; using WebApi.ActionFilters; namespace WebApi.App_Start { public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Filters.Add(new LoggingFilterAttribute()); } } }

Running the application










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:
Now let’s see what happens to our APILog folder in application. Here you find 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 like Timestamp, Method type, URL , Header information (Token), Controller name, action and action parameters. You can also add more details to this log which you deem important for your application.
Logging Done!
Exception Logging
Implementing Exception logging
Step 1: Exception Filter Attribute
GlobalExceptionAttribute
to “ActionFilter” folder and add the code below, the class is derived fromExceptionFilterAttribute
, which is under System.Web.Http.Filters
.OnException()
method and replace the default “ITraceWriter” service with our NLogger
class instance in the controller’s service container, same as we have done in Action logging in above section. Now theGetTraceWriter()
method will return our instance (instance NLogger
class) and Info()
will call trace()
method of NLogger
class.using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http.Filters; using System.Web.Http; using System.Web.Http.Tracing; using WebApi.Helpers; using System.ComponentModel.DataAnnotations; using System.Net.Http; using System.Net; namespace WebApi.ActionFilters { /// <summary> /// Action filter to handle for Global application errors. /// </summary> public class GlobalExceptionAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger()); var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter(); trace.Error(context.Request, "Controller : " + context.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + context.ActionContext.ActionDescriptor.ActionName, context.Exception); var exceptionType = context.Exception.GetType(); if (exceptionType == typeof(ValidationException)) { var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "ValidationException", }; throw new HttpResponseException(resp); } else if (exceptionType == typeof(UnauthorizedAccessException)) { throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.Unauthorized)); } else { throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.InternalServerError)); } } } }
Step 2: Modify NLogger Class
Log()
to handle the exceptions#region Private member methods. /// <summary> /// Logs info/Error to Log file /// </summary> /// <param name="record"></param> private void Log(TraceRecord record) { var message = new StringBuilder(); if (!string.IsNullOrWhiteSpace(record.Message)) message.Append("").Append(record.Message + Environment.NewLine); if (record.Request != null) { if (record.Request.Method != null) message.Append("Method: " + record.Request.Method + Environment.NewLine); if (record.Request.RequestUri != null) message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine); if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null) message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine); } if (!string.IsNullOrWhiteSpace(record.Category)) message.Append("").Append(record.Category); if (!string.IsNullOrWhiteSpace(record.Operator)) message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation); if (record.Exception != null && !string.IsNullOrWhiteSpace(record.Exception.GetBaseException().Message)) { var exceptionType = record.Exception.GetType(); message.Append(Environment.NewLine); message.Append("").Append("Error: " + record.Exception.GetBaseException().Message + Environment.NewLine); } Logger[record.Level](Convert.ToString(message) + Environment.NewLine); }
Step 3: Modify Controller for Exception
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 database with the provided id.// GET api/product/5 [GET("productid/{id?}")] [GET("particularproduct/{id?}")] [GET("myproduct/{id:range(1, 3)}")] public HttpResponseMessage Get(int id) { var product = _productServices.GetProductById(id); if (product != null) return Request.CreateResponse(HttpStatusCode.OK, product); throw new Exception("No product found for this id"); //return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id"); }
Step 4: Run the application





We can see there is an 500/Internal Server Error now in response status, let’s check the API Log:
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.
Custom Exception logging
Step 1: Add Custom Exception Classes
IApiExceptions
” interface to newly created “ErrorHelper
” folder –
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; namespace WebApi.ErrorHelper { /// <summary> /// IApiExceptions Interface /// </summary> public interface IApiExceptions { /// <summary> /// ErrorCode /// </summary> int ErrorCode { get; set; } /// <summary> /// ErrorDescription /// </summary> string ErrorDescription { get; set; } /// <summary> /// HttpStatus /// </summary> HttpStatusCode HttpStatus { get; set; } /// <summary> /// ReasonPhrase /// </summary> string ReasonPhrase { get; set; } } }
- API Exceptions – for API level exceptions.
- Business Exceptions – for exceptions at business logic level.
- Data Exceptions – Data related exceptions.
ApiBusinessException
classes to same folder which implements IApiExceptions interface with following code to the classes.#region Using namespaces. using System; using System.Net; using System.Runtime.Serialization; #endregion namespace WebApi.ErrorHelper { /// <summary> /// Api Exception /// </summary> [Serializable] [DataContract] public class ApiException : Exception, IApiExceptions { #region Public Serializable properties. [DataMember] public int ErrorCode { get; set; } [DataMember] public string ErrorDescription { get; set; } [DataMember] public HttpStatusCode HttpStatus { get; set; } string reasonPhrase = "ApiException"; [DataMember] public string ReasonPhrase { get { return this.reasonPhrase; } set { this.reasonPhrase = value; } } #endregion } }
ReasonPhrase
property with different default values in these classes to differentiate the implementation, you can use implement your custom classes as per your application needs.#region Using namespaces. using System; using System.Net; using System.Runtime.Serialization; #endregion namespace WebApi.ErrorHelper { /// <summary> /// Api Business Exception /// </summary> [Serializable] [DataContract] public class ApiBusinessException : Exception, IApiExceptions { #region Public Serializable properties. [DataMember] public int ErrorCode { get; set; } [DataMember] public string ErrorDescription { get; set; } [DataMember] public HttpStatusCode HttpStatus { get; set; } string reasonPhrase = "ApiBusinessException"; [DataMember] public string ReasonPhrase { get { return this.reasonPhrase; } set { this.reasonPhrase = value; } } #endregion #region Public Constructor. /// <summary> /// Public constructor for Api Business Exception /// </summary> /// <param name="errorCode"></param> /// <param name="errorDescription"></param> /// <param name="httpStatus"></param> public ApiBusinessException(int errorCode, string errorDescription, HttpStatusCode httpStatus) { ErrorCode = errorCode; ErrorDescription = errorDescription; HttpStatus = httpStatus; } #endregion } } #region Using namespaces. using System; using System.Net; using System.Runtime.Serialization; #endregion namespace WebApi.ErrorHelper { /// <summary> /// Api Data Exception /// </summary> [Serializable] [DataContract] public class ApiDataException : Exception, IApiExceptions { #region Public Serializable properties. [DataMember] public int ErrorCode { get; set; } [DataMember] public string ErrorDescription { get; set; } [DataMember] public HttpStatusCode HttpStatus { get; set; } string reasonPhrase = "ApiDataException"; [DataMember] public string ReasonPhrase { get { return this.reasonPhrase; } set { this.reasonPhrase = value; } } #endregion #region Public Constructor. /// <summary> /// Public constructor for Api Data Exception /// </summary> /// <param name="errorCode"></param> /// <param name="errorDescription"></param> /// <param name="httpStatus"></param> public ApiDataException(int errorCode, string errorDescription, HttpStatusCode httpStatus) { ErrorCode = errorCode; ErrorDescription = errorDescription; HttpStatus = httpStatus; } #endregion } }
JSon Serializers
JSONHelper
” class to Helpers folder, with following code:#region Using namespaces. using System.Web.Script.Serialization; using System.Data; using System.Collections.Generic; using System; #endregion namespace WebApi.Helpers { public static class JSONHelper { #region Public extension methods. /// <summary> /// Extened method of object class, Converts an object to a json string. /// </summary> /// <param name="obj"></param> /// <returns></returns> public static string ToJSON(this object obj) { var serializer = new JavaScriptSerializer(); try { return serializer.Serialize(obj); } catch(Exception ex) { return ""; } } #endregion } }
ToJSON()
” method is an extension of base Object class, which serializes supplied the object to a JSON string. The method using “JavaScriptSerializer
” class which exists in “System.Web.Script.Serialization
“.Modify NLogger Class
Log()
method of NLogger, which will now handle the different API exceptions./// <summary> /// Logs info/Error to Log file /// </summary> /// <param name="record"></param> private void Log(TraceRecord record) { var message = new StringBuilder(); if (!string.IsNullOrWhiteSpace(record.Message)) message.Append("").Append(record.Message + Environment.NewLine); if (record.Request != null) { if (record.Request.Method != null) message.Append("Method: " + record.Request.Method + Environment.NewLine); if (record.Request.RequestUri != null) message.Append("").Append("URL: " + record.Request.RequestUri + Environment.NewLine); if (record.Request.Headers != null && record.Request.Headers.Contains("Token") && record.Request.Headers.GetValues("Token") != null && record.Request.Headers.GetValues("Token").FirstOrDefault() != null) message.Append("").Append("Token: " + record.Request.Headers.GetValues("Token").FirstOrDefault() + Environment.NewLine); } if (!string.IsNullOrWhiteSpace(record.Category)) message.Append("").Append(record.Category); if (!string.IsNullOrWhiteSpace(record.Operator)) message.Append(" ").Append(record.Operator).Append(" ").Append(record.Operation); if (record.Exception != null && !string.IsNullOrWhiteSpace(record.Exception.GetBaseException().Message)) { var exceptionType = record.Exception.GetType(); message.Append(Environment.NewLine); if (exceptionType == typeof(ApiException)) { var exception = record.Exception as ApiException; if (exception != null) { message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine); message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine); } } else if (exceptionType == typeof(ApiBusinessException)) { var exception = record.Exception as ApiBusinessException; if (exception != null) { message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine); message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine); } } else if (exceptionType == typeof(ApiDataException)) { var exception = record.Exception as ApiDataException; if (exception != null) { message.Append("").Append("Error: " + exception.ErrorDescription + Environment.NewLine); message.Append("").Append("Error Code: " + exception.ErrorCode + Environment.NewLine); } } else message.Append("").Append("Error: " + record.Exception.GetBaseException().Message + Environment.NewLine); } Logger[record.Level](Convert.ToString(message) + Environment.NewLine); }
TraceRecord
and updates the logger as per the exception type.Modify GlobalExceptionAttribute
GlobalExceptionAttribute
to handle all exceptions 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 .public override void OnException(HttpActionExecutedContext context) { GlobalConfiguration.Configuration.Services.Replace(typeof(ITraceWriter), new NLogger()); var trace = GlobalConfiguration.Configuration.Services.GetTraceWriter(); trace.Error(context.Request, "Controller : " + context.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName + Environment.NewLine + "Action : " + context.ActionContext.ActionDescriptor.ActionName, context.Exception); var exceptionType = context.Exception.GetType(); if (exceptionType == typeof(ValidationException)) { var resp = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "ValidationException", }; throw new HttpResponseException(resp); } else if (exceptionType == typeof(UnauthorizedAccessException)) { throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.Unauthorized, new ServiceStatus() { StatusCode = (int)HttpStatusCode.Unauthorized, StatusMessage = "UnAuthorized", ReasonPhrase = "UnAuthorized Access" })); } else if (exceptionType == typeof(ApiException)) { var webapiException = context.Exception as ApiException; if (webapiException != null) throw new HttpResponseException(context.Request.CreateResponse(webapiException.HttpStatus, new ServiceStatus() { StatusCode = webapiException.ErrorCode, StatusMessage = webapiException.ErrorDescription, ReasonPhrase = webapiException.ReasonPhrase })); } else if (exceptionType == typeof(ApiBusinessException)) { var businessException = context.Exception as ApiBusinessException; if (businessException != null) throw new HttpResponseException(context.Request.CreateResponse(businessException.HttpStatus, new ServiceStatus() { StatusCode = businessException.ErrorCode, StatusMessage = businessException.ErrorDescription, ReasonPhrase = businessException.ReasonPhrase })); } else if (exceptionType == typeof(ApiDataException)) { var dataException = context.Exception as ApiDataException; if (dataException != null) throw new HttpResponseException(context.Request.CreateResponse(dataException.HttpStatus, new ServiceStatus() { StatusCode = dataException.ErrorCode, StatusMessage = dataException.ErrorDescription, ReasonPhrase = dataException.ReasonPhrase })); } else { throw new HttpResponseException(context.Request.CreateResponse(HttpStatusCode.InternalServerError)); } }
Modify Product Controller
// GET api/product/5 [GET("productid/{id?}")] [GET("particularproduct/{id?}")] [GET("myproduct/{id:range(1, 3)}")] public HttpResponseMessage Get(int id) { if (id != null) { var product = _productServices.GetProductById(id); if (product != null) return Request.CreateResponse(HttpStatusCode.OK, product); throw new ApiDataException(1001, "No product found for this id.", HttpStatusCode.NotFound); } throw new ApiException() { ErrorCode = (int)HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..." }; }
Run the application






Update the controller for new Exception Handling
Product Controller
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using AttributeRouting; using AttributeRouting.Web.Http; using BusinessEntities; using BusinessServices; using WebApi.ActionFilters; using WebApi.Filters; using System; using WebApi.ErrorHelper; namespace WebApi.Controllers { [AuthorizationRequired] [RoutePrefix("v1/Products/Product")] public class ProductController : ApiController { #region Private variable. private readonly IProductServices _productServices; #endregion #region Public Constructor /// <summary> /// Public constructor to initialize product service instance /// </summary> public ProductController(IProductServices productServices) { _productServices = productServices; } #endregion // GET api/product [GET("allproducts")] [GET("all")] public HttpResponseMessage Get() { var products = _productServices.GetAllProducts(); var productEntities = products as List ?? products.ToList(); if (productEntities.Any()) return Request.CreateResponse(HttpStatusCode.OK, productEntities); throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound); } // GET api/product/5 [GET("productid/{id?}")] [GET("particularproduct/{id?}")] [GET("myproduct/{id:range(1, 3)}")] public HttpResponseMessage Get(int id) { if (id != null) { var product = _productServices.GetProductById(id); if (product != null) return Request.CreateResponse(HttpStatusCode.OK, product); throw new ApiDataException(1001, "No product found for this id.", HttpStatusCode.NotFound); } throw new ApiException() { ErrorCode = (int)HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..." }; } // POST api/product [POST("Create")] [POST("Register")] public int Post([FromBody] ProductEntity productEntity) { return _productServices.CreateProduct(productEntity); } // PUT api/product/5 [PUT("Update/productid/{id}")] [PUT("Modify/productid/{id}")] public bool Put(int id, [FromBody] ProductEntity productEntity) { if (id > 0) { return _productServices.UpdateProduct(id, productEntity); } return false; } // DELETE api/product/5 [DELETE("remove/productid/{id}")] [DELETE("clear/productid/{id}")] [PUT("delete/productid/{id}")] public bool Delete(int id) { if (id != null && id > 0) { var isSuccess = _productServices.DeleteProduct(id); if (isSuccess) { return isSuccess; } throw new ApiDataException(1002, "Product is already deleted or not exist in system.", HttpStatusCode.NoContent ); } throw new ApiException() {ErrorCode = (int) HttpStatusCode.BadRequest, ErrorDescription = "Bad Request..."}; } } }
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.
Conclusion

Read more:
- C# and ASP.NET Questions (All in one)
- MVC Interview Questions
- C# and ASP.NET Interview Questions and Answers
- Web Services and Windows Services Interview Questions
Other Series
My other series of articles:
For more technical articles you can reach out to CodeTeddy.