剖析ASP.NET Core(Part 4)- 调用MVC中间件(译)
原文: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 = ; 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 > )
{
routers = new List<IRouter>(count.Value);
for (var i = ; i < count.Value; i++)
{
routers.Add(_routers[i]);
}
} var snapshot = new RouteDataSnapshot(
this,
_dataTokens?.Count > ? new RouteValueDictionary(_dataTokens) : null,
routers,
_values?.Count > ? 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 > )
{
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,
,
"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有一个可用于处理请求中的路由和路由数据的动作,则使用此处理程序来处理请求并提供响应。
剖析ASP.NET Core(Part 4)- 调用MVC中间件(译)的更多相关文章
- 剖析ASP.NET Core(Part 2)- AddMvc(译)
原文:https://www.stevejgordon.co.uk/asp-net-core-mvc-anatomy-addmvccore发布于:2017年3月环境:ASP.NET Core 1.1 ...
- 剖析ASP.NET Core(Part 3)- UseMvc(译)
原文:https://www.stevejgordon.co.uk/asp-net-core-anatomy-part-3-addmvc 发布于:2017年4月环境:ASP.NET Core 1.1 ...
- ASP.NET Core中使用自定义MVC过滤器属性的依赖注入
除了将自己的中间件添加到ASP.NET MVC Core应用程序管道之外,您还可以使用自定义MVC过滤器属性来控制响应,并有选择地将它们应用于整个控制器或控制器操作. ASP.NET Core中常用的 ...
- ASP.NET Core中使用默认MVC路由
ASP.NET Core里Route这块的改动不大,只是一些用法上有了调整,提供了一些更加简洁的语法. 而对于自定义路由的支持当然也是没有问题的,这个功能应该是从MVC1.0版本就已经有这个功能. 先 ...
- Asp.Net Core 第06局:中间件
总目录 前言 本文介绍Asp.Net Core 中间件. 环境 1.Visual Studio 2017 2.Asp.Net Core 2.2 开局 第一手:中间件概述 1.中间件:添加到应用 ...
- 剖析ASP.NET Core MVC(Part 1)- AddMvcCore(译)
原文:https://www.stevejgordon.co.uk/asp-net-core-mvc-anatomy-addmvccore发布于:2017年3月环境:ASP.NET Core 1.1 ...
- ASP.NET Core Razor页面 vs MVC
作为.NET Core 2.0发行版的一部分,还有一些ASP.NET的更新.其中之一是添加了一个新的Web框架来创建"页面",而不需要复杂的ASP.NET MVC.新的Razor页 ...
- asp.net core轻松入门之MVC中Options读取配置文件
接上一篇中讲到利用Bind方法读取配置文件 ASP.NET Core轻松入门Bind读取配置文件到C#实例 那么在这篇文章中,我将在上一篇文章的基础上,利用Options方法读取配置文件 首先注册MV ...
- ASP.NET Core 中的那些认证中间件及一些重要知识点
前言 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系列(一,二,三)奠定一下基础. 有关于 Authentication 的知识太广,所以本篇介绍几个在 A ...
随机推荐
- FineReport——JS二次开发(复选框全选)
在进行查询结果选择的时候,我们经常会用到复选框控件,对于如何实现复选框全选,基本思路: 在复选框中的初始化事件中把控件加入到一个全局数组里,然后在全选复选框里对数组里的控件进行遍历赋值. 首先,定义两 ...
- FineReport——权限分配以及自定义首页
权限分配可以有两种方法,第一种方法是根据部门职位分配权限,第二种是根据角色分配权限: FR自带有三个JQ对象,用以保存用户名参数/角色参数/部门参数——$fr_username/$fr_authori ...
- Restful Framework (二)
目录 一.认证 二.权限 三.限制访问频率 四.总结 一.认证(补充的一个点) 回到顶部 认证请求头 #!/usr/bin/env python # -*- coding:utf-8 -*- from ...
- TP-LINK路由器设置内网的一台电脑在外网可以远程操控
1.[IP和MAC绑定]--[静态ARP绑定设置]对MAC和IP进行绑定 2.[转发规则]--[DMZ主机],选择启用并把刚才设置的内网IP填入 3.直接访问路由器的外网IP就可以直接访问绑定的MAC ...
- Java学习笔记(八)——java多线程
[前面的话] 实际项目在用spring框架结合dubbo框架做一个系统,虽然也负责了一块内容,但是自己的能力还是不足,所以还需要好好学习一下基础知识,然后做一些笔记.希望做完了这个项目可以写一些dub ...
- 使用JavaScript实现长方形、直角三角形、平行四边形、等腰三角形、倒三角、数字三角形
[循环嵌套的规律] 1.外层循环控制行数,内层循环控制每行中元素的个数. [图形题思路] 1.确定图形有几行,行数即为外层循环次数: 2.确定每行中有几种元素组成,有几种元素表示有几 ...
- IOS中div contenteditable=true无法输入 fastclick.js在点击一个可输入的div时,ios无法正常唤起输入法键盘
原文地址: https://blog.csdn.net/u010377383/article/details/79838562 前言 为了提升移动端click的响应速度,使用了fastclick.js ...
- php漏洞tips
1.php后缀限制 'php,php3,php4,php5,php6,php7,phpsh,inc,phtml','PHT'; 2.php木马 <?php echo shell_exec($_G ...
- 安卓 内存泄漏检测工具 LeakCanary 使用
韩梦飞沙 yue31313 韩亚飞 han_meng_fei_sha 313134555@qq.com 配置 build.gradle dependencies { debugCompile 'com ...
- 【最短路】【位运算】It's not a Bug, it's a Feature!
[Uva658] It's not a Bug, it's a Feature! 题目略 UVA658 Problem PDF上有 试题分析: 本题可以看到:有<=20个潜在的BUG,那 ...