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:
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.
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.