MVC4 StyleBundle: Can you add a cache-busting query string in Debug mode?

19,402

Solution 1

You can create a custom IBundleTransform class to do this. Here's an example that will append a v=[filehash] parameter using a hash of the file contents.

public class FileHashVersionBundleTransform: IBundleTransform
{
    public void Process(BundleContext context, BundleResponse response)
    {
        foreach(var file in response.Files)
        {
            using(FileStream fs = File.OpenRead(HostingEnvironment.MapPath(file.IncludedVirtualPath)))
            {
                //get hash of file contents
                byte[] fileHash = new SHA256Managed().ComputeHash(fs);

                //encode file hash as a query string param
                string version = HttpServerUtility.UrlTokenEncode(fileHash);
                file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", version);
            }                
        }
    }
}

You can then register the class by adding it to the Transforms collection of your bundles.

new StyleBundle("...").Transforms.Add(new FileHashVersionBundleTransform());

Now the version number will only change if the file contents change.

Solution 2

You just need a unique string. It doesn't have to be Hash. We use the LastModified date of the file and get the Ticks from there. Opening and reading the file is expensive as @Todd noted. Ticks is enough to output a unique number that changes when the file is changed.

internal static class BundleExtensions
{
    public static Bundle WithLastModifiedToken(this Bundle sb)
    {
        sb.Transforms.Add(new LastModifiedBundleTransform());
        return sb;
    }
    public class LastModifiedBundleTransform : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {
            foreach (var file in response.Files)
            {
                var lastWrite = File.GetLastWriteTime(HostingEnvironment.MapPath(file.IncludedVirtualPath)).Ticks.ToString();
                file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", lastWrite);
            }
        }
    }
}

and how to use it:

bundles.Add(new StyleBundle("~/bundles/css")
    .Include("~/Content/*.css")
    .WithLastModifiedToken());

and this is what MVC writes:

<link href="bundles/css/site.css?v=635983900813469054" rel="stylesheet"/>

works fine with Script bundles too.

Solution 3

This library can add the cache-busting hash to your bundle files in debug mode, as well as a few other cache-busting things: https://github.com/kemmis/System.Web.Optimization.HashCache

You can apply HashCache to all bundles in a BundlesCollection

Execute the ApplyHashCache() extension method on the BundlesCollection Instance after all bundles have been added to the collection.

BundleTable.Bundles.ApplyHashCache();

Or you can apply HashCache to a single Bundle

Create an instance of the HashCacheTransform and add it to the bundle instance you want to apply HashCache to.

var myBundle = new ScriptBundle("~/bundle_virtual_path").Include("~/scripts/jsfile.js");
myBundle.Transforms.Add(new HashCacheTransform());

Solution 4

I've had the same problem but with cached versions in client browsers after an upgrade. My solution is to wrap the call to @Styles.Render("~/Content/css") in my own renderer that appends our version number in the query string like this:

    public static IHtmlString RenderCacheSafe(string path)
    {
        var html = Styles.Render(path);
        var version = VersionHelper.GetVersion();
        var stringContent = html.ToString();

        // The version should be inserted just before the closing quotation mark of the href attribute.
        var versionedHtml = stringContent.Replace("\" rel=", string.Format("?v={0}\" rel=", version));
        return new HtmlString(versionedHtml);
    }

And then in the view I do like this:

@RenderHelpers.RenderCacheSafe("~/Content/css")

Solution 5

Not currently but this is slated to be added soon (right now scheduled for the 1.1 stable release, you can track this issue here: Codeplex

Share:
19,402
growse
Author by

growse

Updated on June 03, 2022

Comments

  • growse
    growse almost 2 years

    I've got an MVC application and I'm using the StyleBundle class for rendering out CSS files like this:

    bundles.Add(new StyleBundle("~/bundles/css").Include("~/Content/*.css"));
    

    The problem I have is that in Debug mode, the CSS urls are rendered out individually, and I have a web proxy that aggressively caches these urls. In Release mode, I know a query string is added to the final url to invalidate any caches for each release.

    Is it possible to configure StyleBundle to add a random querystring in Debug mode as well to produce the following output to get around the caching issue?

    <link href="/stylesheet.css?random=some_random_string" rel="stylesheet"/>
    
  • Kind Contributor
    Kind Contributor about 8 years
    Why not just use the file LastWrite date. Then you're not having to do all that disk read and CPU for SHA256. And if you insist on hashing, MD5 is enough - you're not after security, you're after a unique hash(good enough) with low cpu cycles.
  • Kind Contributor
    Kind Contributor about 8 years
    Nope that won't work, your example doesn't work for me - response.Files is an Enumarable of FileInfo objects. (Web.Optimization Version=1.0.0.0). Looks like you need to use Version 1.1+
  • Kind Contributor
    Kind Contributor about 8 years
    This is a great workaround if you're stuck on Web.Optimization Version 1.0.0.0
  • hatsrumandcode
    hatsrumandcode almost 8 years
    Thanks @bingles, for versioning I modified the code slightly, so that I use the main DLL assembly version, which gets auto-incremented after each auto-deployment.
  • Marie
    Marie over 7 years
    Do you have to create a new FileHashVersionBundleTransform for each bundle or can you reuse one?
  • cesaraviles
    cesaraviles over 7 years
    @Marie As far as I know, you should be able to reuse a single instance.
  • andrewCanProgram
    andrewCanProgram over 7 years
    anyone know if this proposed solution will affect googles pagespeed metrics?
  • cesaraviles
    cesaraviles over 7 years
    I'm not familiar with google page speed, but the general effect here is that any time a resource is modified, the url changes which invalidates the browser's cache (if it was cached). So probably depends on if google page speed is caching resources or not.
  • Dan
    Dan about 7 years
    This is great! And can easily be adapted to use a software version number instead of a date. Thanks!
  • Dr. Ogden Wernstrom
    Dr. Ogden Wernstrom about 7 years
    I wouldn't worry about google page speed metrics since this fix is supposed to apply to Debug mode only.
  • Dr. Ogden Wernstrom
    Dr. Ogden Wernstrom about 7 years
    I tried this but it required updating our version of WebGrease from 1.1.0 to 1.5.2, which introduced another bug, so I rolled it back. The initial revision of this package (Version 1.0.0) would not have required us to update WebGrease, however instead of installing that version I decided to go with the accepted answer from @bingles as it given us complete control.
  • Dr. Ogden Wernstrom
    Dr. Ogden Wernstrom about 7 years
    Except now I see that the accepted answer also requires WebGrease 1.5.2.
  • David Kearfott
    David Kearfott over 6 years
    So simple. Thanks so much!
  • user123456
    user123456 over 2 years
    @H Dog not working for me the cshtml file not resolving bundles