Static resource caching and cache-busting with ASP.NET MVC and AppHarbor

Serving static content (javascript, css, images) with headers specifying long cache expirations is easy. However, cache-busting those is hard. In this post, I’ll show how to deliver static content with cache-busting URLs.

I’m going to show code which I wrote specifically to use with AppHarbor, but you may find that you can use the same techniques in different environments.

Cache-busting Technique: Versioned Path

The most reliable method of cache busting is to put some version identifier in the path to the static content such as:

http://localhost/statics/12345678/css/style.css

Where 12345678 is the version identifier. This method even works with CDNs which might otherwise ignore lesser important path tokens like querystring parameters or an identifier after a hash mark.

In the code below, I’ll show a solution which includes the version identifier in the path to the static content Version identifier changes whenever a build is released to AppHarbor.

How It Works

A browser makes a request to your server for your homepage. The server delivers the homepage and references to static resources using the versioned path.

The browser requests those static resources from your server. The server finds the resources requested, adds headers,

Cache-Control: public
Expires:Fri, 15 Feb 2013 02:45:01 GMT // a year from now

and serves them.

The browser keeps the resources in its cache for a long time and future requests for them are served from cache.

You ship new code to AppHarbor. AppHarbor builds and deploys your application. It also adds a configuration setting into your web.config with a key of appharbor.commit_id and the value is set to the last changeset ID. Your server uses that changeset ID as the  version identifier in static resource paths.

The browser makes a request for your homepage again. The server delivers the homepage, but this time references the static resources using the new version identifier.

The browser now must request those static resources again because the version identifier has changed. The server serves up the updated files back to the browser where they’re cached again.

Caveat: browsers will have to request all static resources again every time a build is deployed. I’m very happy to accept this. Alternatively, you could do crazy things like get a hash of the file and serve that as part of the file’s path. See Karl Seguin’s article about cache busting if you’re interested in that sort of thing.

How To Do This In Code

First, you’ll need to be able to provide static resource urls. I like to write them like this:

Which means I’ll need an extension method on the UrlHelper:

In the StaticContent method, I’m populating the staticResourceVersion variable from a configuration value which is in my web.config. If one is not set, like when you’re running your site locally, “_default” is provided.

Another thing to note is that if the staticServer variable is populated, then it’ll be prepended to the returned url. This supports serving static content from a cookie-less domain.

Now that static content can be referenced, we need to have a route and a controller to handle requests for static content.

The first and third routes are the default ones that come with MVC. The middle route understands how to handle requests which start with s/ followed by the version identifier and then whatever the path to the resource is. It directs those requests to the StaticsController’s Index action.

The StaticController’s Index action first needs to check if the requested path is allowed. We certainly wouldn’t want to serve just any content back through here. (I’m showing a really simple IsAllowed implementation, please feel free to be smarter about yours.)

For my own sanity, when I’m deving on my localbox, I don’t ever want content cached. So the caching headers are only set when the request is not local.

And finally, the file is served up with the correct content type. (Again, simple implementation, feel free to be smarter.)

The staticResourceVersion variable is ignored; I haven’t thought of anything to do with it yet.

And the hard work is done! Update your static resource urls to use the Url.StaticContent helper, deploy out to AppHarbor, and bask in the the glory of long-cache-expiration-headered static content.

The End

Special thanks to my fellow devs at Cheezburger for teaching me this technique.

Thanks to Michael Friis for telling me that AppHarbor inserts the commit_id into a deployed web.config.

This entry was posted in Development and tagged , , , , , . Bookmark the permalink.

4 Responses to Static resource caching and cache-busting with ASP.NET MVC and AppHarbor

  1. SLaks says:

    The problem with this technique is that you reset all file caching every time you deploy.

    It would be better to put a hash of the file in the URL. (which you can then cache in-memory for perf)

    See also Cassette, which does all this plus more.

    • Eli Thompson says:

      Thanks for the heads up. I’ll take a look at the project.

    • Eli Thompson says:

      One downside of the approach that Cassette uses is that it relies on querystring values to include the version identifier. Some CDNs, like EdgeCast, respect those. Other CDNs, like Amazon’s CloudFront, do not. However, modifying Cassette to be able to use a versioned path approach might be something I’d look into.

      • Andrew Davey says:

        Cassette only uses querystring cache buster when in debug mode. This is to keep the URLs of scripts reasonable to read in the Chrome debugger, for example.

        When in release mode Cassette will generate URLs that have the cache busting hash in the path segment. e.g. /_cassette/scriptbundle/libs_abc123_js

        Also, Cassette’s URL cache busters are based on the SHA1 hash of the specific bundle content. This means changing one bundle does not invalidate all the other bundles in cache.

Comments are closed.