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 方法都是中间件调用的方法。

ASP.NET Core 框架之中内置有很多中间件,并且我们通过 VS 创建某一个类型的项目时,如 MVC、Razor Page,初始化的项目代码中会帮我们配置好一些中间件。

在这里插入图片描述
以上为官方文档中列出的内置中间件,可以看到在列表中对每个中间件的顺序进行了说明。

管道中的中间件排列是有先后之分的,请求和响应按照中间件的排列顺序进行传递,这也是我们代码逻辑执行的顺序,而且一些中间件需要依赖于其他中间件的处理结果,或者必须在某些中间件前先执行,否则就会出问题了。

而中间件插入到管道中的顺序,就是依据我们在入口文件中调用相应中间注册方法的顺序,所以代码的前后顺序非常重要,一旦写错了就会出现很多意想不到的的 Bug。

向 Program.cs 文件中添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。这是官方文档中的原话。

官方文档给出了典型MVC应用的管道中间件顺序,这里其实不止 MVC,Razor Page、Web Api 也是这样的管道模型,如下图。这里也明确了我们自定义的中间件应该插入到哪个位置。
在这里插入图片描述
更多的内置中间件的作用,以及相应的管道顺序要求,请详细阅读一下官方文档,这里就不细说了。

2.3 自定义中间件

中间件本质上是一个委托,上面的例子中我们将中间件的代码逻辑通过 Use()、Run()、Map() 等方法写在了入口文件中,这样很不优雅。我们可以对这些代码进行封装,最简单的封装方式,就是通过一个静态类将相关的代码写成静态方法,在 Use() 等方法中只需要传入静态方法即可。但是这种方法一样不够优雅,我们可以模仿微软内置中间件和一些第三方组件提供的中间件的封装方式。例如,静态文件中间件的实现源码:
在这里插入图片描述
其实对于中间件的封装,可以不实现某个接口,但是它有一套约定的规则。
(1) 中间件类名必须是 XXXMiddleware 格式
(2) 中间件类中必须有 public Task Invoke(HttpContext context) 方法

所以对于上面例子中的代码,我们可以进行以下的优化

namespace MiddlewareSample.Middlewares
{
    public class HelloMiddleware
    {
        private readonly RequestDelegate _next;
        // 注入相应的依赖,这里是下一个中间件的委托,如果有其他依赖项需要用到,也可以从构造函数注入
        public HelloMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            await context.Response.WriteAsync("Hello Middlerware1 ! ");
            if (context.Request.Query.TryGetValue("query", out var query))
            {
                await context.Response.WriteAsync(query);
            }
            // 调用下一个中间件
            await _next(context);
            await context.Response.WriteAsync("End Middleware1 ! ");
        }
    }
}

之后再提供一个扩展方法,以供使用者便捷地进行注册使用。

using MiddlewareSample.Middlewares;
	
namespace Microsoft.AspNetCore.Builder
{
    public static class HelloExtensions
    {
        public static IApplicationBuilder UseHello(this IApplicationBuilder app) 
        { 
            if(app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }
            // 中间件的注册方式
            app.UseMiddleware<HelloMiddleware>();
            return app;
        }
    }
}

这里使用了另一种中间件的注入方式,通过查看源码,可以看到最终也是调用了 Use() 方法进行注册的。
在这里插入图片描述
在这个过程中,会通过反射等手段通过我们封装好的中间件类生成一个委托。

之后就是配置使用了,在 入口文件中将原本的 Use() 方法替换成扩展方法。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

app.UseHello();
app.Run(async context =>
{
    await context.Response.WriteAsync("Hello last Middleware ! ");
});

app.Run();

最终的执行结果是一样的。

在这里插入图片描述
参考文档:

ASP.NET CORE 6.0 - 中间件

ASP.NET Core 系列总结:
上一篇:ASP.NET Core — 入口文件
下一篇:ASP.NET Core — 依赖注入

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值