最近给SpaceBuilder增加OutputCache 时发现了一些问题,贴在这做个备忘,也方便遇到类似问题的朋友查阅。
目前SpaceBuilder表现层使用是asp.net mvc v1.0,使用了很多RenderAction(关于asp.net mvc的Partial Requests参见Partial Requests in ASP.NET MVC )。希望对于实时性要求不高的内容区域采用客户端缓存来提升性能同时也弥补一下RenderAction对性能的损失。
使用asp.net mvc自带的OutputCache Filter时发现了一个可怕的bug,在View中任何一个RenderAction设置OutputCache却影响了整个View。搜索发现确实是asp.net mvc目前已知的一个bug ,关于该问题的解决也有很多人提出了自己的方法。
关于asp.net mvc的缓存,Haacked写了两篇文章:
Donut Caching in ASP.NET MVC 介绍的是缓存整个页面,对于一部分内容禁用缓存,是在mvc中实现的WebForm的Substitution功能。存在以下弊端:当前一个View中有多个区域需要禁用缓存时使用比较麻烦,另外不能实现对页面的不同的区域使用不同的过期策略。
Donut Hole Caching in ASP.NET MVC 介 绍的是我想要的功能,即只缓存页面的部分区域。但是弊端也非常明显:只能通过WebForm中的声明方式来使用用户控件(:),现在已经有点不适应这种方 式了,而且必须使用WebFormViewEngine),无法直接使用RenderPartial,而且还必须设置强类型的ViewPage,确保在用 户控件中的Model与View中的Model相同。使用太麻烦,限制也多。
Maarten Balliauw在 Creating an ASP.NET MVC OutputCache ActionFilterAttribute 和Extending ASP.NET MVC OutputCache ActionFilterAttribute - Adding substitution 也提出了一个完整的OutputCache解决方案。但是经测试启用客户端缓存时同样会产生与RenderAction同样的问题,还没有时间彻查这个问题,先把客户端缓存禁用,暂时使用服务器端缓存应付一阵。
以Maarten Balliauw的代码为原型,编写了SpaceBuilder的ActionOutputCacheAttribute:
{
private static MethodInfo _switchWriterMethod = typeof (HttpResponse).GetMethod( " SwitchWriter " , BindingFlags.Instance | BindingFlags.NonPublic);
public ActionOutputCacheAttribute( int cacheDuration)
{
_cacheDuration = cacheDuration;
}
// 目前还不能设置为Client缓存,会与OutputCache同样的问题
private CachePolicy _cachePolicy = CachePolicy.Server;
private int _cacheDuration;
private TextWriter _originalWriter;
private string _cacheKey;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Server-side caching?
if (_cachePolicy == CachePolicy.Server || _cachePolicy == CachePolicy.ClientAndServer)
{
_cacheKey = GenerateCacheKey(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)
{
// Server-side caching?
if (_cachePolicy == CachePolicy.Server || _cachePolicy == CachePolicy.ClientAndServer)
{
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), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null );
}
}
}
private string GenerateCacheKey(ActionExecutingContext filterContext)
{
StringBuilder cacheKey = new StringBuilder( " OutputCacheKey: " );
// Controller + action
cacheKey.Append(filterContext.Controller.GetType().FullName.GetHashCode());
if (filterContext.RouteData.Values.ContainsKey( " action " ))
{
cacheKey.Append( " _ " );
cacheKey.Append(filterContext.RouteData.Values[ " action " ].ToString());
}
foreach (KeyValuePair < string , object > pair in filterContext.ActionParameters)
{
cacheKey.Append( " _ " );
cacheKey.Append(pair.Key);
cacheKey.Append( " = " );
if (pair.Value != null )
cacheKey.Append(pair.Value.ToString());
else
cacheKey.Append( string .Empty);
}
return cacheKey.ToString();
}
private class CacheContainer
{
public string Output;
public string ContentType;
public CacheContainer( string data, string contentType)
{
Output = data;
ContentType = contentType;
}
}
public enum CachePolicy
{
NoCache = 0 ,
Client = 1 ,
Server = 2 ,
ClientAndServer = 3
}
}