摘要: 上文分析了UrlRouting模塊何時會被觸發,本文重點分析路由模塊是如何工作,以及如何利用路由模塊實現Area。
先看路由模塊的PostResolveRequestCache事件中被觸發的方法:
public virtual void PostResolveRequestCache(HttpContextBase context) { RouteData routeData = this.RouteCollection.GetRouteData(context); if (routeData == null) { return; } IRouteHandler routeHandler = routeData.RouteHandler; if (routeHandler == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.GetString("UrlRoutingModule_NoRouteHandler"), new object[0])); } if (routeHandler is StopRoutingHandler) { return; } RequestContext requestContext = new RequestContext(context, routeData); context.Request.RequestContext = requestContext; IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); if (httpHandler == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("UrlRoutingModule_NoHttpHandler"), new object[] { routeHandler.GetType() })); } if (!(httpHandler is UrlAuthFailureHandler)) { context.RemapHandler(httpHandler); return; } if (FormsAuthenticationModule.FormsAuthRequired) { UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this); return; } throw new HttpException(401, SR.GetString("Assess_Denied_Description3")); }
這個方法做的工作還是比較清晰的,首先從RouteCollection中獲得RouteData,從RouteData中獲得RouteHandler,從RouteHandler獲得httpHandler,最后調用RemapHandler將控制權交給httpHandler。UrlRoutingModule是System.Web中的通用的路由模塊,並不僅限於給ASP.NET MVC使用,這里處理的今本都是針對抽象接口來處理的。后文會介紹ASP.NET MVC是如何利用這個模塊實現了URL到controller/action的映射的。
RouteCollection是一張路由表,里面包括了很多路由規則(RouteBase),RouteData則是解析好的路由,里面包括了Key-Value對的路由信息,一個routehandler。RouteCollection的GetRouteData方法,是找到符合當前請求的路由規則的RouteData,其內部實現就是遍歷所有的路由規則,調用RouteBase的GetRouteData方法,返回第一個非空的RouteData。IRouteHandler 只有一個方法,
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); }
但是他是從路由模塊完成最重要的工作之一,等到其他模塊執行完畢之后,將由這個IHttpHandler(如果其他模塊沒有更改這個handler)來完成接下來的請求。得到合適的HttpHandler之后,調用了HttpContext的RemapHandler方法,這個方法的核心是
this._remapHandler = handler;
此時尚沒有真正的轉交控制權,當前的handler還是global.asax中的類,在上文中有介紹到在初始化一個請求的時候,由一個StepManager來初始化需要觸發的step,其中有:
HttpApplication.IExecutionStep step = new HttpApplication.MaterializeHandlerExecutionStep(application); application.AddEventMapping("ManagedPipelineHandler", RequestNotification.MapRequestHandler, false, step);
因此在MapRequestHandler的時候,會觸發MaterializeHandlerExecutionStep的Execute方法,其中主要的代碼是:
if (context.RemapHandlerInstance != null)
{
IIS7WorkerRequest.SetScriptMapForRemapHandler();
context.Handler = context.RemapHandlerInstance;
}
在MapRequestHandler之后調用RemapHandler會導致異常,原因是改變handler的時機是在MapRequestHandler。
下面看ASP.MVC框架是如何使用這個routing module的。MVC在RouteCollectionExtensions這個類中定義了一系列擴展方法,擴展了原有的RouteCollection類。RouteCollection類是支持ASP.NET Webform的路由模塊,可以用MapPageRoute方法將url映射到aspx文件上。RouteCollectionExtensions的核心方法是:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) { //…
Route route = new Route(url, new MvcRouteHandler()) { Defaults = new RouteValueDictionary(defaults), Constraints = new RouteValueDictionary(constraints), DataTokens = new RouteValueDictionary() }; if ((namespaces != null) && (namespaces.Length > 0)) { route.DataTokens["Namespaces"] = namespaces; } routes.Add(name, route); return route; }
注意Route構造函數中的第二個參數,這是一個MvcRouteHandler,這個handler也就是RouteData中的RouteHandler的值,其實現為:
protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) { requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext)); return new MvcHandler(requestContext); }
暫時忽略這里的SessionStateBehavior,可以看到最終的httpHandler是MvcHandler。
綜上,routing module在PostResolveRequestCache被觸發,獲得RouteCollection中的Route(System.Web.Routing),解析Route數據獲得RouteData,MVC框架設置RouteData中的RouteHandler為MvcRouteHandler,MvcRouteHandler的GetHttpHandler方法返回的是一個MvcHandler,這個handler最終在MapRequestHandler事件觸發的時候接過request處理流程,開始處理請求,它將利用RouteData中解析好的值去觸發controller/action,這個下文再介紹。ASP.NET MVC的路由模塊主要是用的.NET中的System.Web.Routing.Route類來實現的,Route類的主要工作是解析路由規則,得到一個Key-Value對的序列。這是一個單純而又復雜的過程,這里不分析其實現。
下面簡單介紹下Route類所表示的路由規則。根據上面的代碼(Route的構造函數)可以看到,一條路由規則包括4個部分:Url模式,一組默認值,約束,和DataToken,DataToken是一些額外的信息,Route本身不會使用他們,但是可以提供給其他代碼使用,下面會說明。 Url模式是一個字符串,包括一些固定的字符字面量和占位符,占位符由一對花括號表示{ } 。例如:
| Route definition |
Example of matching URL |
|---|---|
| {controller}/{action}/{id} |
/Products/show/beverages |
| {table}/Details.aspx |
/Products/Details.aspx |
| blog/{action}/{entry} |
/blog/show/123 |
| {reporttype}/{year}/{month}/{day} |
/sales/2008/1/5 |
| {locale}/{action} |
/US/show |
| {language}-{country}/{action} |
/en-US/show |
當一個url滿足一個url模式的時候,Route模塊會將其解析后的數據放在RouteData.Value中,例如第一條url解析之后就是如下的K-V對序列:
controller:Products
action:show
id:beverages
默認值不討論了。
約束是一個匿名對象,例如
routes.MapRoute( "BlogArchive", "Archive/{entryDate}", new { controller = "Blog", action = "Archive" }, new { entryDate = @"d{2}-d{2}-d{4}" } );
表示對entryDate添加約束。屬性的值如果是一個字符串,則代表一個正則表達式,除此之外,還可以是實現IRouteConstraint接口的對象,從而實現自定義的約束。
DataToken可以放一些自定義的數據,例如ASP.NET MVC就在其中放入了Namespace.Namespace用來區分同名的Controller,常用在Area中。Area可以將一個大型的網站划分為相對獨立的區域。MSDN上的這篇文章介紹了如何創建一個Area。VS在創建一個Area的時候創建了如下結構的目錄和文件:
其中Model-Controller-View的結構是和整個站點一致的,還有一個獨立的Web.config文件。關鍵還多了一個AdminAreaRegistration文件,這個自動生成的類是用來注冊Area的路由的:
public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "Admin_default", "Admin/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional } ); }
這個方法和普通的注冊路由非常相似,看下AreaRegistrationContext.MapRoute的實現:
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces) { if (namespaces == null && Namespaces != null) { namespaces = Namespaces.ToArray(); } Route route = Routes.MapRoute(name, url, defaults, constraints, namespaces); route.DataTokens["area"] = AreaName; // disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up // controllers belonging to other areas bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0); route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback; return route; }
最終還是和普通的MapRoute一樣,創建了一個Route對象,不同的只是給DataToken添加了area和UseNamespaceFallback屬性。下面再看看這里的RegisterArea是如何被調用的,以及參數context是什么。注意到在global.asax中的Application_Start方法中第一行代碼就是:
AreaRegistration.RegisterAllAreas();
看這個方法的實現:
internal static void RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, object state) { List<Type> areaRegistrationTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(_typeCacheName, IsAreaRegistrationType, buildManager); foreach (Type areaRegistrationType in areaRegistrationTypes) { AreaRegistration registration = (AreaRegistration)Activator.CreateInstance(areaRegistrationType); registration.CreateContextAndRegister(routes, state); } }
首先枚舉出當前AppDomain中所有的AreaRegistration的子類,關於TypeCacheUtil,下文還會出現,暫不作介紹。然后調用CreateContextAndRegister方法,這個方法的代碼如下:
internal void CreateContextAndRegister(RouteCollection routes, object state) { AreaRegistrationContext context = new AreaRegistrationContext(AreaName, routes, state); string thisNamespace = GetType().Namespace; if (thisNamespace != null) { context.Namespaces.Add(thisNamespace + ".*"); } RegisterArea(context); }
主要做了兩部分事情,首先創建了AreaRegistrationContext,並且把AreaRegistration子類的命名空間加入到context的Namesapces屬性中,最終調用了我們一開始談到的自動生成的RegisterArea方法。因此,在Application_Start一開始,每個AreaRegistration的子類的RegisterArea方法都會被調用,這個方法的效果是在全局路由表中添加一條Area的路由規則,Area的路由規則默認的把當前的AreaRegistration子類的命名空間加到DataToken中,另外還有area和UseNamespaceFallback屬性也加入了DataToken。至於這些值如何被使用,MVC框架是如何實現調用合適的Controller的Action方法的,下文再介紹。

