Leverage browser cache in MVC Web API using an action filter

Last week I was working on some sample application that uses MVC Web API to return results to a client framework. While doing so I noticed that the standard Web API framework does not implement client caching in an easy to use way. Of course we can work with headers inside our controller actions, but as a big fan of DRY I decided to find out if I can use a different route. As a result I show you the ClientCacheAttribute together with its ClientCache.

ClientCacheAttribute and ClientCache code 

public class ClientCacheAttribute : ActionFilterAttribute {
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) {
        base.OnActionExecuted(actionExecutedContext);

        var clientCache = ClientCache.Current;
        var response = actionExecutedContext.Response;

        if (clientCache.IsValid) {
                response.StatusCode = HttpStatusCode.NotModified;
                response.Content = new StringContent("");
                response.Content.Headers.ContentType = null;                    
        }

        response.Content.Headers.LastModified = clientCache.LastModified;
        response.Content.Headers.Expires = clientCache.Expires;

        response.Headers.CacheControl = clientCache;
    }
}

public class ClientCache : CacheControlHeaderValue {
    private const string ItemsKey = "C653F02F-14F9-4E8C-9E74-D22F7E7230A4";
    private const string IfModifiedSinceHeaderKey = "If-Modified-Since";

    protected ClientCache()
        : base() {
        var request = HttpContext.Current.Request;

        if (request.Headers.AllKeys.Contains(IfModifiedSinceHeaderKey)) {
            string modifiedSinceHeaderValue = request.Headers.GetValues(IfModifiedSinceHeaderKey).First();

            DateTimeOffset modifiedSince;
            if (DateTimeOffset.TryParse(modifiedSinceHeaderValue, out modifiedSince)) {
                this.IfModifiedSince = modifiedSince;
            }
        }
    }

    public static ClientCache Current {
        get {
            var currentContext = HttpContext.Current;

            if (!currentContext.Items.Contains(ItemsKey)) {
                lock (currentContext.Items) {
                    if (!currentContext.Items.Contains(ItemsKey)) {
                        currentContext.Items.Add(ItemsKey, new ClientCache());
                    }
                }
            }

            return currentContext.Items[ItemsKey] as ClientCache;
        }
    }

    public DateTimeOffset? IfModifiedSince { get; private set; }

    private DateTimeOffset? lastModified = null;
    public DateTimeOffset? LastModified {
        get {
            return lastModified;
        }
        set {
            this.lastModified = value;
            this.IsValid = (this.IfModifiedSince.HasValue && this.LastModified.HasValue
                            && this.LastModified.Value.Equals(IfModifiedSince.Value));
        }
    }
    public DateTimeOffset? Expires { get; set; }

    public bool IsValid {
        get;
        private set;
    }
}

The ClientCache

The ClientCache.Current returns an instance per request which allows for easy access to the IfModifiedSince request header value and allows for setting the ClientCache response headers in a much easier way than messing around with headers yourself.

The ClientCacheAttribute

The ClientCacheAttribute inherits the ActionFilterAttribute and overides the OnActionExecuted.

In the OnActionExecuted it gets the current ClientCache instance to modify the response. If the ClientCache is still valid the StatusCode is set to HttpStatusCode.NotModified(304). It also changes the Content property of the response to an empty StringContent instance which prevents the execution of any database queries if a non null result was returned from the action method. Since a 304 response does not have a body, it also sets the content-type header to null.

It finally sets the cache control header and done we are.

How to use this?

The most obvious place to use this is on any of the Web Api GET actions.

// GET: odata/Customers
[EnableQuery(PageSize = 100)]
[ClientCache]
public IQueryable<Customer> GetCustomers() {
    var clientCache = ClientCache.Current;
    // get the last modified date from:
    //  the db
    //  some setting (data warehouse last load date)
    // as an example I use DateTime.Today
    clientCache.LastModified = DateTime.Today;


    // set any caching options to your requirements
    clientCache.Private = true;
    clientCache.Expires = DateTime.Today.AddDays(1);

    if (clientCache.IsValid) {
        //don't do work if the client cache is still valid...
        return null;
    }


    return db.Customers;
}

// GET: odata/Customers(5)
[EnableQuery]
[ClientCache]
public SingleResult<Customer> GetCustomer([FromODataUri] int key) {
    var clientCache = ClientCache.Current;
    // get the last modified date from:
    //  the db
    //  some setting (data warehouse last load date)
    // as an example I use DateTime.Today
    clientCache.LastModified = DateTime.Today;


    // set any caching options to your requirements
    clientCache.Private = true;
    clientCache.Expires = DateTime.Today.AddDays(1);

    if (clientCache.IsValid) {
        //don't do work if the client cache is still valid...
        return null;
    }

    return SingleResult.Create(db.Customers.Where(customer => customer.Id == key));
}

The result of the above sample is that on the first request of the day, the client will receive fresh data from the database. Until tomorrow, the client won't even hit the server as we did set the Expires header to tomorrow. If we do force the client to go to the server (F5) it wil pass along the IfModifiedSince header and as a result we can simply return a 304 result, without hitting the database again.

Regards,

Wesley

1 Comment

  • Caching without third party library in a very simple way

    I had a need for this. I implemented some basic authentication for the ASP.NET Web API with Tokens and wanted to cache Tokens temporarily in memory to avoid going into the database for every HTTP request.

    The solution is very simple.
    You just need to use Microsoft class for this called MemoryCache which resides in System.Runtime.Caching dll.

    http://blog.developers.ba/simple-way-implement-caching-asp-net-web-api/

Add a Comment

As it will appear on the website

Not displayed

Your website