深入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方法的,下文再介紹。


免責聲明!

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



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