原文:https://www.stevejgordon.co.uk/invoking-mvc-middleware-asp-net-core-anatomy-part-4
發佈於:2017年5月
環境:ASP.NET Core 1.1web
本系列前三篇文章咱們研究了AddMvcCore,AddMvc和UseMvc做爲程序啓動的一部分所發生的事情。一旦MVC服務和中間件註冊到咱們的ASP.NET Core應用程序中,MVC就能夠開始處理HTTP請求。json
本文我想介紹當一個請求流入MVC中間件時所發生的初始步驟。這是一個至關複雜的領域,要分開來敘述。我將它拆分紅我認爲合理的流程代碼,忽略某些行爲分支和細節,讓本文容易理解。一些我忽略的實現細節我會重點指出,並在之後的文章中論述。api
和先前同樣,我使用原始的基於project.json(1.1.2)的MVC源碼,由於我尚未找到一種可靠的方法來調試MVC源碼,尤爲是包含其餘組件如路由。數組
好了讓咱們開始,看看MVC如何經過一個有效路由來匹配一個請求,而且最終執行一個可處理請求的動做(action)。快速回顧一下, ASP.NET Core程序在Startup.cs文件中配置了中間件管道(middleware pipeline),它定義了請求處理的流程。每一箇中間件將被按照必定順序調用,直到某個中間件肯定能提供適當的響應。服務器
MvcSandbox項目的配置方法以下:mvc
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。它的調用方法以下:app
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實例對象。async
返回Invoke方法,注入的IRouter(本例是在UseMvc設置期間建立的RouteCollection)被添加到RouteContext.RouteData對象上的IRouter對象列表中。值得強調的是RouteData對象爲其集合使用了延遲初始化模式,只有在它們被調用是才分配它們。這種模式體現了在如ASP.NET Core等大型框架中必須考慮的性能。ide
例如,下面是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有一個可用於處理請求中的路由和路由數據的動做,則使用此處理程序來處理請求並提供響應。