文章出處

    這篇隨筆講講路由功能,主要內容在項目Microsoft.AspNetCore.Routing中,可以在GitHub上找到,Routing項目地址

    路由功能是大家都很熟悉的功能,使用起來也十分簡單,從使用的角度來說可講的東西不多。不過閱讀源碼的過程的是個學習的過程,看看頂尖Coder怎么組織代碼也是在提升自己。

    我們知道現在ASP.NET Core中所有用到的功能都是服務,那么Routing服務是什么時候被添加到依賴注入容器的呢?答案是在StartUp類的ConfigureServices方法中。如果我們隨便新建的MVC 6的項目,在VS中模板會自動幫我們添加一些代碼,在Startup類中的兩個方法我們可以找到一下代碼。

        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            // 省略其他框架服務
            services.AddMvc();//添加MVC服務
            // Add application services.
            //省略自定義應用服務
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

     上面兩個StartUp類里的方法,一個是注冊服務,一個使用服務。從它們調用的方法名上應該能區別出來,AddXXX的是注冊服務,UseXXX的是使用服務。這里面用到的方法都是擴展方法,有關路由需要用到的服務都會在AddMvc()中注冊,當然這個方法還會注冊一大堆其他MVC框架需要用的方法。如果想追蹤相關服務的添加語句的話:AddMvc()->AddMvcCore()->ConfigureDefaultServices()->AddRouting(),另外一條線是AddMvc()->AddMvcCore()->AddMvcCoreServices()->TryAddSingleton<MvcDefaultHandler>()。前者是添加與路由模板解析存儲相關的服務,后者是處理請求路由的服務,包括請求路由與模板的配對,以及觸發相應的Action等等。

     我想分別從兩條線來解釋路由(Routing)的工作流程。

  1. 一條是從開發者注冊路由,框架進行解析存儲的角度來解釋,也就是從MapRoute()這個方法開始。
  2. 另外一條是從解析路由的角度,一個請求的路由如何被處理,然后Invoke相應的Action。

     為了解釋清楚相關概念,我想先解釋一下三個詞:Route, Routing, Router。三個詞都可以模糊地翻譯為路由,但是這樣太容易混淆了,懂英語的人應該能一眼就看出其中的不同。

  • Routing是統稱,有關路由的總體概念,就可以以Routing來描述,比如我講的這個項目就叫Routing,或者我們有個添加有關路由服務的方法,就會叫AddRouting();
  • Route是用來描述某一個特定路由的名詞,它有具體的數據項,比如說上面的代碼我們向框架中添加了一個name是default的Route;它還是一個類名,這個類就是干Route應該干的事情:存儲、解析數據之類的;
  • Router可以翻譯為“路由者”帶路黨,是一種Handler的體現,用來處理請求的路由,比如MVC框架默認的MvcRouteHandler就是一個Router。

注冊路由

     首先從MapRoute()方法說起。這個方法會在內部調用這些代碼

1             routeBuilder.Routes.Add(new Route(
2                 routeBuilder.DefaultHandler,
3                 name,
4                 template,
5                 new RouteValueDictionary(defaults),
6                 new RouteValueDictionary(constraints),
7                 new RouteValueDictionary(dataTokens),
8                 inlineConstraintResolver));

     RouteBuilder會在內部維護一個Route(s)的容器,上面的代碼在往容器里面添加新的Route,如果我們用VS默認的模板,那么這里面name="default", template="{controller=Home}/{action=Index}/{id?}",其他參數諸如像defaults,constraints什么的都是沒有的。需要說明的是此時routeBuilder.DefaultHandler已經被設置為MvcDefaultHandler,這是在UseMvc()方法中被設置的。在Route類的構造過程中(RouteBase的構造函數中),template字符串會被解析,包括是不是參數名(parameter)啊,是不是字面值(literal)啊,約束是什么,默認值是什么都可以被解析出來。MVC6的路由和MVC5有一點不一樣,默認值以及約束可以寫在template里,而MVC5的約束和默認值只能再傳遞一個匿名類型進去:defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }。顯然MVC6的路由簡單的多,當然你也可以還用以前的方式傳遞默認值,沒有問題。MVC6的這種方法內置默認值和約束的方式在之前的特性路由已經體現了,用來改進傳統路由也在情理之中。MvcDefaultHandler會被賦給Route的_target字段,這個字段在未來請求來臨時發揮功效。

     解析路由的過程就是一個字符串處理的過程,比較復雜,如果要全部講解篇幅太長。如果做過LeetCode上一些字符串處理的題目的話,看起來會輕松一些,有興趣的童鞋可以去深究源碼。

     UseMvc()這個方法和路由有很大的關系,下面來看一下它的源碼

        public static IApplicationBuilder UseMvc(this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
        {
            //省略了檢查參數的代碼...
            var routes = new RouteBuilder(app)
            {
                DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
            };

            configureRoutes(routes);
            routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
                routes.DefaultHandler,
                app.ApplicationServices));

            return app.UseRouter(routes.Build());
        }

     configureRoutes(routes)就是上面解釋的調用MapRoute()方法的一個Action委托。DefaultHandler在構造RouteBuilder時被設置為默認的MvcRouteHandler,如果我們想要使用其他Handler,可以模仿這個UseMvc()方法重新寫一個拓展方法,傳入你的Handler即可。注意下面那句代碼:它向RouteBuilder.Routes中添加了用于處理特性路由的Route,并用Insert方法將其添加了到容器的起始位置,這說明特性路由要優先于傳統路由。至于為什么要先添加傳統路由,是因為開發者可以在傳入的configureRoutes這個委托中指定自己的Handler,DefaultHandler有可能在configureRoutes(routes)這段代碼中變了,所以特性路由的添加要晚于傳統路由。

     RouteBuilder.Build()方法會生成一個包含當前Route的集合,這些Route攜帶了信息(包括傳統路由被解析的參數,約束等以及特性路由的元數據等),在上面的例子中,這個容器就兩個Route,一個特性路由,一個name="default"的傳統路由。最后app.UseRouter會向ApplicationBuilder中添加一個類型為RouterMiddleware的中間件。此時整個有關路由的第一條工作流程就到此結束了。如果比較一下AddMvc()和UseMvc()會發現前一個方法關系到了非常多的服務,而后一個方法似乎只用到了有關路由的東西,這是因為服務的注冊要一起完成,而使用服務可以即時拉取。當應用程序響應請求時,一開始只用到路由服務,假如請求匹配,才會用到有關Controller和Action的服務,到那時候再拉取即可。

     UseRouter()方法最終會調用ApplicationBuilder.Use()方法,RouterMiddleware的信息最終會以委托的方式存儲在ApplicationBuilder中,有關這方面的流程可以參閱我之前的文章:Microsoft.AspNetCore.Hosting

處理請求路由

    上面說到我們注冊路由時,路由的信息在UseRouter()方法調用時是以Func<RequesetDelegate, RequestDelegate>方式存在。每一個中間件最初都是一種形態,利用這種方式,可以把在程序中用到的中間件構造成一種委托鏈,最后可以構造出一個跟使用順序有關的RequestDelegate:即請求管道。Routing是請求管道中的一部分,假如請求到達routing區域,則相關的RequestDelegate就會被觸發,利用反射構造出RouteMiddleware這個類,然后調用它的Invoke方法來處理有關路由的事務。

    先來看看RouteMiddleware的Invoke方法。

        public async Task Invoke(HttpContext httpContext)
        {
            var context = new RouteContext(httpContext);//構造一個路由上下文,三個屬性:HttpContext,Handler(一個委托),RouteData
            context.RouteData.Routers.Add(_router);

            await _router.RouteAsync(context);//這里的_router默認是MvcRouteHandler

            if (context.Handler == null)
            {
                _logger.RequestDidNotMatchRoutes();
                await _next.Invoke(httpContext);
            }
            else
            {
                httpContext.Features[typeof(IRoutingFeature)] = new RoutingFeature()
                {
                    RouteData = context.RouteData,
                };
                await context.Handler(context.HttpContext);
            }
        }

    整個方法的邏輯就是:

  1. 構造一個路由上下文,此時context.Handler=null;
  2. 由MvcRouteHandler.RouteAsync()方法來處理請求(注意此時請求的路由信息在HttpContext中,而現在它又是RouteContext的一個屬性);
  3. 第二步的工作就是篩選最合適的Action,并把相應的委托賦給RouteContext.Handler;
  4. 如果RouteContext.Handler為null,說明匹配失敗,路由工作到此結束,直接Invoke下一個Middleware;
  5. 如果到這一步,說明路由匹配成功,先把路由信息記錄到HttpContext,然后調用RouteContext.Handler,觸發有關Action的相關操作,那就是另一個主題了;
  6. RouteContext只在這個方法出現,到第五步時,上下文又變回了HttpContext,注意這個方法傳入的時候就是HttpContext就像帥哥HttpContext到了一個叫路由的地方,撩了一個叫RouteContext的妹子,等它離開時就把它拋棄了

    接下來看一下MvcRouteHandler.RouteAsync()方法

 1         public Task RouteAsync(RouteContext context)
 2         {
 3             //省略null檢查
 4             var actionDescriptor = _actionSelector.Select(context);//關鍵!!找到最優的Action,并返回一個攜帶相關信息的數據類
 5             if (actionDescriptor == null)
 6             {
 7                 _logger.NoActionsMatched();
 8                 return TaskCache.CompletedTask;
 9             }
10             //省略action有默認值情況的處理
11             context.Handler = (c) => InvokeActionAsync(c, actionDescriptor);//關鍵!!將RouteContext的Handler屬性置為相應的處理方法
12             return TaskCache.CompletedTask;
13         }
14 
15         private async Task InvokeActionAsync(HttpContext httpContext, ActionDescriptor actionDescriptor)
16         {
17             var routeData = httpContext.GetRouteData();//在RouteMiddleWare.Invoke()時候留下來的RouteData
18             try
19             {
20                 _diagnosticSource.BeforeAction(actionDescriptor, httpContext, routeData);
21 
22                 using (_logger.ActionScope(actionDescriptor))//日志記錄
23                 {
24                     _logger.ExecutingAction(actionDescriptor);
25 
26                     var startTimestamp = _logger.IsEnabled(LogLevel.Information) ? Stopwatch.GetTimestamp() : 0;
27 
28                     var actionContext = new ActionContext(httpContext, routeData, actionDescriptor);//根據相應的數據構造ActionContext上下文
29 
30                     //省略部分非主要邏輯代碼
31 
32                     var invoker = _actionInvokerFactory.CreateInvoker(actionContext);//構建一個有關Action處理的類型
33 
34                     await invoker.InvokeAsync();//觸發Action處理
35 
36                     _logger.ExecutedAction(actionDescriptor, startTimestamp);
37                 }
38             }
39             finally
40             {
41                 _diagnosticSource.AfterAction(actionDescriptor, httpContext, routeData);
42             }
43         }

    注釋已經解釋的比較詳細了。通過RouteContext選出最優的ActionDescriptor,我一筆帶過了,不過這個過程與注冊路由時候的解析一下,比較復雜。涉及到決策樹之類的內容,感興趣的同學可以深究。如果確實有Action匹配的話,RouteContext.Handler會被設置為相應的匿名方法。接著控制權交回給RouteMiddleware,然后觸發InvokeActionAsync()方法,RouteContext的使命就此結束。

    從InvokeActionAsync()方法中可以看出,框架根據相應的ActionDescriptor生成相應的ActionContext,之后進行有關Controller和Action的動作。撩完RouteContext就該輪到ActionContextle。

總結

  1. 注冊路由時,我們傳入的路由模板(TemplateString)會在構造Route的時候被解析,參數名字、默認值以及約束都能在模板中被解析出來;
  2. MVC框架默認的路由Handler是MvcRouteHandler,它會在UseMvc()方法中自動添加;
  3. 注冊路由的這些信息最終和其他服務一樣,會變成Application委托鏈(中間件)的一部分;
  4. 解析請求路由時,路由的信息由HttpContext攜帶著;
  5. 假如MvcRouteHandler能解析請求的路由,就會觸發有關Action的處理;否則路由過程結束,控制權交給下一個Middleware;也就是說,路由的RouterMiddleware會負責有關Action處理的部分
  6. HttpContext是渣男,撩了一個又一個XXXContext。   

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()