• 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。

  • 相关阅读:
    UI涂鸦板设计代码
    UI简单计算器设计代码
    用户需求、己、竞争对手的关系
    总结一下,以软件开发生命周期来说明不同的测试的使用情况
    谈软件工程和计算机科学的区别
    有人认为,”中文编程“是解决中国程序员编程效率的秘密武器,请问它是一个“银弹”吗?
    安装Eclipse SVN插件
    UI中横屏竖屏切换的一些方法(转)
    Object-C总结
    js备忘录
  • 原文地址:https://www.cnblogs.com/majiang/p/2784881.html
Copyright © 2020-2023  润新知