Implementing message handlers to track your ASP.NET Web API usage

Β· 1998 words Β· 10 minutes to read

Today’s focus is going to be on message handlers (or, to be precise, DelegatingHandlers) in ASP.NET Web API. If you are familiar with WCF you can surely recall them - used to be called DelegatingChannels at some point - but the purpose has been the same all along, to provide the first (or last, depending whether you look at requests or responses) extendibility point in your Web API applications. You stack up as many message handlers on top of each other as you wish, and the request will travel through all of them.

This, in turn, gives us developers a possibility to process/alter/modify/decline the incoming HTTP request before it reaches the HttpControllerDispatcher. Then, when the controller creates the response, it goes through the same chain of message handlers again, so we can tamper with the response. One example of the applicability of this is that message handlers are a perfect place to address security related matters i.e. integrating OAuth.

The plan for today is to show how DelegatingHandlers are used by building a functionality that checks for an API key on an incoming API request and then logs all requests and responses, thus allowing you to track the usage of your API.

What are we going to do? πŸ”—

We are going to implement two DelegatingHandlers.

  1. First, will deal with API key checking. Since we are going to use API Keys to log API usage, every request needs to include an API key, and this handler will ensure it. If no key is provided, the requests will not be processed further

  2. Then we will build a second DelegatingHandler, which will capture all request and response data flowing in and out of ApiControllers and log them into a repository. From there, they can be viewed by Admin user.

Models πŸ”—

Let’s start by declaring our models first. This way later on, we can solely focus on Web API specific stuff.

We will have to deal with two types of items - HttpRequestMessage and HttpResponseMessage. Therefore I’m going to set up a base class for all the common properties and two inheriting classes. We’re gonna use primitive types all across the board (i.e. Dictionary instead of HttpHeaders collection, integer instead of StatusCode etc) so that we can easily serialize our objects later.

public class WebApiUsage  
{  
public int id { get; set; }  
public string ApiKey { get; set; }  
public Datetime Timestamp { get; set; }  
public string UsageType { get; set; }  
public string Content { get; set; }  
public Dictionary<string, string> Headers { get; set; }

protected void extractHeaders(HttpHeaders h)  
{  
Dictionary<string, string> dict = new Dictionary<string, string>();  
foreach (var i in h.ToList())  
{  
if (i.Value != null)  
{  
string header = string.Empty;  
foreach (var j in i.Value)  
{  
header += j + " ";  
}  
dict.Add(i.Key, header);  
}  
}  
Headers = dict;  
}  
}  

There is not much exciting stuff here - several properties that can be associated with both requests and responses:

  • ID
  • ApiKey - so that we can associate the API interaction with a given consumer
  • Timestamp - when the interaction happen
  • UsageType - which will indicate whether we deal with a Request or Response
  • Content - the content of the Request or Response
  • Headers - containing headers as key-value string/string pairs

Additionally we include a method callabale from inheriting classes to extract Headers information from HttpHeaders object to a readable and serializable dictionary.

Next, let’s design our derived classes. First Request:

public class WebApiUsageRequest : WebApiUsage  
{  
public string Uri { get; set; }  
public string RequestMethod { get; set; }  
public string IP { get; set; }  
public WebApiUsageRequest(HttpRequestMessage request, string apikey)  
{  
if (request != null)  
{  
UsageType = request.GetType().Name;  
RequestMethod = request.Method.Method;  
Uri = request.RequestUri.ToString();  
IP = ((HttpContextBase)request.Properties["MS_HttpContext"]).Request.UserHostAddress;  
ApiKey = apikey;  
Datetime = Datetime.Now;  
base.extractHeaders(request.Headers);  
}  
else  
{  
throw new ArgumentNullException("request cannot be null");  
}  
}  
}  

We introduce three additional properties here, specific for Requests only:

  • Uri - what has been requested
  • RequestMethod - GET, POST, PUT,
  • IP - from which IP has the request been issued

The constructor will take an HttpRequestMessage object and the API key string.

The response model looks like this:

public class WebApiUsageResponse : WebApiUsage  
{  
public int StatusCode { get; set; }  
public WebApiUsageResponse(HttpResponseMessage response, string apikey)  
{  
if (response != null)  
{  
UsageType = response.GetType().Name;  
StatusCode = Convert.ToInt32(response.StatusCode);  
base.extractHeaders(response.Headers);  
Datetime = Datetime.Now;  
ApiKey = apikey;  
}  
else  
{  
throw new ArgumentNullException("response cannot be null");  
}  
}  
}  

This one, in turn, has just one type-specific proeprty, to hold the StatusCode of the response. The constructor takes an HttpResponseMessage object and the API key string.

Now, before we can say that the models are ready, we need to decorate them with some Serialization attributes. This is necessary since System.Runtime.Serialization doesn’t automatically click with inherited objects. Below is the complete code after the modifciations:

[DataContract]  
[KnownType(typeof(WebApiUsageRequest))]  
[KnownType(typeof(WebApiUsageResponse))]  
public class WebApiUsage  
{  
[DataMember]  
public int id { get; set; }  
[DataMember]  
public string ApiKey { get; set; }  
[DataMember]  
public Datetime Timestamp { get; set; }  
[DataMember]  
public string UsageType { get; set; }  
[DataMember]  
public string Content { get; set; }  
[DataMember]  
public Dictionary<string, string> Headers { get; set; }

protected void extractHeaders(HttpHeaders h)  
{  
Dictionary<string, string> dict = new Dictionary<string, string>();  
foreach (var i in h.ToList())  
{  
if (i.Value != null)  
{  
string header = string.Empty;  
foreach (var j in i.Value)  
{  
header += j + " ";  
}  
dict.Add(i.Key, header);  
}  
}  
Headers = dict;  
}  
}

[DataContract]  
public class WebApiUsageRequest : WebApiUsage  
{  
[DataMember]  
public string Uri { get; set; }  
[DataMember]  
public string RequestMethod { get; set; }  
[DataMember]  
public string IP { get; set; }

public WebApiUsageRequest(HttpRequestMessage request, string apikey)  
{  
if (request != null)  
{  
UsageType = request.GetType().Name;  
RequestMethod = request.Method.Method;  
Uri = request.RequestUri.ToString();  
IP = ((HttpContextBase)request.Properties["MS_HttpContext"]).Request.UserHostAddress;  
ApiKey = apikey;  
Timestamp = Datetime.Now;  
base.extractHeaders(request.Headers);  
}  
else  
{  
throw new ArgumentNullException("request cannot be null");  
}  
}  
}

[DataContract]  
public class WebApiUsageResponse : WebApiUsage  
{  
[DataMember]  
public int StatusCode { get; set; }

public WebApiUsageResponse(HttpResponseMessage response, string apikey)  
{  
if (response != null)  
{  
UsageType = response.GetType().Name;  
StatusCode = Convert.ToInt32(response.StatusCode);  
base.extractHeaders(response.Headers);  
Timestamp = Datetime.Now;  
ApiKey = apikey;  
}  
else  
{  
throw new ArgumentNullException("response cannot be null");  
}  
}  
}  

Implementing a simple repository πŸ”—

Now, let’s implement a simple repository to store our logged API usage. I will not play with the database here, so I’m going to dump everything into a static property which can be accessed for testing purposes. Obviously in a normal applciation you’d replace it with some data storage solution.

Interface:

public interface IApiUsageRepository  
{  
IEnumerable<WebApiUsage> GetAll();  
IEnumerable<WebApiUsage> GetAll(string key);  
WebApiUsage Get(int id);  
WebApiUsage Add(WebApiUsage au);  
}  

And the class implementation:

public class ApiUsageRepository : IApiUsageRepository  
{  
private int _nextId = 1;  
private static ConcurrentQueue<WebApiUsage> _aus = new ConcurrentQueue<WebApiUsage>();

public IEnumerable<WebApiUsage> GetAll()  
{  
return _aus.AsQueryable();  
}

public WebApiUsage Get(int id)  
{  
return _aus.ToList().Find(i => i.id == id);  
}

public IEnumerable<WebApiUsage> GetAll(string key)  
{  
return _aus.ToList().FindAll(i => i.ApiKey == key);  
}

public WebApiUsage Add(WebApiUsage aus)  
{  
aus.id = _nextId++;  
_aus.Enqueue(aus);  
return aus;  
}  
}  

As mentioned, the static instance of ConcurrentQueue will be our data repository for this exercise.

Creating the DelegatingHandlers πŸ”—

First let’s do the API key Handlers. As explained earlier, it’s sole purpose is to check if the request came in with an Apikey – if yes, we continue, if not we return an error to the client. Of course this is the proper place to introduce any additional key verification (i.e. matching the allowed keys and so on), but for simplicity we are not going to do that. A good reference (albeit WCF specific) can be found here in Pablo’s post.

We process the request by writing a new class inheriting from System.Net.Http.DelegatingHandler class and overriding its SendAsync method.

public class WebApiKeyHandler : DelegatingHandler  
{  
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
{  
string apikey = HttpUtility.ParseQueryString(request.RequestUri.Query).Get("apikey");  
if (string.IsNullOrWhiteSpace(apikey)) {  
HttpResponseMessage response = request.CreateErrorResponse(HttpStatusCode.Forbidden, "You can't use the API without the key.");  
throw new HttpResponseException(response);  
} else {  
return base.SendAsync(request, cancellationToken);  
}  
}  

The handler intercepts the requests, and checks for API Key. We are using the new (introduced just last week to the source of ASP.NET Web API) , content-negotiated mechanism of returning a menaingful Error to the client through request through HttpRequestMessage. CreateErrorResponse() method.

If for some reason you can’t get this latest source to work you can create the error accordingly instead (albeit that’s not content negotiated) – up to you.

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
{  
string apikey = HttpUtility.ParseQueryString(request.RequestUri.Query).Get("apikey");

if (string.IsNullOrWhiteSpace(apikey)) {  
return SendError("You can't use the API without the key.", HttpStatusCode.Forbidden);  
} else {  
return base.SendAsync(request, cancellationToken);  
}  
}

private Task<HttpResponseMessage> SendError(string error, HttpStatusCode code)  
{  
var response = new HttpResponseMessage();  
response.Content = new StringContent(error);  
response.StatusCode = code;

return Task<HttpResponseMessage>.Factory.StartNew(() => response);  
}  

If you try to run the application now and make any ApiController request, you’d see that you get an error.

That’s because from now, any request needs to come in with ?apikey=XXX in the querysrting.

Wiring the Api Usage Handler πŸ”—

This is going to be surprisingly easy. We have the Models ready, we have the repository ready, and we have the first handler in place. We start off with the class dervied from System.Net.Http.DelegatingHandler, as before, and a private instance of the repo.

public class WebApiUsageHandler : DelegatingHandler  
{  
private static readonly IApiUsageRepository _repo = new ApiUsageRepository();

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
{  
//stuff happens here  
}  
}  

Now we just add the following:

  • upon arrival of the request, we create a new instance of our WebApiUsageRequest, read asynchronously the Request’s body and add the item to the repo
  • then we continue with the normal flow of the events (meaning the request is send back along the ASP.NET Web API pipeline)
  • finally we intercept the Response, instantiate our WebApiUsageResponse and add that to the repo as well
public class WebApiUsageHandler : DelegatingHandler  
{  
private static readonly IApiUsageRepository _repo = new ApiUsageRepository();

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
{  
string apikey = HttpUtility.ParseQueryString(request.RequestUri.Query).Get("apikey");

var apiRequest = new WebApiUsageRequest(request, apikey);

apiRequest.Content = request.Content.ReadAsStringAsync().Result;  
_repo.Add(apiRequest);

return base.SendAsync(request, cancellationToken).ContinueWith(  
task =>  
{  
var apiResponse = new WebApiUsageResponse(task.Result, apikey);  
apiResponse.Content = task.Result.Content.ReadAsStringAsync().Result;  
_repo.Add(apiResponse);  
return task.Result;  
}  
);  
}  
}  

This was fairly easy, wasn’t it?

Now we just need to register our DelegatingHandlers in the global.asax, inside Application_Start().

GlobalConfiguration.Configuration.MessageHandlers.Add(new WebApiUsageHandler());  
GlobalConfiguration.Configuration.MessageHandlers.Add(new WebApiKeyHandler());  

The important thing to remember is the order. Last added handler gets called first.

Adding a controller to view usage data πŸ”—

Now, just for the pure ability to test what we have done, let’s add a controller which will expose all the usage data.

public class AdminController : ApiController  
{  
private IApiUsageRepository _usage;

public AdminController()  
{  
_usage = new ApiUsageRepository();  
}

public IEnumerable<WebApiUsage> getAll()  
{  
return _usage.GetAll();  
}

public WebApiUsage get(string key)  
{  
return _usage.Get(key);  
}

}  

We can use this controller to grab the data of API usage. Notice that since we decorated our Models carefully with appropriate serialization attributes, we can just safely return our Models (no DTO or anything like that needed).

Trying it πŸ”—

Let’s fire up the browser and make a few requests:

  • http://localhost:50656/api/values/?apikey=testkey
  • http://localhost:50656/api/values/1?apikey=differentkey
  • http://localhost:50656/api/values/2?apikey=testkey

And now go to http://localhost:50656/api/admin/?apikey=adminkey to view everything that has been logged. (Note: even this request needs to come with a key!).

As you can see everything comes out nicely in JSON format (IP is showing like this since it’s running off localhost).

We can use the other method of the repository, to get all usage data for a specific key i.e. β€œdifferentkey" – in this case we navigate to http://localhost:50656/api/admin/?key=differentkey&apikey=adminkey

Finally, we can just pull one of the entries by ID – i.e.

Summary and source πŸ”—

Message handlers can be extremely useful and powerful tools. I hope we managed to built today something that we’ll be useful in your projects - obviously there are many directions you can go forward with this. In the meantime, let me just say that, as always the projetct’s source is available on github. Till next time!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP