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
:
Or run in Debug mode and get this, without the bundling, minification, and the extra query string stuff:
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.