Cache busting CSS and JS files by hash value

Maybe you are using the Web Essentials Visual Studio extension or you get your minified CSS and JS files from a Grunt task or similar. Here's a concept of how to serve them on version unique URLs without using querystrings or changing filenames.

NOTE 1: About using querystrings for cachebusting, there used to be a PageSpeed recommendation to remove query strings from static resources which is probably good practise to still follow.

NOTE 2: After building this I found Mads Kristensen's Cache busting in ASP.NET post which uses the same basic concepts. However I prefer using the file's hash instead of last changed date's ticks and with my example the minified file is served from the same directory as the original files which reduces the risks for problems in my experience. My post also have some pointers for use with an Episerver Alloy MVC Site.

First I added these standard rewrite feature rules to my Web.config.

<system.webServer>
    ..
    <rewrite>
        <rules>
            <rule name="CssRewrite"
                    stopProcessing="true">
                <match url="^gui/css/krompaco.min.(.*).css$" />
                <action type="Rewrite"
                        url="/gui/css/krompaco.min.css" />
            </rule>
            <rule name="JsRewrite"
                    stopProcessing="true">
                <match url="^gui/js/krompaco.min.(.*).js$" />
                <action type="Rewrite"
                        url="/gui/js/krompaco.min.js" />
            </rule>
        </rules>
    </rewrite>
</system.webServer>

C# samples below follows Episerver Alloy's structure.

public class LayoutModel
{
    public string CssHash { get; set; }
    public string JsHash { get; set; }

An appropriate place to set the layout properties would be in PageViewContextFactory.

model.CssHash = GetHash("~/gui/css/krompaco.min.css");
model.JsHash = GetHash("~/gui/js/krompaco.min.js");
..
private static string GetHash(string path)
{
    string ck = "HashFor" + path;

    if (HttpRuntime.Cache[ck] == null)
    {
        string localPath = HostingEnvironment.MapPath(path);

        using (var md5 = MD5.Create())
        {
            using (var stream = File.OpenRead(localPath))
            {
                var hash = md5.ComputeHash(stream);
                string nicerNameHash = BitConverter.ToString(hash)
                    .Replace("-", string.Empty).ToLower();

                // Cache with a file dependency
                HttpRuntime.Cache.Insert(ck, nicerNameHash,
                    new System.Web.Caching.CacheDependency(localPath));
            }
        }
    }

    return (string)HttpRuntime.Cache[ck];
}

In the layout view I link my files. For troubleshooting I support fetching the original files using a querystring parameter.

@if (Request.QueryString["debug"] == "1") {
    <link rel="stylesheet" media="all" href="/gui/css/basic.css" />
    <link rel="stylesheet" media="all" href="/gui/css/global.css" />
    <link rel="stylesheet" media="all" href="/gui/css/print.css" />
}
else
{
    <link rel="stylesheet" media="all"
        href="/gui/css/krompaco.min.@(Model.Layout.CssHash).css" />
}

And finally before closing BODY.

@if (Request.QueryString["debug"] == "1")
{
    <script src="/gui/js/vendor/jquery.js"></script>
    <script src="/gui/js/startup.js"></script>
}
else
{
    <script src="/gui/js/krompaco.min.@(Model.Layout.JsHash).js"></script>
}

Published and tagged with these categories: Episerver, ASP.NET, Development, MVC