MVC4 StyleBundle: Can you add a cache-busting query string in Debug mode?
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
growse
Updated on June 03, 2022Comments
-
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. InRelease
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 inDebug
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 about 8 yearsWhy 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 about 8 yearsNope 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 about 8 yearsThis is a great workaround if you're stuck on Web.Optimization Version 1.0.0.0
-
hatsrumandcode almost 8 yearsThanks @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 over 7 yearsDo you have to create a new FileHashVersionBundleTransform for each bundle or can you reuse one?
-
cesaraviles over 7 years@Marie As far as I know, you should be able to reuse a single instance.
-
andrewCanProgram over 7 yearsanyone know if this proposed solution will affect googles pagespeed metrics?
-
cesaraviles over 7 yearsI'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 about 7 yearsThis is great! And can easily be adapted to use a software version number instead of a date. Thanks!
-
Dr. Ogden Wernstrom about 7 yearsI wouldn't worry about google page speed metrics since this fix is supposed to apply to Debug mode only.
-
Dr. Ogden Wernstrom about 7 yearsI 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 about 7 yearsExcept now I see that the accepted answer also requires WebGrease 1.5.2.
-
David Kearfott over 6 yearsSo simple. Thanks so much!
-
user123456 over 2 years@H Dog not working for me the cshtml file not resolving bundles