Click here to Skip to main content
13,799,813 members
Click here to Skip to main content
Add your own
alternative version

Stats

9K views
240 downloads
21 bookmarked
Posted 3 Jul 2018
Licenced CPOL

Logging and Exception handling, Versioning in ASP.NET WEB API

, 3 Jul 2018
Rate this:
Please Sign up or sign in to vote.
In this article, we are going to learn how to log each request and response of an API such that it helps to maintain log

Introduction

In this article, we are going to learn how to log each request and response of an API such that it helps to maintain logs, next we are going to handle all API exception such that if an error occurs, we can store errors and fix it as soon as possible, and last part is versioning of the API.

  1. Exception handling
  2. Logging
  3. Versioning

Because all these parts are key when you are developing a production API.

Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY

1.     Exception handling

Let’s start, we create a simple Web API application “WebDemoAPI”.

After creating a simple Web API solution, you will get a default Home controller and the Values API Controller. Let’s first run application and call get request.

Note: - You can use any Rest Client for sending a request for this demo I am going to use POSTMAN rest client.

URL:-  http://localhost:50664/api/values

Sending Get Request

After sending a request to API we got a response.

Now let’s make a change in Get Method, here I am going to throw an exception.

public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string> Get()
    {
        throw new NotImplementedException("");
        //return new string[] { "value1", "value2" };
    }
   }

Now if we send request to values API get request then it will throw error in response.

Response before handling the exception

Now we got the error let’s see how to handle this error globally.

Handling API Exception using ExceptionHandler class

 For handling exceptions, we are going to create a class “GlobalExceptionHandler” which will inherit from “ExceptionHandler” abstract class inside this we are going to implement Handle method. Before that we are going to create “CustomHandler” folder in this folder we are going to add “GlobalExceptionHandler” class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.ExceptionHandling;

namespace WebDemoAPI.CustomHandler
{
    public class GlobalExceptionHandler : ExceptionHandler
    {
        public override void Handle(ExceptionHandlerContext context)
        {
            var result = new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("Internal Server Error Occurred"),
                ReasonPhrase = "Exception"
            };

            context.Result = new ErrorMessageResult(context.Request, result);
        }

        public class ErrorMessageResult : IHttpActionResult
        {
            private HttpRequestMessage _request;
            private readonly HttpResponseMessage _httpResponseMessage;

            public ErrorMessageResult(HttpRequestMessage request, HttpResponseMessage httpResponseMessage)
            {
                _request = request;
                _httpResponseMessage = httpResponseMessage;
            }

            public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
            {
                return Task.FromResult(_httpResponseMessage);
            }
        }
    }
}

Now we have implemented Handle method from ExceptionHandler class.

Before doing it first we go to create HttpResponseMessage for that we are going to add a class “ErrorMessageResult” which will inherit from “IHttpActionResult” interface. This class will have a Parameterized Constructor which takes 2 parameters 1. HttpRequestMessage ,2. HttpResponseMessage the HttpResponseMessage which we took parameters will be used by ExecuteAsync to create HttpResponseMessage.

Then this HttpResponseMessage we are going to assign it to “context.Result”

After handling the exception, next we need to register this handler.

Registering Exception handler

We are going to Register “GlobalExceptionHandler” in WebApiConfig class, such that any web API exception can be handled globally.

//Registering GlobalExceptionHandler
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.Http.ExceptionHandling;
using WebDemoAPI.CustomHandler;

namespace WebDemoAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            // Web API routes
            config.MapHttpAttributeRoutes();
            //Registering GlobalExceptionHandler
            config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

Now let’s run this application and check do we handle exception now.

Snapshot of exception thrown

After throwing an exception now we display proper error message not error stack trace to consumers.

Response after handling the exception

Now we have handled the exception, but we have not logged the exception.

Exception Logging

In this part, we are going store exception into the database, for doing that let’s first have a look at table structure where we are going to store it.

API_Error

 

After having a look at table structure further I have written a simple procedure to store this exception in the table

Now we have Complete with database part, next let’s add classes and method to write an exception into the database.

APIError Class

public class ApiError
{
    public string Message { get; set; }
    public string RequestMethod { get; set; }
    public string RequestUri { get; set; }
    public DateTime TimeUtc { get; set; }
}

Note: - Stored Procedures and table scripts are available for download

SqlErrorLogging Class

In this part we are going to write error in the database, in this class, we have InsertErrorLog method which takes the ApiError class as an input parameter.

 

public class SqlErrorLogging
{
    public void InsertErrorLog(ApiError apiError)
    {
        try
        {
            using (var sqlConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["APILoggingConnection"].ConnectionString))
            {
                sqlConnection.Open();
                var cmd =
                    new SqlCommand("API_ErrorLogging", connection: sqlConnection)
                    {
                        CommandType = CommandType.StoredProcedure
                    };
                cmd.Parameters.AddWithValue("@TimeUtc", apiError.TimeUtc);
                cmd.Parameters.AddWithValue("@RequestUri", apiError.RequestUri);
                cmd.Parameters.AddWithValue("@Message", apiError.Message);
                cmd.Parameters.AddWithValue("@RequestMethod", apiError.RequestMethod);

               cmd.ExecuteNonQuery();
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}

After adding classes and Method next we are going to add Class “UnhandledExceptionLogger” which will inherit from “ExceptionLogger” abstract class.

UnhandledExceptionLogger Class

We are going to add a class “UnhandledExceptionLogger” which will inherit from “ExceptionLogger” an abstract class in that we are going to Override “Log” Method, in this method we are going to get an exception which has occurred from that exception, we are going to pull information such as Source, StackTrace, TargetSite and assign it to ApiError class for storing in the database.

using System;
using System.Web.Http.ExceptionHandling;
using WebDemoAPI.Models;

namespace WebDemoAPI.CustomHandler
{
    public class UnhandledExceptionLogger : ExceptionLogger
    {
        public override void Log(ExceptionLoggerContext context)
        {
            var ex = context.Exception;

           string strLogText = "";
            strLogText += Environment.NewLine + "Source ---\n{0}" + ex.Source;
            strLogText += Environment.NewLine + "StackTrace ---\n{0}" + ex.StackTrace;
            strLogText += Environment.NewLine + "TargetSite ---\n{0}" + ex.TargetSite;

            if (ex.InnerException != null)
            {
                strLogText += Environment.NewLine + "Inner Exception is {0}" + ex.InnerException;//error prone
            }
            if (ex.HelpLink != null)
            {
                strLogText += Environment.NewLine + "HelpLink ---\n{0}" + ex.HelpLink;//error prone
            }

            var requestedURi = (string)context.Request.RequestUri.AbsoluteUri;
            var requestMethod = context.Request.Method.ToString();
            var timeUtc = DateTime.Now;

            SqlErrorLogging sqlErrorLogging = new SqlErrorLogging();
            ApiError apiError = new ApiError()
            {
                Message = strLogText,
                RequestUri = requestedURi,
                RequestMethod = requestMethod,
                TimeUtc = DateTime.Now
            };
            sqlErrorLogging.InsertErrorLog(apiError);
        }
    }
}

After creating “UnhandledExceptionLogger” class and writing error into database next we are going to register this class globally in WebApiConfig class.

//Registering UnhandledExceptionLogger
config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());

Registering UnhandledExceptionLogger in WebApiConfig class

using System.Web.Http;
using System.Web.Http.ExceptionHandling;
using WebDemoAPI.CustomHandler;

namespace WebDemoAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            // Web API routes
            config.MapHttpAttributeRoutes();
            //Registering GlobalExceptionHandler
            config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
            //Registering UnhandledExceptionLogger
            config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

After registering UnhandledExceptionLogger class now let’s run the application and see does it store exception occurred in the database.

After getting the error we have handled it and displayed a proper error message to the user and also logged the error in the database.

Response after handling exception and Logging exception

Storing Exception

After handling and logging Exception next we are going log each request and response to Web API.

2.     Logging Request and Response

 

In this part, we are going to log each request and response of WEB API.

In doing that we are going to inherit an abstract class “DelegatingHandler” and override SendAsync method.

If you see below table you will get a clear idea what all data we are storing from request and response into the database.

Let’s first start with creating an “API_Log” table where we are going to store this request in response.

After creating a table, we have created a simple stored procedure for inserting Log into API_Log table this stored procedure is available for download.

Next, we are going to add “ApiLog” Class to pass data to the stored procedure.

namespace WebDemoAPI.Models
{
    public class ApiLog
    {
        public string Host { get; set; }
        public string Headers { get; set; }
        public string StatusCode { get; set; }
        public string RequestBody { get; set; }
        public string RequestedMethod { get; set; }
        public string UserHostAddress { get; set; }
        public string Useragent { get; set; }
        public string AbsoluteUri { get; set; }
        public string RequestType { get; set; }
    }
}

After adding ApiLog class next we are going Add an ApiLogging class in that class we are going to add InsertLog method which will take ApiLog class as a parameter and ApiLog class data will be mapped to SQL parameters to insert data into database.

public class ApiLogging
{
    public void InsertLog(ApiLog apiLog)
    {
        try
        {
            using (var sqlConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["APILoggingConnection"].ConnectionString))
            {
                sqlConnection.Open();
                var cmd =
                    new SqlCommand("API_Logging", connection: sqlConnection)
                    {
                        CommandType = CommandType.StoredProcedure
                    };
                cmd.Parameters.AddWithValue("@Host", apiLog.Host);
                cmd.Parameters.AddWithValue("@Headers", apiLog.Headers);
                cmd.Parameters.AddWithValue("@StatusCode", apiLog.StatusCode);
                cmd.Parameters.AddWithValue("@RequestBody", apiLog.RequestBody);
                cmd.Parameters.AddWithValue("@RequestedMethod", apiLog.RequestedMethod);
                cmd.Parameters.AddWithValue("@UserHostAddress", apiLog.UserHostAddress);
                cmd.Parameters.AddWithValue("@Useragent", apiLog.Useragent);
                cmd.Parameters.AddWithValue("@AbsoluteUri", apiLog.AbsoluteUri);
                cmd.Parameters.AddWithValue("@RequestType", apiLog.RequestType);
                cmd.ExecuteNonQuery();
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}

After completing with the adding ApiLogging class next we are going to Right the main heart of this process is adding the Custom handler.

Creating Custom Handler

We are going to add a class with name “RequestResponseHandler” and then we are going to inherit from DelegatingHandler abstract class and Override SendAsync method.

public class RequestResponseHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
    }
}

Before implementing SendAsync Method I have written a simple class MessageLogging which has 2 methods in it IncomingMessageAsync and OutgoingMessageAsync I have created this method for just assigning Request types and to call both methods separately.

public class MessageLogging
{
    public void IncomingMessageAsync(ApiLog apiLog)
    {
        apiLog.RequestType = "Request";
        var sqlErrorLogging = new ApiLogging();
        sqlErrorLogging.InsertLog(apiLog);
    }

    public void OutgoingMessageAsync(ApiLog apiLog)
    {
        apiLog.RequestType = "Response";
        var sqlErrorLogging = new ApiLogging();
        sqlErrorLogging.InsertLog(apiLog);
    }
}

Now after adding MessageLogging class next we are going to implement SendAsync method from DelegatingHandler abstract class.

public class RequestResponseHandler: DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var requestedMethod = request.Method;
        var userHostAddress = HttpContext.Current != null ? HttpContext.Current.Request.UserHostAddress : "0.0.0.0";
        var useragent = request.Headers.UserAgent.ToString();
        var requestMessage = await request.Content.ReadAsByteArrayAsync();
        var uriAccessed = request.RequestUri.AbsoluteUri;

        var responseHeadersString = new StringBuilder();
        foreach (var header in request.Headers)
        {
            responseHeadersString.Append($"{header.Key}: {String.Join(", ", header.Value)}{Environment.NewLine}");
        }

        var messageLoggingHandler = new MessageLogging();

        var requestLog = new ApiLog()
        {
            Headers = responseHeadersString.ToString(),
            AbsoluteUri = uriAccessed,
            Host = userHostAddress,
            RequestBody = Encoding.UTF8.GetString(requestMessage),
            UserHostAddress = userHostAddress,
            Useragent = useragent,
            RequestedMethod = requestedMethod.ToString(),
            StatusCode = string.Empty
        };

        messageLoggingHandler.IncomingMessageAsync(requestLog);

        var response = await base.SendAsync(request, cancellationToken);

        byte[] responseMessage;
        if (response.IsSuccessStatusCode)
            responseMessage = await response.Content.ReadAsByteArrayAsync();
        else
            responseMessage = Encoding.UTF8.GetBytes(response.ReasonPhrase);

        var responseLog = new ApiLog()
        {
            Headers = responseHeadersString.ToString(),
            AbsoluteUri = uriAccessed,
            Host = userHostAddress,
            RequestBody = Encoding.UTF8.GetString(responseMessage),
            UserHostAddress = userHostAddress,
            Useragent = useragent,
            RequestedMethod = requestedMethod.ToString(),
            StatusCode = string.Empty
        };

        messageLoggingHandler.OutgoingMessageAsync(responseLog);
        return response;
    }
}

Let’s understand what we have written in the SendAsync method.

Request Method

var requestedMethod = request.Method;

we are storing request method does it was a POST PUT DELETE or GET.

Host Address

var userHostAddress = HttpContext.Current != null ? HttpContext.Current.Request.UserHostAddress : "0.0.0.0";

we are getting IP Address from where this request came from.

UserAgent

var useragent = request.Headers.UserAgent.ToString();

UserAgent gives you a raw string about the browser.

Request Body

var requestMessage = await request.Content.ReadAsByteArrayAsync();

 

Absolute Uri

var uriAccessed = request.RequestUri.AbsoluteUri;

Headers

var responseHeadersString = new StringBuilder();
foreach (var header in request.Headers)
{
    responseHeadersString.Append($"{header.Key}: {String.Join(", ", header.Value)}{Environment.NewLine}");
}

Assign Value to ApiLog Class

var messageLoggingHandler = new MessageLogging();

var requestLog = new ApiLog()
{
    Headers = responseHeadersString.ToString(),
    AbsoluteUri = uriAccessed,
    Host = userHostAddress,
    RequestBody = Encoding.UTF8.GetString(requestMessage),
    UserHostAddress = userHostAddress,
    Useragent = useragent,
    RequestedMethod = requestedMethod.ToString(),
    StatusCode = string.Empty
};

Incoming Request Logging

messageLoggingHandler.IncomingMessageAsync(requestLog);

Outgoing Response Logging

messageLoggingHandler.OutgoingMessageAsync(responseLog);

Registering RequestResponseHandler

using System.Web.Http;
using System.Web.Http.ExceptionHandling;
using WebDemoAPI.CustomHandler;

namespace WebDemoAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            //Registering GlobalExceptionHandler
            config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
            
            //Registering UnhandledExceptionLogger
            config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());

            //Registering RequestResponseHandler
            config.MessageHandlers.Add(new RequestResponseHandler());

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

Now we got an idea how this process work lets run the application and see does it works.

Accessing Values API Controller

 

Request and Response Web API Logging

3.     Versioning

 Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY

It is the most important part of Web API development as we keep refining application, we keep making changes to application, if we make changes to the API which are already in production and many users are consuming it will break working application, solution for this is to version your AIPs such that older user which are consuming your API will not have any effect on it.

Let’s start implementing versioning in the Asp.net Web API in with simple steps.

First, we are going to add “Microsoft.AspNet.WebApi.Versioning” NuGet package to the application.

 

 

After installing NuGet Package next, we are going to Register AddApiVersioning method in WebApiConfig.cs file.   

The ApiVersioningOptions class allows you to configure, customize, and extend the default behaviors when you add an API versioning to your application.

Referenced from: - https://github.com/Microsoft/aspnet-api-versioning/wiki/API-Versioning-Options

Code Snippet of AddApiVersioning Method

config.AddApiVersioning(o =>
    {
        o.ReportApiVersions = true;
        o.AssumeDefaultVersionWhenUnspecified = true;
        o.DefaultApiVersion = new ApiVersion(2, 0);
        o.ApiVersionReader = new HeaderApiVersionReader("version");
        o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o);
    }
);

Complete Code Snippet of WebApiConfig

In this part, we are going to comment default routing and enable attribute base routing.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.AddApiVersioning(o =>
            {
                o.ReportApiVersions = true;
                o.AssumeDefaultVersionWhenUnspecified = true;
                o.DefaultApiVersion = new ApiVersion(2, 0);
                o.ApiVersionReader = new HeaderApiVersionReader("version");
                o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o);
            }
        );
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        //Registering GlobalExceptionHandler
        config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());

        //Registering UnhandledExceptionLogger
        config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());

        //Registering RequestResponseHandler
        config.MessageHandlers.Add(new RequestResponseHandler());

        //config.Routes.MapHttpRoute(
        //    name: "DefaultApi",
        //    routeTemplate: "api/{controller}/{id}",
        //    defaults: new { id = RouteParameter.Optional }
        //);
    }
}

After completing with registering method next we are going to add another API controller with the name “Values2Controller”.

Adding Values2Controller API controller

If you see we have added Values2 name API controller we have added a version in the name of the controller is not mandatory to add but the name must be unique and easy to understand.

public class Values2Controller : ApiController
{
    // GET: api/Values2
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET: api/Values2/5
    public string Get(int id)
    {
        return "value";
    }

    // POST: api/Values2
    public void Post([FromBody]string value){}

    // PUT: api/Values2/5
    public void Put(int id, [FromBody]string value) {}

    // DELETE: api/Values2/5
    public void Delete(int id) {}
}

After adding Values2 API controller next we are going to add Route Attributes to both API controller the old one also and the new one also.

Adding ApiVersion Attribute and Route Attribute to Values API controller

[ApiVersion("1.0")]
[Route("api/values")]
public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string> Get()
    {
        //throw new NotImplementedException("");
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    public string Get(int id) {  return "value";}

    // POST api/values
    public void Post([FromBody]string value){}

    // PUT api/values/5
    public void Put(int id, [FromBody]string value){}

    // DELETE api/values/5
    public void Delete(int id) {} 
}

Adding ApiVersion Attribute and Route Attribute to Values2 API controller

[ApiVersion("2.0")]
[Route("api/values")]
public class Values2Controller : ApiController
{
    // GET api/Values2
    public IEnumerable<string> Get()
    {
        //throw new NotImplementedException("");
        return new string[] { "version2", " version2" };
    }

    // GET api/Values2/5
    public string Get(int id){return "value";}

    // POST api/Values2
    public void Post([FromBody]string value) { }

    // PUT api/Values2/5
    public void Put(int id, [FromBody]string value) { }

    // DELETE api/Values2/5
    public void Delete(int id) { }
}

After adding routes and version attribute next save and run the application.

Now to call the API, we need to pass an API version from the header and the name of the header is “version”.

We are going to pass header name “version” and value as 1.0 to the calling values controller.

Requesting Values API

After completing with calling version 1.0 values API next in the same way we are going to call values2 API with version 2.0 header.

We are going to pass header name “version” and value as 2.0 to the calling Values2 controller.

After accessing values controller (values2controller) with version 2.0 header we got a valid response which we were expecting.

Conclusion

In this article we have learned how to “handle exceptions”, “Log Exceptions” and also learned how to log each incoming and outgoing request and response of web API along with it how we can do versioning of web api such that I should not break existing working APIs, this all we learned in step by step way and in detail manner such we can directly integrate with Live Projects.

Thank you, I hope you liked my article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

saineshwar bageri
Software Developer (Senior)
India India
I am Microsoft MVP | C# Corner MVP working on.Net Web Technology
[ASP.NET MVC,.Net Core,ASP.NET CORE, C#, Sqlserver, MYSQL, MongoDB, Windows]

Microsoft MVP Profile Link
https://mvp.microsoft.com/en-us/PublicProfile/5003160?fullName=Saineshwar%20%20Bageri

Prize.

First Prize: Best Web Dev Article of August 2016 with 10 Points to Secure Your ASP.NET MVC Applications.

Second Prize: Best Web Dev Article of April 2017 with Securing ASP.NET Web API using Custom Token Based Authentication

Second Prize:
Best Web Dev Article of February 2018 with Securing ASP.NET CORE Web API using Custom API Key based Authentication




You may also be interested in...

Comments and Discussions

 
PraiseNice Article Pin
madhan20086-Jul-18 0:04
membermadhan20086-Jul-18 0:04 
GeneralRe: Nice Article Pin
saineshwar bageri8-Jul-18 3:39
membersaineshwar bageri8-Jul-18 3:39 
GeneralRe: Nice Article Pin
madhan20088-Jul-18 16:54
membermadhan20088-Jul-18 16:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.181214.1 | Last Updated 3 Jul 2018
Article Copyright 2018 by saineshwar bageri
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid