首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> 關於ASP.NET

ASP.NET MVC Partial OutputCache 在 SpaceBuilder 實戰:解決 RenderAction 快取污染問題

     編輯:關於ASP.NET

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。

社群方案比較

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() { ... }

關鍵技術點

  1. 反射呼叫 SwitchWriter:非公開 API,切換 HttpResponse 內部輸出寫入器,攔截子 Action 輸出。
  2. 快取鍵包含 Controller/Action/參數:確保不同參數產生獨立快取。
  3. 僅啟用伺服器端快取:CachePolicy.Server,避免瀏覽器端快取觸發原 Bug。
  4. ContentType 保存與還原:確保回應標頭正確。

遷移建議

  • MVC 3+ 內建 [ChildActionOnly] + [OutputCache] 正確支援片段快取,升級即可移除自訂屬性。
  • 現代架構建議:ViewComponent (ASP.NET Core) / 片段快取中介軟體 / Redis 輸出快取。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方案核心思想優點缺點
Donut Caching (Haacked)快取整頁,用 Substitution 排除動態區塊官方支援多排除區塊麻煩、無法區塊差異過期
Donut Hole Caching (Haacked)用戶控件 (.ascx) + OutputCache 指令真片段快取需 WebFormViewEngine、強型別限制、無法直接用 RenderPartial
Maarten Balliauw 自訂 ActionFilter攔截 Action 執行,手動切換 Response.Filter 快取輸出字串支援 RenderAction、靈活過期策略Client Cache (瀏覽器端) 會觸發同樣 Bug