A HtmlHelper to help with cache busting

Nov 11th
2013

Ever had the following conversation with a client?

Client: "I just checked the website to see the changes you told me you'd deployed, but I can't see any. Are you sure you actually deployed it?"

You: "Are you looking at it in your browser now?"

Client: "Yes"

You: "Try holding down the Ctrl key and press F5"

Client: "Wait I'll put down my phone … … Ooooh there it is, what did you do?"

You: "Never mind".

Then you probably wished you've gotten around to implement some form of cache busting technique with the website you'd deployed.

My take on cache busting

What I want from cache busting, is the ability to just include the urls for the ressources I want to include, and then have the rest handled by my default setup. My solution to this problem in ASP.NET MVC sites is:

  1. Create HtmlHelper addon methods that enables inserting just the ressource link without, having to think about cache busting when I insert them, but still behaves as plain old link/javascript/img tags.

  2. Make it possible to update the cache busting data from one central location, which will affect all the places I've used my HtmlHelper methods. This means that I'm willing to trade the ability to cache bust individual ressources, for ease of development.

  3. Configure the MVC site so that the my HtmlHelper functions are available in the MVC views.

Here's how I do it.

HtmlHelper methods

To create add-on HtmlHelper methods I place a static class in a /Helpers folder in my Web Application project. In the class i create static method like this:

namespace MyWebApplication.Helpers
{
    public static class HtmlHelpers
    {
        // Attach this method to the HtmlHelper by setting in a `this HtmlHelper helper`
        // as the first parameter in the method
        public static MvcHtmlString VersionedCssUrl(this HtmlHelper helper, string resUrl)
        {
            // Check if the supplied resUrl is not empty and if it maps to an actual file
            if (string.IsNullOrWhiteSpace(resUrl) || !File.Exists(Server.MapPath(resUrl)))
            {
                // Emit an empty string if the file isn't there
                return MvcHtmlString.Create(""); 
                // Maybe also do some logging ??
            }

            // While we're at why not check to see if we're running without debugging
            // and switch to the minified version of the ressource
            if (!HttpContext.Current.IsDebuggingEnabled)resUrl = resUrl.Replace(".css", ".min.css");

            // Get the version number of the Web Application dll.
            var version = typeof(HtmlHelpers).Assembly.GetName().Version.ToString();

            // Build the HTML string to return appending the version number to the ressource url
            var result = string.Format(@"<link rel=""stylesheet"" href=""{0}?v={1}"" />", 
                                        resUrl, version);

            // Return the HTML in a format that can be written in razor script
            return MvcHtmlString.Create(result);
        }
    }
}

So basically I create a method that takes a ressource string url as a parameter, check if the passed in ressource url is valid, and then build the corresponding html for the ressource with a version number attached.

As you can see in the code I also do a check to see if the application is running with debugging enabled, and switch to a minified version of the script if we're not. This is building on a convention we use where i work, where the minified version of css- and js-files has .min inserted before the file type ending.

Usage in a razor file will look like this:

<head>
    @Html.VersionedCssUrl("/css/site.css")
</head>

Setting the version number

As you can see, the version number for the ressource url is gleaned from the Assembly our class is running in. This version number is controlled in the file AssemblyInfo.cs found in the Properties in your project. At least if your project is a Web Application project. In this file you'll find the lines

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

And this is where you can control the version number. There a several ways to control this version number: First of all you might want to do it manually, or perhaps you work in a continous integration environment, where the version umber is controlled by the build server. There is also build tasks available on the net in the MSBuild Extension Pack that can do this for you see this task.

My approach is to use the possibility of letting the compiler control the Revision and Build numbers by setting the version numbers like this:

[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.*")]

By doing this I get a version number that will be incremented every time i do a build, and it will look something like this: 1.0.5062.34347. The number is based on some kind of date conversion, and is incremented over time, so that a higher version number always represents a later build. The assemblyinfo.cs file doesn't change using this method, but so far it works out for our purposes, and we still got the major and minor parts of the version number in our control.

Adding my methods to the HtmlHelper

In order to make the your helper methods accessible in your views, you either need to import the namespace in every view you're going to use your HtmlHelpers methods in. Or you can add them to your view using a web.config file in the root of your view files folders.

<configuration>
    <system.web.webPages.razor>
        <pages pageBaseType="System.Web.Mvc.WebViewPage">
            <add namespace="MyWebApplication.Helpers" />
        </pages>
    </system.web.webPages.razor>
</configuration>

Remember to use the namespace you actually use for your own class in your own project. And that should be it, your helper methods will now be available in every view, plus it gets picked up by Visual Studios Intellisense. All is good in VS-Land.

Finishing up on the helper methods

So with this framework in place we just need to finish up on the helper methods class

using System;
using System.IO;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Web;
using File = System.IO.File;

namespace MyWebApplication.Helpers
{
    /// <summary>
    /// This class contains methods for attaching to the MVC HtmlHelper class
    /// </summary>
    public static class HtmlHelpers
    {
        /// <summary>
        /// Writes out at css link tag with a ?v=*assemblyversionnumber* 
        /// appended for cache busting.
        /// </summary>
        /// <param name="helper">
        /// Used to attach the method to the @Html helper used in mvc views
        /// </param>
        /// <param name="resUrl">
        /// The relative url for the css file
        /// </param>
        /// <returns>
        /// A Html string inserted via @Html.VersionedCssUrl("/path/to/css")
        /// </returns>
        public static MvcHtmlString VersionedCssUrl(this HtmlHelper helper, string resUrl)
        {
            if (string.IsNullOrWhiteSpace(resUrl) || !File.Exists(Server.MapPath(resUrl)))
                return MvcHtmlString.Create("");

            var version = typeof(HtmlHelpers).Assembly.GetName().Version.ToString();
            if (!HttpContext.Current.IsDebuggingEnabled)resUrl = resUrl.Replace(".css", ".min.css"); 
            var result = string.Format(@"<link rel=""stylesheet"" href=""{0}?v={1}"" />", 
                                       resUrl, version);
            return MvcHtmlString.Create(result);
        }

        /// <summary>
        /// Writes out a JavaScript tag with a versioned link to 
        /// the provided JavaScript file url
        /// </summary>
        /// <param name="helper">
        /// Used to attach this method to the @Html helper used in MVC views
        /// </param>
        /// <param name="resurl">
        /// The relative url for the JavaScript file
        /// </param>
        /// <returns>
        /// A Html string inserted via @Html.VersionedJsUrl("/path/to/jsfile")
        /// </returns>
        public static MvcHtmlString VersionedJsUrl(this HtmlHelper helper, string resUrl)
        {
            if (string.IsNullOrWhiteSpace(resUrl) || !File.Exists(Server.MapPath(resUrl)))
                return MvcHtmlString.Create("");

            var version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
            if (!HttpContext.Current.IsDebuggingEnabled) resUrl = resUrl.Replace(".js", ".min.js");
            var result = string.Format(@"<script type=""JavaScript"" src=""{0}?v={1}""></script>", 
                                       resUrl, version);
            return MvcHtmlString.Create(result);
        }

        /// <summary>
        /// Writes out an img tag with a versioned src link 
        /// to the provided img file url
        /// </summary>
        /// <param name="helper">
        /// Used to attach this method to the @Html helper used in MVC views
        /// </param>
        /// <param name="resurl">
        /// The relative url for the image file
        /// </param>
        /// <param name="altTxt">
        /// Optional, inserted in the alt attribute
        /// </param>
        /// <param name="width">
        /// Optional, inserted in the width attribute
        /// </param>
        /// <param name="height">
        /// Optional, inserted in the height attribute
        /// </param>
        /// <returns>
        /// A Html string inserted via @Html.VersionedJsUrl("/path/to/jsfile")
        /// </returns>
        public static MvcHtmlString VersionedImgUrl(this HtmlHelper helper, string resUrl, 
                                                    string altTxt = null, string width = null, 
                                                    string height = null)
        {
            if (string.IsNullOrWhiteSpace(resUrl) 
                || !File.Exists(HttpContext.Current.Server.MapPath(resUrl)))
                return MvcHtmlString.Create("");

            var version = typeof(HtmlHelpers).Assembly.GetName().Version.ToString();
            var attrs = new StringBuilder();
            if (!string.IsNullOrWhiteSpace(altTxt))
                attrs.AppendFormat(@" alt=""{0}""", altTxt);
            if (!string.IsNullOrWhiteSpace(width))
                attrs.AppendFormat(@" width=""{0}""", width);
            if (!string.IsNullOrWhiteSpace(height))
                attrs.AppendFormat(@" height=""{0}""", height);
            var result = string.Format(@"<img src=""{0}?v={1}""{2} />", resUrl, version, attrs);
            return MvcHtmlString.Create(result);
    }
}

We do this because we have a convention, that these file types will have a minified version available supposed to used in production environments. And our production environments runs without debugging enabled. These code lines saves us the work of changing respurce urls around when going into production.

The complete set of helpers can be used for css-, js- and img-files. Feel free to copy them to your own default helper class to enjoy.

URL parameters on static resources

As you can read in this Google Developers "Make the web faster" article, some proxy servers won't cache any resources with a url parameter (a query string) in it. And that you're better of if you embed the versioning scheme in the path to the file instead. If you want to avoid taking a page speed score hit, you will have to change the logic for building the urls, and add a URL Rewriting rule to your web.config, to catch these urls and rewrite them to something else. See Mads Kristensens article for more info about how to do this.