ASP.NET Core - 请求管道与中间件
1. 请求管道
请求管道是什么?请求管道描述的是一个请求进到我们的后端应用,后端应用如何处理的过程,从接收到请求,之后请求怎么流转,经过哪些处理,后怎么返回响应。请求管道就是一次请求在后端应用的生命周期。了解请求管道,有助于我们明白后端应用是怎么工作的,我们的代码是怎么工作的,在我们的业务代码执行前后经过哪些步骤,有助于我们之后更好的实现一些AOP操作。
请求管道是 .net 应用的一个基本的概念。在 .net core 中,微软对框架底层进行了全新的设计,相对于原本的ASP.NET中的全家桶模式的管道模型,.net core的管道模型更加灵活便捷,可做到热插拔,通过管道可以随意注册自己想要的服务或者第三方服务插件,这也是.net core性能更好的原因。
以上是微软官方文档中的管道模型图。从图中可以看到 服务器接收到请求之后,将接收到的请求向后传递,依次经过一个个 Middleware 进行处理,然后由后一个 MiddleWare 生成响应内容并回传,再反向依次经过每一个 Middleware,直到由服务器发送出去。整个过程就像一条流水线一样,管道这个词是很形象的,而 Middleware 就像一层一层的“滤网”,过滤所有的请求和响应。
2. 中间件
管道之中,对请求、响应进行加工处理的模块是 Middleware
,也就是中间件。中间件本质上是一个委托。
2.1 工作模式
从上面的图可以看出,每一个中间件都会被执行两次,在下一个中间件执行之前和之后各执行一次,分别是在处理请求和处理响应,只有一个中间件是例外的,那就是后一个中间件,它后面没有下一个中间件,所以执行到它管道就会回转。
这代表了中间件的两种工作模式,也是中间件的两种基本注册方式。中间件本质上是一个委托,在代码实现上就体现在委托的入参有所不同以及注册调用的方法不同。
中间件两种基本的注册方式:
- Use 方法注册
- use 注册的中间件会传入next参数,在处理完本身的逻辑之后可以调用 next() 去执行下一个中间件
- 如果不执行,就等于Run
- Run 方法注册
- Run 只是执行,没有去调用Next ,一般作为终结点。
- Run 方法注册,只是一个扩展方法,终还是调用Use方法
在代码中分别是以下方式:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello Middlerware !");
if(context.Request.Query.TryGetValue("query", out var query))
{
await context.Response.WriteAsync(query);
}
await next();
await context.Response.WriteAsync("End Middleware !");
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello last Middleware");
});
后的执行结果如下,也可以从代码执行的先后顺序看出管道流动的顺序。当前中间件手动调用 next() 之后,就进入下一个中间件,下一个中间件处理完成之后,按照管道的顺序再一个一个回传。在这个过程中一直不变,被管道传递的就是HttpContext,而我们拿到 HttpContext,即可以通过 Request 和 Response 对当前这一次的请求做任何处理了。
通过分析asp.net core的源码,可以看到在我们调用 Run() 的时候,实际上还是调用了 Use() 方法。
而 Use() 方法中,主要的逻辑仅仅只是将相应的委托存放到集合中
之后在 build 方法调用的时候才一个一个地调用中间件委托。
除了上面的 Use() 、Run() 两个基本的方法注册中间件之外,还有另外一些方法,如通过 Map() 方法注册中间件,这种方式会创建一个新的管道分支,在路由满足Map的规则时,请求则转型新的管道分支,后沿着管道分支返回响应,而不走原有的管道。
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello Middlerware1 ! ");
if(context.Request.Query.TryGetValue("query", out var query))
{
await context.Response.WriteAsync(query);
}
await next();
await context.Response.WriteAsync("End Middleware1 ! ");
});
app.Map("/map", app =>
{
// map方法中的委托,传入的时IApplicationBuilder, 在这里相当于一个新的管道,也可以和主管道一样进行任意操作
app.Run(async context =>
{
await context.Response.WriteAsync("Hello map Middleware pipeline ! ");
});
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello last Middleware ! ");
});
执行结果如下:
其他的分支管道创建方式,如 MapWhen,和 Map 大同小异,只是对于匹配判断的方式有所不同。像微软内置中间件中的静态文件中间件,MVC 中间件,其实都是以分支管道的方式实现的,一旦匹配到请求就会走管道分支。
2.2 中间件的使用配置
使用一个中间件需要在 .net core 的入口文件中进行配置,如果是 .net 6版本,那只要在 program.cs 文件中进行配置即可,通过 WebApplication 对象,也就是 app 调用相关的方法。
如果是 .net 6 以下版本,可以在 startup.cs 文件中的 Configure 方法中配置。.net 6 与之前版本入口文件的不同上一篇文章也讲过,这里就不赘述了。
这里可以看得到,一些中间件的调用并没有直接使用 Use() 和 Run(),毕竟将各个中间件的处理逻辑全部放在入口文件很不好管理,而且也很不优雅。这里涉及到了中间件封装的约定规则,一般情况下封装一个中间件都会提供一个 Use[Middleware] 方法以供使用者进行中间件的调用,WebApplication 对象的 UseXXX 方法都是中间件调用的方法。
2.3 ASP.NET Core 框架内置中间件
ASP.NET Core 框架之中内置有很多中间件,并且我们通过 VS 创建某一个类型的项目时,如MVC、Razor Page,初始化的项目代码中会帮我们配置好一些中间件。
以上为官方文档中列出的内置中间件,可以看到在列表中对每个中间件的顺序进行了说明。
管道中的中间件排列是有先后之分的,请求和响应按照中间件的排列顺序进行传递,这也是我们代码逻辑执行的顺序,而且一些中间件需要依赖于其他中间件的处理结果,或者必须在某些中间件前先执行,否则就会出问题了。
而中间件插入到管道中的顺序,就是依据我们在入口文件中调用相应中间注册方法的顺序,所以代码的前后顺序非常重要,一旦写错了就会出现很多意想不到的的Bug。
向 Program.cs 文件中添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。这是官方文档中的原话。
官方文档给出了典型MVC应用的管道中间件顺序,这里其实不止MVC,Razor Page、Web Api 也是这样的管道模型,如下图。这里也明确了我们自定义的中间件应该插入到哪个位置。
更多的内置中间件的作用,以及相应的管道顺序要求,请详细阅读一下官方文档,这里就不细说了。
相关文章