Implementing Entity Tag in ASP.NET Web API

Entity tags are a way of incorporating caching into the http protocol. When a server returns the response it attached a unique identifier (ETag) to the response back to the client, which represents the state of the object. So when the client makes a request for the same response, it will send the ETag in its request using the If-None-Match header and the server will use to determine whether to send a new response or response with a 304 (Not modified response which the gives the client a go ahead to use the local cache copy) The main advantage for this is to save the bandwidth since if the response has not changed there is no need to resend the same response as before. In this article we will implement ETag using action filter and so we will extend ActionFilterAttribute. Here is the full implementation

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace WebAPIETag.Models
{
public class EnableETag : ActionFilterAttribute
{
//This could be from a network source e.g database
private static ConcurrentDictionary<string, EntityTagHeaderValue> _etTagHeaderValues =
new ConcurrentDictionary<string, EntityTagHeaderValue>();

public override void OnActionExecuting(HttpActionContext actionContext)
{
//Get the request
var request = actionContext.Request;
if (request.Method == HttpMethod.Get)
{
var key = GetKey(request);

//Get if If-None match header
ICollection clientEtags = request.Headers.IfNoneMatch;

if (clientEtags.Count > 0)
{
EntityTagHeaderValue etag = null;
if (_etTagHeaderValues.TryGetValue(key, out etag) && clientEtags.Any(t => t.Tag == etag.Tag))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.NotModified);
SetCacheControl(actionContext.Response);
}
}
}
}

private void SetCacheControl(HttpResponseMessage httpResponseMessage)
{
httpResponseMessage.Headers.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(6),
MustRevalidate = true,
Private = true
};
}

private string GetKey(HttpRequestMessage request)
{
return request.RequestUri.ToString();
}

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var request = actionExecutedContext.Request;
var key = GetKey(request);

EntityTagHeaderValue entityTag;

if (!_etTagHeaderValues.TryGetValue(key, out entityTag) || request.Method == HttpMethod.Post || request.Method == HttpMethod.Post)
{
entityTag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\"");
_etTagHeaderValues.AddOrUpdate(key, entityTag, (k, val) => entityTag);
}

actionExecutedContext.Response.Headers.ETag = entityTag;
SetCacheControl(actionExecutedContext.Response);
}
}
}

And this is how you apply the filter to the actions

[EnableETag]
public Order Get(int id)
{
return new Order
{
Id = "One",
OrderDate = DateTime.Now,
Owner = "Joe Doe"
};
}

On the index.cshtml we have

<div id="body">
<div>
<div>
<h1>Orders</h1>
<input id="details" type="button" value="Details" />
</div>
<div>
<ul id="orders" />
</div>
<div>
<ul id="employee" />
</div>
</div>

</div>
@section scripts{
<script type="text/javascript">
$(document).ready(function () {

$('#details').click(function () {
$('#orders').empty();
$.getJSON("/api/Order/12345", function (order) {
var now = new Date();
var ts = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
var content = order.Id + ' ' + order.Owner;
content = content + ' ' + order.OrderDate + ' ' + ts;
$('#orders').append($('<li/>', { text: content }));
});
});
});
</script>
}

You will now notice that once a request is sent, the result are cached on the client for 6 seconds. But after 6 seconds the cache expires and a new request is sent to the server. And since the result has not been modified since the last request, the response is 304.

In the initial request you will see this in the response headers

ETag:"3c45741e-df14-42d8-8e93-86a773620e3d"

And in the consequent request for the same resource then the request header will include:

If-None-Match:"3c45741e-df14-42d8-8e93-86a773620e3d"

NB: The value might change since I am using guid for this

Of  importance to note are the two functions override
-OnActionExecuting: This runs before the action and looks for an Etag in the if-none-match header. If the header is present it is compared to check if it exists in the dictionary, and if  it matches then a 304 is sent back
-OnActionExecuted: runs after the action method. If the HTTP POST or a PUT method is used, then a new ETag is issued and gets stored in the dictionary and gets sent in the ETag header

Happy coding!!!

Comments are highly appreciated and lets meet here again…:)

Advertisements

Tags: ,

About Piusn

Enthusiastic Software Developer

3 responses to “Implementing Entity Tag in ASP.NET Web API”

  1. Otis says :

    Hello I am so grateful I found your website,
    I really found you by accident, while I was
    researching on Yahoo for something else, Anyways I am here now and would just like to say
    thanks for a tremendous post and a all round thrilling blog (I also love the theme/design), I don’t have
    time to look over it all at the moment but I have saved it and
    also included your RSS feeds, so when I have time I will be back to read
    a great deal more, Please do keep up the great work.

  2. kattoremontti helsinki says :

    Good replies in return of this difficulty with firm arguments
    and telling the whole thing on the topic of that.

Trackbacks / Pingbacks

  1. Exploring Web API 2 Caching | Software Engineering - May 18, 2014

What's your thought on the subject?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: