您的位置:首页 > 其它

Orchard源码分析(7.1):Routing(路由)相关

2013-03-11 13:13 363 查看
概述
关于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方法:
[align=left] // 以下代码来在Orchard.Blogs.Routes类[/align]
[align=left] public IEnumerable<RouteDescriptor> GetRoutes() {[/align]
[align=left] return new [] {[/align]
[align=left] new RouteDescriptor {[/align]
[align=left] Route = new Route ([/align]
[align=left] "Admin/Blogs/Create",[/align]
[align=left] new RouteValueDictionary {[/align]
[align=left] {"area", "Orchard.Blogs" },[/align]
[align=left] {"controller" , "BlogAdmin"},[/align]
[align=left] {"action", "Create" }[/align]
[align=left] },[/align]
[align=left] new RouteValueDictionary (),[/align]
[align=left] new RouteValueDictionary {[/align]
[align=left] {"area", "Orchard.Blogs" }[/align]
[align=left] },[/align]
[align=left] new MvcRouteHandler ())[/align]
[align=left] },[/align]

[align=left] //......[/align]

GetRoutes方法返回一个路由描述RouteDescriptor对象集合。
RouteDescriptor类包装了一个RouteBase类,并有Name和Priority属性:
[align=left] public class RouteDescriptor {[/align]
[align=left] public string Name { get; set; }[/align]
[align=left] public int Priority { get; set; }[/align]
[align=left] public RouteBase Route { get; set; }[/align]
[align=left] public SessionStateBehavior SessionState { get; set; }[/align]
[align=left] }[/align]
[align=left]一般在定义路由时用到的是Route类,它继承了RouteBase类。[/align]

通过Priority属性,我们可以更好的控制路由的注册顺序,而不是按定义的先后顺序进行注册。

[align=left]在路由注册时,通过一系列的RouteDescriptor对象就够获取到对应的RouteBase对象了。[/align]
[align=left] [/align]

IHttpRouteProvider接口的实现类的作用类似,只是专为WebApi服务而已。有兴趣的可以看看Orchard.Mvc.Routes.StandardExtensionRouteProvider类,顺便也留意一下HttpRouteDescriptor:RouteDescriptor类。

[align=left] [/align]
二、路由的注册
在Shell被激活时,会将分散到不同的模块的路由收集起来,并由RoutePublisher注册到全局路由表中:
[align=left] // 以下代码来在Orchard.Environment.DefaultOrchardShell类[/align]
[align=left] public void Activate() {[/align]
[align=left] var allRoutes = new List< RouteDescriptor>();[/align]
[align=left] allRoutes.AddRange(_routeProviders.SelectMany(provider => provider.GetRoutes()));[/align]
[align=left] allRoutes.AddRange(_httpRouteProviders.SelectMany(provider => provider.GetRoutes()));[/align]
[align=left][/align]
[align=left] _routePublisher.Publish(allRoutes);[/align]
[align=left] _modelBinderPublisher.Publish(_modelBinderProviders.SelectMany(provider => provider.GetModelBinders()));[/align]
[align=left] [/align]
[align=left] using (var events = _eventsFactory()) {[/align]
[align=left] events.Value.Activated();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] _sweepGenerator.Activate();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left]_routeProviders是一个IEnumerable<IRouteProvider>型的私有字段,Autofac在创建DefaultOrchardShell对象时会通过构造器注入的方式初始化该字段。实际上就是相应Shell需要用到的各个模块中的的IRouteProvider对象,通过调用IRouteProvider.GetRoutes方法则可将RouteDescriptor对象收集起来。 [/align]
[align=left] [/align]
[align=left]_httpRouteProviders是一个IEnumerable<IHttpRouteProvider>型私有字段,实际上IHttpRouteProvider接口IRouteProvider接口完全一样。_httpRouteProviders和_routeProviders的初始化方式也一样。不同的是_httpRouteProviders是为WebApi服务的。[/align]
[align=left] [/align]
_routePublisher是一个Orchard.Mvc.Routes.RoutePublisher对象,其Publish方法中,将RouteDescriptor对象对应的RouteBase(一般为Route)对象,包装成ShellRoute对象注册到MVC的全局路由表中:
[align=left] // 以下代码来在Orchard.Mvc.Routes.RoutePublisher类[/align]
[align=left] public void Publish(IEnumerable <RouteDescriptor> routes) {[/align]
[align=left] var routesArray = routes[/align]
[align=left] .OrderByDescending(r => r.Priority)[/align]
[align=left] .ToArray();[/align]
[align=left] [/align]
[align=left] // this is not called often, but is intended to surface problems before[/align]
[align=left] // the actual collection is modified[/align]
[align=left] var preloading = new RouteCollection();[/align]
[align=left] foreach (var routeDescriptor in routesArray) {[/align]
[align=left] [/align]
[align=left] // extract the WebApi route implementation[/align]
[align=left] var httpRouteDescriptor = routeDescriptor as HttpRouteDescriptor;[/align]
[align=left] if (httpRouteDescriptor != null ) {[/align]
[align=left] var httpRouteCollection = new RouteCollection();[/align]
[align=left] httpRouteCollection.MapHttpRoute(httpRouteDescriptor.Name, httpRouteDescriptor.RouteTemplate, httpRouteDescriptor.Defaults);[/align]
[align=left] routeDescriptor.Route = httpRouteCollection.First();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] preloading.Add(routeDescriptor.Name, routeDescriptor.Route);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] using (_routeCollection.GetWriteLock()) {[/align]
[align=left] // existing routes are removed while the collection is briefly inaccessable[/align]
[align=left] var cropArray = _routeCollection[/align]
[align=left] .OfType< ShellRoute>()[/align]
[align=left] .Where(sr => sr.ShellSettingsName == _shellSettings.Name)[/align]
[align=left] .ToArray();[/align]
[align=left] [/align]
[align=left] foreach(var crop in cropArray) {[/align]
[align=left] _routeCollection.Remove(crop);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] // new routes are added[/align]
[align=left] foreach (var routeDescriptor in routesArray) {[/align]
[align=left] // Loading session state information.[/align]
[align=left] var defaultSessionState = SessionStateBehavior .Default;[/align]
[align=left] [/align]
[align=left] ExtensionDescriptor extensionDescriptor = null ;[/align]
[align=left] if(routeDescriptor.Route is Route) {[/align]
[align=left] object extensionId;[/align]
[align=left] var route = routeDescriptor.Route as Route;[/align]
[align=left] if(route.DataTokens != null && route.DataTokens.TryGetValue("area", out extensionId) ||[/align]
[align=left] route.Defaults != null && route.Defaults.TryGetValue("area", out extensionId)) {[/align]
[align=left] extensionDescriptor = _extensionManager.GetExtension(extensionId.ToString());[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] else if (routeDescriptor.Route is IRouteWithArea) {[/align]
[align=left] var route = routeDescriptor.Route as IRouteWithArea;[/align]
[align=left] extensionDescriptor = _extensionManager.GetExtension(route.Area);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] if (extensionDescriptor != null ) {[/align]
[align=left] // if session state is not define explicitly, use the one define for the extension[/align]
[align=left] if (routeDescriptor.SessionState == SessionStateBehavior.Default) {[/align]
[align=left] Enum.TryParse(extensionDescriptor.SessionState, true /*ignoreCase*/, out defaultSessionState);[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] // Route-level setting overrides module-level setting (from manifest).[/align]
[align=left] var sessionStateBehavior = routeDescriptor.SessionState == SessionStateBehavior.Default ? defaultSessionState : routeDescriptor.SessionState ;[/align]
[align=left] [/align]
[align=left] var shellRoute = new ShellRoute(routeDescriptor.Route, _shellSettings, _workContextAccessor, _runningShellTable) {[/align]
[align=left] IsHttpRoute = routeDescriptor is HttpRouteDescriptor ,[/align]
[align=left] SessionState = sessionStateBehavior[/align]
[align=left] };[/align]
[align=left] _routeCollection.Add(routeDescriptor.Name, shellRoute);[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]

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)
[align=left] [/align]
[align=left]从Url角度上讲,怎么区分两个Shell呢?首先两个Shell可以拥有不同的域名,或者拥有相同的域名但不同的Url前缀。如:[/align]

[align=left](1)、其中一个Shell无域名[/align]
[align=left]Shell 1 - 无[/align]
[align=left]Shell 2 - www.yourdomain2.com[/align]
[align=left](2)、不同的域名[/align]
[align=left]Shell 1 - www.yourdomain1.com[/align]
[align=left]Shell 2 - www.yourdomain2.com[/align]

[align=left](3)、相同的域名,不同的Url前缀[/align]

[align=left]Shell 1 - www.yourdomain1.com/abc[/align]

[align=left]Shell 2 - www.yourdomain1.com/def[/align]
[align=left](4)、相同的域名,只有一个Shell的Url前缀[/align]

[align=left]Shell 1 - www.yourdomain1.com[/align]

[align=left]Shell 2 - www.yourdomain1.com/def[/align]
[align=left]这种情况会先检查Url是否匹配Shell 2,然后再检查是否匹配Shell 1。Url前缀长度越长,越优先检查。[/align]
[align=left]引申:[/align]

[align=left]Shell 1 - www.yourdomain1.com/abc/def[/align]

[align=left]Shell 2 - www.yourdomain1.com/abc[/align]

[align=left](5)、一个Shell可以对应单个或多个域名[/align]

[align=left]Shell 1 - www.yourdomain1.com[/align]

[align=left]Shell 2 - www.yourdomain2.com和 www.yourdomain3.com[/align]

[align=left](6)、更复杂的配置[/align]
[align=left] [/align]
为了方便分析,这里我们假设Orchard中配置了两个Shell,ShellSettings设置如下:

Shell 1:ShellSettings.RequestUrlHost ="www.yourdomain1.com",ShellSettings.RequestUrlPrefix=String.Empty

[align=left]Shell 2:ShellSettings.RequestUrlHost ="www.yourdomain2.com",ShellSettings.RequestUrlPrefix="abc"[/align]

并且某模块被这两个Shell使用,该模块的Routes:IRouteProvider类中定义了一个匹配"{controller}/{action}"的路由。需要注意一点,虽然这里只定义一个路由,但是这里两个Shell都会用到,所以会被包装成两个ShellRoute对象注册到全局路由表中。
再假设一个新的Http请求进入,Url是:http://www.yourdomain2.com/abc/home/index

首先System.Web.Routing.UrlRouteModule会遍历全局路由表中的路由,期待获取一个RouteData对象。当遍历到我们刚刚注册的路由时,会调用路由的GetRouteData方法:
[align=left] // 以下代码来在Orchard.Mvc.Routes.ShellRoute类[/align]

[align=left] public override RouteData GetRouteData( HttpContextBase httpContext) {[/align]
[align=left] // locate appropriate shell settings for request[/align]
[align=left] var settings = _runningShellTable.Match(httpContext);[/align]
[align=left] [/align]
[align=left] // only proceed if there was a match, and it was for this client[/align]
[align=left] if (settings == null || settings.Name != _shellSettings.Name)[/align]
[align=left] return null ;[/align]
[align=left] [/align]
[align=left] var effectiveHttpContext = httpContext;[/align]
[align=left] if (_urlPrefix != null )[/align]
[align=left] effectiveHttpContext = new UrlPrefixAdjustedHttpContext (httpContext, _urlPrefix);[/align]
[align=left] [/align]
[align=left] var routeData = _route.GetRouteData(effectiveHttpContext);[/align]
[align=left] if (routeData == null )[/align]
[align=left] return null ;[/align]
[align=left] [/align]
[align=left] // push provided session state behavior to underlying MvcHandler[/align]
[align=left] effectiveHttpContext.SetSessionStateBehavior(SessionState);[/align]
[align=left] [/align]
[align=left] // otherwise wrap handler and return it[/align]
[align=left] routeData.RouteHandler = new RouteHandler (_workContextAccessor, routeData.RouteHandler, SessionState);[/align]
[align=left] routeData.DataTokens[ "IWorkContextAccessor"] = _workContextAccessor;[/align]
[align=left] [/align]
[align=left] if (IsHttpRoute) {[/align]
[align=left] routeData.Values[ "IWorkContextAccessor"] = _workContextAccessor; // for WebApi[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] return routeData;[/align]
[align=left] }[/align]

Shell被成功激活后,其对应的ShellSettings会存入在一个RunningShellTable对象中。在这里也就是_runningShellTable变量。
根据传入的Url,找到匹配的ShellSettings存入局部变量_settings:
[align=left] var settings = _runningShellTable.Match(httpContext);[/align]
下面看看Match的过程:

[align=left] /// 该方法位于Orchard.Environment.RunningShellTable类中[/align]

[align=left] public ShellSettings Match(string host, string appRelativePath) {[/align]
[align=left] var hostLength = host.IndexOf(':' );[/align]
[align=left] if (hostLength != -1)[/align]
[align=left] host = host.Substring(0, hostLength);[/align]
[align=left] [/align]
[align=left] var mostQualifiedMatch = _shellsByHost[/align]
[align=left] .Where(group => host.EndsWith(group.Key, StringComparison.OrdinalIgnoreCase))[/align]
[align=left] .SelectMany(group => group[/align]
[align=left] .OrderByDescending(settings => (settings.RequestUrlPrefix ?? string.Empty).Length))[/align]
[align=left] .FirstOrDefault(settings => settings.State.CurrentState != TenantState.State .Disabled && appRelativePath.StartsWith("~/" + (settings.RequestUrlPrefix ?? string.Empty), StringComparison.OrdinalIgnoreCase));[/align]
[align=left] [/align]
[align=left] return mostQualifiedMatch ?? _fallback;[/align]
[align=left] }[/align]
[align=left] [/align]
所以http://www.yourdomain2.com/abc/home/index匹配到的Shell为Shell 2。

GetRouteData方法接下来有个判断:

[align=left] if (settings == null || settings.Name != _shellSettings.Name)[/align]
[align=left] return null ;[/align]

settings可能为null这好理解,但其Name值为什么可能不相等呢?请留意RunningShellTable.Match方法的最后一行的_fallback变量,这里就不再详述。
如果Shell包含Url前缀,则调整HttpContext:
[align=left] var effectiveHttpContext = httpContext;[/align]
[align=left] if (_urlPrefix != null )[/align]
[align=left] effectiveHttpContext = new UrlPrefixAdjustedHttpContext (httpContext, _urlPrefix);[/align]
[align=left] [/align]

[align=left]_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"。[/align]

UrlPrefixAdjustedHttpContext类最主要的目的是替换掉原来的HttpRequest,以使得HttpRequest的AppRelativeCurrentExecutionFilePath属性能够返回一个去掉Url前缀的值。这样做得目的是为了能够按"常规"方式获取到RouteData。
如ShellRoute的RequestUrlPrefix属性值为"abc",请求的Url是: http://www.yourdomain2.com/abc/home/index 则AppRelativeCurrentExecutionFilePath返回的值是:
~/home/index

_route.GetRouteData方法的调用,也就是刚才说的"常规"方式:

[align=left] var routeData = _route.GetRouteData(effectiveHttpContext);[/align]
[align=left] if (routeData == null )[/align]
[align=left] return null ;[/align]
[align=left] [/align]

GetRouteData最后的代码也简单:

[align=left] // push provided session state behavior to underlying MvcHandler[/align]
[align=left] effectiveHttpContext.SetSessionStateBehavior(SessionState);[/align]
[align=left] [/align]
[align=left] // otherwise wrap handler and return it[/align]
[align=left] routeData.RouteHandler = new RouteHandler (_workContextAccessor, routeData.RouteHandler, SessionState);[/align]
[align=left] routeData.DataTokens[ "IWorkContextAccessor"] = _workContextAccessor;[/align]
[align=left] [/align]
[align=left] if (IsHttpRoute) {[/align]
[align=left] routeData.Values[ "IWorkContextAccessor"] = _workContextAccessor; // for WebApi[/align]
[align=left] }[/align]

[align=left] [/align]
[align=left]这里的RouteHandler类是ShellRoute的私有嵌套类,其通过装饰器模式包装了一个IRouteHandler对象。相关类型还有私有嵌套类HttpHandler和HttpAsyncHandler。RouteHandler是为了Autofac容器的应用到IHttpHandler中。[/align]
[align=left]在上面提到的Orchard.Blogs.Routes类中,定义的Route的RouteHandler是MvcRouteHandler,这里重新包装成RouteHandler对象再赋给routeData的RouteHandler属性。[/align]

后面再将_workContextAccessor保存进routeData的DataTokens中。

四、根据路由规则生成Url

[align=left] public override VirtualPathData GetVirtualPath( RequestContext requestContext, RouteValueDictionary values) {[/align]
[align=left] // locate appropriate shell settings for request[/align]
[align=left] var settings = _runningShellTable.Match(requestContext.HttpContext);[/align]
[align=left] [/align]
[align=left] // only proceed if there was a match, and it was for this client[/align]
[align=left] if (settings == null || settings.Name != _shellSettings.Name)[/align]
[align=left] return null ;[/align]
[align=left] [/align]
[align=left] var effectiveRequestContext = requestContext;[/align]
[align=left] if (_urlPrefix != null )[/align]
[align=left] effectiveRequestContext = new RequestContext (new UrlPrefixAdjustedHttpContext (requestContext.HttpContext, _urlPrefix), requestContext.RouteData);[/align]
[align=left] [/align]
[align=left] var virtualPath = _route.GetVirtualPath(effectiveRequestContext, values);[/align]
[align=left] if (virtualPath == null )[/align]
[align=left] return null ;[/align]
[align=left] [/align]
[align=left] if (_urlPrefix != null )[/align]
[align=left] virtualPath.VirtualPath = _urlPrefix.PrependLeadingSegments(virtualPath.VirtualPath);[/align]
[align=left] [/align]
[align=left] return virtualPath;[/align]
[align=left] }[/align]

[align=left] [/align]
[align=left]前面几行代码和GetRouteData类似,关注点在UrlPrefixAdjustedHttpContext类和UrlPrefix类,在分析GetRouteData方法时已有简单分析。[/align]
[align=left] [/align]
[align=left] [/align]

相关类型:
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: