Microsoft.AspNetCore.Routing路由

这篇随笔讲讲路由功能,主要内容在项目Microsoft.AspNetCore.Routing中,可以在GitHub上找到,Routing项目地址

路由功能是大家都很熟悉的功能,使用起来也十分简单,从使用的角度来说可讲的东西不多。不过阅读源码的过程的是个学习的过程,看看顶尖Coder怎么组织代码也是在提升自己。

我们知道现在ASP.NET Core中所有用到的功能都是服务,那么Routing服务是什么时候被添加到依赖注入容器的呢?答案是在StartUp类的ConfigureServices方法中。如果我们随便新建的MVC 6的项目,在VS中模板会自动帮我们添加一些代码,在Startup类中的两个方法我们可以找到一下代码。

        public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
// 省略其他框架服务
services.AddMvc();//添加MVC服务
// Add application services.
//省略自定义应用服务
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

上面两个StartUp类里的方法,一个是注册服务,一个使用服务。从它们调用的方法名上应该能区别出来,AddXXX的是注册服务,UseXXX的是使用服务。这里面用到的方法都是扩展方法,有关路由需要用到的服务都会在AddMvc()中注册,当然这个方法还会注册一大堆其他MVC框架需要用的方法。如果想追踪相关服务的添加语句的话:AddMvc()->AddMvcCore()->ConfigureDefaultServices()->AddRouting(),另外一条线是AddMvc()->AddMvcCore()->AddMvcCoreServices()->TryAddSingleton<MvcDefaultHandler>()。前者是添加与路由模板解析存储相关的服务,后者是处理请求路由的服务,包括请求路由与模板的配对,以及触发相应的Action等等。

我想分别从两条线来解释路由(Routing)的工作流程。

  1. 一条是从开发者注册路由,框架进行解析存储的角度来解释,也就是从MapRoute()这个方法开始。
  2. 另外一条是从解析路由的角度,一个请求的路由如何被处理,然后Invoke相应的Action。

为了解释清楚相关概念,我想先解释一下三个词:Route, Routing, Router。三个词都可以模糊地翻译为路由,但是这样太容易混淆了,懂英语的人应该能一眼就看出其中的不同。

  • Routing是统称,有关路由的总体概念,就可以以Routing来描述,比如我讲的这个项目就叫Routing,或者我们有个添加有关路由服务的方法,就会叫AddRouting();
  • Route是用来描述某一个特定路由的名词,它有具体的数据项,比如说上面的代码我们向框架中添加了一个name是default的Route;它还是一个类名,这个类就是干Route应该干的事情:存储、解析数据之类的;
  • Router可以翻译为“路由者”带路党,是一种Handler的体现,用来处理请求的路由,比如MVC框架默认的MvcRouteHandler就是一个Router。

注册路由

首先从MapRoute()方法说起。这个方法会在内部调用这些代码

1             routeBuilder.Routes.Add(new Route(
2 routeBuilder.DefaultHandler,
3 name,
4 template,
5 new RouteValueDictionary(defaults),
6 new RouteValueDictionary(constraints),
7 new RouteValueDictionary(dataTokens),
8 inlineConstraintResolver));

RouteBuilder会在内部维护一个Route(s)的容器,上面的代码在往容器里面添加新的Route,如果我们用VS默认的模板,那么这里面name="default", template="{controller=Home}/{action=Index}/{id?}",其他参数诸如像defaults,constraints什么的都是没有的。需要说明的是此时routeBuilder.DefaultHandler已经被设置为MvcDefaultHandler,这是在UseMvc()方法中被设置的。在Route类的构造过程中(RouteBase的构造函数中),template字符串会被解析,包括是不是参数名(parameter)啊,是不是字面值(literal)啊,约束是什么,默认值是什么都可以被解析出来。MVC6的路由和MVC5有一点不一样,默认值以及约束可以写在template里,而MVC5的约束和默认值只能再传递一个匿名类型进去:defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }。显然MVC6的路由简单的多,当然你也可以还用以前的方式传递默认值,没有问题。MVC6的这种方法内置默认值和约束的方式在之前的特性路由已经体现了,用来改进传统路由也在情理之中。MvcDefaultHandler会被赋给Route的_target字段,这个字段在未来请求来临时发挥功效。

解析路由的过程就是一个字符串处理的过程,比较复杂,如果要全部讲解篇幅太长。如果做过LeetCode上一些字符串处理的题目的话,看起来会轻松一些,有兴趣的童鞋可以去深究源码。

UseMvc()这个方法和路由有很大的关系,下面来看一下它的源码

        public static IApplicationBuilder UseMvc(this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
{
//省略了检查参数的代码...
var routes = new RouteBuilder(app)
{
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
}; configureRoutes(routes);
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
routes.DefaultHandler,
app.ApplicationServices)); return app.UseRouter(routes.Build());
}

configureRoutes(routes)就是上面解释的调用MapRoute()方法的一个Action委托。DefaultHandler在构造RouteBuilder时被设置为默认的MvcRouteHandler,如果我们想要使用其他Handler,可以模仿这个UseMvc()方法重新写一个拓展方法,传入你的Handler即可。注意下面那句代码:它向RouteBuilder.Routes中添加了用于处理特性路由的Route,并用Insert方法将其添加了到容器的起始位置,这说明特性路由要优先于传统路由。至于为什么要先添加传统路由,是因为开发者可以在传入的configureRoutes这个委托中指定自己的Handler,DefaultHandler有可能在configureRoutes(routes)这段代码中变了,所以特性路由的添加要晚于传统路由。

RouteBuilder.Build()方法会生成一个包含当前Route的集合,这些Route携带了信息(包括传统路由被解析的参数,约束等以及特性路由的元数据等),在上面的例子中,这个容器就两个Route,一个特性路由,一个name="default"的传统路由。最后app.UseRouter会向ApplicationBuilder中添加一个类型为RouterMiddleware的中间件。此时整个有关路由的第一条工作流程就到此结束了。如果比较一下AddMvc()和UseMvc()会发现前一个方法关系到了非常多的服务,而后一个方法似乎只用到了有关路由的东西,这是因为服务的注册要一起完成,而使用服务可以即时拉取。当应用程序响应请求时,一开始只用到路由服务,假如请求匹配,才会用到有关Controller和Action的服务,到那时候再拉取即可。

UseRouter()方法最终会调用ApplicationBuilder.Use()方法,RouterMiddleware的信息最终会以委托的方式存储在ApplicationBuilder中,有关这方面的流程可以参阅我之前的文章:Microsoft.AspNetCore.Hosting

处理请求路由

上面说到我们注册路由时,路由的信息在UseRouter()方法调用时是以Func<RequesetDelegate, RequestDelegate>方式存在。每一个中间件最初都是一种形态,利用这种方式,可以把在程序中用到的中间件构造成一种委托链,最后可以构造出一个跟使用顺序有关的RequestDelegate:即请求管道。Routing是请求管道中的一部分,假如请求到达routing区域,则相关的RequestDelegate就会被触发,利用反射构造出RouteMiddleware这个类,然后调用它的Invoke方法来处理有关路由的事务。

先来看看RouteMiddleware的Invoke方法。

        public async Task Invoke(HttpContext httpContext)
{
var context = new RouteContext(httpContext);//构造一个路由上下文,三个属性:HttpContext,Handler(一个委托),RouteData
context.RouteData.Routers.Add(_router); await _router.RouteAsync(context);//这里的_router默认是MvcRouteHandler 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);
}
}

整个方法的逻辑就是:

  1. 构造一个路由上下文,此时context.Handler=null;
  2. 由MvcRouteHandler.RouteAsync()方法来处理请求(注意此时请求的路由信息在HttpContext中,而现在它又是RouteContext的一个属性);
  3. 第二步的工作就是筛选最合适的Action,并把相应的委托赋给RouteContext.Handler;
  4. 如果RouteContext.Handler为null,说明匹配失败,路由工作到此结束,直接Invoke下一个Middleware;
  5. 如果到这一步,说明路由匹配成功,先把路由信息记录到HttpContext,然后调用RouteContext.Handler,触发有关Action的相关操作,那就是另一个主题了;
  6. RouteContext只在这个方法出现,到第五步时,上下文又变回了HttpContext,注意这个方法传入的时候就是HttpContext。就像帅哥HttpContext到了一个叫路由的地方,撩了一个叫RouteContext的妹子,等它离开时就把它抛弃了。

接下来看一下MvcRouteHandler.RouteAsync()方法

 1         public Task RouteAsync(RouteContext context)
2 {
3 //省略null检查
4 var actionDescriptor = _actionSelector.Select(context);//关键!!找到最优的Action,并返回一个携带相关信息的数据类
5 if (actionDescriptor == null)
6 {
7 _logger.NoActionsMatched();
8 return TaskCache.CompletedTask;
9 }
10 //省略action有默认值情况的处理
11 context.Handler = (c) => InvokeActionAsync(c, actionDescriptor);//关键!!将RouteContext的Handler属性置为相应的处理方法
12 return TaskCache.CompletedTask;
13 }
14
15 private async Task InvokeActionAsync(HttpContext httpContext, ActionDescriptor actionDescriptor)
16 {
17 var routeData = httpContext.GetRouteData();//在RouteMiddleWare.Invoke()时候留下来的RouteData
18 try
19 {
20 _diagnosticSource.BeforeAction(actionDescriptor, httpContext, routeData);
21
22 using (_logger.ActionScope(actionDescriptor))//日志记录
23 {
24 _logger.ExecutingAction(actionDescriptor);
25
26 var startTimestamp = _logger.IsEnabled(LogLevel.Information) ? Stopwatch.GetTimestamp() : 0;
27
28 var actionContext = new ActionContext(httpContext, routeData, actionDescriptor);//根据相应的数据构造ActionContext上下文
29
30 //省略部分非主要逻辑代码
31
32 var invoker = _actionInvokerFactory.CreateInvoker(actionContext);//构建一个有关Action处理的类型
33
34 await invoker.InvokeAsync();//触发Action处理
35
36 _logger.ExecutedAction(actionDescriptor, startTimestamp);
37 }
38 }
39 finally
40 {
41 _diagnosticSource.AfterAction(actionDescriptor, httpContext, routeData);
42 }
43 }

注释已经解释的比较详细了。通过RouteContext选出最优的ActionDescriptor,我一笔带过了,不过这个过程与注册路由时候的解析一下,比较复杂。涉及到决策树之类的内容,感兴趣的同学可以深究。如果确实有Action匹配的话,RouteContext.Handler会被设置为相应的匿名方法。接着控制权交回给RouteMiddleware,然后触发InvokeActionAsync()方法,RouteContext的使命就此结束。

从InvokeActionAsync()方法中可以看出,框架根据相应的ActionDescriptor生成相应的ActionContext,之后进行有关Controller和Action的动作。撩完RouteContext就该轮到ActionContextle。

总结

  1. 注册路由时,我们传入的路由模板(TemplateString)会在构造Route的时候被解析,参数名字、默认值以及约束都能在模板中被解析出来;
  2. MVC框架默认的路由Handler是MvcRouteHandler,它会在UseMvc()方法中自动添加;
  3. 注册路由的这些信息最终和其他服务一样,会变成Application委托链(中间件)的一部分;
  4. 解析请求路由时,路由的信息由HttpContext携带着;
  5. 假如MvcRouteHandler能解析请求的路由,就会触发有关Action的处理;否则路由过程结束,控制权交给下一个Middleware;也就是说,路由的RouterMiddleware会负责有关Action处理的部分。
  6. HttpContext是渣男,撩了一个又一个XXXContext。

Microsoft.AspNetCore.Routing路由的更多相关文章

  1. ASP.NET Core 源码阅读笔记(5) ---Microsoft.AspNetCore.Routing路由

    这篇随笔讲讲路由功能,主要内容在项目Microsoft.AspNetCore.Routing中,可以在GitHub上找到,Routing项目地址. 路由功能是大家都很熟悉的功能,使用起来也十分简单,从 ...

  2. ASP.NET Core MVC 源码学习:Routing 路由

    前言 最近打算抽时间看一下 ASP.NET Core MVC 的源码,特此把自己学习到的内容记录下来,也算是做个笔记吧. 路由作为 MVC 的基本部分,所以在学习 MVC 的其他源码之前还是先学习一下 ...

  3. 错误记录——fail: Microsoft.AspNetCore.Server.Kestrel[13]

    fail: Microsoft.AspNetCore.Server.Kestrel[13] Connection id "0HLPN4417RVEM", Request id &q ...

  4. 解读ASP.NET 5 & MVC6系列(11):Routing路由

    新版Routing功能介绍 在ASP.NET 5和MVC6中,Routing功能被全部重写了,虽然用法有些类似,但和之前的Routing原理完全不太一样了,该Routing框架不仅可以支持MVC和We ...

  5. .NET/ASP.NET Routing路由(深入解析路由系统架构原理)

    阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模型的入口 4.ASP.NET Routing 路由对象模型的内部结构 4 ...

  6. .NET/ASP.NET Routing路由(深入解析路由系统架构原理)http://wangqingpei557.blog.51cto.com/1009349/1312422

    阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模型的入口 4.ASP.NET Routing 路由对象模型的内部结构 4 ...

  7. NET/ASP.NET Routing路由(深入解析路由系统架构原理)(转载)

    NET/ASP.NET Routing路由(深入解析路由系统架构原理) 阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模 ...

  8. Routing路由

    Routing路由 新版Routing功能介绍 在ASP.NET 5和MVC6中,Routing功能被全部重写了,虽然用法有些类似,但和之前的Routing原理完全不太一样了,该Routing框架不仅 ...

  9. 一、ASP.NET Routing路由(深入解析路由系统架构原理)

    阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模型的入口 4.ASP.NET Routing 路由对象模型的内部结构 4 ...

随机推荐

  1. MySql 初次安装登陆

    名称:随便写 服务器:127.0.0.1或者localhost 端口:在安装mysql应该看到是3306 用户:root 密码:(默认的是空,如果你设置过自己应该知道) 其他就可以不用设置

  2. hdu 4372 第一类stirling数的应用/。。。好题

    /** 大意: 给定一系列楼房,都在一条水平线上,高度从1到n,从左侧看能看到f个, 从右侧看,能看到b个,问有多少种这样的序列.. 思路: 因为肯定能看到最高的,,那我们先假定最高的楼房位置确定,那 ...

  3. Jsunpack

    http://www.baidu.com/#wd=jsunpack&rsv_bp=0&tn=baidu&rsv_spt=3&ie=utf-8&rsv_sug3= ...

  4. QT VS检测内存泄漏

    测试程序:http://download.csdn.net/detail/ajaxhe/4085447 vld-2.2.3: http://vld.codeplex.com/releases/view ...

  5. 转:C++:从子类访问父类的私有函数

    众所周知,c和c++的数组都是不安全的,因为无论c还是c++都不提供数组边界检查功能,这使得数组溢出成为可能.从某个意义上说,c和c++是一种缺少监督的语言,然而这也正是其魅力所在.c++给予程序员更 ...

  6. fedora 下安装 文泉驿正黑体

    1. 可以到文泉驿正黑体的 主页 http://wenq.org/wqy2/index.cgi?%E9%A6%96%E9%A1%B5 最好能去官网表示一下支持 2. 也可以直接使用命令 :  sudo ...

  7. vs提示“当前不会命中断点,源代码与原始版本不同”的一种解决办法

    将出问题的cpp文件用notepad打开,另存为‘unicode’类型,覆盖源文件即可

  8. OTN交换&amp;P-OTN有效减少100G网络成本(一)

    近年来.网络运营商一直严重依赖基于ROADM的光传送设备,利用固定的点到点WDN联接.利用10G波长在整个城域网和广域网中汇聚及传送client业务.假设这些网络经过精细的设计规划,也能够合理.有效地 ...

  9. Android:基于Eclipse编译调试系统级应用源代码

    一.      概要描述 在使用Eclipse导入android工程源代码以后,我们可以使用ddms调试和跟踪源代码. 本文讲述动态调试源代码和静态调试源代码的两种方法,避免build system. ...

  10. Eucalyptus和Openstack最近版本的改动简单对比

    Eucalyptus3.3四月底发布的,Q4会发布3.4,最新代码都可以在Github上获得 3.3里主要加入的东西有: Auto Scaling 自动扩展,虚拟机资源随需要实时变更 ELB 负载均衡 ...