How To Avoid Problems Caused By Clients' Browser Cached Resource Files (JS, CSS, ....) With Every New Build

Posted by Ahmed Tarek Hasan on 12/22/2013 07:38:00 PM with No comments

Javascript & Css

Browsers like IE, Firefox, Chrome and others have their own way to decide if a file should be cached or not. If a file link (URL) is requested more than a certain number of times the browser decides to cache this file to avoid repeated requests and their relevant responses. So, after a file is cached by the browser and a new request is performed for this file, the browser responses with the cached version of the file instead of retrieving the file for the server.

But how does the browser know if we are requesting the same file? The browser knows that by comparing the URL of the requested file to the URL of the file it had already cached before. This means that any slight change on the requested file URL will be recognized by the browser as a completely new file.

So, what is the problem?
The problem is that sometimes between builds there are some changes applied on the application javascript and style files. Although the new code is sent to the client, they get javascript errors and some of the styles are messy. Why? this is due to the client's browser caching of the javascript and styles files. Although we replaced the old files with the new ones but the client's browser is still using the old cached ones because the URLs are still the same.

What is the solution?
There are many approaches to take to fix this issue but they are not all proper ones. Let's check some of these solutions.

Some of the solutions are:
  1. Ask the client to ask all of his system users to clear the browser cache
  2. Ask the client to ask all of his system users to disable browser caching
  3. For each build rename JS and CSS file names
  4. For each build add a dummy query string to all resources URLs
Now, let's see. I think we will all agree that the first two options are not practical at all. For the third option, it will work for sure but this is not acceptable as changing the files names will require changing all references to these files in all application pages and code which is dangerous and not acceptable by any means in terms of code maintainability.

This leaves us with the fourth option which seems like the third one but believe me they are not completely the same. For sure I don't mean to do it in a manual form like browsing through the whole code and changing the extra dummy query string for all resources URLs, there is a more generic and respectable way to do it without even caring about re-visiting the URLs for each new build.

The solution is to implement a server control to be used to register the resources instead of using the regular script and link tags. This control will be responsible for generating the URLs with the dummy query strings and making sure these query strings are not changed unless a new build is deployed.

Now, let's see some code.

Server Control:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Globalization;
using System.Web.UI.WebControls;

namespace DevelopmentSimplyPut.CustomWebControls
{
    public class VersionedResourceRegisterer : WebControl, INamingContainer
    {
        JsTags js;
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public JsTags JS
        {
            get
            {
                return js;
            }
        }

        CssTags css;
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public CssTags CSS
        {
            get
            {
                return css;
            }
        }

        public VersionedResourceRegisterer()
        {
            js = new JsTags();
            css = new CssTags();
        }

        protected override void Render(System.Web.UI.HtmlTextWriter output)
        {
            string fullTag = "";
            string version = AppConstants.Version;

            if (null != JS && JS.Count > 0)
            {
                foreach (Tag js in JS)
                {
                    string path = js.path;
                    path = GetAbsolutePath(path);

                    if (!string.IsNullOrEmpty(path))
                    {
                        fullTag += string.Format(CultureInfo.InvariantCulture, "<script src=\"{0}?v={1}\" type=\"text/javascript\"></script>", path, version); 
                    }
                }
            }

            if (null != CSS && CSS.Count > 0)
            {
                foreach (Tag css in CSS)
                {
                    string path = css.path;
                    path = GetAbsolutePath(path);

                    if (!string.IsNullOrEmpty(path))
                    {
                        fullTag += string.Format(CultureInfo.InvariantCulture, "<link href=\"{0}?v={1}\" type=\"text/css\" rel=\"stylesheet\" />", path, version);
                    }
                }
            }

            output.Write(fullTag);
        }

        private string GetAbsolutePath(string path)
        {
            string result = path;

            if(!string.IsNullOrEmpty(path))
            {
                if (!path.Contains("://"))
                {
                    if (path.StartsWith("~"))
                    {
                        HttpRequest req = HttpContext.Current.Request;
                        string applicationPath = req.Url.Scheme + "://" + req.Url.Authority + req.ApplicationPath;

                        if(!applicationPath.EndsWith("/"))
                        {
                            applicationPath += "/";
                        }

                        path = path.Replace("~", "").Replace("//", "/");

                        if (path.StartsWith("/"))
                        {
                            if (path.Length > 1)
                            {
                                path = path.Substring(1, path.Length - 1);
                            }
                            else
                            {
                                path = "";
                            }
                        }

                        result = applicationPath + path;
                    }
                }
            }

            return result;
        }
    }

    public class Tag
    {
        public string path { set; get; }
    }

    public class JsTags : List<Tag>
    {
    }

    public class CssTags : List<Tag>
    {
    }
}

Version Generation:
public static class AppConstants
{
    private static string version;
    public static string Version
    {
        get
        {
            return version;
        }
    }

    static AppConstants()
    {
        version = (Guid.NewGuid()).ToString().HtmlEncode();
    }
}
As you see the AppConstants class is a static class and inside its static constructor the version is generated once. This means that with each IIS reset a new version will be generated and accordingly with each build we get a new version.

Using Control On Pages:
<ucVersionedResourceRegisterer:VersionedResourceRegisterer runat="server">
 <JS>
  <ucVersionedResourceRegisterer:Tag path="Scripts/jquery-1.10.2.min.js" />
  <ucVersionedResourceRegisterer:Tag path="Scripts/jquery-migrate-1.2.1.min.js" />
  <ucVersionedResourceRegisterer:Tag path="Scripts/jquery.alerts.min.js" />
 </JS>
 <CSS>
  <ucVersionedResourceRegisterer:Tag path="Styles/jquery.alerts.css" />
 </CSS>
</ucVersionedResourceRegisterer:VersionedResourceRegisterer>

Finally, this is not the only advantage of using the server control as you can always use it to gain more control on your resource files. One of the tasks in which I made use of this control is applying automatic minification and bundling of my resource files to enhance my application performance.

That's it. Hope you find this post helpful someday.
Good luck.



Categories: , , ,