ASP.NET Core 3.x 中间件流程与路由体系
中间件分类
asp.net core 中间件的配置方法可以分为以上三种,对应的helper方法分别是:run(), use(), map()。
- run(),使用run调用中间件的时候,会直接返回一个响应,所以后续的中间件将不会被执行了。
- use(),它会对请求做一些工作或处理,例如添加一些请求的上下文数据,有时候甚至什么也不做,直接把请求交给下一个中间件。
- map(),它会把请求重新路由到其它的中间件路径上去。
实际中呢,use()这个helper方法用的最多。
run():
这是一个使用run方法调用的中间件,run方法会终止整个中间件管道,它应该返回某种类型的响应。
use():
use看起来和run差不多,但是多了一个next参数。next可以用来调用请求管道中的下一个中间件。而当前的中间件也可以自己返回响应,这就忽略掉了next调用。
在next调用之前,我们可以写一些请求进来的逻辑,而在next调用之后,就相当于返回响应了,这时候也可以写一些逻辑。
在本例中,我们下面还使用了run方法注册了另一个中间件。因为中间件会按照它们注册的顺序进行调用,所以在第一个use方法里执行next.invoke()的时候,就会执行下面run所调用的中间件。
map():
map方法可以把请求路由到其它的中间件上面。
在这里,如果请求的路径以/jump结尾,那么它所对应的handler方法,也就是hereiam方法就会被调用,并返回一个响应。
而如果请求的路径不是以/jump结尾,那么hereiam方法和里面的中间件就不会被调用。
中间件class
上面的例子,我都是使用的inline写法的中间件。
而实际上,中间件通常是自成一个类。中间件的类需要类似这样:
自定义的中间件类需要由这几部分组成:
- 接受一个requestdelegate类型的参数next的构造函数。
- 按约定,还需要定义一个叫做invoke的方法。该方法里会包含主要的业务逻辑,并且它会被请求管道所执行。invoke方法可以忽略里面的_next调用,并返回一个响应;也可以调用_next.invoke()把请求发送到管道的下一站。
中间件流程图
endpoint routing 路由系统
asp.net core 3.x 使用了一套叫做 endpoint routing 的路由系统。这套路由系统在asp.net core 2.2的时候就已经露面了。
这套endpoint routing路由系统提供了更强大的功能和灵活性,以便能更好的处理请求。
早期asp.net core的路由系统
我们先回顾一下早期版本的asp.net core的路由系统:
在早期的asp.net core框架里,http请求进入中间件管道,在管道的结尾处,有一个router中间件,也就是路由中间件。这个路由中间件会把http请求和路由数据发送给mvc的一个组件,它叫做mvc router handler。
这个mvc 路由 handler就会使用这些路由数据来决定哪个controller的action方法应该来负责处理这个请求。
然后 router中间件就会执行被选中的action方法,并生成响应,而这个响应就会顺着中间件的管道原路返回。
问题出在哪?
为什么早期的这套路由系统被抛弃了?它有什么问题?
第一个问题就是,在被mvc处理之前,其它的中间件不知道最后哪个action方法会被选中来处理这个请求。这对像authorization(授权),cors这样的中间件会造成很大的困扰,因为他们不能提前知道该请求会被送往哪里。
第二个问题就是,这套流程会把mvc和路由的职责紧密的耦合在一起,而实际mvc的本职工作应该仅仅就是生成响应。
endpoint routing 路由系统前来营救
endpoint routing 路由系统,它把mvc的路由功能抽象剥离出来,并放置到中间件管道里,从而解决了早期asp.net core路由系统的那两个问题。
而在endpoint routing 路由系统里,其实一共有两个中间件,它们的名称有点容易混淆,但是你只要记住他们的职责即可:
- endpoint routing 中间件。它决定了在程序中注册的哪个endpoint应该用来处理请求。
- endpoint 中间件。它是用来执行选中的endpoint,从而让其生成响应的。
所以,endpoint routing的流程图大致如下:
在这里,endpoint routing 中间件会分析进来的请求,并把它和在程序中注册的endpoints进行比较。它会使用这些 endpoints 上面的元数据来决定哪个是处理该请求的最佳人选。然后,这个选中的endpoint 就会被赋给请求的对象,而其它后续的中间件就可以根据这个选中的endpoint,来做一些自己的决策。在所有的中间件都执行完之后,这个被选中的endpoint最终将被 endpoint中间件所执行,而与之关联的action方法就会被执行。
endpoint是什么?
endpoint是这样的一些类,这些类包含一个请求的委托(request delegate)和其它的一些元数据,使用这些东西,endpoint类可以生成一个响应。
而在mvc的上下文中,这个请求委托就是一个包装类,它包装了一个方法,这个方法可以实例化一个controller并执行选中的action方法。
endpoint还包含元数据,这些元数据用来决定他们的请求委托是否应该用于当前的请求,还是另有其它用途。
说起来可能有点迷糊,一会我们看看源码。
startup.cs
之前我们见过,asp.net core里面的startup.cs里面有两个方法,分别是configureservices()和configure(),它们的职责就是注册应用的一些服务和构建中间件请求管道。
而startup.cs同时也是应用的路由以及endpoint作为其它步骤的一分部进行注册的地方。
看图:
在asp.net core应用程序启动的时候,一个叫做controlleractionendpointdatasource的类作为应用程序级别的服务被创建了。
这个类里面有一个叫做createendpoints()的方法,它会获取所有controller的action方法。
然后针对每个action方法,它会创建一个endpoint实例。这些endpoint实例就是包装了controller和action方法的执行的请求委托(request delegate)。
controlleractionendpointdatasource里面包存储着在应用程序里注册的路由模板。
而针对每个endpoint,它要么与某个按约定的路由模板相关联,要么与某个controller action上的attribute路由信息相关联。而这些路由在稍后就会被用来将endpoint与进来的请求进行匹配。
从endpoint的角度查看请求-响应流程图
app启动那部分就不说了。
第一个http请求进来的时候,endpoint routing中间件就会把请求映射到一个endpoint上。它会使用之app启动时创建好的endpointdatasource,来遍历查找所有可用的endpoint,并检查和它关联的路由以及元数据,来找到最匹配的endpoint。
一旦某个endpoint实例被选中,它就会被附加在请求的对象上,这样它就可以被后续的中间件所使用了。
最后在管道的尽头,当 endpoint中间件运行的时候,它就会执行endpoint所关联的请求委托。这个请求委托就会触发和实例化选中的controller和action方法,并产生响应。最后响应再从中间件管道原路返回。