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 exceptions such that if an error occurs, we can store errors and fix it as soon as possible, and the last part is versioning of the API.
- Exception handling
- Logging
- Versioning
All these parts are key when you are developing a production API.
Icons made by Freepik from www.flaticon.com are licensed by CC 3.0 BY:
1. Exception Handling
To begin, 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
{
public IEnumerable<string> Get()
{
throw new NotImplementedException("");
}
}
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 two parameters:
HttpRequestMessage
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.
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)
{
config.MapHttpAttributeRoutes();
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 whether 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 to 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 completed the 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;
}
if (ex.HelpLink != null)
{
strLogText += Environment.NewLine + "HelpLink ---\n{0}" +
ex.HelpLink;
}
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.
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)
{
config.MapHttpAttributeRoutes();
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
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 whether it stores 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 to 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 the below table, you will get a clear idea about 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 to 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 write the main heart of this process which 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 two 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 whether 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.
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)
{
config.MapHttpAttributeRoutes();
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());
config.MessageHandlers.Add(new RequestResponseHandler());
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
Now we got an idea about how this process works. Let's run the application and see whether 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 the 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 APIs such that older users who 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);
}
);
config.MapHttpAttributeRoutes();
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());
config.MessageHandlers.Add(new RequestResponseHandler());
}
}
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
{
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
public string Get(int id)
{
return "value";
}
public void Post([FromBody]string value){}
public void Put(int id, [FromBody]string value) {}
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
{
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
public string Get(int id) { return "value";}
public void Post([FromBody]string value){}
public void Put(int id, [FromBody]string value){}
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
{
public IEnumerable<string> Get()
{
return new string[] { "version2", " version2" };
}
public string Get(int id){return "value";}
public void Post([FromBody]string value) { }
public void Put(int id, [FromBody]string value) { }
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 a step by step manner and in detailed manner such that we can directly integrate with Live Projects.
Thank you! I hope you liked my article.
History
- 3rd July, 2018: Initial version