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.Http RouteDescriptor之間的關系。
 
一、路由的定義
如果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類有兩個重要的方法:RemoveLeadingSegments PrependLeadingSegments。如果 _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


免責聲明!

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



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