ASP.NET Core 從2.2版本開始,採用了一個新的名為Endpoint的路由方案,與原來的方案在使用上差別不大,但從內部運行方式上來說,差別還是很大的。上一篇詳細介紹了原版路由方案的運行機制,本文仍然通過一幅圖來瞭解一下新版的運行機制,最後再總結一下二者的異同點。(ASP.NET Core ...
ASP.NET Core 從2.2版本開始,採用了一個新的名為Endpoint的路由方案,與原來的方案在使用上差別不大,但從內部運行方式上來說,差別還是很大的。上一篇詳細介紹了原版路由方案的運行機制,本文仍然通過一幅圖來瞭解一下新版的運行機制,最後再總結一下二者的異同點。(ASP.NET Core 系列目錄)
一、概述
此方案從2.2版本開始,被稱作終結點路由(下文以“新版”稱呼),它是預設開啟的,若想採用原來的方案(<=2.1,下文以原版稱呼),可以在AddMvc的時候進行設置
services.AddMvc(option=>option.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
EnableEndpointRouting 預設為true,也就是啟用新的Endpoint方案,設置為false則採用舊版(<=2.1)的路由方案。
在配置方法上來說,系統仍然採用在Startup中的use.Mvc()中配置,而實際上內部的處理中間件已由原來的RouterMiddleware改為EndpointMiddleware和EndpointRoutingMiddleware兩個中間件處理,下麵依舊通過一幅圖來詳細看一下:
二、流程及解析
圖一
為了方便查看,依然對幾個“重點對象”做了顏色標識(點擊圖片可以看大圖):
1. 路由的初始化配置(圖的前兩個泳道)
- ① 一切依然是從Startup開始,而且和舊版一樣,是通過UseMvc方法進行配置,傳入routes.MapRoute(...)這樣的一個或多個配置, 不做贅述。
- 下麵著重說一下後面的流程,看一下MvcApplicationBuilderExtensions中的UseMvc方法:
1 public static IApplicationBuilder UseMvc( 2 this IApplicationBuilder app, 3 Action<IRouteBuilder> configureRoutes) 4 { 5 //此處各種驗證,略。。 6 var options = app.ApplicationServices.GetRequiredService<IOptions<MvcOptions>>(); 7 if (options.Value.EnableEndpointRouting) 8 { 9 var mvcEndpointDataSource = app.ApplicationServices 10 .GetRequiredService<IEnumerable<EndpointDataSource>>() 11 .OfType<MvcEndpointDataSource>() 12 .First(); 13 var parameterPolicyFactory = app.ApplicationServices 14 .GetRequiredService<ParameterPolicyFactory>(); 15 16 var endpointRouteBuilder = new EndpointRouteBuilder(app); 17 18 configureRoutes(endpointRouteBuilder); 19 20 foreach (var router in endpointRouteBuilder.Routes) 21 { 22 // Only accept Microsoft.AspNetCore.Routing.Route when converting to endpoint 23 // Sub-types could have additional customization that we can't knowingly convert 24 if (router is Route route && router.GetType() == typeof(Route)) 25 { 26 var endpointInfo = new MvcEndpointInfo( 27 route.Name, 28 route.RouteTemplate, 29 route.Defaults, 30 route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value), 31 route.DataTokens, 32 parameterPolicyFactory); 33 mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); 34 } 35 else 36 { 37 throw new InvalidOperationException($"Cannot use '{router.GetType().FullName}' with Endpoint Routing."); 38 } 39 } 40 if (!app.Properties.TryGetValue(EndpointRoutingRegisteredKey, out _)) 41 { 42 // Matching middleware has not been registered yet 43 // For back-compat register middleware so an endpoint is matched and then immediately used 44 app.UseEndpointRouting(); 45 } 46 return app.UseEndpoint(); 47 } 48 else 49 { 50 //舊版路由方案 51 } 52 }
② 第6行,這裡會獲取並判斷設置的EnableEndpointRouting的值,若為false,則採用舊版路由,詳見上一篇文章;該值預設為true,即採用新版路由。
③ 對應第9行,MvcEndpointDataSource在新版路由中是個非常非常重要的角色,在啟動初始化階段,它完成了路由表存儲和轉換,此處先用顏色重點標記一下,大家記住它,在後面的流程中詳細介紹。
④ 對應第16行,同舊版的RouteBuilder一樣,這裡會new一個 endpointRouteBuilder,二者都是一個IRouteBuilder,所以也同樣調用configureRoutes(endpointRouteBuilder)方法(也就是startup中的配置)獲取了一個Route的集合(IList<IRouter>)賦值給endpointRouteBuilder.Routes,這裡有個特別該註意的地方if (router is Route route && router.GetType() == typeof(Route)) ,也就是這裡只接受route類型,終結點路由系統不支持基於 IRouter的可擴展性,包括從 Route繼承。
⑤ 對應第20行,這裡對剛獲取到的endpointRouteBuilder.Routes進行遍歷,轉換成了一個MvcEndpointInfo的集和,賦值給mvcEndpointDataSource.ConventionalEndpointInfos。
⑥ 之後就是向管道塞中間件了,這裡的處理中間件由原來的RouterMiddleware改為EndpointMiddleware和EndpointRoutingMiddleware。
2.請求的處理(圖的後兩個泳道)
請求的處理大部分功能在中間件EndpointRoutingMiddleware,他有個重要的屬性_endpointDataSource保存了上文中初始化階段生成的MvcEndpointDataSource,而中間件EndpointMiddleware的功能比較簡單,主要是在EndpointRoutingMiddleware篩選出endpoint之後,調用該endpoint的endpoint.RequestDelegate(httpContext)進行請求處理。
⑦ InitializeAsync()方法主要是用於調用InitializeCoreAsync()創建一個matcher,而通過這個方法的代碼可以看出它只是在第一次請求的時候執行一次。
private Task<Matcher> InitializeAsync() { var initializationTask = _initializationTask; if (initializationTask != null) { return initializationTask; } return InitializeCoreAsync(); }
⑧ MvcEndpointDataSource一個重要的方法UpdateEndpoints(),作用是讀取所有action,並將這個action列表與它的ConventionalEndpointInfos列表(見⑤)進行匹配,最終生成一個新的列表。如下圖,我們預設情況下只配置了一個"{controller=Home}/{action=Index}/{id?}"這樣的路由,預設的HomeController有三個action,添加了一個名為FlyLoloController的controller並添加了一個帶屬性路由的action,最終生成了7個Endpoint,這有點像路由與action的“乘積”。當然,這裡只是用預設程式舉了個簡單的例子,實際項目中可能會有更多的路由模板註冊、會有更多的Controller和Action以及屬性路由等。
圖二
具體代碼如下:
1 private void UpdateEndpoints() 2 { 3 lock (_lock) 4 { 5 var endpoints = new List<Endpoint>(); 6 StringBuilder patternStringBuilder = null; 7 8 foreach (var action in _actions.ActionDescriptors.Items) 9 { 10 if (action.AttributeRouteInfo == null) 11 { 12 // In traditional conventional routing setup, the routes defined by a user have a static order 13 // defined by how they are added into the list. We would like to maintain the same order when building 14 // up the endpoints too. 15 // 16 // Start with an order of '1' for conventional routes as attribute routes have a default order of '0'. 17 // This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world. 18 var conventionalRouteOrder = 1; 19 20 // Check each of the conventional patterns to see if the action would be reachable 21 // If the action and pattern are compatible then create an endpoint with the 22 // area/controller/action parameter parts replaced with literals 23 // 24 // e.g. {controller}/{action} with HomeController.Index and HomeController.Login 25 // would result in endpoints: 26 // - Home/Index 27 // - Home/Login 28 foreach (var endpointInfo in ConventionalEndpointInfos) 29 { 30 // An 'endpointInfo' is applicable if: 31 // 1. it has a parameter (or default value) for 'required' non-null route value 32 // 2. it does not have a parameter (or default value) for 'required' null route value 33 var isApplicable = true; 34 foreach (var routeKey in action.RouteValues.Keys) 35 { 36 if (!MatchRouteValue(action, endpointInfo, routeKey)) 37 { 38 isApplicable = false; 39 break; 40 } 41 } 42 43 if (!isApplicable) 44 { 45 continue; 46 } 47 48 conventionalRouteOrder = CreateEndpoints( 49 endpoints, 50 ref patternStringBuilder, 51 action, 52 conventionalRouteOrder, 53 endpointInfo.ParsedPattern, 54 endpointInfo.MergedDefaults, 55 endpointInfo.Defaults, 56 endpointInfo.Name, 57 endpointInfo.DataTokens, 58 endpointInfo.ParameterPolicies, 59 suppressLinkGeneration: false, 60 suppressPathMatching: false); 61 } 62 } 63 else 64 { 65 var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); 66 67 CreateEndpoints( 68 endpoints, 69 ref patternStringBuilder, 70 action, 71 action.AttributeRouteInfo.Order, 72 attributeRoutePattern, 73 attributeRoutePattern.Defaults, 74 nonInlineDefaults: null, 75 action.AttributeRouteInfo.Name, 76 dataTokens: null, 77 allParameterPolicies: null, 78 action.AttributeRouteInfo.SuppressLinkGeneration, 79 action.AttributeRouteInfo.SuppressPathMatching); 80 } 81 } 82 83 // See comments in DefaultActionDescriptorCollectionProvider. These steps are done 84 // in a specific order to ensure callers always see a consistent state. 85 86 // Step 1 - capture old token 87 var oldCancellationTokenSource = _cancellationTokenSource; 88 89 // Step 2 - update endpoints 90 _endpoints = endpoints; 91 92 // Step 3 - create new change token 93 _cancellationTokenSource = new CancellationTokenSource(); 94 _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); 95 96 // Step 4 - trigger old token 97 oldCancellationTokenSource?.Cancel(); 98 } 99 }View Code
本質就是計算出一個個可能被請求的請求終結點,也就是Endpoint。由此可見,如上一篇文章那樣想自定義一個handler來處理特殊模板的方式(如 routes.MapRoute("flylolo/{code}/{name}", MyRouteHandler.Handler);)將被忽略掉,因其無法生成 Endpoint,且此種方式完全可以自定義一個中間件來實現,沒必要混在路由中。
⑨ 就是用上面生成的Matcher,攜帶Endpoint列表與請求URL做匹配,並將匹配到的Endpoint賦值給feature.Endpoint。
⑩ 獲取feature.Endpoint,若存在則調用其RequestDelegate處理請求httpContext。
三、新版與舊版的異同點總結
簡要從應用系統啟動和請求處理兩個階段對比說一下兩個版本的區別:
1.啟動階段:
這個階段大部分都差不多,都是通過Startup的app.UseMvc()方法配置一個路由表,一個Route的集合Routes(IList<IRouter>),然後將其簡單轉換一下
<=2.1: 將Routes轉換為RouteCollection
2.2+ : 將Routes轉換為List<MvcEndpointInfo>
二者區別不大,雖然名字不同,但本質上還是差不多,都仍可理解為Route的集合的包裝。
2.請求處理階段:
<=2.1: 1. 將請求的URL與RouteCollection中記錄的路由模板進行匹配。
2. 找到匹配的Route之後,再根據這個請求的URL判斷是否存在對應的Controlled和Action。
3. 若以上均通過,則調用Route的Handler對HttpContext進行處理。
2.2+ : 1. 第一次處理請求時,首先根據啟動階段所配置的路由集合List<MvcEndpointInfo>和_actions.ActionDescriptors.Items(所有的action的信息)做匹配,生成一個列表,這個列表存儲了所有可能被匹配的URL模板,如圖二,這個列表同樣是List<MvcEndpointInfo>,記錄了所有可能的URL模式,實際上是列出了一個個可以被訪問的詳細地址,已經算是最終地址了,即終結點,或許就是為什麼叫Endpoint路由的原因。
2.請求的Url和這個生成的表做匹配,找到對應的MvcEndpointInfo。
3. 調用被匹配的MvcEndpointInfo的RequestDelegate方法對請求進行處理。
二者區別就是對於_actions.ActionDescriptors.Items(所有的action的信息)的匹配上,原版是先根據路由模板匹配後,再根據ActionDescriptors判斷是否存在對應的Controller和action,而新版是先利用了action信息與路由模板匹配,然後再用請求的URL進行匹配,由於這樣的工作只在第一次請求的時候執行,所以雖然沒有做執行效率上的測試,但感覺應該是比之前快的。