原文: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有一个可用于处理请求中的路由和路由数据的动做,则使用此处理程序来处理请求并提供响应。