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类有两个重要的方法: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

时间: 2024-10-12 02:32:35

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

Orchard源码分析(5):Host相关(Orchard.Environment.DefaultOrchardHost类)

概述 Host 是应用程序域级的单例,代表了Orchard应用程序.其处理应用程序生命周期中的初始化.BeginRequest事件.EndRequest事件等. 可以简单理解为HttpApplication的功能转移到了Host身上.从源码角度上看,Host对应的是实现了IOrchardHost接口的 DefaultOrchardHost类. 回顾一下之前对Orchard.Web.MvcApplication类的分析.在Orchard启动时,会创建一个DefaultOrchardHost对象:

Cordova Android源码分析系列二(CordovaWebView相关类分析)

本篇文章是Cordova Android源码分析系列文章的第二篇,主要分析CordovaWebView和CordovaWebViewClient类,通过分析代码可以知道Web网页加载的过程,错误出来,多线程处理等. CordovaWebView类分析 CordovaWebView类继承了Android WebView类,这是一个很自然的实现,共1000多行代码.包含了PluginManager pluginManager,BroadcastReceiver receiver,CordovaInt

Orchard源码分析(7):ASP.NET MVC相关

概述 Orchard归根结底是一个ASP.NET MVC(以后都简称为MVC)应用,但在前面的分析中,与MVC相关内容的涉及得很少.MVC提供了非常多的扩展点,本文主要关注Orchard所做的扩展.主要集中在如下几个方面: 1.Routing(路由)相关 2.Controller相关 3.ViewEngines相关 4.Model绑定(Binding)相关 5.Model验证器(Validator)相关 我们将分几个篇幅来分别分析. 参考资料: How ASP.NET MVC Works(强烈推

Orchard源码分析(1):Orchard架构

本文主要参考官方文档"How Orchard works"以及Orchardch上的翻译. 源码分析应该做到庖丁解牛,而不是以管窥豹或瞎子摸象.所以先对Orchard架构有个整体的了解,以及对一些基本概念有所认识. 创建一个基于Web的CMS(内容管理系统)不同于创建一个普通的Web应用程序:它更像是建立一个应用程序容器. 这样一个系统,必须拥有优良的开放性.可扩展性.但是作为一个可扩展系统,它可能会面临应用程序"可用性"的挑战:在系统中的核心模块与未知的未来模块的

Orchard源码分析(1):Orchard架构 (转)

源码分析应该做到庖丁解牛,而不是以管窥豹或瞎子摸象.所以先对Orchard架构有个整体的了解,以及对一些基本概念有所认识. 创建一个基于Web的CMS(内容管理系统)不同于创建一个普通的Web应用程序:它更像是建立一个应用程序容器. 这样一个系统,必须拥有优秀的开放性.可扩展性.但是作为一个可扩展系统,它可能会面临应用程序"可用性"的挑战:在系统中的核心模块与未知的未来模块的组合,包括用户界面级别的整合.编排所有这些小零件,让互不知道的彼此的模块成一个连贯的整体,是Orchard是关键

Orchard源码分析(2):Orchard.Web.MvcApplication类(Global)

概述 分析一个的ASP.NET项目源码,首先可以浏览其项目结构,大致一窥项目其全貌,了解项目之间的依赖关系.其次可以浏览Web.config和Global.asax文件,找到应用程序的入口点. 本 文主要分析Orchard项目的Global.asax文件,而真正的分析入口点在Global.asax的CodeBehind文件 Global.asax.cs中,即Orchard.Web.MvcApplication类(以下简称MvcApplication类). MvcApplication类处理了三个

Orchard源码分析(3):Orchard.WarmupStarter程序集

概述 Orchard.WarmupStarter程序集包含三个类:WarmupUtility.WarmupHttpModule和Starter<T>.该程序集主要为Orchard应用启动初始化服务.一.WarmupUtility类 该类是一个静态工具类,包含一个静态只读String型字段WarmupFilesPath,以及三个方法EncodeUrl.ToUrlString和DoBeginRequest. 1.WarmupFilesPath其值为"~/App_Data/Warmup/&

Orchard源码分析(4.4):Orchard.Caching.CacheModule类

概述 CacheModule也是一个Autofac模块. 一.CacheModule类 CacheModule将DefaultCacheManager注册为ICacheManager: public class CacheModule : Module { protected override void Load( ContainerBuilder builder) { builder.RegisterType<DefaultCacheManager>() .As< ICacheMana

Orchard源码分析(4.3):Orchard.Events.EventsModule类(Event Bus)

概述 采用Event Bus模式(事件总线),可以使观察者模式中的观察者和被观察者实现解耦. 在.Net 中使用观察者模式,可以使用事件(委托)和接口(类).Orchard Event  Bus使用的是接口的形式,这样方便将“观察者”注册到Autofac容器中.EventsModule模块是构成Orchard Event  Bus的一部分.这里先分开分析Orchard Event Bus涉及的类型和知识点,然后在将他们组合起来分析Orchard Event  Bus的机制. 一.Registra