zoukankan      html  css  js  c++  java
  • Orchard源码分析(7.1):Routing(路由)相关

    概述
    关于ASP.NET MVC中路由有两个基本核心作用,一是通过Http请求中的Url参数等信息获取路由数据(RouteData),路由数据包含了area、controller、action的名称等信息。只有获取了匹配的路由数据,才有可能转入ASP.NET MVC管道;二是根据由规则生成Url,比如要根据某些数据生成View上显示的链接。
     
    Orchard对路由进行扩展主要基于如下原因:
    (1)、路由定义在各个模块中。在Orchard应用程序初始化时将分散在各个模块的路由定义收集起来统一注册。
    (2)、路由定义一次,对于多Shell系统,则会被多次注册以匹配Shell的前缀。
    (3)、当请求进入时需要确认进入了哪个Shell,并且在成Url时也需要加上Shell的Url前缀。
    (4)、将WorkContextAccessor放入路由数据的DataTokens中。WorkContextAccessor工作上下文访问器封装了HTTP上下文、Autofa容器等信息。
    (5)、重置IRouteHandler和IHttpHandler,以包含WorkContextAccessor、包含Shell的配置(ShellSettings)、包含应用程序域中正在运行的Shell(RunningShellTable)、设置SessionState等。
     
    请留意下文描述中System.Web.Routing.RouteBase、Route、RouteData、Orchard.Mvc.Routes.ShellRoute、Orchard.Mvc.Routes.RouteDescriptor及Orchard.Mvc.Routes.HttpRouteDescriptor之间的关系。
     
    一、路由的定义
    如果Orchard模块需要路由,并不是在Global.asax.cs等地方直接配置,而是先将路由定义在模块源码一个或多个实现了Orchard.Mvc.Routes.IRouteProvide.IRouteProvider接口或Orchard.WebApi.Routes.IHttpRouteProvider的类的IEnumerable<RouteDescriptor> GetRoutes()方法中。
    如Orchard.Blogs模块就定义了一个名为Routes的类,该类就实现了IRouteProvider接口,主要关注GetRoutes方法:
            // 以下代码来在Orchard.Blogs.Routes类
            public IEnumerable<RouteDescriptor> GetRoutes() {
                return new [] {
                                 new RouteDescriptor {
                                                         Route = new Route (
                                                             "Admin/Blogs/Create",
                                                             new RouteValueDictionary {
                                                                                          {"area", "Orchard.Blogs" },
                                                                                          {"controller" , "BlogAdmin"},
                                                                                          {"action", "Create" }
                                                                                      },
                                                             new RouteValueDictionary (),
                                                             new RouteValueDictionary {
                                                                                          {"area", "Orchard.Blogs" }
                                                                                      },
                                                             new MvcRouteHandler ())
                                                     },
            //......
    GetRoutes方法返回一个路由描述RouteDescriptor对象集合。
    RouteDescriptor类包装了一个RouteBase类,并有Name和Priority属性:
        public class RouteDescriptor {
            public string Name { get; set; }
            public int Priority { get; set; }
            public RouteBase Route { get; set; }
            public SessionStateBehavior SessionState { get; set; }
        }
    一般在定义路由时用到的是Route类,它继承了RouteBase类。
    通过Priority属性,我们可以更好的控制路由的注册顺序,而不是按定义的先后顺序进行注册。
    在路由注册时,通过一系列的RouteDescriptor对象就够获取到对应的RouteBase对象了。
     
    IHttpRouteProvider接口的实现类的作用类似,只是专为WebApi服务而已。有兴趣的可以看看Orchard.Mvc.Routes.StandardExtensionRouteProvider类,顺便也留意一下HttpRouteDescriptor:RouteDescriptor类。
     
    二、路由的注册
    在Shell被激活时,会将分散到不同的模块的路由收集起来,并由RoutePublisher注册到全局路由表中:
            // 以下代码来在Orchard.Environment.DefaultOrchardShell类
           public void Activate() {
                var allRoutes = new List< RouteDescriptor>();
                allRoutes.AddRange(_routeProviders.SelectMany(provider => provider.GetRoutes()));
                allRoutes.AddRange(_httpRouteProviders.SelectMany(provider => provider.GetRoutes()));
     
                _routePublisher.Publish(allRoutes);
                _modelBinderPublisher.Publish(_modelBinderProviders.SelectMany(provider => provider.GetModelBinders()));
     
                using (var events = _eventsFactory()) {
                    events.Value.Activated();
                }
     
                _sweepGenerator.Activate();
            }
     
    _routeProviders是一个IEnumerable<IRouteProvider>型的私有字段,Autofac在创建DefaultOrchardShell对象时会通过构造器注入的方式初始化该字段。实际上就是相应Shell需要用到的各个模块中的的IRouteProvider对象,通过调用IRouteProvider.GetRoutes方法则可将RouteDescriptor对象收集起来。 
     
    _httpRouteProviders是一个IEnumerable<IHttpRouteProvider>型私有字段,实际上IHttpRouteProvider接口IRouteProvider接口完全一样。_httpRouteProviders和_routeProviders的初始化方式也一样。不同的是_httpRouteProviders是为WebApi服务的。
     
    _routePublisher是一个Orchard.Mvc.Routes.RoutePublisher对象,其Publish方法中,将RouteDescriptor对象对应的RouteBase(一般为Route)对象,包装成ShellRoute对象注册到MVC的全局路由表中:
            // 以下代码来在Orchard.Mvc.Routes.RoutePublisher类
             public void Publish(IEnumerable <RouteDescriptor> routes) {
                var routesArray = routes
                    .OrderByDescending(r => r.Priority)
                    .ToArray();
     
                // this is not called often, but is intended to surface problems before
                // the actual collection is modified
                var preloading = new RouteCollection();
                foreach (var routeDescriptor in routesArray) {
     
                    // extract the WebApi route implementation
                    var httpRouteDescriptor = routeDescriptor as HttpRouteDescriptor;
                    if (httpRouteDescriptor != null ) {
                        var httpRouteCollection = new RouteCollection();
                        httpRouteCollection.MapHttpRoute(httpRouteDescriptor.Name, httpRouteDescriptor.RouteTemplate, httpRouteDescriptor.Defaults);
                        routeDescriptor.Route = httpRouteCollection.First();
                    }
     
                    preloading.Add(routeDescriptor.Name, routeDescriptor.Route);
                }
                   
     
                using (_routeCollection.GetWriteLock()) {
                    // existing routes are removed while the collection is briefly inaccessable
                    var cropArray = _routeCollection
                        .OfType< ShellRoute>()
                        .Where(sr => sr.ShellSettingsName == _shellSettings.Name)
                        .ToArray();
     
                    foreach(var crop in cropArray) {
                        _routeCollection.Remove(crop);
                    }
     
                    // new routes are added
                    foreach (var routeDescriptor in routesArray) {
                        // Loading session state information.
                        var defaultSessionState = SessionStateBehavior .Default;
     
                        ExtensionDescriptor extensionDescriptor = null ;
                        if(routeDescriptor.Route is Route) {
                            object extensionId;
                            var route = routeDescriptor.Route as Route;
                            if(route.DataTokens != null && route.DataTokens.TryGetValue("area", out extensionId) ||
                               route.Defaults != null && route.Defaults.TryGetValue("area", out extensionId)) {
                                extensionDescriptor = _extensionManager.GetExtension(extensionId.ToString());
                            }
                        }
                        else if (routeDescriptor.Route is IRouteWithArea) {
                            var route = routeDescriptor.Route as IRouteWithArea;
                            extensionDescriptor = _extensionManager.GetExtension(route.Area);
                        }
     
                        if (extensionDescriptor != null ) {
                            // if session state is not define explicitly, use the one define for the extension
                            if (routeDescriptor.SessionState == SessionStateBehavior.Default) {
                                Enum.TryParse(extensionDescriptor.SessionState, true /*ignoreCase*/, out defaultSessionState);
                            }
                        }
     
                        // Route-level setting overrides module-level setting (from manifest).
                        var sessionStateBehavior = routeDescriptor.SessionState == SessionStateBehavior.Default ? defaultSessionState : routeDescriptor.SessionState ;
     
                        var shellRoute = new ShellRoute(routeDescriptor.Route, _shellSettings, _workContextAccessor, _runningShellTable) {
                            IsHttpRoute = routeDescriptor is HttpRouteDescriptor ,
                            SessionState = sessionStateBehavior
                        };
                        _routeCollection.Add(routeDescriptor.Name, shellRoute);
                    }
                }
            }
     
    ShellRoute类通过装饰器模式包装了一个System.Web.Routing.RouteBase类,其本身也是继承自RouteBase类。
    要特别留意创建ShellRoute对象时为构造函数提供的几个参数:
    routeDescriptor.Route:ShellRoute所包含的Route。
    _shellSettings:ShellRoute对应的ShellSettings。
    _workContextAccessor:WorkContextAccessor是Shell级的单例,其在WorkContextModule中被注册。它包装了一个Shell相关的Autofac子容器,通过该容器可以Resolve出Shell作用域的对象。
    _runningShellTable:正在运行的Shell对应的ShellSettings表。
     
    三、路由映射——根据请求路径查找匹配的路由数据(RouteData)
     
    从Url角度上讲,怎么区分两个Shell呢?首先两个Shell可以拥有不同的域名,或者拥有相同的域名但不同的Url前缀。如:
    (1)、其中一个Shell无域名
    Shell 1 - 无
    Shell 2 - www.yourdomain2.com
    (2)、不同的域名
    Shell 1 - www.yourdomain1.com
    Shell 2 - www.yourdomain2.com
    (3)、相同的域名,不同的Url前缀
    Shell 1 - www.yourdomain1.com/abc
    Shell 2 - www.yourdomain1.com/def
    (4)、相同的域名,只有一个Shell的Url前缀
    Shell 1 - www.yourdomain1.com
    Shell 2 - www.yourdomain1.com/def
    这种情况会先检查Url是否匹配Shell 2,然后再检查是否匹配Shell 1。Url前缀长度越长,越优先检查。
    引申:
    Shell 1 - www.yourdomain1.com/abc/def
    Shell 2 - www.yourdomain1.com/abc
    (5)、一个Shell可以对应单个或多个域名
    Shell 1 - www.yourdomain1.com
    Shell 2 - www.yourdomain2.com和 www.yourdomain3.com
    (6)、更复杂的配置
     
    为了方便分析,这里我们假设Orchard中配置了两个Shell,ShellSettings设置如下:
    Shell 1:ShellSettings.RequestUrlHost ="www.yourdomain1.com",ShellSettings.RequestUrlPrefix=String.Empty
    Shell 2:ShellSettings.RequestUrlHost ="www.yourdomain2.com",ShellSettings.RequestUrlPrefix="abc"
    并且某模块被这两个Shell使用,该模块的Routes:IRouteProvider类中定义了一个匹配"{controller}/{action}"的路由。需要注意一点,虽然这里只定义一个路由,但是这里两个Shell都会用到,所以会被包装成两个ShellRoute对象注册到全局路由表中。
    再假设一个新的Http请求进入,Url是:http://www.yourdomain2.com/abc/home/index
     
    首先System.Web.Routing.UrlRouteModule会遍历全局路由表中的路由,期待获取一个RouteData对象。当遍历到我们刚刚注册的路由时,会调用路由的GetRouteData方法:
            // 以下代码来在Orchard.Mvc.Routes.ShellRoute类
            public override RouteData GetRouteData( HttpContextBase httpContext) {
                // locate appropriate shell settings for request
                var settings = _runningShellTable.Match(httpContext);
     
                // only proceed if there was a match, and it was for this client
                if (settings == null || settings.Name != _shellSettings.Name)
                    return null ;
     
                var effectiveHttpContext = httpContext;
                if (_urlPrefix != null )
                    effectiveHttpContext = new UrlPrefixAdjustedHttpContext (httpContext, _urlPrefix);
     
                var routeData = _route.GetRouteData(effectiveHttpContext);
                if (routeData == null )
                    return null ;
     
                // push provided session state behavior to underlying MvcHandler
                effectiveHttpContext.SetSessionStateBehavior(SessionState);
     
                // otherwise wrap handler and return it
                routeData.RouteHandler = new RouteHandler (_workContextAccessor, routeData.RouteHandler, SessionState);
                routeData.DataTokens[ "IWorkContextAccessor"] = _workContextAccessor;
     
                if (IsHttpRoute) {
                    routeData.Values[ "IWorkContextAccessor"] = _workContextAccessor; // for WebApi
                }
               
                return routeData;
            }
     
    Shell被成功激活后,其对应的ShellSettings会存入在一个RunningShellTable对象中。在这里也就是_runningShellTable变量。
    根据传入的Url,找到匹配的ShellSettings存入局部变量_settings:
                var settings = _runningShellTable.Match(httpContext);
    下面看看Match的过程:
            /// 该方法位于Orchard.Environment.RunningShellTable类中
           public ShellSettings Match(string host, string appRelativePath) {
                var hostLength = host.IndexOf(':' );
                if (hostLength != -1)
                    host = host.Substring(0, hostLength);
     
                var mostQualifiedMatch = _shellsByHost
                    .Where(group => host.EndsWith(group.Key, StringComparison.OrdinalIgnoreCase))
                    .SelectMany(group => group
                        .OrderByDescending(settings => (settings.RequestUrlPrefix ?? string.Empty).Length))
                        .FirstOrDefault(settings => settings.State.CurrentState != TenantState.State .Disabled && appRelativePath.StartsWith("~/" + (settings.RequestUrlPrefix ?? string.Empty), StringComparison.OrdinalIgnoreCase));
     
                return mostQualifiedMatch ?? _fallback;
            }
      
    所以http://www.yourdomain2.com/abc/home/index匹配到的Shell为Shell 2。
     
    GetRouteData方法接下来有个判断:
                if (settings == null || settings.Name != _shellSettings.Name)
                    return null ;
    settings可能为null这好理解,但其Name值为什么可能不相等呢?请留意RunningShellTable.Match方法的最后一行的_fallback变量,这里就不再详述。
    如果Shell包含Url前缀,则调整HttpContext:
                var effectiveHttpContext = httpContext;
                if (_urlPrefix != null )
                    effectiveHttpContext = new UrlPrefixAdjustedHttpContext (httpContext, _urlPrefix);
     
    _urlPrefix是一个Orchard.Mvc.Routes.UrlPrefix对象,它包装了一个用来表示Shell的Url前缀字符串。如果RoutePublisher在创建ShellRoute时,传入的_shellSettings参数的RequestUrlPrefix属性不为null或空,则_urlPrefix不会为null。UrlPrefix类有两个重要的方法:RemoveLeadingSegmentsPrependLeadingSegments。如果_urlPrefix包装的Url前缀字符串为"abc",则_urlPrefix.RemoveLeadingSegments("~/abc/home/index")返回的值是"~/abc/home/index",而_urlPrefix.PrependLeadingSegments("~/home/index")返回的值是"~/abc/home/index"。
    UrlPrefixAdjustedHttpContext类最主要的目的是替换掉原来的HttpRequest,以使得HttpRequest的AppRelativeCurrentExecutionFilePath属性能够返回一个去掉Url前缀的值。这样做得目的是为了能够按"常规"方式获取到RouteData。
    ShellRoute的RequestUrlPrefix属性值为"abc",请求的Url是:
    http://www.yourdomain2.com/abc/home/index
    AppRelativeCurrentExecutionFilePath返回的值是:
    ~/home/index
     
    _route.GetRouteData方法的调用,也就是刚才说的"常规"方式:
                var routeData = _route.GetRouteData(effectiveHttpContext);
                if (routeData == null )
                    return null ;
     
    GetRouteData最后的代码也简单:
                 // push provided session state behavior to underlying MvcHandler
                effectiveHttpContext.SetSessionStateBehavior(SessionState);
     
                // otherwise wrap handler and return it
                routeData.RouteHandler = new RouteHandler (_workContextAccessor, routeData.RouteHandler, SessionState);
                routeData.DataTokens[ "IWorkContextAccessor"] = _workContextAccessor;
     
                if (IsHttpRoute) {
                    routeData.Values[ "IWorkContextAccessor"] = _workContextAccessor; // for WebApi
                }
     
    这里的RouteHandler类是ShellRoute的私有嵌套类,其通过装饰器模式包装了一个IRouteHandler对象。相关类型还有私有嵌套类HttpHandler和HttpAsyncHandler。RouteHandler是为了Autofac容器的应用到IHttpHandler中。
    在上面提到的Orchard.Blogs.Routes类中,定义的Route的RouteHandler是MvcRouteHandler,这里重新包装成RouteHandler对象再赋给routeData的RouteHandler属性。
    后面再将_workContextAccessor保存进routeData的DataTokens中。
     
    四、根据路由规则生成Url
             public override VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values) {
                // locate appropriate shell settings for request
                var settings = _runningShellTable.Match(requestContext.HttpContext);
     
                // only proceed if there was a match, and it was for this client
                if (settings == null || settings.Name != _shellSettings.Name)
                    return null ;
     
                var effectiveRequestContext = requestContext;
                if (_urlPrefix != null )
                    effectiveRequestContext = new RequestContext (new UrlPrefixAdjustedHttpContext (requestContext.HttpContext, _urlPrefix), requestContext.RouteData);
     
                var virtualPath = _route.GetVirtualPath(effectiveRequestContext, values);
                if (virtualPath == null )
                    return null ;
     
                if (_urlPrefix != null )
                    virtualPath.VirtualPath = _urlPrefix.PrependLeadingSegments(virtualPath.VirtualPath);
     
                return virtualPath;
            }
      
    前面几行代码和GetRouteData类似,关注点在UrlPrefixAdjustedHttpContext类和UrlPrefix类,在分析GetRouteData方法时已有简单分析。
     
     
    相关类型:
    Orchard.Mvc.Routes.ShellRoute : RouteBase, IRouteWithArea
    Orchard.Mvc.Routes.RouteDescriptor
    Orchard.Mvc.Routes.HttpRouteDescriptor
    Orchard.Mvc.Routes.IRouteProvider : IDependency
    Orchard.Mvc.Routes.IHttpRouteProvider : IDependency
    Orchard.Mvc.Routes.DefaultRouteProvider:IRouteProvider 
    Orchard.Mvc.Routes.StandardExtensionRouteProvider:IRouteProvider 
    Orchard.Mvc.Routes.RoutePublisher : IRoutePublisher
    Orchard.Mvc.Routes.UrlPrefix
    Orchard.Mvc.Routes.UrlPrefixAdjustedHttpContext
    Orchard.Environment.RunningShellTable : IRunningShellTable
    Orchard.Environment.WorkContextAccessor : IWorkContextAccessor
    Orchard.WorkContext
  • 相关阅读:
    pat00-自测5. Shuffling Machine (20)
    Spiral Matrix
    Search in Rotated Sorted Array II
    Search in Rotated Sorted Array
    Best Time to Buy and Sell Stock II
    4Sum
    3Sum Closest
    3Sum
    MySQL存储过程、函数和游标
    Word Ladder
  • 原文地址:https://www.cnblogs.com/alby/p/2953844.html
Copyright © 2011-2022 走看看