How to set different Cache expire times for Client and Server caches
Asked Answered
S

3

13

I would like to have certain pages have a 10 minute Cache for clients and 24 hours for the server. The reason is if the page changes, the client will fetch the updated version within 10 minutes, but if nothing changes the server will only have to rebuild the page once a day.

The problem is that Output Cache settings seem to override the Client settings. Here is what I have setup:

Custom ActionFilterAttribute Class

public class ClientCacheAttribute : ActionFilterAttribute
{
    private bool _noClientCache;

    public int ExpireMinutes { get; set; }

    public ClientCacheAttribute(bool noClientCache) 
    {
        _noClientCache = noClientCache;
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (_noClientCache || ExpireMinutes <= 0)
        {
            filterContext.HttpContext.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
            filterContext.HttpContext.Response.Cache.SetValidUntilExpires(false);
            filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
            filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            filterContext.HttpContext.Response.Cache.SetNoStore();
        }
        else
        {
            filterContext.HttpContext.Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(ExpireMinutes));
        }

        base.OnResultExecuting(filterContext);
    }
}

Web Config settings

  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="Cache24Hours" location="Server" duration="86400" varyByParam="none" />
    </outputCacheProfiles>
  </outputCacheSettings>

How I'm calling it:

[OutputCache(CacheProfile = "Cache24Hours")]
[ClientCacheAttribute(false, ExpireMinutes = 10)]
public class HomeController : Controller
{
  [...]
}

But looking at the HTTP Header shows:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/html; charset=utf-8
Expires: -1

How can I implement this properly? It is an ASP.NET MVC 4 application.

Speight answered 11/2, 2013 at 23:44 Comment(1)
I'm not familiar with this syntax for caching. I use duration. Had success with it. That doesn't help debugging yours, but its an alternate suggestionCiro
C
8

You need to implement your own solution for server side caching and for client side caching either use ClientCacheAttribute or OutputCache. Here are the reason for why you would require your custom solution for server side cache.

  • ClientCacheAttribute sets cache policy to Response.Cache which is type of HttpCachePolicyBase
  • and built-in OutputCache also sets cache policy to Response.Cache

Here what I'm trying to highlight is that we don't have collection of HttpCachePolicyBase but we only have one object of HttpCachePolicyBase so we can't set multiple cache policy for given response.

Even if we can set Http Cacheability to HttpCacheability.ServerAndPrivate but again you will run in other issue with cache duration (i.e. for client 10 minute & server 24 hours)

What I would suggest is that use OutputCache for client side caching and implement your own caching mechanism for server side caching.

Callimachus answered 19/2, 2013 at 13:6 Comment(3)
I was trying to avoid that, it is much easier to create a custom client side (basically managing http headers) then it is to create a custom server side solution since you then have to manage page copies in cache. Unfortunately when I tried having OutputCache manage just the server cache, it overwrote the http headers that I had set manually for the client. Do you know of a way of overwrite the http headers that Output Cache sets when in Server mode?Speight
Josh it is likely to happen & it is obvious too because logically results are cached after result executed while in most cases we manage headers from action method or before result executedCallimachus
and that's why I suggested to use OutputCache for client side because just setting response headers is not recommended rather use Cache Policy for client cache.Callimachus
A
4

In your OutputCache profile, set the location to ServerAndClient. Your action filter should properly override the output once you do this.

Arteaga answered 12/2, 2013 at 3:41 Comment(6)
Doing so adds the max-age value to the Header, but also sets Expires: HTTP/1.1 200 OK Cache-Control: private, max-age=86400 Content-Type: text/html; charset=utf-8 Expires: Tue, 12 Feb 2013 13:33:41 GMTSpeight
I'm not 100% sure of what final result you're after, but I think if you add a call to Response.Cache.SetMaxAge in your action filter, CC:max-age and Expires will match up and you'll be good to go.Arteaga
I tried setting the Response.Cache.SetMaxAge and it was overwritten by Output Cached when in ServerAndClient mode. And just ignored when in just Filter mode. My goal is to have Cache-Control set to "public, max-age=10" and expires 10 minutes from now (GMT). The Cache-Control is controlled by Output Cache and doesn't seem I can overwrite it.Speight
Try setting OutputCache Location to Any, and use SetMaxAge(TimeSpan.FromMinutes(ExpireMinutes)) in your filter, because max-age is measured in seconds.Arteaga
That did set the headers correctly to "Cache-Control: public, max-age=600" but it appears that Server cached copy then uses the new max-age instead of the 86400 value in profile because after 10 minutes the browser receives a new copy of the page instead of the copy from cache. I'm guessing this just can't be done. BTW, this is hard thing Google.Speight
Makes sense, I guess. You might have better luck with the ServiceStack cache, or implementing your own solution.Arteaga
C
3

Under .Net 4.5, it IS possible to have different client and server caching policies without writing custom cache providers:

// Set the cache response expiration to 3600 seconds (use your own value here).
HttpContext.Current.Response.Cache.SetExpires(DateTime.UtcNow.AddSeconds(3600));

// Set both server and browser caching.
HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);

// Prevent browser's default max-age=0 header on first request
// from invalidating the server cache on each request.
HttpContext.Current.Response.Cache.SetValidUntilExpires(true);

// Set an HTTP ETag header on the page using a random GUID.
HttpContext.Current.Response.Cache.SetETag(System.Guid.NewGuid()
                                           .ToString().Replace("-", ""));

// Set last modified time.
HttpContext.Current.Response.Cache.SetLastModified(DateTime.UtcNow);

// Now here is the critical piece that forces revalidation on each request!:
HttpContext.Current.Response.Cache.AppendCacheExtension(
    "must-revalidate, proxy-revalidate, max-age=0");

The result is that each time the page gets regenerated in the server cache, it gets a new ETag. This causes the If-None-Match request to return the full page back to the browser. If the browser's cached copy is the same as what's generated in the server cache (same ETag), the browser gets back a 304 Not Modified.

Note that none of the cache headers I appended there in AppendCacheExtension conflict with the headers emitted by native caching. Whenever you attempt to modify the caching headers emitted by .Net caching itself, .Net will always supersede what you're trying to do. The trick is to add new non-conflicting headers, not try to change what .Net is already emitting. Most importantly, you must append the max-age header. You must NOT use .SetMaxAge(), as this also sets the maximum age of the server cached copy of the page.

This took quite a bit of effort to figure out, but it DOES work, at least in .Net 4.5.

Courbevoie answered 17/6, 2014 at 17:5 Comment(1)
This is not technically setting a different client side cache time, the last line I believe is no different from setting HttpContext.Current.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); I upvoted the answer because it is much better to use this workaround than to reimplement the server side caching. The overhead in the client revalidating the cache against the server is minimal so I think this is a great answer.Mitchellmitchem

© 2022 - 2024 — McMap. All rights reserved.