ASP.NET Core: Using Assembly Build Date For Cache Busting

ASP.NET Core contains a variety of useful Tag Helpers to enable server-side code to participate in creating and rendering HTML elements in our Views. One Tag Helper, in particular, has the ability to cache bust links to static resources such as Image, CSS and JavaScript by appending an asp-append-version="true" attribute.

The asp-append-version attribute automatically adds a version number to the file name using a SHA256 hashing algorithm, so whenever the file is updated, the server generates a new unique version. For a deeper understanding on how ASP.NET Core performs this piece of functionality, give the following StackOverflow post a read: How does javascript version (asp-append-version) work in ASP.NET Core MVC?.

This approach works perfectly if you're linking to your static resources using the relevant HTML tag, for example img, script or link. In my scenario, I'm using a JavaScript library called LabJS - a dynamic script loader that gives the ability to control the loading and execution of different plugins. For example:

<script>
  $LAB
  .script("http://remote.tld/jquery.js").wait()
  .script("/local/plugin1.jquery.js")
  .script("/local/plugin2.jquery.js").wait()
  .script("/local/init.js").wait(function(){
      initMyPage();
  });
</script>

I need to be able to append a query string parameter to one of the JavaScript file references. One thing that came to mind was to use the applications last build-time as the cache busting value. Whenever the application is updated, this value will automatically be updated so no manual intervention is required.

I found code examples from meziantou.net that demonstrated various approaches to acquiring an applications build date. I modified the "Linker timestamp" example to return a Unix timestamp in a newly created class called AssemblyUtils.

public class AssemblyUtils
{
    #region Properties

    public int UnixTimestamp { get; set; }

    #endregion

    /// <summary>
    /// Get timestamp in Unix seconds for the last build.
    /// </summary>
    /// <returns></returns>
    public static int GetBuildTimestamp()
    {
        const int peHeaderOffset = 60;
        const int timestampOffset = 8;

        byte[] bytes = new byte[2048];

        using (FileStream file = new FileStream(Assembly.GetExecutingAssembly().Location, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            file.Read(bytes, 0, bytes.Length);

        int headerPos = BitConverter.ToInt32(bytes, peHeaderOffset);
        int unixTime = BitConverter.ToInt32(bytes, headerPos + timestampOffset);

        return unixTime;
    }
}

The code will only return the Assembly information if your Visual Studio .csproj file (from version 15.4 onwards) includes the following setting within the <PropertyGroup> settings:

<Deterministic>False</Deterministic>

It would be a waste to constantly call the GetBuildTimestamp() method to acquire assembly information directly within the page View, when the most ideal approach would be to make this call once on application startup.

public void ConfigureServices(IServiceCollection services)
{
    #region Assembly Utils - Build Time

    Action<AssemblyUtils> assemblyBuildOptions = (opt =>
    {
        opt.UnixTimestamp = AssemblyUtils.GetBuildTimestamp();
    });

    services.Configure(assemblyBuildOptions);
    services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<AssemblyUtils>>().Value);

    #endregion
}

We can access the build timestamp value using Dependency Injection within a base controller that gets inherited by all controllers.

public class BaseController : Controller
{
    private int _buildTimetamp { get; set; }

    public BaseController(AssemblyUtils assemblyUtls)
    {
        _buildTimetamp = assemblyUtls.UnixTimestamp;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        base.OnActionExecuting(context);

        // Assign build timestamp to a View Bag.
        ViewBag.CacheBustingValue = _buildTimetamp;
    }
}

The timestamp is assigned to a ViewBag that can then be accessed at View level.

<script>
  $LAB
  .script("http://remote.tld/jquery.js").wait()
  .script("/local/plugin1.jquery.js")
  .script("/local/plugin2.jquery.js").wait()
  .script("/local/init.js?v=@ViewBag.CacheBustingValue").wait(function(){
      initMyPage();
  });
</script>

This will result in the following output:

<script>
  $LAB
  .script("http://remote.tld/jquery.js").wait()
  .script("/local/plugin1.jquery.js")
  .script("/local/plugin2.jquery.js").wait()
  .script("/local/init.js?v=1609610821").wait(function(){
      initMyPage();
  });
</script>

Leave A Comment

If you have any questions or suggestions, feel free to leave a comment. I do get inundated with messages regarding my posts via LinkedIn and leaving a comment below is a better place to have an open discussion. Your comment will not only help others, but also myself. :-)