asp.net mvc源碼分析-OutputCache


在mvc中有一個相對比較獨立的類OutputCacheAttribute,一看它的名字我們就知道應該與什么緩存有關了吧。

 public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter 在這個類中涉及到兩個重要的東西OutputCacheParameters緩存配置、ObjectCache緩存的管理方式,這兩個主要是用來干什么的我們后面再說吧。
OutputCacheAttribute繼承於ActionFilterAttribute特性,那么我們就來看看它那4個方法是怎么實現的吧:

        public override void OnActionExecuting(ActionExecutingContext filterContext) {
            if (filterContext == null) {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.IsChildAction) {
                ValidateChildActionConfiguration();

                // Already actively being captured? (i.e., cached child action inside of cached child action)
                // Realistically, this needs write substitution to do properly (including things like authentication)
                if (GetChildActionFilterFinishCallback(filterContext) != null) {
                    throw new InvalidOperationException(MvcResources.OutputCacheAttribute_CannotNestChildCache);
                }

                // Already cached?
                string uniqueId = GetChildActionUniqueId(filterContext);
                string cachedValue = ChildActionCacheInternal.Get(uniqueId) as string;
                if (cachedValue != null) {
                    filterContext.Result = new ContentResult() { Content = cachedValue };
                    return;
                }

                // Swap in a new TextWriter so we can capture the output
                StringWriter cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
                TextWriter originalWriter = filterContext.HttpContext.Response.Output;
                filterContext.HttpContext.Response.Output = cachingWriter;

                // Set a finish callback to clean up
                SetChildActionFilterFinishCallback(filterContext, wasException => {
                    // Restore original writer
                    filterContext.HttpContext.Response.Output = originalWriter;

                    // Grab output and write it
                    string capturedText = cachingWriter.ToString();
                    filterContext.HttpContext.Response.Write(capturedText);

                    // Only cache output if this wasn't an error
                    if (!wasException) {
                        ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration));
                    }
                });
            }
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext) {
            if (filterContext == null) {
                throw new ArgumentNullException("filterContext");
            }

            // Complete the request if the child action threw an exception
            if (filterContext.IsChildAction && filterContext.Exception != null) {
                CompleteChildAction(filterContext, wasException: true);
            }
        }

        public override void OnResultExecuting(ResultExecutingContext filterContext) {
            if (filterContext == null) {
                throw new ArgumentNullException("filterContext");
            }

            if (!filterContext.IsChildAction) {
                // we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
                using (OutputCachedPage page = new OutputCachedPage(_cacheSettings)) {
                    page.ProcessRequest(HttpContext.Current);
                }
            }

 從這4個方法我們可以知道一個普通的Action和一個子Action的處理方式是不同的。

首先我們來看看一個主Action的緩存處理方式:OnActionExecuting、OnActionExecuted、 OnResultExecuted都沒做什么處理,唯一處理的是OnResultExecuting方法,同時該方法對子Action也沒做什么處理。 OnResultExecuting的處理很簡單

 if (!filterContext.IsChildAction) {
                // we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
                using (OutputCachedPage page = new OutputCachedPage(_cacheSettings)) {
                    page.ProcessRequest(HttpContext.Current);
                }
            }

OutputCachedPage是一個普通的Page類

 private sealed class OutputCachedPage : Page {
            private OutputCacheParameters _cacheSettings;

            public OutputCachedPage(OutputCacheParameters cacheSettings) {
                // Tracing requires Page IDs to be unique.
                ID = Guid.NewGuid().ToString();
                _cacheSettings = cacheSettings;
            }

            protected override void FrameworkInitialize() {
                // when you put the <%@ OutputCache %> directive on a page, the generated code calls InitOutputCache() from here
                base.FrameworkInitialize();
                InitOutputCache(_cacheSettings);
            }
        }



        }

       public override void OnResultExecuted(ResultExecutedContext filterContext) {
            if (filterContext == null) {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.IsChildAction) {
                CompleteChildAction(filterContext, wasException: filterContext.Exception != null);
            }
        }

 

從這里可以看出來主Action是完成一次標准的Http請求,它的處理方式和傳統的asp.net的緩存處理方式是一樣的,由OutputCacheModule來處理所以OutputCacheAttribute中有一個OutputCacheParameters東東就是用在這個時候的

為什么我們的子Action要區別對待了?

    RouteData routeData = CreateRouteData(vpd.Route, routeValues, vpd.DataTokens, htmlHelper.ViewContext);
            HttpContextBase httpContext = htmlHelper.ViewContext.HttpContext;
            RequestContext requestContext = new RequestContext(httpContext, routeData);
            ChildActionMvcHandler handler = new ChildActionMvcHandler(requestContext);
            httpContext.Server.Execute(HttpHandlerUtil.WrapForServerExecute(handler), textWriter, true /* preserveForm */);

這里是調用子Action的關鍵代碼,從這里我們可以知道子Action的調用並沒有創建新的HttpContext,它還是沿用主Action的 HttpContext,也就是說它並不是一次完整、標准的http請求,這里只是主Action調用了一個handler而已,只是這個handler 把mvc該做的工作差不多都做了而已。

現在我們來看子Action的處理方式,在OnActionExecuting方法處理相對要多一點,首先調用ValidateChildActionConfiguration方法來驗證緩存配置

 private void ValidateChildActionConfiguration() {
            if (Duration <= 0) {
                throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidDuration);
            }

            if (String.IsNullOrWhiteSpace(VaryByParam)) {
                throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidVaryByParam);
            }

            if (!String.IsNullOrWhiteSpace(CacheProfile) ||
                !String.IsNullOrWhiteSpace(SqlDependency) ||
                !String.IsNullOrWhiteSpace(VaryByContentEncoding) ||
                !String.IsNullOrWhiteSpace(VaryByHeader) ||
                _locationWasSet || _noStoreWasSet) {

                throw new InvalidOperationException(MvcResources.OutputCacheAttribute_ChildAction_UnsupportedSetting);
            }
        }

  public OutputCacheLocation Location {
            get {
                return _cacheSettings.Location;
            }
            set {
                _cacheSettings.Location = value;
                _locationWasSet = true;
            }
        }

  public bool NoStore {
            get {
                return _cacheSettings.NoStore;
            }
            set {
                _cacheSettings.NoStore = value;
                _noStoreWasSet = true;
            }
        }

大家一定要注意這個檢查方法啊:Location、NoStore屬性石完全不能設置 的,CacheProfile 、SqlDependency 、VaryByContentEncoding、 VaryByHeader、 VaryByHeader這幾個值差不多也不能設置,要設置頁只能設置空字符那和不設置也沒什么區別因為默認就是null

接下來就是通過當前的ActionExecutingContext來獲取緩存key進而獲取緩存對象。

     string uniqueId = GetChildActionUniqueId(filterContext);
       string cachedValue = ChildActionCacheInternal.Get(uniqueId) as string;

ChildActionCacheInternal 相關代碼如下:

  private Func<ObjectCache> _childActionCacheThunk = () => ChildActionCache;
  internal OutputCacheAttribute(ObjectCache childActionCache) {
            _childActionCacheThunk = () => childActionCache;
        }
  public static ObjectCache ChildActionCache {
            get {
                return _childActionCache ?? MemoryCache.Default;
            }
            set {
                _childActionCache = value;
            }
        }
 private ObjectCache ChildActionCacheInternal {
            get {
                return _childActionCacheThunk();
            }
        }

 

我想現在大家應該對OutputCacheAttribute中的ObjectCache有個了解了吧,它就是用來緩存子Action的處理。一旦我們獲取到緩存結果我們就返回一個ContentResult給  filterContext.Result 屬性並退出該方法。

  if (cachedValue != null) {
                    filterContext.Result = new ContentResult() { Content = cachedValue };
                    return;
                }

從前面的文章我們知道Filter和Action的調用時在ControllerActionInvoker的InvokeActionMethodFilter方法,這個方法的主要邏輯如下:

 internal static ActionExecutedContext InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func<ActionExecutedContext> continuation) {
            filter.OnActionExecuting(preContext);
            if (preContext.Result != null) {
                return new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */) {
                    Result = preContext.Result
                };
            }


            bool wasError = false;
            ActionExecutedContext postContext = null;
            try {
                postContext = continuation();
            }
            catch (ThreadAbortException) {
              ..
            }
            catch (Exception ex) {
               ...
            }
            if (!wasError) {
                filter.OnActionExecuted(postContext);
            }
            return postContext;
        }

一旦ActionExecutingContext的Result有值我們就退出該方法,這里的退出就保證了Action的不執行

如果我們沒有找到緩存對象,那么我們就創建一個臨時StringWriter實例,讓它替換當前HttpContext.Response.Output實例,還需要把HttpContext.Response.Output保存起來,已備后面還原。

  StringWriter cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
   TextWriter originalWriter = filterContext.HttpContext.Response.Output;
     filterContext.HttpContext.Response.Output = cachingWriter; 后面調用SetChildActionFilterFinishCallback注冊一個回調方法,在當前HttpContext.Items中插 入暫存數據,於此方法相關的還有幾個方法GetChildActionFilterFinishCallback 、CompleteChildAction 、ClearChildActionFilterFinishCallback

  private static void SetChildActionFilterFinishCallback(ControllerContext controllerContext, Action<bool> callback) {
            controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] = callback;

        }

 private static Action<bool> GetChildActionFilterFinishCallback(ControllerContext controllerContext) {
            return controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] as Action<bool>;
        }

  private static void CompleteChildAction(ControllerContext filterContext, bool wasException) {
            Action<bool> callback = GetChildActionFilterFinishCallback(filterContext);

            if (callback != null) {
                ClearChildActionFilterFinishCallback(filterContext);
                callback(wasException);
            }
        }

   private static void ClearChildActionFilterFinishCallback(ControllerContext controllerContext) {
            controllerContext.HttpContext.Items.Remove(_childActionFilterFinishCallbackKey);
        }

 

現在OnActionExecuting方法結束了,OnActionExecuted方法也沒什么特殊的處理,主要就是看看有沒有異常出現,代碼如下:

  if (filterContext.IsChildAction && filterContext.Exception != null) {
                CompleteChildAction(filterContext, wasException: true);
            }

而OnResultExecuting方法也沒有什么處理,最后剩下的就是OnResultExecuted方法,

  if (filterContext.IsChildAction) {
                CompleteChildAction(filterContext, wasException: filterContext.Exception != null);
            }

說白了就是調用我們先前注冊的回調方法。方法的具體類容是:

  filterContext.HttpContext.Response.Output = originalWriter;

                    // Grab output and write it
                    string capturedText = cachingWriter.ToString();
                    filterContext.HttpContext.Response.Write(capturedText);

                    // Only cache output if this wasn't an error
                    if (!wasException) {
                        ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration));
                    }

這里的capturedText就是我們子Action所對應view的文本結果,originalWriter是主Action的 Response.Output對象,即是把子Action的返回結果寫到主Action的輸出流中。最后看看是否有錯誤發生,如果沒有就把此次緩存類容 放到緩存ObjectCache中。

在這里我補充一下,前面提到一個GetChildActionUniqueId是根據ActionExecutingContext來創建緩存key的,那么這個key與哪些東西相關了。

 internal string GetChildActionUniqueId(ActionExecutingContext filterContext) {
            StringBuilder uniqueIdBuilder = new StringBuilder();

            // Start with a prefix, presuming that we share the cache with other users
            uniqueIdBuilder.Append(_cacheKeyPrefix);

            // Unique ID of the action description
            uniqueIdBuilder.Append(filterContext.ActionDescriptor.UniqueId);

            // Unique ID from the VaryByCustom settings, if any
            uniqueIdBuilder.Append(DescriptorUtil.CreateUniqueId(VaryByCustom));
            if (!String.IsNullOrEmpty(VaryByCustom)) {
                string varyByCustomResult = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, VaryByCustom);
                uniqueIdBuilder.Append(varyByCustomResult);
            }

            // Unique ID from the VaryByParam settings, if any
            uniqueIdBuilder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(VaryByParam)));

            // The key is typically too long to be useful, so we use a cryptographic hash
            // as the actual key (better randomization and key distribution, so small vary
            // values will generate dramtically different keys).
            using (SHA256 sha = SHA256.Create()) {
                return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(uniqueIdBuilder.ToString())));
            }
        }

 從GetChildActionUniqueId的方法我們知道這個key與Action本生 有關,不同的Action其key不同,與緩存的VaryByCustom屬性有關,即使是同一個VaryByCustom取值不同其可以也不同,這里調 用了HttpApplication的GetVaryByCustomString方法

總結一下吧:mvc的緩存分類2部分 一部分是主Action的緩存,主Action是一個完整的http請,它是借助OutputCacheModule來緩存的,而子Action並不是一個完整的http請求它只是一個簡單的數據緩存,借助於ObjectCache。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM