Partial Output Caching in ASP.NET MVC updated

Thanks to Miha Valenčič I’ve found this great article about Partial Output Caching in ASP.NET MVC. It actually explains how to do ActionResult caching in ASP.NET MVC. It was exactly what I was looking for. I had the same idea as in this blog post but didn’t know about SwitchWriter method nor I was aware of ActionFilterAttribute. IOW I had the idea but I didn’t know how to implement it. Which the mentioned blog post does.

The encoding

Problem

While this approach works I immediately stumbled on an encoding issue. The cached output is always encoded with UTF-16 encoding which might not be always a good thing. In fact IE7 protested immediately that one can’t switch between encodings (my original encoding is UTF-8) just like that. Hm.

So I went looking who is the culprit and soon found out that it is the temporary StringWriter created in the method below:

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


See, StringWriter uses UTF-16 and won’t allow any other encoding to be set. Easily that is.

Solution

Luckily the solution is an easy one. It involves a StringWriter derived class that accepts any encoding, such as this one (from Jon Skeet’s message):

public class StringWriterWithEncoding : StringWriter
{
Encoding encoding;
public StringWriterWithEncoding(Encoding encoding)
{
this.encoding = encoding;
}
public override Encoding Encoding
{
get { return encoding; }
}
}

This new class should be used in creating that temporary writer, like this:

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

And that’s it. New version of temporary writer will automatically use whatever encoding is set by HttpResponse.ContentEncoding.

The ContentType

Problem

The other problem involves ContentType not being cached. In my case I am testing with SyndicationFeed and ContentType has to be “application/rss+xml”. However it is ignored by the original caching mechanism where only response content is cached but not ContentType.

Solution

I’ll declare a new class that will store both content and content type.

class CacheContainer
{
public string Output;
public string ContentType;
public CacheContainer(string data, string contentType)
{
Output = data;
ContentType = contentType;
}
}

I’ll use this class to store cached content, like this:

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);
}
}

See, I am caching both Response.ContentType and its content now.

The last step is to use the cached ContentType, like this:

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 });
}
}

There you go.

ActionOutputCacheAttribute.cs (3.51 kb)

Comments (1) -

  • hazzik

    4/10/2009 11:33:08 AM | Reply

    Hi, very usefulSmile thnx.

    i wrote extension method, wich encapsulates you hack with _switchMethod:

    public static class PublicSwitchWriterOnHttpResponceExtension {
      public static readonly MethodInfo _switchWriterMethod = typeof(HttpResponse).GetMethod("SwitchWriter", BindingFlags.Instance | BindingFlags.NonPublic);

      public static TextWriter SwitchWriter(this HttpResponse response, TextWriter writer) {
        return (TextWriter)_switchWriterMethod.Invoke(response, new object[] { writer });
      }
    }


Loading