NS News & Views

By Clive Norman
List all 36 articles

Cache-busting in C# without querystrings

| Tags: Razor Website JavaScript CSS C# Caching

A key benefit of working with the web (or more specifically http) is it’s native ability to cache.  This can be ‘tuned’ to make websites extremely fast and slick, especially after initial page load.

The simplest example could be a file (be it css, JavaScript or even a jpeg image) that once downloaded to a user’s device, will remain stored on that device ‘locally’ ensuring each subsequent request for this same file, no longer needs to be re-downloaded.  Indeed, the file could remain on the device until a pre-defined expiry date, or until the user clears their local temporary internet files etc.

In addition, if you use a popular CDN to serve up common files (e.g. jQuery etc) then you could benefit from a user already having that file in their local cache, from another website; thus your site appears faster, thanks to this file having already being downloaded – sweet!

I love caching and where possible, always enable it.


But What Happens when you change a file?

So we accept that caching is a good thing.  However, when you deliberately change a file (update your css or JavaScript etc), of course you don’t want the cached version being served to the end user – and no, it isn’t good enough to just say ‘refresh your browser’.

The obvious answer, is to rename your file.  But of course, you then have to rename all references to this file, within your website – not ideal!

Enter Cache-Busting

Who you gonna call…..” Ok…don’t worry, I won’t go there, far too cheesy!

For a long time, I used Microsoft’s own Bundling and Minification solution, which for the record, I still think is excellent.  However, when it comes to cache-busting, the Microsoft solution, resolves this by appending a unique query-string to the end of the file.

For example: a file called ‘styles.css’ could become ‘styles?v=r0sLDicvP58AIXN

This solutions works well, in the sense that it certainly provides a ‘new’ file name – but on my travels I have subsequently learnt, that the query-string method is not the best accepted practice for cache-busting, and can fail under certain circumstances.

An Alternative Approach

I use the excellent MiniBlog framework for my blog site, and love the way it deals with caching and cache busting (amongst other things).

MiniBlog (and no doubt others) take an approach of actually changing the path as against the filename*.*  This path is created, based on a time stamp (or specifically, time-ticks) and is remarkably simple.  It’s now my preferred method of cache-busting.


The Steps

Firstly, you need to create a c# class, that will be responsible for creating the unique new path to any given file.

In the below example, you may note that this class also allows for the use of a CDN path, in which the cache-bust would not be required.

public static string FingerPrint(string rootRelativePath, string cdnPath = "")
{
    if (!string.IsNullOrEmpty(cdnPath) && !HttpContext.Current.IsDebuggingEnabled)
    {
        return cdnPath;
    }
        if (HttpRuntime.Cache[rootRelativePath] == null)
    {
        string relative = VirtualPathUtility.ToAbsolute("~" + rootRelativePath);
        string absolute = HostingEnvironment.MapPath(relative);
        if (!File.Exists(absolute))
        {
            throw new FileNotFoundException("File not found", absolute);
        }
        DateTime date = File.GetLastWriteTime(absolute);
        int index = relative.LastIndexOf('/');
        string result = relative.Insert(index, "/v-" + date.Ticks);
        HttpRuntime.Cache.Insert(rootRelativePath, result, new CacheDependency(absolute));
    }
        return HttpRuntime.Cache[rootRelativePath] as string;
}

The key to the unique path creation is the File.GetLastWriteTime syntax – this literally establishes the last time a file was changed, and uses that, as a basis to create a unique numerical value of time-ticks.

Of course, we will need to somehow establish the real path, as to where the file is located (as against this virtual one); in short, we need some routing.

This is very easily done, with the below code snippet added to the web.config file – using some regex wizardry, this tells incoming requests, that when it comes across one of these ‘special paths’, to route it to the correct location.

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

Finally, to actually use the cache-busting feature in your page, just path your appropriate links and scripts (css, js etc) using the below method.

<link rel="stylesheet" href="@CacheBust.FingerPrint("/css/mainStyle.css")" />
<link rel="shortcut icon" href="@CacheBust.FingerPrint("/favicon.ico")" type="image/x-icon" />
<script src="@CacheBust.FingerPrint("/scripts/mainScripts.js")"></script>

The Result

As you can see from the below screen shot, the example css and JavaScript paths are indeed, very unique!

I have created a very basic ASP.net website, which you can download from my GitHub account here.  This example will display a webpage detailing the path to a css file, a JavaScript file and a favicon image.

Making changes to any of the files within the project, will clearly demonstrate a new path on a refresh.

Give it a go - it certainly works for me!