最近給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:
public class ActionOutputCacheAttribute : ActionFilterAttribute
{
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
}
}
{
encoding;
StringWriterWithEncoding
public class StringWriterWithEncoding : StringWriter
{
Encoding encoding;
public StringWriterWithEncoding(Encoding encoding)
{
this.encoding = encoding;
}
public override Encoding Encoding
{
get { return encoding; }
}
}