Skip to main content

Partial Output Caching in ASP.NET MVC.

// Partial Output Caching in ASP.NET MVC
//
// Data caching continues to work perfectly well in ASP.NET MVC, because it's
// just about getting objects in and out of a collection, and isn't specific
// to any particular UI technology.
//
// Drop the class (`ActionOutputCacheAttribute`) somewhere in
// your MVC project.
//
// ---------------
//  EXAMPLE USAGE
// ---------------
//
// public class BlogController : Controller
// {
//     [ActionOutputCache(60)] // Caches for 60 seconds
//     public ActionResult LatestPosts()
//     {
//         ViewData["currentTime"] = DateTime.Now;
//         ViewData["posts"] = new[] {
//             "Here's a post",
//             "Here's another post. Marvellous.",
//             "Programmer escapes from custody"
//         };
//         return View();
//     }
// }
//
// Original: http://blog.stevensanderson.com/2008/10/15/partial-output-caching-in-aspnet-mvc/
// Updated: http://blog.rthand.com/post/2009/03/21/Partial-Output-Caching-in-ASPNET-MVC-updated.aspx
//  - Fixes output content encoding issues.
//

public class ActionOutputCacheAttribute : ActionFilterAttribute
{
    // This hack is optional; I'll explain it later in the blog post
    private static MethodInfo _switchWriterMethod = typeof(HttpResponse).GetMethod("SwitchWriter", BindingFlags.Instance | BindingFlags.NonPublic);

    public ActionOutputCacheAttribute(int cacheDuration)
    {
        _cacheDuration = cacheDuration;
    }

    private int _cacheDuration;
    private TextWriter _originalWriter;
    private string _cacheKey;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        _cacheKey = ComputeCacheKey(filterContext);
        CacheContainer cachedOutput = (CacheContainer)filterContext.HttpContext.Cache[_cacheKey];
        if (cachedOutput != null)
        {
            filterContext.HttpContext.Response.ContentType = cachedOutput.ContentType;
            filterContext.Result = new ContentResult { Content = cachedOutput.Output };
        }
        else
        {
            StringWriter stringWriter = new StringWriterWithEncoding(filterContext.HttpContext.Response.ContentEncoding);
            HtmlTextWriter newWriter = new HtmlTextWriter(stringWriter);
            _originalWriter = (TextWriter)_switchWriterMethod.Invoke(HttpContext.Current.Response, new object[] { newWriter });
        }
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        if (_originalWriter != null) // Must complete the caching
        {
            HtmlTextWriter cacheWriter = (HtmlTextWriter)_switchWriterMethod.Invoke(HttpContext.Current.Response, new object[] { _originalWriter });
            string textWritten = ((StringWriter)cacheWriter.InnerWriter).ToString();
            filterContext.HttpContext.Response.Write(textWritten);
            CacheContainer container = new CacheContainer(textWritten, filterContext.HttpContext.Response.ContentType);
            filterContext.HttpContext.Cache.Add(_cacheKey, container, null, DateTime.Now.AddSeconds(_cacheDuration), Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
        }
    }

    private string ComputeCacheKey(ActionExecutingContext filterContext)
    {
        var keyBuilder = new StringBuilder();
        foreach (var pair in filterContext.RouteData.Values)
            keyBuilder.AppendFormat("rd{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
        foreach (var pair in filterContext.ActionParameters)
            keyBuilder.AppendFormat("ap{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
        return keyBuilder.ToString();
    }
}

class CacheContainer
{
    public string Output;
    public string ContentType;

    /// <summary>
    /// Initializes a new instance of the CacheContainer class.
    /// </summary>
    /// <param name="data"></param>
    /// <param name="contentType"></param>
    public CacheContainer(string data, string contentType)
    {
        Output = data;
        ContentType = contentType;
    }
}