Partial Output Caching in ASP.NET MVC updated

by Miha Markič 21. March 2009 13:10

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)

Tags:

.net 3.5 | asp.net mvc

Comments (1) -

hazzik
hazzik Russia
4/10/2009 12:33:08 PM #

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


Reply

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

Miha Markic

About me
Righthand
 
Microsoft MVP
 
Developer Express' DXSquad
INETA Country Leader for Slovenia
INETA Country Leader for Slovenia

Slovene Developer Users Group Lead
Friends of Red-Gate
LLBLGenPro Partner

Miha currently works as a free lance consultant and software developer specialized in .net area.
He graduated in Computer and information science at the University of Ljubljana, Slovenia. He has accumulated experience in various programming languages such as Java, Visual Basic 3-6 (MCP), Visual C++, Delphi, C# and VB.Net through years.
He has experience in practically all (technical) stages of project development, including planning, framework development, user interface, business processes, as well as testing and documenting. He has worked on big and small projects in Slovenia and abroad (e.g. participated in completing level 3 IS for the Nucor steel plant, Hertford, USA).
Currently he enjoys programming in .net environment using C#. Since 2000 he has been active in Developer Express' DX Squad and has been ECDL trainer and tester. He also gives lectures on conferences and other events in Slovenia.

Month List

Tag cloud

Most comments

Paulius Paulius
1 comments
us United States
Meh Meh
1 comments
us United States
bart dm bart dm
1 comments
nl Netherlands

RecentComments

Comment RSS