skip to Main Content

I have a js module, let’s call it A. It uses versioning by appending ?v=xxxxxxxxxxxx into its URL (like <script src="/Scripts/A.js?v=637082108844148373"></script>). v changes everytime we make changes in the file.

Here is the code:

public static class UrlHelperExtensions
{
    public static string Content(this UrlHelper helper, string filename, bool versioned)
    {
        var result = filename;

        if (versioned)
        {
            var lastWriteTimeToken = CalculateToken(helper.RequestContext.HttpContext, filename);
            result = filename + "?v=" + lastWriteTimeToken;
        }

        return helper.Content(result);
    }
}

And then we can use it in Razor views as this:

// Sample.cshtml
// ... code omitted for the sake of brevity ...

@section scripts {
    <script type="module" src="@Url.Content("~/Scripts/A.js", true)"></script>
}

// ... code omitted for the sake of brevity ...

The module imports modules B.js and C.js:

// A.js

import {Foo} from "./B.js";
import {Bar} from "./C.js";

If I change something in module A.js, client browser’s cache is busted since we have ?v parameter which is changing every time we make any changes in A.js. But if I change something in module B.js or C.js, its version remains the same and I have to clear cache manually (CTRL + F5) to see the changes.

In other words, we can’t use ?v parameter for the lines:

import {Foo} from "./B.js";
import {Bar} from "./C.js";

How to solve this problem of cache busting for imported files in MVC 5?

4

Answers


  1. HTTP header

    You could use the HTTP header to instruct the browser to no-cache or wipe cache after a short time.

    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control

    New paths for B and C

    If you like to stick to versioning, you could also put the modules B and C in a new folder (version number) and rewrite the paths in module A. Of course you don’t want to do this manually, but via a script (that ideally executes on save).

    Transpile A, B and C into a single file

    Easier would be to transpile the bundles into a single file, for which your versioning already works. You can use esbuild with --bundle for that or something else – there are multiple options (also extensions that execute “on save”).

    Login or Signup to reply.
  2. I would work backwards from the desired final deployed state of your index.html file. My Demo SPA produces this output, but the application code does not need to deal with any cache related concerns.

    <!DOCTYPE html>
    <html lang='en'>
        <head>
            <meta charset='utf-8'>
            <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
    
            <base href='/spa/' />
            <title>OAuth Demo App</title>
    
            <link rel='stylesheet' href='bootstrap.min.css?t=1648582395628' integrity='sha256-YvdLHPgkqJ8DVUxjjnGVlMMJtNimJ6dYkowFFvp4kKs='>
            <link rel='stylesheet' href='app.css?t=1648582395628' integrity='sha256-B7pu+gcFspulW4zXfgczVtPcEuZ81tZRFYeRciEzWro='>
        <body>
            <div id='root' class='container'></div>
    
            <script type='module' src='vendor.bundle.js?t=1648582395628' integrity='sha256-g0/+kYJcpXM7K5tvtIwBx//nKV3mCR8Y6NktYGHgpW0='></script>
            <script type='module' src='app.bundle.js?t=1648582395628' integrity='sha256-YY15iWJ0R9wnYkc1BP9yBYMlPNCeGFJBWFhio6z8Y1Q='></script>
        </body>
    </html>
    

    BUILD STEP

    In my case I am using a Webpack build step, which keeps the application code simple. Meanwhile application modules depend on each other in a simple way.

    SERVER SIDE TECH

    This will be harder if you are mixing server side and client side code to manage Javascript, and personally I am not a fan of such tech stacks. The same principles apply though, so I would look into bundling options and a build step to solve your problem. The application code should not know anything about cache busting URLs.

    Login or Signup to reply.
  3. For simplicity, if verbose in the script tag, you could include a list of files to check.

    <script type="module" src="@Url.Content("~/Scripts/A.js", true, new string[]{ "~/Scripts/B.js","~/Scripts/C.js"})"></script>
    

    Then amend your helper to include those in the token.

    public static class UrlHelperExtensions
    {
        public static string Content(this UrlHelper helper, string filename, bool versioned, string[] cacheCheck)
        {
            var result = filename;
    
            if (versioned)
            {
                var cacheChecks = cacheCheck.ToList();
                cacheChecks.Add(filename);
                var lastWriteTimeToken = new List<string>();
                foreach (var item in cacheChecks)
                {
                    lastWriteTimeToken.Add(CalculateToken(helper.RequestContext.HttpContext, item));
                }
                result = filename + "?v=" + String.Join("", lastWriteTimeToken.ToArray());
            }
    
            return helper.Content(result);
        }
    }
    

    Untested 🙂

    Login or Signup to reply.
  4. With ASP.NET Core, you may use the TagHelper attribute: asp-append-version to perform cache busting.

    <script src="/Scripts/A.js" asp-append-version="true"></script>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search