2012年11月5日星期一

深入ASP.NET MVC之二:路由模块如何工作

深入ASP.NET MVC之二:路由模块如何工作

摘要: 上文分析了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的时候创建了如下结构的目录和文件:

image

其中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方法的,下文再介绍。




TAG: