原文:https://www.stevejgordon.co.uk/invoking-mvc-middleware-asp-net-core-anatomy-part-4 發佈於:2017年5月環境:ASP.NET Core 1.1 本系列前三篇文章我們研究了AddMvcCore,AddMvc和Us ...
原文:https://www.stevejgordon.co.uk/invoking-mvc-middleware-asp-net-core-anatomy-part-4
發佈於:2017年5月
環境:ASP.NET Core 1.1
本系列前三篇文章我們研究了AddMvcCore,AddMvc和UseMvc作為程式啟動的一部分所發生的事情。一旦MVC服務和中間件註冊到我們的ASP.NET Core應用程式中,MVC就可以開始處理HTTP請求。
本文我想介紹當一個請求流入MVC中間件時所發生的初始步驟。這是一個相當複雜的領域,要分開來敘述。我將它拆分成我認為合理的流程代碼,忽略某些行為分支和細節,讓本文容易理解。一些我忽略的實現細節我會重點指出,併在以後的文章中論述。
和先前一樣,我使用原始的基於project.json(1.1.2)的MVC源碼,因為我還沒有找到一種可靠的方法來調試MVC源碼,尤其是包含其他組件如路由。
好了讓我們開始,看看MVC如何通過一個有效路由來匹配一個請求,並且最終執行一個可處理請求的動作(action)。快速回顧一下, ASP.NET Core程式在Startup.cs文件中配置了中間件管道(middleware pipeline),它定義了請求處理的流程。每個中間件將被按照一定順序調用,直到某個中間件確定能提供適當的響應。
MvcSandbox項目的配置方法如下:
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { app.UseDeveloperExceptionPage(); app.UseStaticFiles(); loggerFactory.AddConsole(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
假設之前的中間件(UseDeveloperExceptionPage,UseStaticFiles)都沒有處理請求,我們通過調用UseMvc來到達MVC管道和中間件。一旦請求到達MVC管道,我們碰到的中間件就是 RouterMiddleware。它的調用方法如下:
public async Task Invoke(HttpContext httpContext) { var context = new RouteContext(httpContext); context.RouteData.Routers.Add(_router); await _router.RouteAsync(context); 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); } }
Invoke所做的第一件事是將當前的HttpContext對象傳遞給構造函數,構造一個新的RouteContext。
public RouteContext(HttpContext httpContext) { HttpContext = httpContext; RouteData = new RouteData(); }
HttpContext作為參數傳遞給RouteContext,然後生成一個新的RouteData實例對象。
返回Invoke方法,註入的IRouter(本例是在UseMvc設置期間創建的RouteCollection)被添加到RouteContext.RouteData對象上的IRouter對象列表中。值得強調的是RouteData對象為其集合使用了延遲初始化模式,只有在它們被調用是才分配它們。這種模式體現了在如ASP.NET Core等大型框架中必須考慮的性能。
例如,下麵是Routers如何定義屬性:
public IList<IRouter> Routers { get { if (_routers == null) { _routers = new List<IRouter>(); } return _routers; } }
第一次訪問該屬性時,一個新的List將分配和存儲到一個內部欄位。
返回Invoke方法,在RouteCollection上調用RouteAsync:
public async virtual Task RouteAsync(RouteContext context) { // Perf: We want to avoid allocating a new RouteData for each route we need to process. // We can do this by snapshotting the state at the beginning and then restoring it // for each router we execute. var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null); for (var i = 0; i < Count; i++) { var route = this[i]; context.RouteData.Routers.Add(route); try { await route.RouteAsync(context); if (context.Handler != null) { break; } } finally { if (context.Handler == null) { snapshot.Restore(); } } } }
首先RouteAsync通過RouteCollection創建一個RouteDataSnapshot。如註釋所示,不是每次路由處理都會分配一個RouteData對象。為避免這種情況,RouteData對象的快照會被創建一次,並允許每次迭代時重置它。這是ASP.NET Core團隊對性能考慮的另一個例子。
snapshot通過調用RouteData類中的PushState實現:
public RouteDataSnapshot PushState(IRouter router, RouteValueDictionary values, RouteValueDictionary dataTokens) { // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in // Array.CopyTo inside the List(IEnumerable<T>) constructor. List<IRouter> routers = null; var count = _routers?.Count; if (count > 0) { routers = new List<IRouter>(count.Value); for (var i = 0; i < count.Value; i++) { routers.Add(_routers[i]); } } var snapshot = new RouteDataSnapshot( this, _dataTokens?.Count > 0 ? new RouteValueDictionary(_dataTokens) : null, routers, _values?.Count > 0 ? new RouteValueDictionary(_values) : null); if (router != null) { Routers.Add(router); } if (values != null) { foreach (var kvp in values) { if (kvp.Value != null) { Values[kvp.Key] = kvp.Value; } } } if (dataTokens != null) { foreach (var kvp in dataTokens) { DataTokens[kvp.Key] = kvp.Value; } } return snapshot; }
首先創建一個List<IRoute>。為了儘可能的保持性能,只有在包含RouteData路由器的私有欄位(_routers)中至少有一個IRouter時,才會分配一個列表。如果是這樣,將使用正確的大小(特定大小)來創建一個新的列表,避免內部Array.CopyTo調用時改變底層數組的大小。從本質上講,這個方法現在有一個複製的RouteData的內部IRouter列表實例。
接下來 RouteDataSnapshot對象被創建。RouteDataSnapshot定義為結構體(struct)。它的構造函數簽名如下所示:
public RouteDataSnapshot( RouteData routeData, RouteValueDictionary dataTokens, IList<IRouter> routers, RouteValueDictionary values)
RouteCollection為所有參數調用PushState,其值為空值。在使用非空IRoute參數調用PushState方法的情況下,它會被添加到路由器列表中。值和DataTokens以相同的方式處理。如果PushState參數中包含任何參數,則會更新RouteData上的Values和DataTokens屬性中的相應項。
最後,snapshot返回到RouteCollection中的RouteAsync。
接下來一個for迴圈開始,直到達到屬性數量值。 Count只是暴露了RouteCollection上的Routers(List <IRouter>)數量。
在迴圈內部,它首先通過值迴圈(value of the loop)獲得一個route。如下:
public IRouter this[int index] { get { return _routes[index]; } }
這隻是從列表中返回特定索引的IRouter。在MvcSandbox示例中,索引為0的第一個IRouter是AttributeRoute。
在Try / Finally塊中,在IRouter(AttributeRoute)上調用RouteAsync方法。我們最終希望找到一個匹配路由數據(route data)的合適的Handler(RequestDelegate)。
我們將在後面的文章中深入研究AttributeRoute.RouteAsync方法內部發生的事情,因為在那裡發生了很多事情,目前我們還沒有在MvcSandbox中定義任何AttributeRoutes。在我們的例子中,因為沒有定義AttributeRoutes,所以Handler值保持為空。
在finally塊內部,當Handler為空時,在RouteDataSnapshot上調用Restore方法。此方法將在創建快照時將RouteData對象恢復到其狀態。由於RouteAsync方法在處理過程中可能已經修改了RouteData,因此可以確保我們回到對象的初始狀態。
在MvcSandbox示例中,列表中的第二個IRouter是名為“default”的路由,它是Route的一個實例。這個類不覆蓋基類上的RouteAsync方法,因此將調用RouteBase抽象類中的代碼。
public virtual Task RouteAsync(RouteContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } EnsureMatcher(); EnsureLoggers(context.HttpContext); var requestPath = context.HttpContext.Request.Path; if (!_matcher.TryMatch(requestPath, context.RouteData.Values)) { // If we got back a null value set, that means the URI did not match return TaskCache.CompletedTask; } // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all // created lazily. if (DataTokens.Count > 0) { MergeValues(context.RouteData.DataTokens, DataTokens); } if (!RouteConstraintMatcher.Match( Constraints, context.RouteData.Values, context.HttpContext, this, RouteDirection.IncomingRequest, _constraintLogger)) { return TaskCache.CompletedTask; } _logger.MatchedRoute(Name, ParsedTemplate.TemplateText); return OnRouteMatched(context); }
首先調用私有方法EnsureMatcher,如下所示:
private void EnsureMatcher() { if (_matcher == null) { _matcher = new TemplateMatcher(ParsedTemplate, Defaults); } }
這個方法將實例化一個新的TemplateMatcher,傳入兩個參數。同樣,這似乎是一個分配優化(allocation optimisation),只有在傳遞給構造函數的屬性可用時,才會創建此對象。
如果你想知道這些屬性設置在哪裡,是發生在RouteBase類的構造函數內部。這個構造函數是在預設路由被調用時,由MvcSandbox啟動類的配置方法調用UseMvc擴展方法中的MapRoute而創建的。
RouteBase.RouteAsync方法內部,下一步調用的是EnsureLoggers:
private void EnsureLoggers(HttpContext context) { if (_logger == null) { var factory = context.RequestServices.GetRequiredService<ILoggerFactory>(); _logger = factory.CreateLogger(typeof(RouteBase).FullName); _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } }
此方法從RequestServices獲取ILoggerFactory實例,並使用它來設置兩個ILogger。第一個是RouteBase類本身,第二個將由RouteConstraintMatcher使用。
接下來它存儲一個局部變數,該變數持有從HttpContext中獲取的請求的路徑。
再往下,調用TemplateMatcher中的TryMatch,傳入請求路徑以及任何路由數據。我們將在另一篇文章中深入分析TemplateMathcer內部。現在,假設TryMatch返回true,我們的例子中就是這種情況。如果不匹配(TryMatch返回false)將返回TaskCache.CompletedTask,只是將任務(Task)設置為完成。
再往下,如果有任何DataTokens(RouteValueDictionary對象)設置,則context.RouteData.DataTokens會按需更新。正如註釋中提到的,只有在值被實際更新的時候才會這樣做。RouteData中的屬性DataTokens只是在其第一次被調用(lazily instantiated 延遲實例化)時創建。因此,在沒有更新的值時調用它可能會冒險分配一個新的不需要的RouteValueDictionary。
在我們使用的MvcSandbox中,沒有DataTokens,所以MergeValues不會被調用。但為了完整性,它的代碼如下:
private static void MergeValues(RouteValueDictionary destination, RouteValueDictionary values) { foreach (var kvp in values) { // This will replace the original value for the specified key. // Values from the matched route will take preference over previous // data in the route context. destination[kvp.Key] = kvp.Value; } }
當被調用時,它從RouteBase類的DataTokens參數中的RouteValueDictionary中迴圈任何值,並更新context.RouteData.DataTokens屬性上匹配鍵的目標值。
接下來,返回RouteAsync方法,RouteConstraintMatcher.Match被調用。這個靜態方法遍歷任何傳入的IRouteContaints,並確定它們是否全部滿足條件。Route constraints允許使用附加的匹配規則。例如,路由參數可以被約束為僅使用整數。我們的示列中沒有約束,因此返回true。我們將在另一篇文章中查看帶有約束的URL。
ILoger的擴展方法MatchedRoute生成了一個logger項。這是一個有趣的模式,可以根據特定需求重覆使用更複雜的日誌消息格式。
這個類的代碼:
internal static class TreeRouterLoggerExtensions { private static readonly Action<ILogger, string, string, Exception> _matchedRoute; static TreeRouterLoggerExtensions() { _matchedRoute = LoggerMessage.Define<string, string>( LogLevel.Debug, 1, "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'."); } public static void MatchedRoute( this ILogger logger, string routeName, string routeTemplate) { _matchedRoute(logger, routeName, routeTemplate, null); } }
當TreeRouterLoggerExtensions類第一次被構造時定義了一個action代理,該代理定義了日誌消息該如何格式化。
當MatchRoute擴展方法被調用時,將路由名和模板字元串作為參數傳遞。然後將它們傳遞給_matchedRoute動作(Action)。該動作使用提供的參數創建調試級別的日誌項。在visual studio中調試時,你會看到它出現在輸出(output)視窗中。
返回RouteAsync;OnRouteMatched方法被調用。這被定義為RouteBase上的一個抽象方法,所以實現來自繼承類。在我們的例子中,它是Route類。OnRouteMatched的重寫方法如下:
protected override Task OnRouteMatched(RouteContext context) { context.RouteData.Routers.Add(_target); return _target.RouteAsync(context); }
其名為_target的IRouter欄位被添加到context.RouteData.Routers列表中。在這種情況下,它是MVC的預設處理程式MvcRouteHandler。
然後在MvcRouteHandler上調用RouteAsync方法。該方法的細節相當重要,所以我保留下來作為未來討論的主題。總之,MvcRouteHandler.RouteAsync將嘗試建立一個合適的處理請求的操作方法。有一件重要的事情要知道,當一個動作被髮現時,RouteContext上的Handler屬性是通過lambda表達式定義的。我們可以再次深入該代碼,但總結一下,RequestDelegate是一個接受HttpContext並且可以處理請求的函數。
回到RouterMiddleware上的invoke方法,我們可能已經有一個MVC已確定的處理程式(handler)可以處理請求。如果沒有,則調用_logger.RequestDidNotMatchRoutes()。這是我們前面探討的logger擴展風格的另一個例子。他將添加一條調試信息,指示路由不匹配。在這種情況下,ASP.NET中間件管道中的下一個RequestDelegate被調用,因為MVC已經確定它不能處理請求。
在客戶端web/api應用程式的常規配置中,在UseMvc之後不會再有任何中間件的定義。在這種情況下,但我們到達管道末端時,ASP.NET Core返回一個預設的404未找到的HTTP狀態碼響應。
在我們有一個可以處理請求路由的處理程式的情況下,我們將進入Invoke方法else塊。
一個新的RoutingFeature被實例化並被添加到HttpContext的Features集合中。簡單地說,features(特性)是ASP.NET Core的一個概念,它允許伺服器定義接收請求的特征。這包括數據在整個請求生命周期中的流動。像RouterMiddleware這樣的中間件可以添加/修改特征集合,並可以將其用作通過請求傳遞數據的機制。在我們的例子中,RouteContext中的RouteData作為IRoutingFeature定義的一部分添加,以便其他中間件和請求處理程式可以使用它。
然後該方法調用Handler RequestDelegate,它將最終通過適當的MVC動作(action)來處理請求。到此為止,本文就要結束了。接下來會發生什麼,以及我跳過的項目將構成本系列的下一部分。
小結:
我們已經看到MVC是如何作為中間件管道的一部分被調用的。一旦調用,MVC RouterMiddleware確定MVC是否知道如何處理傳入的請求路徑和值。如果MVC有一個可用於處理請求中的路由和路由數據的動作,則使用此處理程式來處理請求並提供響應。