MVC4 Script Bundles Caching issue

2019-06-22 04:39发布

问题:

We have an MVS application where we are bundling the javascript code using Bundle class ( don't do minification).

Bundling just works fine, but when we run the application, Cache value is set to Cache-Control:no-cache and at the same time every time we refresh the page the request always has a 200 OK. This means the js is not getting cached on client even though nothing was changed.

Also is there a way to verify if the bundled js is built dynamically or getting it from server cache?

Thanks

回答1:

I was seeing the same behavior as described in codeplex link that mentions the issue:

i.e. if I visit these URLs in the following order then the behaviour is -

bundle.css?v=1234 : no-cache
bundle.css : public
bundle.css?v=1234 : public

I decided to dig into the System.Web.Optimization source code a bit to see what was going on. On the Bundle class, there is a private method setting headers, and it appears to be falling into this code:

if (noCache) {
    cachePolicy.SetCacheability(HttpCacheability.NoCache);
}

The noCache variable is set via a parameter. The calling method in this case is setting it:

// Set to no-cache if the version requested does not match
bool noCache = false;
var request = context.HttpContext.Request;
if (request != null) {
    string queryVersion = request.QueryString.Get(VersionQueryString);
        if (queryVersion != null && bundleResponse.GetContentHashCode() != queryVersion) {
                noCache = true;
    }
}

Long story short, we had switched to using Azure CDN for our bundles and changed the version query string parameter to something like ?v=1.0.0.0 based on the assembly version (similar to on this question). The bundle code is comparing "1.0.0.0" with the SHA256 hash code of the bundle content and flagging the bundle for no-cache as a result.

I resolved this by updating our query string to match the content hash.

Unfortunately the access level for the GetContentHashCode method is marked internal, so it was necessary to replicate the implementation. I ended up creating a class that inherited from Bundle so that it could apply the version number to the CdnPath as a transform:

public class ProxiedCdnBundle : Bundle
{
    private readonly string _cdnHost;

    public ProxiedCdnBundle(string virtualPath, string cdnHost = "")
        : base(virtualPath)
    {
        _cdnHost = cdnHost;
    }

    public override BundleResponse ApplyTransforms(BundleContext context, string bundleContent, IEnumerable<BundleFile> bundleFiles)
    {
        var response = base.ApplyTransforms(context, bundleContent, bundleFiles);

        if (context.BundleCollection.UseCdn && !String.IsNullOrWhiteSpace(_cdnHost))
        {
            string path = System.Web.VirtualPathUtility.ToAbsolute(context.BundleVirtualPath);
            base.CdnPath = string.Format("{0}{1}?v={2}", _cdnHost, path, GetBundleHash(response));
        }

        return response;
    }


    private static string GetBundleHash(BundleResponse response)
    {
        using (var hashAlgorithm = CreateHashAlgorithm())
        {
            return HttpServerUtility.UrlTokenEncode(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(response.Content)));
        }
    }

    private static SHA256 CreateHashAlgorithm()
    {
        if (CryptoConfig.AllowOnlyFipsAlgorithms)
        {
            return new SHA256CryptoServiceProvider();
        }

        return new SHA256Managed();
    }
}


回答2:

The problem appears to be with the Microsoft.AspNet.Web.Optimization NuGet package. After downgrading the version from 1.3.0 to 1.1.0, everything seems to be working fine.

Link to blog post on codeplex which mentioned the same issue



回答3:

As the answers above did not help me (unsure of the consequences), I found a workaround for this issue.

The problem is, as stated already here, that, when you send a ?v on the query string and the value does not match the actual hash, it will return no-cache.

Not sending anything at all is not an option (cache may never expire). Sending a cache busting parameter is not an option either. If you do so and you have multiple instances, you may cache the wrong value during the deploy (if you don't remove from the load balancer the old instances).

To fix this issue, just set UseCdn to false and change the following during the bundle configuration:

Scripts.DefaultTagFormat = string.Format(@"<script src=""{0}{{0}}""></script>", CdnRoot);

Hope, I've helped.