Stefan Holm Olsen

Cache busting 2.0: an update for ASP.Net Core

Since I wrote my first blog post about long browser caching of frontend resources, times have changed and ASP.Net Core has matured a great deal.

This blog post is therefore dedicated to update you on a new implementation to Cache Busting in .Net. The solution I describe works for both ASP.Net MVC and for ASP.Net Core MVC.

Requirements to browser caching

For information on what cache busting is, why it is needed and what a file fingerprint is, you should read my old blog post. If you have not read that, here comes a summary.

In short, the web browser needs to:

  1. Cache our static files for a very long time (days, weeks or months).
  2. Ignore the cached files and instead refresh static frontend files whenever they change on the server. This need to happen automatically.

Cache busting is the process of forcing the browser to ignore an already cached file and download a new version.

Caching files for a long time

Telling the browser how long to cache files can be done by setting a HTTP header, called Cache-Control, to a very high number of seconds. When these seconds pass the file expires from the cache, but in practice they may be removed long before.

In ASP.Net Core this is one way of specifying the Cache-Control for static files.

This works for files that exist in the wwwroot folder. It does not apply to MVC controllers that return file data.

Fingerprinting the files

When a HTML document links to a file at a URL like the following, the browser may find it in cache.

/html/dist/logo.svg

If this file is changed on the server, and the link remains exactly the same, the browser will not download it again, because we instructed the server to cache the file at this URL for a long time. The URL needs to change slightly for the browser to download the file again.

Fingerprinting the file is about adding a parameter to the file that will change whenever the file is changed. This parameter can be a random value, the file’s last modification date, the version number of the website assembly or a hash value.

In my previous blog post, I used the last modified date of a specific files, in the form of a 64-bit number of ticks.

Whenever the file was changed, the modification date, and in turn the fingerprint, would change too.

Then a fingerprinted URL looked something like this:

/html/dist/logo.svg?v=636343694099634717

Changes from my version 1

In this updated version of my cache busting helper, I decided not to use the file’s modification date anymore.

These are my reasons: - I chose not to rely on a cache provider (neither IMemoryCache nor IDistributedCache) to keep things simple. - The last modified date of a file does not necessarily tell when the file was actually changed. Build processes in Continuous Deployment set-ups will typically rebuild frontend files time and time again, each time setting a new last modified date. This is not going to work for pure long-time caching, where only real changes should force a refresh of files.

My new cache busting helper is really simple:

  • It utilizes a ConcurrentDictionary instead of a cache provider. For this case I prefer this over the regular Dictionary, because the latter is not thread-safe (and putting locks around it would be very bad).
  • It calculates a SHA1 hash of each file on first request. This way the contents of the files have to be changed for the fingerprint to change.
  • There is no cache dependency on physical files anymore. So, changing something in a file that is already hashed in the dictionary, does not remove hash and trigger a new hash calculation. To enforce a recalculation of the hash, requires either a redeploy or a restart of the website. But this is all fine by me, because how often do we really change or deploy single files to production anyway?

With the new cache helper, the fingerprinted URL now looks like this:

/html/dist/logo.svg?v=8f746d6576f7e72cf013a6ac200368cf36784918bd7ae3ea800cde83a9d78fb3

Like my old solution the code can be applied like this. But it could also be implemented as a TagHelper.

<script src="@Url.ContentVersioned("~/assets/main.js")"/>
<link rel="stylesheet" href="@Url.ContentVersioned("~/assets/screen.css")"/>

This is how the updated code looks.