详解ASP.NET Core MVC 源码学习:Routing 路由
前言
最近打算抽时间看一下ASP.NETCoreMVC的源码,特此把自己学习到的内容记录下来,也算是做个笔记吧。
路由作为MVC的基本部分,所以在学习MVC的其他源码之前还是先学习一下路由系统,ASP.NETCore的路由系统相对于以前的Mvc变化很大,它重新整合了WebApi和MVC。
路由源码地址:Routing-dev_jb51.rar
路由(Routing)功能介绍
路由是MVC的一个重要组成部分,它主要负责将接收到的Http请求映射到具体的一个路由处理程序上,在MVC中也就是说路由到具体的某个Controller的Action上。
路由的启动方式是在ASP.NETCoreMVC应用程序启动的时候作为一个中间件来启动的,详细信息会在下一篇的文章中给出。
通俗的来说就是,路由从请求的URL地址中提取信息,然后根据这些信息进行匹配,从而映射到具体的处理程序上,因此路由是基于URL构建的一个中间件框架。
路由还有一个作用是生成响应的的URL,也就是说生成一个链接地址可以进行重定向或者链接。
路由中间件主要包含以下几个部分:
- URL匹配
- URL生成
- IRouter接口
- 路由模板
- 模板约束
GettingStarted
ASP.NETCoreRouting主要分为两个项目,分别是Microsoft.AspNetCore.Routing.Abstractions,Microsoft.AspNetCore.Routing。前者是一个路由提供各功能的抽象,后者是具体实现。
我们在阅读源码的过程中,我建议还是先大致浏览一下项目结构,然后找出关键类,再由入口程序进行阅读。
Microsoft.AspNetCore.Routing.Abstractions
大致看完整个结构之后,我可能发现了几个关键的接口,理解了这几个接口的作用后能够帮助我们在后续的阅读中事半功倍。
IRouter
在Microsoft.AspNetCore.Routing.Abstractions中有一个关键的接口就是IRouter:
publicinterfaceIRouter { TaskRouteAsync(RouteContextcontext); VirtualPathDataGetVirtualPath(VirtualPathContextcontext); }
这个接口主要干两件事情,第一件是根据路由上下文来进行路由处理,第二件是根据虚拟路径上下文获取VirtualPathData。
IRouteHandler
另外一个关键接口是IRouteHandler,根据名字可以看出主要是对路由处理程序机型抽象以及定义的一个接口。
publicinterfaceIRouteHandler { RequestDelegateGetRequestHandler(HttpContexthttpContext,RouteDatarouteData); }
它返回一个RequestDelegate的一个委托,这个委托可能大家比较熟悉了,封装了处理Http请求的方法,位于Microsoft.AspNetCore.Http.Abstractions中,看过我之前博客的同学应该比较了解。
这个接口中GetRequestHandler方法有两个参数,第一个是HttpContext,就不多说了,主要是来看一下第二个参数RouteData。
RouteData,封装了当前路由中的数据信息,它包含三个主要属性,分别是DataTokens,Routers,Values。
DataTokens:是匹配的路径中附带的一些相关属性的键值对字典。
Routers:是一个Ilist
Values:当前路由的路径下包含的键值。
还有一个RouteValueDictionary,它是一个集合类,主要是用来存放路由中的一些数据信息的,没有直接使用IEnumerable
IRoutingFeature
我根据这个接口的命名一眼就看出来了这个接口的用途,还记得我在之前博客中讲述Http管道流程中得时候提到过一个叫工具箱的东西么,这个IRoutingFeature也是其中的一个组成部分。我们看一下它的定义:
publicinterfaceIRoutingFeature { RouteDataRouteData{get;set;} }
原来他只是包装了RouteData,到HttpContext中啊。
IRouteConstraint
这个接口我在阅读的时候看了一下注释,原来路由中的参数参数检查主要是靠这个接口来完成的。
我们都知道在我们写一个RouteUrl地址表达式的时候,有时候会这样写:Route("/Product/{ProductId:long}"),在这个表达式中有一个{ProductId:long}的参数约束,那么它的主要功能实现就是靠这个接口来完成的。
///DefinesthecontractthataclassmustimplementinordertocheckwhetheraURLparameter ///valueisvalidforaconstraint. publicinterfaceIRouteConstraint { boolMatch( HttpContexthttpContext, IRouterroute, stringrouteKey, RouteValueDictionaryvalues, RouteDirectionrouteDirection); }
Microsoft.AspNetCore.Routing
Microsoft.AspNetCore.Routing主要是对Abstractions的一个主要实现,我们阅读代码的时候可以从它的入口开始阅读。
RoutingServiceCollectionExtensions是一个扩展ASP.NETCoreDI的一个扩展类,在这个方法中用来进行ConfigService,Routing对外暴露了一个IRoutingBuilder接口用来让用户添加自己的路由规则,我们来看一下:
publicstaticIApplicationBuilderUseRouter(thisIApplicationBuilderbuilder,Actionaction) { //...略 //构造一个RouterBuilder提供给action委托宫配置 varrouteBuilder=newRouteBuilder(builder); action(routeBuilder); //调用下面的一个扩展方法,routeBuilder.Build()见下文 returnbuilder.UseRouter(routeBuilder.Build()); } publicstaticIApplicationBuilderUseRouter(thisIApplicationBuilderbuilder,IRouterrouter) { //...略 returnbuilder.UseMiddleware (router); }
routeBuilder.Build()构建了一个集合RouteCollection,用来保存所有的IRouter处理程序信息,包括用户配置的Router。
RouteCollection本身也实现了IRouter,所以它也具有路由处理的能力。
Routing中间件的入口是RouterMiddleware这个类,通过这个中间件注册到Http的管道处理流程中,ASP.NETCoreMVC会把它默认的作为其配置项的一部分,当然你也可以把Routing单独拿出来使用。
我们来看一下Invoke方法里面做了什么,它位于RouterMiddleware.cs文件中。
publicasyncTaskInvoke(HttpContexthttpContext) { varcontext=newRouteContext(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)]=newRoutingFeature() { RouteData=context.RouteData, }; awaitcontext.Handler(context.HttpContext); } }
首先,通过httpContext来初始化路由上下文(RouteContext),然后把用户配置的路由规则添加到路由上下文RouteData中的Routers中去。
接下来await_router.RouteAsync(context),就是用到了IRouter接口中的RouteAsync方法了。
我们接着跟踪RouteAsync这个函数,看其内部都做了什么?我们又跟踪到了RouteCollection.cs这个类:
我们看一下RouteAsync的流程:
publicasyncvirtualTaskRouteAsync(RouteContextcontext) { varsnapshot=context.RouteData.PushState(null,values:null,dataTokens:null); for(vari=0;i我觉得这个类,包括函数设计的很巧妙,如果是我的话,我不一定能够想的出来,所以我们通过看源码也能够学到很多新知识。
为什么说设计的巧妙呢?RouteCollection继承了IRouter但是并没有具体的对路由进行处理,而是通过循环来重新将路由上下文分发的具体的路由处理程序上。我们来看一下他的流程:
1、为了提高性能,创建了一个RouteDataSnapshot快照对象,RouteDataSnapshot是一个结构体,它存储了Route中的路由数据信息。
2、循环当前RouteCollection中的Router,添加到RouterContext里的Routers中,然后把RouterContext交给Router来处理。
3、当没有处理程序处理当前路由snapshot.Restore()重新初始化快照状态。
接下来就要看具体的路由处理对象了,我们从RouteBase开始。
1、RouteBase的构造函数会初始化RouteTemplate,Name,DataTokens,Defaults.
Defaults是默认配置的路由参数。2、RouteAsync中会进行一系列检查,如果没有匹配到URL对应的路由就会直接返回。
3、使用路由参数匹配器RouteConstraintMatcher进行匹配,如果没有匹配到,同样直接返回。
4、如果匹配成功,会触发OnRouteMatched(RouteContextcontext)函数,它是一个抽象函数,具体实现位于Route.cs中。
然后,我们再继续跟踪到Route.cs中的OnRouteMatch,一起来看一下:
protectedoverrideTaskOnRouteMatched(RouteContextcontext) { context.RouteData.Routers.Add(_target); return_target.RouteAsync(context); }_target值得当前路由的处理程序,那么具体是哪个路由处理程序呢?我们一起探索一下。
我们知道,我们创建路由一共有MapRoute,MapGet,MapPost,MapPut,MapDelete,MapVerb...等等这写方式,我们分别对应说一下每一种它的路由处理程序是怎么样的,下面是一个示例:
app.UseRouter(routes=>{ routes.DefaultHandler=newRouteHandler((httpContext)=> { varrequest=httpContext.Request; returnhttpContext.Response.WriteAsync($""); }); routes .MapGet("api/get/{id}",(request,response,routeData)=>{}) .MapMiddlewareRoute("api/middleware",(appBuilder)=> appBuilder.Use((httpContext,next)=>httpContext.Response.WriteAsync("Middleware!") )) .MapRoute( name:"AllVerbs", template:"api/all/{name}/{lastName?}", defaults:new{lastName="Doe"}, constraints:new{lastName=newRegexRouteConstraint(newRegex("[a-zA-Z]{3}",RegexOptions.CultureInvariant,RegexMatchTimeout))}); });按照上面的示例解释一下,
MapRoute:使用这种方式的话,必须要给DefaultHandler赋值处理程序,否则会抛出异常,通常情况下我们会使用RouteHandler类。
MapVerb:MapPost,MapPut等等都和它类似,它将处理程序作为一个RequestDelegate委托提供了出来,也就是说我们实际上在自己处理HttpContext的东西,不会经过RouteHandler处理。
MapMiddlewareRoute:需要传入一个IApplicationBuilder委托,实际上IApplicationBuilderBuild之后也是一个RequestDelegate,它会在内部new一个RouteHandler类,然后调用的MapRoute。
这些所有的矛头都指向了RouteHandler,我们来看看RouteHandler吧。
publicclassRouteHandler:IRouteHandler,IRouter { //...略 publicTaskRouteAsync(RouteContextcontext) { context.Handler=_requestDelegate; returnTaskCache.CompletedTask; } }什么都没干,仅仅是将传入进来的RequestDelegate赋值给了RouteContext的处理程序。
最后,代码会执行到RouterMiddleware类中的Invoke方法的最后一行awaitcontext.Handler(context.HttpContext),这个时候开始调用Handler委托,执行用户代码。
总结
我们来总结一下以上流程:
首先传入请求会到注册的RouterMiddleware中间件,然后它RouteAsync按顺序调用每个路由上的方法。当一个请求到来的时候,IRouter实例选择是否处理已经设置到RouteContextHandler上的一个非空RequestDelegate。如果Route已经为该请求设置处理程序,则路由处理会中止并且开始调用设置的Hanlder处理程序以处理请求。如果当前请求尝试了所有路由都没有找到处理程序的话,则调用next,将请求交给管道中的下一个中间件。
关于路由模板和参数约束源码处理流程就不一一说了,有兴趣可以直接看下源码。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。