Thursday, February 9, 2012

Cache implementation for your Web & WCF resource


There are different ways of implementing the caching.
1.       At the server, where you store data in-memory using the ASP.Net Cache / Application classes.
2.       In the client (Browser). This definitely gives more advantage in terms of performance.

This article describes in detail about the second type of caching.


What are the resources that can be cached?
Static resources like html, scripts - js, style sheets – css, images – gif, jpg etc
Ex: http://www.domain.com/images/banner.jpg
Dynamic resources which are REST based urls  - if you think that these resources don’t change their data very frequently then they can be a good candidate for caching.
Ex:

                                                               
What do we intend to save by caching on the browser?
Loading the server to fetch the data and pass it across
Even making an http request to the server. There are other ways to save the number of requests to server by using minification process. Where we can merge several js/css files into single file. You can read my previous articles to know more about it.


Implementing Caching
This is simple by adding Expires Header (when you know exactly when to expire) to the IIS. http://technet.microsoft.com/en-us/library/cc770661(WS.10).aspx
You can also use the Max Age header when you are not sure of the number days you need to cache the resource. The above link describes for both.


Challenges
Finding the exact Expires/MaxAge becomes hard as we will not be able to predict our bug fix cycles due to hot fixes etc.. specially in case of smaller projects.


Solution
Two common solutions both deal with manipulating the URL:
1.       Manipulate the calls to the resource by adding a query string. This requires to do a find a replace to all the locations where the resource is being referenced. Ex: http://www.domain.com/app/images/banner.jpg?rev=2345 . This becomes a painful process to manipulate it every time a resource is changed. There are very high chances that we may miss out modifying a revision query string. This also has some minor drawbacks that some proxy servers don’t cache urls which contain query string.

For REST based WCF Service, add this piece of code before the response.
OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
ctx.Headers.Add(HttpResponseHeader.CacheControl, String.Format("public, max-age={0}", maxAge)); //in seconds.

2.       The second more robust solution “Fingerprinting” is to inject some kind of hash into the url. Ex: http://www.domain.com/CBUSTZqTMzNV8qFU/0.jpg
Here the part of the url before the file name is the fingerprint or a hash generated based on the contents of the resource being accessed. This requires that we use some mechanism like a URL Rewrite module to do a Rewrite of requests. Ex: Search for a pattern (.*)/CBUST[A-F0-9]{32}(/.*) and do a rewrite to {R:1}{R:2} For more information on URL Rewrite http://www.iis.net/download/urlrewrite
In your aspx files, you will reference your resources like <script type="text/javascript" src="<%=Utilities.CacheBusterUrl("/_layouts/scripts/jquery-1.4.3.min.js"%>">script>

Add this piece of code in your Page_Load if it is a page.
if (!this.Page.IsPostBack)
{
this.DataBind();
}

If it is a user/web control then ensure that the page that is adding this user/web control has the above piece of code.

The Fingerprinting method has its own limitation that any resource referenced in .css file or being rendered from SharePoint library cannot be cached like this.

Implementation of the CacheBuster utility is as follows.
    public static class Utilities
    {
        private static Hashtable Md5Map = Hashtable.Synchronized(new Hashtable());

        public static string CacheBusterUrl(string sourceUrl)
        {
            try
            {
                string filesystemPath = HttpContext.Current.Server.MapPath(sourceUrl);
                string fileMD5;
                lock (Md5Map.SyncRoot)
                {
                    if (Md5Map.ContainsKey(filesystemPath))
                    {
                        fileMD5 = Md5Map[filesystemPath].ToString();
                    }
                    else
                    {
                        fileMD5 = CreateHash(filesystemPath);
                        Md5Map.Add(filesystemPath, fileMD5);
                    }
                }
                return BuildCacheBusterURL(sourceUrl, fileMD5);
            }
            catch (Exception ex)
            {
                return sourceUrl;
            }
        }
    }


Security Warning
Be aware that if your Web Application stores cookies then caching resources at the proxy servers can be a security threat. So make sure to use https whenever you decide to store cookies.

No comments:

Post a Comment