Leverage browser cache in MVC using an action filter

Along the lines of my previous post I wanted an easy way to implement client cache in a standard MVC web application. The OutputCache attribute does not allow us to dynamically change the cache settings for the current request. As MVC and Web API both rely on different libraries I could not reuse the Web API action filter in MVC so I had to come up with a new solution. This again resulted in two simple classes.

public class ClientCacheAttribute : ActionFilterAttribute {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        base.OnActionExecuted(filterContext);

        var clientCache = ClientCache.Current;

        if (clientCache.IsValid) {
            filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.NotModified);
        }
    }
}

public class ClientCache : HttpCachePolicyWrapper {
    private const string ItemsKey = "C653F02F-14F9-4E8C-9E74-D22F7E7230A4";
    private const string IfModifiedSinceHeaderKey = "If-Modified-Since";

    protected ClientCache()
        : base(HttpContext.Current.Response.Cache) {        
        var request = HttpContext.Current.Request;
        if (request.Headers.AllKeys.Contains(IfModifiedSinceHeaderKey)) {
            string modifiedSinceHeaderValue = request.Headers.GetValues(IfModifiedSinceHeaderKey).First();

            DateTimeOffset modifiedSince;
            if (DateTimeOffset.TryParse(modifiedSinceHeaderValue, out modifiedSince)) {
                this.IfModifiedSince = modifiedSince;
            }
        }
    }

        
    public static ClientCache Current {
        get {
            var currentContext = HttpContext.Current;

            if (!currentContext.Items.Contains(ItemsKey)) {
                lock (currentContext.Items) {
                    if (!currentContext.Items.Contains(ItemsKey)) {
                        currentContext.Items.Add(ItemsKey, new ClientCache());
                    }
                }
            }

            return currentContext.Items[ItemsKey] as ClientCache;
        }
    }

    public DateTimeOffset? IfModifiedSince {
        get;
        private set;
    }


    public DateTimeOffset? LastModified {
        get;
        private set;
    }


    public override void SetLastModified(DateTime date) {
        this.LastModified = new DateTimeOffset(date);
        this.IsValid = (this.IfModifiedSince.HasValue && this.LastModified.Value.Equals(IfModifiedSince.Value));
        base.SetLastModified(date);
    }

    public bool IsValid {
        get;
        private set;
    }
}

These classes again give easy access to the cache headers. This allows us to not do any work at all based on the If-Modified-Since or set appropriate cache headers. The code below shows how the ClientCacheAttribute and ClientCache class can be put to use.

[ClientCache]
public ActionResult Index() {
    var clientCache = ClientCache.Current;
    clientCache.SetLastModified(DateTime.Today);
    clientCache.SetExpires(DateTime.Today.AddDays(1));
    clientCache.SetCacheability(HttpCacheability.Private);

    if (clientCache.IsValid) {
        //don't do work if the client cache is still valid...
        return null;
    }

    return View();
}

The net result is that the ActionResult that is returned from the action might not even be executed based on these settings.

Regards,

Wesley

No Comments