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:

@using Core.Html
<!DOCTYPE html>
<html>
<head>
    <link rel="Stylesheet" href="@Url.StaticContent("~/css/style.css")" />
    <script type="text/javascript" src="@Url.StaticContent("~/Scripts/script.js")"></script>
</head>
<body>
    
</body>
</html>

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

using System;
using System.Configuration;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;

namespace Core.Html
{
    public static class HtmlHelperExtensions
    {
        public static string StaticContent(this UrlHelper helper, string contentPath)
        {
            var staticResourceVersion = (ConfigurationManager.AppSettings.Get("appharbor.commit_id") ?? "_default").Substring(0, 8);
            var staticServer = ConfigurationManager.AppSettings.Get(ConfigKeys.StaticServer);
            var prefix = "";
            if (!string.IsNullOrEmpty(staticServer))
                prefix = "//" + staticServer;
            
            return prefix + "/s/" + staticResourceVersion + helper.Content(contentPath);
        }
    }
}

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.

namespace Website
{
    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Statics",
                "s/{staticResourceVersion}/{*path}",
                new { controller = "Statics", action = "Index" }
            );

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );
        }
    }
}

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.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Website.Controllers
{
    public class StaticsController : Controller
    {
        private readonly HttpResponseBase _response;
        private readonly HttpRequestBase _request;

        public StaticsController(HttpResponseBase response, HttpRequestBase request)
        {
            _response = response;
            _request = request;
        }

        public ActionResult Index(string staticResourceVersion, string path)
        {
            if (!IsAllowed(path))
                return new HttpStatusCodeResult(404);

            if (!_request.IsLocal)
            {
                _response.Cache.SetCacheability(HttpCacheability.Public);
                _response.Cache.SetExpires(DateTime.Now.AddYears(1));
            }

            return File(Server.MapPath("~/" + path), GetContentType(path));
        }

        private bool IsAllowed(string path)
        {
            if (string.IsNullOrEmpty(path))
                return false;

            var lowerPath = path.ToLower();

            if (!lowerPath.StartsWith("img/") && !lowerPath.StartsWith("scripts/") && !lowerPath.StartsWith("css/"))
                return false;

            if (!lowerPath.EndsWith(".png") && !lowerPath.EndsWith(".css") && !lowerPath.EndsWith(".js"))
                return false;

            if (lowerPath.Contains(".."))
                return false;

            return true;
        }

        private static string GetContentType(string path)
        {
            var extension = Path.GetExtension(path);
            switch (extension)
            {
                case ".js":
                    return "text/javascript";
                case ".css":
                    return "text/css";
                case ".png":
                    return "image/png";
            }
            return null;
        }

    }
}

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.

Posted in Development | Tagged , , , , , | 4 Comments

jQuery UI Sortable + Bootstrap’s Buttons = Perfect Performance Storm

I ran into this crazy performance problem on:

  • jQuery 1.7.1
  • jQuery UI 1.8.16
  • Twitter’s Bootstrap 1.4.0
  • Windows 7 / Chrome 16.0.912.63

See how the button starts dragging from the top left corner and drags really slowly? I stripped down the CSS element by element using Chrome’s dev tools and was able to isolate the problem to the -webkit-transition and box-shadow styles on the .btn class. Setting those to none fixed the problem.

Posted in Development | Tagged , , , , , | Leave a comment

Using jQuery to provide an inline confirmation on buttons

I was working on a side project and wanted the user to confirm when they wanted to delete something. The normal window.confirm method was just too primitive so I decided to write something I liked a little more.

Click the delete button below to see it in action. Click the JavaScript tab below to see how it works.

This was built for use with the amazing Bootstrap CSS framework. Only the classes that are added/removed are dependent on Bootstrap, so adjust to your needs.

Posted in Development | Tagged , , , , | Leave a comment

Getting the version number of your own Chrome Extension

This is so kludgy. I hope they make it better sometime, though :-/

 

Posted in Development | Tagged , , , , | Leave a comment

Unit testing CoffeeScript with QUnit

I got bitten by the CoffeeScript bug and decided that I liked it enough to try to add CoffeeScript support to Cheezburger.com. Paul Betts’ very cool SassAndCoffee library has a method which compiles CoffeeScript to JavaScript which I could easily integrate into our script serving system.

I’ve also been bitten by the QUnit bug so before I enable CoffeeScript on Cheezburger.com, I want to make sure that I can unit test whatever CoffeeScript we write. A quick Google search yielded next to nothing about testing CoffeeScript but I was feeling inventive and came up with the following way to use QUnit with CoffeeScript.

Important Gotcha

Since we’re going to load .coffee scripts via jQuery.get(), they must be served by a web server. They will not load off the local file system if you simply open index.htm locally. If anyone finds a way around this, feel free to comment on the gist.

I enabled IIS Express to serve .coffee files by adding the following element to applicationhost.config in the configuration/system.webserver/staticContent element:

<mimeMap fileExtension=”.coffee” mimeType=”text/plain” />

/Gotcha

Here is a contrived Dog class and a test for it:

class Dog
    speak: -> "woof"
    legs: 4
view raw dog.coffee This Gist brought to you by GitHub.
module "Dog"

test "dog says woof", 1, () ->
    dog = new Dog()
    actual = dog.speak()
    equal actual, "woof"

test "dogs have four legs", 1, () ->
    dog = new Dog()
    actual = dog.legs
    equal actual, 4

And here’s the index.htm which runs the tests:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="qunit/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="qunit/qunit.js"></script>

        <script type="text/javascript" src="http://jashkenas.github.com/coffee-script/extras/coffee-script.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script type="text/coffeescript">
$ ->
    scriptsToTest = ["dog.coffee"]
    tests = ["dog.test.coffee"]
    loadCoffee = (files) ->
        $head = $ "head"
        load = (file) ->
             $.get file, (content) ->
                compiled = CoffeeScript.compile content, {bare: on}
                $("<script />").attr("type", "text/javascript").html(compiled).appendTo $head
        load file for file in files
    loadCoffee scriptsToTest
    loadCoffee tests
</script>
</head>
<body>
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup</div>
</body>
</html>
view raw index.htm This Gist brought to you by GitHub.

I started out with the normal set of QUnit files (index.htm, the css, and js), then included the coffee-script.js which enables us to execute CoffeeScript written in a <script type=”text/coffeescript”></script> tag. My usage of this tag in the code was completely unnecessary, but I thought this example would be more interesting if it was ALL CoffeeScript.

The scriptsToTest variable is a list of paths to .coffee files which will get loaded on the page. The tests variable is similar, but holds the tests. I split these up simply for readability.

The loadCoffee function spins through the contents of each of the files handed to it, executes the CoffeeScript compiler on them, and injects a script tag containing the compiled code into the page.

It’s important to include the “bare: on” option when calling the CoffeeScript.compile function. If bare is off (false), then the compiled code will be wrapped in a function so that declared variables get declared globally. My tests expect the Dog class to be declared globally, so I set bare to on.

Here’s a zip file full of all the files to make this example run (except for IIS Express):

QUnitAndCoffeeScript

Posted in Development, Uncategorized | 5 Comments

Turntable.fm: Mute until next track starts

Drag the link below up to your bookmark bar. When clicked, this bookmarklet will mute the current song in Turntable.fm and then unmute when the next song starts playing.

Updated 7/5/2011 - Turntable.fm changed the name of their player control, so instead of calling it directly, we’re just invoking the event handlers for the actual mute button. Should be more stable!

Mute Until Next Track Starts

I requested this feature via the Turntable.fm feedback button but I got tired waiting and decided to dive in and figure how to do it myself. The JavaScript simply activates the click event on the mute button, then binds a handler which waits for a chat message of ” started playing” to appear. When it does, the volume it turned all the way up again and the handler is unbound.

Here’s the code powering the bookmarklet:

Posted in Development, Uncategorized | 1 Comment

Events on JavaScript Objects

I did a search for javascript events and came up with lots of articles about onclick and onload and all the lovely things we use to make our pages interactive. But in this case, I wanted a C#-style event, like an event on an object. With a little more digging, I found this article about C#-style events which pointed me towards the approach of just keeping a list of the functions that should be triggered by that event. However, I didn’t like the way it was written so I decided to re-implement the same idea in a JavaScript writing style more like this article about JavaScript instant iterators. Here’s the result:

Posted in Development, Uncategorized | Leave a comment

Building Chrome extensions from a service account

So I got our build system to call the chrome.exe to build my Chrome extension, but it didn’t work. It said:

Could not find exported function RelaunchChromeBrowserWithNewCommandLineIfNeeded

The build system runs in a web application, so I’d have to guess that chrome was mad because it wasn’t running as a real user account.

I found this post that suggested moving the chrome.exe into the application/{version} folder. Once it was there, my build worked.

Clipped from code.google.com
I also faced this problem.
Exact deatails are as follows, In my Organisation Group policy does not allows for downloading and installing any s/w on computers from internet.Since i like chrome very much, I copied the chrome installation file(Chrome folder) from c:doc&settingsusernamelocalsettingapplication datagooglechrome and tried to run chrome and recieved this error( exported function not available)
To solve this problem I copied the chrome.exe from ~application folder into ~chromeapplication8.0.552.215 folder and tried to run chrome , it started working and i recieved no error
qustion is why chrome.exe in ~application folder was not able to read dlls into 8.0552.215 folder though it is hard wired to do so( I think so atleast).
It is a bug which devs should atleast try to correct it. 

I would also suggest to users who are facing this problem to copy chrome.exe from~application folder to ~applicationversion Id folder and running chrome.

Lets see what happens

Regards
Adi

Read more at code.google.com

Posted in Development, Uncategorized | Tagged , , , , | Leave a comment

Still relevant


Posted in Uncategorized | Tagged , , , , | Comments Off

Basics of Statistics in SQL Server 2005 – Developer.com

I had a table that was too large to get a count of. I tried Select Count(*) from {Table} but the query didn’t want to come back very quickly.

Another way to get the size is to get statistical information on the primary key. You can do sp_helpstats ‘{Schema}.{Table}’, ‘ALL’ to get a list of objects with statistical information. Find the primary key, then do DBCC SHOW_STATISTICS(‘{Schema}.{Table}’, {value of statistics_name column from previous query}


Posted in Development, Uncategorized | Tagged , , , , | Comments Off