Browser caching is great, except when it's not.  What if you deploy a new version to production, but the fix you made to a JavaScript or CSS file isn't showing up?  How can we force the browser to download files at least once with each build?

In previous posts, I showed how to use RequireJS in an MVC app, and how to optimize JavaScript and CSS using r.js in that project.  Now we'll tackle cache busting to fix this problem.

The code for this project is available for view/download on GitHub.

Concept

We're going to put a small query string on every optimized JavaScript and CSS file with a unique code tied to that deployment.  I'll use the build number of the main web assembly, since that's easy to get.  The pattern will be like:

/Scripts-Build/app/main-fileName.js?v=1234

If you don't like using the build number for whatever reason, you can use anything you like, as long as it it changes when you build or deploy the files, but doesn't change all the time.  So something like a build date makes sense, but not the current date/time, because that will always be different on each request.

Finally, be aware this is not a perfect solution.  If someone is sitting behind a proxy server, they may not see the changes IF the proxy server doesn't honor the query strings and refetch the files.  If that applies in your situation, you may need a path and some extra routing work to get past the proxy with something like:

/Scripts-Build/app/1234/main-fileName.js

I'll only be showing the build version style, and I've found that works in most situations.

Getting the build version

This is not the kind of code you write very often, but here's how I got the build number from the current assembly:

using System.Globalization;
using System.Reflection;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;

namespace RequireJSWithMVC.Extensions
{
    public static class ApplicationVersionHelpers
    {
        private const string _assemblyRevisionNumberKey = "AssemblyRevisionNumber";

        public static string AssemblyRevisionNumber(this HtmlHelper helper)
        {
#if (DEBUG) 
            return string.Empty;
#else
            if (HttpRuntime.Cache[_assemblyRevisionNumberKey] == null)
            {
                var assembly = Assembly.GetExecutingAssembly();
                var assemblyRevisionNumber = assembly.GetName().Version.Revision.ToString(CultureInfo.InvariantCulture);

                HttpRuntime.Cache.Insert(_assemblyRevisionNumberKey, assemblyRevisionNumber,
                    new CacheDependency(assembly.Location));
            }

            return HttpRuntime.Cache[_assemblyRevisionNumberKey] as string;
#endif
        }
    }
}

We are just returning an empty string in Debug mode.  In Release mode, we are caching the build revision number so we don't have to re-lookup that value every time we request it.

Now we need a way to access this version number from JavaScript.  Let's put this version number in a global variable inside _Layout.cshtml in the <head> section so it's available on every page.

<script>var version = "@Html.AssemblyRevisionNumber()";</script>

We all know global variables are a sin, so paste it in and say your penance.

Using the build version

In the previous post, we had a PathHelpers.cs file that toggled between the /Scripts and the /Scripts-Build and between the /Styles and the /Styles-Build folders.  We need to update that to use our version number based on Debug/Release build mode:

using System.Web;
using System.Web.Mvc;

namespace RequireJSWithMVC.Extensions
{
    public static class PathHelpers
    {
        public static string ScriptsPath(this HtmlHelper helper, string pathWithoutScripts)
        {
            var fullPath = "";
#if (DEBUG)
            var scriptsPath = "~/Scripts/";
            fullPath = VirtualPathUtility.ToAbsolute(scriptsPath + pathWithoutScripts);
#else
            var scriptsPath = "~/Scripts-Build/";
            fullPath = VirtualPathUtility.ToAbsolute(scriptsPath + pathWithoutScripts + "?v=" + helper.AssemblyRevisionNumber());
#endif
            return fullPath;
        }

        public static string StylesPath(this HtmlHelper helper, string pathWithoutStyles)
        {
            var fullPath = "";
#if (DEBUG)
            var stylesPath = "~/Styles/";
            fullPath = VirtualPathUtility.ToAbsolute(stylesPath + pathWithoutStyles);
#else
            var stylesPath = "~/Styles-Build/";
            fullPath = VirtualPathUtility.ToAbsolute(stylesPath + pathWithoutStyles + "?v=" + helper.AssemblyRevisionNumber());
#endif
            return fullPath;
        }
    }
}

This covers the _Layout.cshtml scripts, but  we also need to update RequireJsHelpers.cs, our helper that is used for page-level scripts and pulls in main.js:

using System;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace RequireJSWithMVC.Extensions
{
    public static class RequireJsHelpers
    {
        public static MvcHtmlString InitPageMainModule(this HtmlHelper helper, string pageModule)
        {
            var require = new StringBuilder();

#if (DEBUG)
            var scriptsPath = "~/Scripts/";
#else
            var scriptsPath = "~/Scripts-Build/";
#endif

            var absolutePath = VirtualPathUtility.ToAbsolute(scriptsPath);

            require.AppendLine("<script>");
#if (DEBUG)
            require.AppendFormat("    require([\"{0}main.js\"]," + Environment.NewLine, absolutePath);
#else
            require.AppendFormat("    require([\"{0}main.js?v={1}\"]," + Environment.NewLine, absolutePath, helper.AssemblyRevisionNumber());
#endif
            require.AppendLine("        function() {");
            require.AppendFormat("            require([\"{0}\", \"domReady!\"]);" + Environment.NewLine, pageModule);
            require.AppendLine("        }");
            require.AppendLine("    );");
            require.AppendLine("</script>");

            return new MvcHtmlString(require.ToString());
        }
    }
}

Both of these now put ?v=1234 in the JavaScript or CSS path.

We still need to do some work so our /app and /lib scripts get the same treatment.  We'll do that in main.js by adding this line:

urlArgs: version === "" ? "" : "v=" + version

Remember our awful global variable "version" in _Layout.cshtml?  This is where it's being used.  In my main.js file, I have this right under the baseUrl setting:

require.config({
    baseUrl: "/Scripts/app",
    urlArgs: version === "" ? "" : "v=" + version
    paths: {
        jquery: "../lib/jquery-2.1.1",
        jqueryValidate: "../lib/jquery.validate",
        jqueryValidateUnobtrusive: "../lib/jquery.validate.unobtrusive",
        bootstrap: "../lib/bootstrap",
        moment: "../lib/moment",
        domReady: "../lib/domReady",
    },
    shim: {
        jqueryValidate: ["jquery"],
        jqueryValidateUnobtrusive: ["jquery", "jqueryValidate"]
    }
});

require(["kickoff"], function(kickoff) {
        kickoff.init();
    }
);

Now you should be able to run the web app in Release mode and get this, with ?v=1234:

https://cdn.volaresoftware.com/images/posts/2019/12/cache_bust_with_querystring_js.png https://cdn.volaresoftware.com/images/posts/2019/12/cache_bust_with_querystring_css.png

Or run in Debug mode and get this, without the bundling, minification, and the extra query string stuff:

https://cdn.volaresoftware.com/images/posts/2019/12/cache_bust_without_querystring_js.png https://cdn.volaresoftware.com/images/posts/2019/12/cache_bust_without_querystring_css.png

If you get this far and your build version in the query strings is 0, you need to update your AssemblyInfo.cs file:

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

The * is the auto increment of the build revision number.