29

By default a MVC bundle is cached on client for 1 year. Is it possible to set it's client headers manually (for 1 specific bundle)?

What I need is to set custom expire headers for one of my bundles. I can't rely on the "v=hash" querystring because this bundle is for an external website, and they won't change the url pointing to my bundle each time I change it.

What I've tried is to create a custom Bundle class (inherit Bundle) and overridde the GenerateBundleResponse() method. This way I can control the server caching, but the only way of customizing client caching is to set BundleResponse.Cacheability (public, private, nocache etc). But I can't set headers manually. I have access to the BundleContext (and it's HttpContext), but when I set headers on that context, it will have effect for all other requests as well.

tereško
  • 58,060
  • 25
  • 98
  • 150
stian.net
  • 3,928
  • 4
  • 25
  • 38
  • Sounds like you need VaryByXXX, but it looks like that flexibility is not there yet: http://stackoverflow.com/questions/13065330/mvc4-bundling-cache-headers – Grimace of Despair Jun 05 '13 at 12:28

6 Answers6

12

Unfortunately there is no way. You can find the reason in the internal implementation of bundling. in the BundleHandler class ProcessRequest calls the ProcessRequest, internal method of the Bundle class and it calls SetHeaders just before the HttpContext.Response.Write. Therefore the client cache is set to one year just before the response write.

Note: BundleHandler is a internal sealed class: internal sealed class BundleHandler : IHttpHandler

In the BundleHandler class:

public void ProcessRequest(HttpContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }
    context.Response.Clear();
    BundleContext context2 = new BundleContext(new HttpContextWrapper(context), BundleTable.Bundles, this.BundleVirtualPath);
    if (!Bundle.GetInstrumentationMode(context2.HttpContext) && !string.IsNullOrEmpty(context.Request.Headers["If-Modified-Since"]))
    {
        context.Response.StatusCode = 304;
    }
    else
    {
        this.RequestBundle.ProcessRequest(context2);
    }
}

In the Bundle class:

internal void ProcessRequest(BundleContext context)
{
    context.EnableInstrumentation = GetInstrumentationMode(context.HttpContext);
    BundleResponse bundleResponse = this.GetBundleResponse(context);
    SetHeaders(bundleResponse, context);
    context.HttpContext.Response.Write(bundleResponse.Content);
}

private static void SetHeaders(BundleResponse bundle, BundleContext context)
{
    if (bundle.ContentType != null)
    {
        context.HttpContext.Response.ContentType = bundle.ContentType;
    }
    if (!context.EnableInstrumentation)
    {
        HttpCachePolicyBase cache = context.HttpContext.Response.Cache;
        cache.SetCacheability(bundle.Cacheability);
        cache.SetOmitVaryStar(true);
        cache.SetExpires(DateTime.Now.AddYears(1));
        cache.SetValidUntilExpires(true);
        cache.SetLastModified(DateTime.Now);
        cache.VaryByHeaders["User-Agent"] = true;
    }
}
ErikE
  • 48,881
  • 23
  • 151
  • 196
Kambiz Shahim
  • 2,560
  • 14
  • 21
4

The default behavior of the ASP.NET MVC bundling feature is that if any of the files that compose a bundle change - the query string for that bundle will automatically change - assuming you are using the following in your view's code:

@Scripts.Render("bundle name")

So this means if you have a new version of a file that is in a bundle, the next time your page renders a view that uses that bundle, it will send a script tag that the client browser will not found in its cache (since the query string is different).

So it seems like this will solve your problem - depends on what you mean by:

and they won't change the url pointing to my bundle each time I change it

Adam
  • 28,537
  • 15
  • 60
  • 73
  • Thanks for your answer but as I try to explain I can't rely on the "v=hash" querystring because this bundle is for an external website. – stian.net Feb 28 '13 at 09:32
  • 1
    I think I understand --- you should edit your question to be a little clearer - that you want an external website's page to include – Adam Mar 01 '13 at 15:05
  • @stian.net: Just add ticks to the query string of the bundle, whatever the version is. That will invalidate client caching. – Stefan Steiger Aug 19 '15 at 07:23
3

What seems to work for me is giving the bundle a version number in the bundle config, then reference the new version in your markup.

  • 3
    @cdomination Why though? If this is a viable solution, what's wrong with posting it as such? – DevOhrion Mar 10 '18 at 00:27
  • @cdomination only smart users can post in comments section, as at least 50 points needed otherwise you can only answer a question – sairfan Mar 14 '19 at 20:35
2

While there is not a better way to setup bundles cacheability, you can create a HttpModule which identifies the requests to the bundle and set the content cacheability.

You have the same effect doing this on the Global.asax:

    public override void Init()
    {
        this.EndRequest += MvcApplication_EndRequest;
        base.Init();
    }

    void MvcApplication_EndRequest(object sender, EventArgs e)
    {
        var request = this.Request;
        var response = this.Response;

        if (request.RawUrl.Contains("Content/"))
        {
            response.Cache.SetCacheability(HttpCacheability.NoCache);
        }
    }
Adilson de Almeida Jr
  • 2,761
  • 21
  • 37
0

Pass an extra query string parameter to the url and change it everey time you want the cache to refresh.

e.g: https://www.google.co.in/?gfe_rd=cr&ei=EwJeVbHWLcX08wfgwoCoBA&gws_rd=ssl&custom=abc

the last parameter is custom.

0

This is a modification of Adilson's answer, but without having to create an HttpModule:

In the global.asax.cs of the MVC project:

protected void Application_EndRequest(object sender, EventArgs e) {
    if (Request.RawUrl.Contains("/bundles/")) {
        // My bundles all have a /bundles/ prefix in the URL
        Response.Cache.SetExpires(DateTime.Now.AddHours(2));
    }
}
Brandon
  • 695
  • 10
  • 29