As with ASP.NET Forms, MVC offers some out-of-the-box caching with their OutputCacheAttribute, however as with classic ASP.NET, one quickly realizes its limitations when building complex systems. In particular, it's very difficult, and often times impossible to flush/clear the cache based on various events that happen within your application.
For example, consider a main menu which has an ‘Admin’ button for appropriately authorized users. When your administrator initially views the page, the system will cache the HTML, including the Admin link. If you later revoked this privilege, the site would continue serving the cached link even though they were technically no longer authorized to access this part of the site.
Not good.
So, with a little to-ing and fro-ing, I’ve finalized my own FilterAttribute
which does this for you. The advantage of writing your own is that you can pass in whatever parameters you like, as well as directly have access to the current HttpContext
, which in turn means you can check user-specific values, access the database – whatever you need to do.
How It Works
The attribute essentially consists of just a couple of methods, both overrides of the IResultFilter
and IActionFilter
attributes:
OnActionExecuting
- This method fires before your Action even begins. By checking for a cache value here, we can abort the process before any long-running code in your Action
method or View rendering executes. OnResultExecuting
- This method fires just before HTML is rendered to our output stream. It is here that we inject cached content (if it exists). Otherwise, we capture the output for next time.
The Code
I’ve commented the code below so you can follow more-or-less what is going on. I won’t go into too much detail, but needless to say if you copy/paste this straight into your work, it won’t compile due to the namespace references. I’m also using Microsoft Unity for dependency injection, so don’t be confused by ICurrentUser
, etc.
Finally, I’ve got a custom cache class, whose source code I haven’t included – just switch out my lines to access your own cache instead.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using BlackBall.Common;
using BlackBall.Common.Localisation;
using BlackBall.Contracts.Cache;
using BlackBall.Contracts.Enums;
using BlackBall.Contracts.Exporting;
using BlackBall.Contracts.Localisation;
using BlackBall.Contracts.Security;
using BlackBall.Common.Extensions;
using BlackBall.Logic.Cache;
namespace BlackBall.MVC.Code.Mvc.Attributes
{
public class ResultOutputCachingAttribute : FilterAttribute, IResultFilter, IActionFilter
{
#region Properties & Constructors
private string ThisRequestOutput = "";
private bool VaryByUser = true;
private ICurrentUser _CurrentUser = null;
private ICurrentUser CurrentUser
{
get
{
if (_CurrentUser == null) _CurrentUser = Dependency.Resolve<ICurrentUser>();
return _CurrentUser;
}
}
public ResultOutputCachingAttribute(bool varyByUser = true)
{
this.VaryByUser = varyByUser;
}
private string _CacheKey = null;
private string CacheKey
{
get { return _CacheKey; }
set
{
_CacheKey = value;
}
}
#endregion
private void CacheResult(ResultExecutingContext filterContext)
{
using (var sw = new StringWriter())
{
if (filterContext.Result is PartialViewResult)
{
var partialView = (PartialViewResult)filterContext.Result;
var viewResult = ViewEngines.Engines.FindPartialView
(filterContext.Controller.ControllerContext, partialView.ViewName);
var viewContext = new ViewContext(filterContext.Controller.ControllerContext,
viewResult.View, filterContext.Controller.ViewData,
filterContext.Controller.TempData, sw);
viewResult.View.Render(viewContext, sw);
}else if (filterContext.Result is ViewResult)
{
var partialView = (ViewResult)filterContext.Result;
var viewResult = ViewEngines.Engines.FindView
(filterContext.Controller.ControllerContext,
partialView.ViewName, partialView.MasterName);
var viewContext = new ViewContext(filterContext.Controller.ControllerContext,
viewResult.View, filterContext.Controller.ViewData,
filterContext.Controller.TempData, sw);
viewResult.View.Render(viewContext, sw);
}
var html = sw.GetStringBuilder().ToString();
if (!string.IsNullOrWhiteSpace(html))
{
var cache = new CacheManager<CachableString>();
var cachedObject = new CachableString()
{ CacheKey = CreateKey(filterContext), Value = html };
cachedObject.AddTag(CacheTags.Project, CurrentUser.CurrentProjectID);
if (this.VaryByUser) cachedObject.AddTag
(CacheTags.Person, this.CurrentUser.PersonID);
cache.Save(cachedObject);
}
}
}
public void OnResultExecuting(ResultExecutingContext filterContext)
{
var cacheKey = CreateKey(filterContext);
if (!string.IsNullOrWhiteSpace(this.ThisRequestOutput))
{
filterContext.HttpContext.Response.Write
("<!-- Cache start " + cacheKey + " -->");
filterContext.HttpContext.Response.Write(this.ThisRequestOutput);
filterContext.HttpContext.Response.Write
("<!-- Cache end " + cacheKey + " -->");
return;
}
CacheResult(filterContext);
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!Configuration.Current.UseOutputCaching) return;
Func<string, CachableString> func = (ck) => new CachableString() { CacheKey = ck };
var cache = new CacheManager<CachableString>();
var cacheKey = new CachableString() { CacheKey = CreateKey(filterContext) };
var cachedObject = cache.Load(cacheKey, func);
this.ThisRequestOutput = cachedObject.Value;
Refer to http:
canceling-the-actionexecutingcontext-in-the-onactionexecuting-actionfilter/
if (!string.IsNullOrWhiteSpace(this.ThisRequestOutput))
{
filterContext.Result = new ContentResult();
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
}
private string CreateKey(ControllerContext filterContext)
{
var cacheKey = new StringBuilder();
cacheKey.Append(Configuration.Current.AssemblyVersion + "_");
if (this.VaryByUser)
cacheKey.Append(this.CurrentUser.PersonID.GetValueOrDefault(0) + "_");
cacheKey.Append(filterContext.Controller.GetType().FullName + "_");
if (filterContext.RouteData.Values.ContainsKey("action"))
{
cacheKey.Append(filterContext.RouteData.Values["action"].ToString() + "_");
}
foreach (var param in filterContext.RouteData.Values)
{
cacheKey.Append((param.Key ?? "") + "-" +
(param.Value == null ? "null" : param.Value.ToString()) + "_");
}
return cacheKey.ToString();
}
}
}
Alright, hope that helps – there’s nothing like HTML caching to make you feel like the best website builder in the world!