Spiga

ASP.NET Core 7 源码阅读4:MVC核心中间件

2023-06-26 16:24:03

一、核心流程概述

  1. 执行AddMVC() 将MVC的核心服务注册到容器,且通过反射扫描把相关dll收集起来
  2. 执行app.UseRouting() 将EndpointRoutingMiddleware中间件注册到http管道
  3. 执行app. MapControllerRoute() 将本程序集定义的所有Controller-Action-ParameterConversion转换为一个个的ControllerActionDescriptor放到路由中间件的配置对象RouteOptions中,注册并传入EndpointMiddleware中间件注册到http管道中
  4. 请求来了,管道模型Build---没啥动作,倒序执行中间件的实例化
  5. 收到一条http请求,先进入EndpointRoutingMiddleware,首次请求时会完成ControllerActionDescriptor到EndPoint的转化,然后通过DFA算法匹配一个Endpoint,并放到HttpContext中去
  6. 鉴权/授权/其他中间件可以根据根据Endpoint的信息对这个请求进行鉴权授权或其他操作。
  7. EndpointMiddleware中间件执行Endpoint中的RequestDelegate逻辑,即执行FilterController-Action-Result等系列操作(MVC)

二、AddMVC

1. 上帝视角

AddMVC(AddControllersWithViews)是最初发生的,是IOC注册环节的事儿,在管道注册之前,其职责有2个:

  1. 添加MVC各种IOC注册,超多。。
  2. 反射遍历相关程序集,封装成ApplicationPart,供后续使用

2. 源码解读

  1. 从MvcServiceCollectionExtensions开始,绕一圈,最终是AddMvcCore()方法
  2. 先实例化ApplicationPartManager-----拿着项目名字,通过反射加载Dll,将信息封装到ApplciationPartManager的ApplicationParts属性中---扩展点
  3. ConfigureDefaultFeatureProviders(partManager);的调用,这行代码是创建了一个新的ControllerFeatureProvider实例放进了partManager的FeatureProviders属性中,注意这个ControllerFeatureProvider对象在后面遍历ApplicationPart的时候负责找出里面的Controller
  4. ConfigureDefaultServices其实是AddRouting,通过RoutingServiceCollectionExtensions放了一堆Routing的IOC注册
  5. AddMvcCoreServices其实就是添加MVC框架需要的各种服务,东西很多

3. 设计理解

AddMVC其实就是添加MVC各种IOC注册,然后反射遍历相关程序集,封装成ApplicationPart,供后续使用

  1. 小扩展点,就是controller-action可以写在独立类库,完全一样的效果
  2. 动态热插拔,运行时dll能被路由识别?其实要扩展这个ApplicationPart

AddMVC东西多,但说起来简单,细节不用都看

三、EndPoint

1. 开始EndPoint

AddMVC为啥要先dll反射加载全部dll?这里有个关于路由的设计需要理解! MVC用的是路由匹配,是请求来了,再一一去查找合适ControllerAction?还是提前把全部的ControllerAction都收集好,去里面做筛选? ---加载后是dll--- controller-action-parameter-route,得有个对象打包一下这些。

**EndPoint:**终结点,一个抽象的概念,两个核心属性:

  1. RequestDelegate---请求处理动作
  2. Metadata----是各种元数据

EndPoint是路由匹配的结果,当http请求到来时,路由模块就会将请求匹配到某个EndPoint。然后EndPoint这个类封装了action的信息,比如: Controller类型、 Action方法、 [Attribute]情况等。然后就能做到路由匹配和具体执行分开,中间可以放入其他动作,如鉴权授权等

**终极理解:**在程序启动完成后,请求来临时,MVC已经把所有的action转化成了Endpoint并交给“路由” 模块去管理了!如何从ApplicationPart变成EndPoint? ----UseRouting和MapControllerRoute!

2. 中间件顺序问题

UseRouting----MapControllerRoute都是在注册中间件,但因为二者的时空交叉在一起,不能单独看一个中间件了,得按时间顺序先理解3个时间点:

  1. Use: Use中间件—顺序
  2. Build:组装管道模型---倒序的
  3. 请求来了: --顺序的

3. Use环节

中间件第一波是Use,通常是完成基础注册,其实也可以加入很多逻辑

  1. UseRouting()---实例化了一个DefaultEndpointRouteBuilder传到中间件,注册了 EndpointRoutingMiddleware中间件----AddRouting前面已完成

  2. MapControllerRoute()核心是GetOrCreateDataSource,里面发生了大事儿:

    • 里面会构造ControllerActionEndpointDataSourceFactory,这里面最终注册 了ChangeToken到UpdateCollection的订阅---该方法完成ApplicationPart转成 ActionDescriptorCollection
    • 里面有个ControllerActionEndpointDataSource对象的构造函数里面,注意 那个Subscribe(),是注册了一个UpdateEndPoint()方法到ChangeToken,这里会触发一次 UpdateCollection(),完成转变ActionDescriptorCollection

    然后UpdateEndPoint()是完成ActionDescriptorCollection到EndPoint的转换的 ---这里将二者订阅,等于是ActionDescriptorCollection有啥变化,EndPoint就得更新 下--没毛病!

UpdateEndPoint()又是啥时候触发的?

4. Build环节

Build管道模型时,会把所有注册中间件倒序执行一遍,这里用的是Use其实就是简单构造下2个中间件,注意是倒着来的,先EndpointMiddleware构造,再EndpointRoutingMiddleware构造这里没啥东西,但理解下转变时间点:

  1. AddMVC只是扫描Dll---没有其他操作,因为还不知道怎么应用
  2. 中间件组装时是转成ActionDescriptorCollection---只是各种元数据,还不是EndPoint-----还没有跟路由关联起来,因为还没请求来,免得浪费资源

啥时候转变成EndPoint? 请求第一次来的时候!

5. 请求来了-Route

先看EndpointRoutingMiddleware的Invoke方法发生了什么:

  1. 第一次来请求时,会CreateMatcher,期间new DataSourceDependentMatcher()的构造函数里面,通过 _cache.EnsureInitialized()调用了dataSource.GetChangeToken()触发了前面的UpdateEndPoint(),完成EndPoint生成,并缓存起来
  2. 后续再来请求,就不需要CreateMatcher,直接去匹配路由---使用DFAMatcher,找到合适的EndPoint,保存到Context
  3. 然后SetRoutingAndContinue去下一环节 ---等待后续环节处理,可以鉴权/授权/其他,当然也可以直接去EndpointMiddleware了

6. 请求来了-Endpoint

EndpointMiddleware的Invoke方法,其实就是具体处理请求了

  1. Invoke方法,做几个检测,然后调用endpoint.RequestDelegate(httpContext)---会进入Filter-Controller-Action-View
  2. 看看endpoint. Metadata,其实是各种元数据信息
  3. 看看endpoint. RequestDelegate,其实是个委托
  4. 再来一次请求,这次就直接路由匹配—到EndPointMiddleware,看看RequestDelegate究竟是啥?其实是ControllerRequestDelegateFactory的一个委托! ---肯定是个委托呀,因为endpoint里面不可能实例化控制器

7. 再说EndPoint

EndPoint是.NET Core2.2推出的到这里, AddMVC---UseRouting---MapRoute就都串联在一起了,请求也进入MVC了,然后是更进一步的细节,EndPoint究竟是啥?

  1. 诞生是为了满足路由匹配的机制,应该先扫描,存储,再进行匹配---基本职责
  2. RoutingMiddleware是匹配得到EndPoint,保存到Context,然后EndPointMiddleware又从context里去获取EndPoint,再去执行,为啥呀?-----将路由匹配和动作执行分开,期间可以植入逻辑,比如鉴权授权,比如其他的案例
  3. 执行时间点真的很细腻---真的有请求来了,才转成EndPoint委托---直到需要的时候,才去处理,按需处理

8. 中间件顺序问题

这还是个经典的考题---考验对中间件的理解:

  1. UseExceptionHandler:必须在最外层,全局异常处理,兜底的
  2. UseStaticFiles:先处理静态,再处理动态,所以第2
  3. UseSession:动态数据才需要session
  4. UseRouting:得先路由匹配(endpoint初始化)
  5. UseAuthentication:路由匹配后,才知道需要鉴权
  6. UseAuthorization:路由匹配后,才知道需要授权,必须鉴权后才能授权
  7. MapControllerRoute:然后才能完成MVC处理---鉴权授权都通过了,才有意义

四、核心三组件

UseRouting----MapControllerRoute---AddMVC核心三组件,是MVC密切相关的流程:

  1. AddMVC本职工作是各种MVC相关的IOC注册,且把进程相关dll反射加载为ApplicationPart
  2. UseRouting-Use时本职工作是路由中间件注册;
  3. MapControllerRoute-Use时本职工作注册EndPoint中间件,且完成2个ChangeToken注册,触发了ApplicationPart到ActionDescriptorCollection的转化
  4. UseRouting-Build时本职工作是实例化EndPointRoutingMiddlewareMapControllerRoute-Build时本职工作是实例化EndPointMiddleware
  5. EndPointRoutingMiddleware—请求来了时是头次请求时触发ChangeToken,将ActionDescriptorCollection转换成EndPoint,且组装DFA路由匹配----本职工作是匹配路由,得到EndPoint,保存到context
  6. EndPointMiddleware---请求来了时,本职工作就是调用EndPoint的RequestDelegate,也就是后续的MVC流程

1. 从ApplicationPart到ActionDescriptorCollection

因为路由匹配,所以得先反射加载dll,然后转成ActionDescriptorCollection(就是各种控制器 action filter等元数据, 旧版本路由匹配)

  1. 从MapControllerRoute进去,核心就在GetOrCreateDataSource(endpoints)方法,构造ControllerActionEndpointDataSourceFactory时,注册了UpdateCollection的订阅2
  2. factory.Create(orderProvider.GetOrCreateOrderedEndpointsSequenceProvider(endpoints));时,构造ControllerActionEndpointDataSource的构造函数里面的 Subscribe()方法----这里会直接调用一次collectionProviderWithChangeToken. GetChangeToken()的,然后触发了前面注册的UpdateCollection(),完成ActionDescriptorCollection转换
  3. 然后是UpdateCollection()---- GetDescriptors()---找控制器(遍历ApplicationPart,遍历里面的类,按规则筛选,满足的存起来)---找Filter(全局Filter)---ControllerModel(类型+Filter特性)—找Action(遍历方法,把Action+特性封装了一个ActionModel)---就是把Controller—Property—Action---Parameter+特性都找出来并且封装
  4. 然后还有个ApplyConventions---这又是一个扩展点! ---细节很多
  • 细节1:Controller规则 (ControllerFeatureProvider)

    • 必须是类
    • 不能是抽象的
    • 必须是public的
    • 不能包含泛型参数
    • 不能具有[NonController]特性
    • 名字以“Controller” 结尾或者具有[Controller]特性
  • 细节2:Action规则 (DefaultApplicationModelProvider)

    • 不能是特殊的方法(运算符重载,属性的set/get方法)
    • 不能是[NonAction]
    • 不能是object继承下来的
    • 不能是dispose方法
    • 不能是静态方法
    • 不能是抽象方法
    • 不能是构造函数
    • 不能是泛型方法
    • 必须是public方法
  • 细节3:ModelConventions

    在ApplicationPart经过严格的controller-action等规则筛选后,还支持了一个ApplyConventions,用的是Options.Conversions的,支持对各个元素进行ModelConventions模型约定,这其实是个扩展点

    1. 从ApplicationModel开始,里面包括了Controller、 Action等多种要素,然后有支持一套规则Convention来处理这些模型,支持自定义约束来扩展
    2. 要操作ModelConvention,有以下几个接口,都有个Apply方法,接受的参数就是对应的Model,而model里面的东西就特别多了,啥都有 IApplicationModelConvention IControllerModelConvention IActionModelConvention IParameterModelConvention IPageRouteModelConvention

2. 从ActionDescriptorCollection到EndPoint

上面完成Controller和Action的找出来,最终通过Build生成的是ControllerActionDescriptor---然后也绑定了ControllerActionDescriptor到EndPoint的订阅,啥时候触发呢?

是第一个请求来了,在RoutingMiddleware里面触发,再执行UpdateEndpoints

从var matcherTask = InitializeAsync();开始,到_matcherFactory.CreateMatcher,里面new DataSourceDependentMatcher时, _调用cache.EnsureInitialized(); 里面回去调用_dataSource.GetChangeToken(); 终于触发之前的再执行UpdateEndpoints()方法

UpdateEndPoints

UpdateEndpoints()---ControllerActionEndpointDataSource 里面完成把Action转成了EndPoint

循环每个Action 其实这里只是把各种信息都交给了ActionEndPointFactory,各种检测,然后遍历路由—匹配Action,组成RoutePattern,期间创建RequestDelegate(是个委托) ----

  1. 得到RoutePattern结合—Action+Route
  2. 得到一系列的EndPoint里面其实也就是各种元数据,然后RequestDelegate委托,里面并未发生调用

理解一下RequestDelegate :这里还不是具体请求响应,只是路由匹配,所以肯定没有实例化Controller,而只是用委托保存了一下

描述: UpdateEndPoints把controller-action-property-parameter跟路由进行交叉遍历,组成一系列匹配源(规则-EndPoint)---- EndPoint就是保存了元数据metadate,第二就是个委托(毫无技术含量,就是个普通的委托声明)

Why EndPoint?

  1. 由于路由匹配机制,所以得先反射加载dll,然后转成ActionDescriptorCollection,已经能满足路由机制需求了,为啥最终要转成EndPoint? 为了匹配和运行的分离!为了方便中间的扩展---案例----之前就是匹配完就立即处理没法扩展
  2. 比如鉴权-授权,就应该在匹配完控制器-Action-Filter等之后立即执行,才是最高效的---如果匹配和执行连在一起,那就只能执行的过程中再鉴权授权

Why 三层

  1. 为啥ApplicationPart(AddMVC) ---ActionDescriptorCollection(Use)---EndPoint(请求来了)这么多步骤?为了兼容旧版本, .NET Core2.2才出的EndPoint----
  2. 三层如何保障同步更新?多层ChangeToken.Onchange绑定---尤其是ActionDescriptorCollection---EndPoint是后期添加的,来个OnChange---分拆到不同的时间点去执行

五、Route

1. 先理解Route

官方说明:路由系统提供的请求URL模式与对应终结点(Endpoint)之间的映射关系,我们可以将具有相同URL模式的请求分发给应用的终结点进行处理。

接地气儿:Http请求到aspx/ashx、或者是匹配到静态文件、或者是到某个Action,都不是无缘无故的,一定得有一个解析-匹配的过程---也就是根据请求的URL(也可能包含其他信息:参数、 HttpMethod等),解析为调用某个控制器某个方法,包括解析出URL中的参数(部分参数),这个就是路由了

2. 路由规则

  • 默认路由
  • 伪静态路由
  • 特性路由
    • 特性路由不需要注册了(相对ASP.NET时代),靠的是metadata收集
    • 支持多个特性路由
    • 有特性路由等同于是个约束了,默认路由不再匹配了,不再匹配全局路由

3. 路由约束

路由约束,限定变量的范围类型等,框架默认提供了一系列---也能自定义

  1. 约束是有效的
  2. No constraint居然不会拦截请求— 新版路由的特点— 靠的是优先级(精准度)
  3. Required咋报错了— 说明这个优先级也很高---这事儿记不住,只能靠测试

4. Host约束

Host约束 将约束应用于需要指定主机的路由

5. MapGet

直接指定的规则和RequetDelegate---类似的还有MapPost

  1. MapGet在默认路由后,但是会被匹配上---优先级问题
  2. 匹配路由后,直接指定RequetDelegate,不走Filter流程

6. 动态路由

截止目前,路由都是静态写死的---能否去数据库/Redis去校验? --同样请求,能随着数据库信息而变化,动态路由------

  1. app.UseDynamicRouteDefault();

  2. services.AddDynamicRoute();

  3. 全套TranslationTransformer实现,能做到自定义动态规则匹配路由,写在CustomTranslationSource

    http://localhost:5726/en/route/info

    http://localhost:5726/ch/route1/info1

    http://localhost:5726/hk/route2/info2

    能支持动态数据库配置请求响应---只能说很炫,实际效果一般,不能每次都去数据库

    使用场景: 控制器版本/Action版本---V1—全版本升级到V2

7. 路由使用总结

  1. 路由就是路由,用来从URL到具体处理的转换
  2. 依赖URL、 HttpMethod、约束、自定义规则
  3. 已扩展的:现有URL规则配置、特性路由、约束&正则、自定义约束、动态路由

理解下执行时机,分3层:

  1. 常规路由、正则约束等等,皆是通过路由匹配,走Controller-Action
  2. 路由匹配后,能指定不同的handler(requestdelegate)---其实是MapGet
  3. 不走路由匹配,命中后走的是新的中间件流程-----mapwhen useWhen等

六、Route 源码

1. Route生命周期

Route源码分了2个:一是生命周期,二是实现机制

  1. UseRouting时,注册了RoutingMiddleware----MapControllerRoute时,订阅了UpdateEndPoint
  2. 请求第一次来时, RoutingMiddleware会初始化DFAMatcher,期间触发UpdateEndPoint并将路由和EndPoint映射起来,组成RouteEndpoint
  3. 全部请求都是通过DFAMatcher来Match,同时检测多个路由状态机,找出最合适的RouteEndpoint,保存到IEndpointFeature,后续从中获取EndPoint

实现机制又非常复杂---关系到DFA算法

2. Route核心对象

  1. RouteEndPoint:继承自EndPoint,多了2个属性,也就是路由匹配结果实际是这个-----传统的路由以IRouter--RouteHandler对象为核心,对请求匹配后得到的是一个具体handler(aspx--mvchandler)----然后从ASP.NET Core 2.2中的新路由系统,开始使用EndPoint
  2. Order:就是优先级---越小 优先级越高
  3. RoutePattern里面包含了全部的规则和约束,由RoutePatternFactory生成的,其中RawText属性就是匹配的具体路由模板

路由匹配后,得到RouteEndPoint---一是终结点,二是匹配的信息---这是个匹配结果,其实是在之前生成的,请求匹配就是通过RoutePattern在匹配

4. Route规则和顺序

路由匹配的规则和顺序,都放在RoutePattern里面

  1. 规则:地址路径吻合---HttpMethod吻合---路径约束满足
  2. 多个吻合的路由间如何筛选?按注册顺序循环,找到第一个满足条件的结束,要求路由注册顺序? ------以前是这样的,现在用的是DFA算法,同时校验各种路由,注册顺序没用了,是根据优先级返回

UseEndpoints 内的操作顺序并不会影响路由行为,但有一个例外MapControllerRoute 和MapAreaRoute 会根据调用顺序,自动将顺序值分配给其终结点。

5. RoutePattern

RoutePattern+ DFA

  1. 断点看看RoutePattern对象:有多个属性,去完整描述全部的规则
  2. 也去找找源码RoutePatternFactory:一个个属性去拼装---会把地址用斜线拆成segment数组---为了后续DFA匹配
  3. DfaGraphWriter扩展,呈现矢量图:就是课树,需要实现初始化,然后请求来按segment匹配

RoutePattern:是个规则结合体,一方面在匹配前,必须将全部Action都转成RouteEndpoint---二方面是请求来了,按照RoutePattern进行匹配筛选---按路径分segment来做匹配(所以跟路由注册顺序没关系)

6. RoutePattern矢量图

  1. 实现GraphEndpointMiddleware,注入了EndpointDataSource:就是程序启动时,将 Action-Route映射转换的全部数据结合----DfaGraphWriter: DFA图形展示
  2. 做个中间件注册, app.Map("/graph", branch => branch.UseMiddleware());
  3. dotnet run --urls=http://*:5728 然后访问: http://localhost:5728/graph
  4. 去站点https://graphviz.christine.website/

完成了对DFA数据源头的可视化,能看到这个数据源

7. DFA匹配算法

刚才都是在拼凑匹配的源头---EndpointDataSource---那是如何匹配出来的呢?请求来了Home/index,通过RouteParse得到 Home,Index---然后去树里面匹配

DFA,全称 Deterministic Finite Automaton 即确定有穷自动机:从一个状态通过一系列的事件转换到另一个状态,即 state -> event -> state。确定:状态以及引起状态转换的事件都是可确定的,不存在“意外” 。有穷:状态以及事件的数量都是可穷举的。

简而言之:就是一堆字符串里面匹配时,双层for循环---关键字触发事件,只检测固定几个值