SpaceBuilder 專案 (ASP.NET MVC 1.0) 大量使用 Html.RenderAction 組合頁面,引入 OutputCache 提升效能時發現嚴重 Bug:任一區塊設定快取會導致整個頁面被快取。本文記錄排查過程、社群方案比較,並提供自訂 ActionOutputCacheAttribute 實作 (基於 Maarten Balliauw 方案修改) 實現片段快取。
問題現象
<% Html.RenderAction("HotTopics", "Forum"); %> <%-- 此處加 [OutputCache(60)] --%>
<% Html.RenderAction("LatestNews", "News"); %> <%-- 未加快取 --%>
結果:整個頁面被快取 60 秒,LatestNews 也跟著靜態化,違反片段快取預期。
根因分析
ASP.NET MVC 1.0/2.0 已知 Bug:OutputCacheAttribute 作用於 Action 時,實際快取的是 **父 View 的完整回應**,而非子 Action 輸出。RenderAction 會切換 HttpResponse.Filter,但快取模組在 OnResultExecuted 攔截的是最外層 Response。
社群方案比較
| 方案 | 核心思想 | 優點 | 缺點 |
| Donut Caching (Haacked) | 快取整頁,用 Substitution 排除動態區塊 | 官方支援 | 多排除區塊麻煩、無法區塊差異過期 |
| Donut Hole Caching (Haacked) | 用戶控件 (.ascx) + OutputCache 指令 | 真片段快取 | 需 WebFormViewEngine、強型別限制、無法直接用 RenderPartial |
| Maarten Balliauw 自訂 ActionFilter | 攔截 Action 執行,手動切換 Response.Filter 快取輸出字串 | 支援 RenderAction、靈活過期策略 | Client Cache (瀏覽器端) 會觸發同樣 Bug |
SpaceBuilder 採用方案:ActionOutputCacheAttribute (僅 Server Cache)
public class ActionOutputCacheAttribute : ActionFilterAttribute
{
private static readonly MethodInfo _switchWriterMethod =
typeof(HttpResponse).GetMethod("SwitchWriter", BindingFlags.Instance | BindingFlags.NonPublic);
private readonly int _cacheDuration;
private TextWriter _originalWriter;
private string _cacheKey;
public ActionOutputCacheAttribute(int cacheDuration) { _cacheDuration = cacheDuration; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
_cacheKey = GenerateCacheKey(filterContext);
var cached = filterContext.HttpContext.Cache[_cacheKey] as CacheContainer;
if (cached != null)
{
filterContext.HttpContext.Response.ContentType = cached.ContentType;
filterContext.Result = new ContentResult { Content = cached.Output };
return;
}
// 切換輸出流至 StringWriter 攔截輸出
var stringWriter = new StringWriterWithEncoding(filterContext.HttpContext.Response.ContentEncoding);
var htmlWriter = new HtmlTextWriter(stringWriter);
_originalWriter = (TextWriter)_switchWriterMethod.Invoke(HttpContext.Current.Response, new object[] { htmlWriter });
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (_originalWriter == null) return; // 已命中快取,無需處理
// 還原原始輸出流
var cacheWriter = (HtmlTextWriter)_switchWriterMethod.Invoke(HttpContext.Current.Response, new object[] { _originalWriter });
var output = ((StringWriter)cacheWriter.InnerWriter).ToString();
// 寫入原始回應並存入快取
filterContext.HttpContext.Response.Write(output);
filterContext.HttpContext.Cache.Add(_cacheKey, new CacheContainer(output, filterContext.HttpContext.Response.ContentType), null,
DateTime.Now.AddSeconds(_cacheDuration), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
}
private string GenerateCacheKey(ActionExecutingContext filterContext)
{
var sb = new StringBuilder("OutputCacheKey:");
sb.Append(filterContext.Controller.GetType().FullName.GetHashCode());
if (filterContext.RouteData.Values.ContainsKey("action"))
sb.Append("_").Append(filterContext.RouteData.Values["action"]);
foreach (var p in filterContext.ActionParameters)
{
sb.Append("_").Append(p.Key).Append("=").Append(p.Value?.ToString() ?? string.Empty);
}
return sb.ToString();
}
private class CacheContainer
{
public string Output { get; set; }
public string ContentType { get; set; }
public CacheContainer(string output, string contentType) { Output = output; ContentType = contentType; }
}
}
public class StringWriterWithEncoding : StringWriter
{
private readonly Encoding _encoding;
public StringWriterWithEncoding(Encoding encoding) { _encoding = encoding; }
public override Encoding Encoding => _encoding;
}
使用方式
// 僅支援 Server Cache,Client Cache 會觸發 Bug
[ActionOutputCache(60)] // 快取 60 秒
public ActionResult HotTopics() { ... }
關鍵技術點
- 反射呼叫 SwitchWriter:非公開 API,切換
HttpResponse 內部輸出寫入器,攔截子 Action 輸出。
- 快取鍵包含 Controller/Action/參數:確保不同參數產生獨立快取。
- 僅啟用伺服器端快取:
CachePolicy.Server,避免瀏覽器端快取觸發原 Bug。
- ContentType 保存與還原:確保回應標頭正確。
遷移建議
- MVC 3+ 內建
[ChildActionOnly] + [OutputCache] 正確支援片段快取,升級即可移除自訂屬性。
- 現代架構建議:ViewComponent (ASP.NET Core) / 片段快取中介軟體 / Redis 輸出快取。
- 上一篇文章: ASP.NET MVC 案例教程(一):環境建置與 Mock 模型準備
- 下一篇文章: ASP.NET MVC 驗證與授權實作:基於 ActionFilter 的角色權限控制方案
Copyright ©
程式碼之家 All Rights Reserved