Skip to main content

Great article on how to implement a simple, but effective cache busting technique in ASP.NET.

---
title: Cache busting in ASP.NET
author: Mads Kristensen
date: March 5, 2014
source: https://madskristensen.net/blog/cache-busting-in-aspnet/
notoc: false
---

Optimizing for website performance includes setting long expiration dates on our
static resources, such s images, stylesheets and JavaScript files. Doing that
tells the browser to cache our files so it doesn't have to request them every
time the user loads a page. This is one of the most important things to do when
optimizing websites.

In ASP.NET on IIS7+ it's really easy. Just add this chunk of XML to the
web.config's `<system.webServer>` element:

```xml
<staticcontent>
  <!-- tell browsers to automatically cache all static resources for 365 days -->
  <clientcache cachecontrolmode="UseMaxAge" cachecontrolmaxage="365.00:00:00" />
</staticcontent>
```

The above code tells the browsers to automatically cache all static resources
for 365 days. That's good and you should do this right now.

The issue becomes clear the first time you make a change to any static file. How
is the browser going to know that you made a change, so it can download the
latest version of the file? The answer is that it can't. It will keep serving
the same cached version of the file for the next 365 days regardless of any
changes you are making to the files.

## Fingerprinting

The good news is that it is fairly trivial to make a change to our code, that
changes the URL pointing to the static files and thereby tricking the browser
into believing it's a brand new resource that needs to be downloaded.

Here's a little class that I use on several websites, that adds a fingerprint,
or timestamp, to the URL of the static file.

```csharp
using System;
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;

public class Fingerprint
{
    public static string Tag(string rootRelativePath)
    {
        if (HttpRuntime.Cache[rootRelativePath] == null)
        {
            string absolute = HostingEnvironment.MapPath("~" + rootRelativePath);

            DateTime date = File.GetLastWriteTime(absolute);
            int index = rootRelativePath.LastIndexOf('/');

            string result = rootRelativePath.Insert(index, "/v-" + date.Ticks);
            HttpRuntime.Cache.Insert(rootRelativePath, result, new CacheDependency(absolute));
        }

        return HttpRuntime.Cache[rootRelativePath] as string;
    }
}
```

All you need to change in order to use this class, is to modify the references
to the static files.

### Modify references

Here's what it looks like in **Razor** for the stylesheet reference:

```xml
<link rel="stylesheet" href="@Fingerprint.Tag("/content/site.css")" />
```

...and in **WebForms**:

```xml
<link rel="stylesheet" href="<%=Fingerprint.Tag(" />content/site.css") %>" />
```

The result of using the `FingerPrint.Tag()` method will in this case be:

```xml
<link rel="stylesheet" href="/content/v-634933238684083941/site.css" />
```

Since the URL now has a reference to a non-existing folder
(`v-634933238684083941`), we need to make the web server pretend it exist. We do
that with URL rewriting.

### URL rewrite

By adding this snippet of XML to the **web.config's** `<system.webServer>`
section, we instruct IIS 7+ to intercept all URLs with a folder name containing
`v=[numbers]` and rewrite the URL to the original file path.

```xml
<rewrite>
  <rules>
    <rule name="fingerprint">
      <match url="([\S]+)(/v-[0-9]+/)([\S]+)" />
      <action type="Rewrite" url="{R:1}/{R:3}" />
    </rule>
  </rules>
</rewrite>
```

You can use this technique for all your JavaScript and image files as well.

The beauty is, that every time you change one of the referenced static files,
the fingerprint will change as well. This creates a brand new URL every time so
the browsers will download the updated files.

> **NOTE** you need to run the AppPool in [Integrated Pipeline mode] for the
> `<system.webServer>` section to have any effect.

---

### Extra

> *WARNING: [this section](#extra) came from the Comments section of the article
> and has not been verified to actually work... use at your own risk.*

An HtmlHelper Extension method posted by *Carsten Petersen* in the article
Comments section (January 10, 2013) that automatically converts the
`rootRelativePath`.

```csharp
public static class HtmlHelper
{
    private readonly static Regex re_Version = new Regex(@"(\?v=([^$]+)$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private readonly static Regex re_LastFolder = new Regex(@"(/[^/$]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

    public static IHtmlString ToTag(this IHtmlString htmlString, int cacheDuration = 10)
    {
        string rootRelativePath = htmlString.ToHtmlString();

        if (HttpRuntime.Cache[rootRelativePath] == null)
        {
            var result = rootRelativePath;
            if (re_Version.IsMatch(result))
            {
                var versionString = re_Version.Match(result).Groups[2].Value;
                result = re_Version.Replace(result, "");
                if (re_LastFolder.IsMatch(result))
                {
                    var lastFolderSegment = re_LastFolder.Match(result).Groups[1].Value;
                    result = string.Concat(re_LastFolder.Replace(result, ""), "/v-", versionString, lastFolderSegment);
                }
            }
            HttpRuntime.Cache.Insert(rootRelativePath, result, null, DateTime.Now.AddMinutes(cacheDuration), new TimeSpan(0));
        }

        return MvcHtmlString.Create(HttpRuntime.Cache[rootRelativePath] as string);
    }
}
```

The **web.config** url rewrite rule needs to be changed from the original to:

```xml
<rewrite>
  <rules>
    <rule name="fingerprint" stopProcessing="false">
      <match url="([\S]+)[b](/v-[^/]+/)[/b]([\S]+)" ignoreCase="true" negate="false" />
      <action type="Rewrite" url="{R:1}/{R:3}" />
    </rule>
  </rules>
</rewrite>
```

**Example usage**

Razor view example (e.g.: `_Layout.cshtml`):

```xml
<script src="@Scripts.Url("~/bundles/jquery").ToTag()" type="text/javascript"></script>
```

[Integrated Pipeline mode]: http://msdn.microsoft.com/en-us/magazine/cc135973.aspx