How to log request and response metadata in ASP.Net Web API

Take advantage of a custom message handler to log request and response metadata in Web API for debugging, tracing and inspection of the incoming and outgoing calls

How to log request and response metadata in ASP.Net Web API
Thinkstock

Like authentication, caching, and exception management, logging is a crosscutting concern – a function that affects the entire application – that should be centralized. We often log application data that may include the sequence of method calls or events, user actions, or even errors that may occur when the application executes. There are many logging frameworks you could take advantage of, but in this article we’ll focus on how we can log requests and responses in ASP.Net Web API.

Logging requests and responses in Web API is helpful in debugging, tracing, and inspection of the incoming and outgoing service calls. By logging all requests and responses in one place, detecting problems in any requests and responses becomes easy. In this post, we will create a custom message handler to monitor and log requests and responses in Web API. The message handler will be used to intercept calls and log all requests and responses centrally in one place.

Strategies to inject crosscutting concerns in Web API

There are multiple ways to inject logging and other crosscutting concerns in Web API. One way is to create a custom ApiController class, or a base class for all of our controllers, and then override the ExecuteAsync method. Another way is to use a custom action filter. However, both of these strategies have their limitations. In the former case, we would have to ensure that all of our controllers extend the custom base controller class. In the latter, we would have to ensure that the filter is applied on all of the controllers we use.

The best strategy in my opinion is to use a message handler because you write it only once and then register it in one place. Also, because the custom message handler will be called much earlier in the pipeline, i.e., even before the HttpControllerDispatcher, it is well suited to injecting crosscutting concerns. Incidentally, message handlers are classes that inherit the abstract HttpMessageHandler class. Hence, we will be taking advantage of a message handler to inject our custom logger in this post.

If you want to build and execute the source code illustrated in this post, you should have Visual Studio up and running in your system. Also, you should have NLog installed. If you want to know how to install, configure, and use NLog, take a look at my article on NLog here.  

Building our customer logger for Web API

Create a new Web API project in Visual Studio and save it with your desired name. We will be taking advantage of a custom delegating handler here to intercept the calls to the Web API. First off, let’s build a custom POCO class that will store all of the information from our requests and responses.

public class LogMetadata
    {
        public string RequestContentType { get; set; }
        public string RequestUri { get; set; }
        public string RequestMethod { get; set; }
        public DateTime? RequestTimestamp { get; set; }
        public string ResponseContentType { get; set; }
        public HttpStatusCode ResponseStatusCode { get; set; }
        public DateTime? ResponseTimestamp { get; set; }
    }

Now we’ll implement a custom class called LogHandler. This is essentially a message handler that extends the DelegatingHandler class.

public class CustomLogHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
           return base.SendAsync(request, cancellationToken);
        }
    }

The following code snippet shows how you can build request metadata. This method will be called from the SendAsync method of our custom message handler and will return an instance of the LogMetadata class.

private LogMetadata BuildRequestMetadata(HttpRequestMessage request)
    {
        LogMetadata log = new LogMetadata
        {
            RequestMethod = request.Method.Method,
            RequestTimestamp = DateTime.Now,
            RequestUri = request.RequestUri.ToString()
        };
        return log;
    }

The next thing we need to do is update the log metadata instance with information from the response object. Here’s how this can be achieved.

private LogMetadata BuildResponseMetadata(LogMetadata logMetadata, HttpResponseMessage response)
    {
        logMetadata.ResponseStatusCode = response.StatusCode;
        logMetadata.ResponseTimestamp = DateTime.Now;
        logMetadata.ResponseContentType = response.Content.Headers.ContentType.MediaType;
        return logMetadata;
    }

Here is the complete source code of the custom message handler for your reference.

public class CustomLogHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var logMetadata = BuildRequestMetadata(request);
            var response = await base.SendAsync(request, cancellationToken);
            logMetadata = BuildResponseMetadata(logMetadata, response);
            await SendToLog(logMetadata);
            return response;
        }
        private LogMetadata BuildRequestMetadata(HttpRequestMessage request)
        {
            LogMetadata log = new LogMetadata
            {
                RequestMethod = request.Method.Method,
                RequestTimestamp = DateTime.Now,
                RequestUri = request.RequestUri.ToString()
            };
            return log;
        }
        private LogMetadata BuildResponseMetadata(LogMetadata logMetadata, HttpResponseMessage response)
        {
            logMetadata.ResponseStatusCode = response.StatusCode;
            logMetadata.ResponseTimestamp = DateTime.Now;
            logMetadata.ResponseContentType = response.Content.Headers.ContentType.MediaType;
            return logMetadata;
        }
        private async Task<bool> SendToLog(LogMetadata logMetadata)
        {
            // TODO: Write code here to store the logMetadata instance to a pre-configured log store...
            return true;
        }
    }

Note that you need to write the necessary code to store the logMetadata instance shown in the SendToLog method to a pre-configured log target, i.e., a file or a database. I prefer using NLog to log this metadata. Again, you can refer to my article on NLog to see how this can be done.

Registering the message handler

To register the custom message handler you can take advantage of the Application_Start event in the Global.asax.cs file or the Register method of the WebApiConfig class. The following code snippet illustrates how you can register the handler using the Register method of the WebApiConfig class.

public static void Register(HttpConfiguration config)
    {
      // Write your usual code here...
      config.MessageHandlers.Add(new CustomLogHandler());
    }

In this article we examined how we can log requests and responses in Web API using a custom message handler. Message handlers are an excellent way to inject crosscutting concerns into the Web API pipeline. Though we have other ways to inject logging into Web API, such as a custom ApiController class or a custom action filter, using a custom message handler is a simpler approach. You can feel free to tweak this implementation based on your requirements, e.g, to add more custom metadata.