Loading

asp.net core之中间件

中间件介绍

在asp.net core中,中间件中间件是一种装配到应用管道以处理请求和响应的软件。
每个组件:

  • 选择是否将请求传递到管道中的下一个组件。
  • 可在管道中的下一个组件前后执行工作。

请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。
ASP.NET Core 请求管道包含一系列请求委托,依次调用。每个委托均可在下一个委托前后执行操作。 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。 如下图所示:

编写中间件

在asp.net core中已经内置了挺多的中间件,包括身份验证,授权等等,详细的可以看官方文档内置中间件列表。
接下来主要讲一下如何编写我们自己的中间件,在前面的文章中我们也用到了自己写的中间件,用的是最简单的app.Use的方式。
Use 扩展可以使用两个重载:

  • 一个重载采用 HttpContext 和 Func。 不使用任何参数调用 Func
  • 另一个重载采用 HttpContext 和 RequestDelegate。 通过传递 HttpContext 调用 RequestDelegate。

优先使用后面的重载,因为它省去了使用其他重载时所需的两个内部每请求分配。

app.Use(async (context, next) =>
{
    // 下游中间件执行前
    await next.Invoke(); //往下执行中间件
    // 下游中间件执行后
});

上面写法就是一个最简单的没有任何操作的中间件。
在调用await next.Invoke()前我们写的操作就是在下游中间件执行之前做的事情,对应的,在之后写的操作则是在下游中间件响应后做的事情。
举个例子,当我们要在下游中间件执行之前,做一些参数的赋值,如我想在Headers中添加一个头部,

app.Use(async (context, next) =>
{
    context.Request.Headers.Add("TestMiddlewareAdd", "Abc");
    await next.Invoke();
});

在添加之后,下游就可以获取Headers中TestMiddlewareAdd的值。
我们来实操一下,创建一个WebApi项目,然后在Program中MapControllers()之前添加上述中间件。
image.png
可以看到,Headers中已经加上了我们之前加的内容。
对应的,如果写在await next.Invoke()后面,则是不生效的,这个可以自行测试。那么在await next.Invoke()后面我们可以做一些什么操作呢?比如记录请求响应完成后的内容,或对相应内容做进一步的处理等等,根据我们的实际需要去写。

除了app.Use(),在asp.net core中还有几种中间件的编写方式。

app.Map();
app.MpaWhen();
app.Run();
app.UseMiddleware();

Map扩展用作约定来创建管道分支。 Map 基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。
MapWhen基于给定谓词的结果创建请求管道分支。 Func<HttpContext, bool> 类型的任何谓词均可用于将请求映射到管道的新分支。
Run 委托不会收到 next 参数。 第一个 Run 委托始终为终端,用于终止管道。 Run 是一种约定。 某些中间件组件可能会公开在管道末尾运行的 Run[Middleware] 方法:

UseMiddleware

UseMiddleware是我们最常用的封装中间件的方式,中间件类是基于约定编写的。其约定如下:

  • 具有类型为 RequestDelegate 的参数的公共构造函数。
  • 名为 Invoke 或 InvokeAsync 的公共方法。 此方法必须:
    • 返回 Task。
    • 接受类型 HttpContext 的第一个参数。

构造函数和 Invoke/InvokeAsync 的其他参数由依赖关系注入 (DI) 填充。

接下来我们来实操基于约定编写一个Middleware类

    public class AMiddleware
    {
        private readonly RequestDelegate _next;

        public AMiddleware(RequestDelegate next)
            => _next = next;

        public async Task InvokeAsync(HttpContext context, ILogger<AMiddleware> logger)
        {
            logger.LogInformation("AMiddleware Invoke");

            await _next(context);
        }
    }

在Program使用UseMiddleware把中间件加入管道

app.UseAuthorization();
app.Use(async (context, next) =>
{
    context.Request.Headers.Add("TestMiddlewareAdd", "Abc");
    await next.Invoke();
});
app.UseMiddleware<AMiddleware>();
app.MapControllers();

app.Run();

启动项目发出请求。可以看到下图结果:
image.png
需要注意的是,这里的Middleware会自动注册为一个单例,所以在构造器注入时,无法注入Scope生命周期的服务。
如果注入,启动会直接报错

public class AMiddleware
{
    private readonly RequestDelegate _next;
    private readonly TestMiddlewareDi _testMiddlewareDi;

    public AMiddleware(RequestDelegate next, TestMiddlewareDi testMiddlewareDi)
    {
        _next = next;
        _testMiddlewareDi = testMiddlewareDi;
    }

    public async Task InvokeAsync(HttpContext context, ILogger<AMiddleware> logger)
    {
        logger.LogInformation("AMiddleware Invoke");
        logger.LogInformation($"AMiddleware _testMiddlewareDi: {_testMiddlewareDi.Id}");

        await _next(context);
    }
}
builder.Services.AddScoped<TestMiddlewareDi>();

image.png
当我们需要注入Scope生命周期的服务时,直接在InvokeAsync方法中添加注入。

public class AMiddleware
{
    private readonly RequestDelegate _next;

    public AMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ILogger<AMiddleware> logger, TestMiddlewareDi testMiddleware)
    {
        logger.LogInformation("AMiddleware Invoke");
        logger.LogInformation($"AMiddleware _testMiddlewareDi: {testMiddleware.Id}");

        await _next(context);
    }
}

运行结果可以看到,正常运行,并且每次请求Id都是不一样的。
image.png

IMiddleware

除了基于约定实现中间件,asp.net core还有一个基于工厂的中间件激活扩展。
IMiddlewareFactory/IMiddleware 是中间件激活的扩展点,具有以下优势:

  • 按客户端请求(作用域服务的注入)激活
  • 让中间件强类型化

UseMiddleware 扩展方法检查中间件的已注册类型是否实现 IMiddleware。 如果是,则使用在容器中注册的 IMiddlewareFactory 实例来解析 IMiddleware 实现,而不使用基于约定的中间件激活逻辑。 中间件在应用的服务容器中注册为作用域或瞬态服务。
接下来我们来实现一个IMiddleware

public class FactoryMiddleware : IMiddleware
{
    private readonly ILogger _logger;
    private readonly TestMiddlewareDi _testMiddleware;

    public FactoryMiddleware(ILogger<FactoryMiddleware> logger, TestMiddlewareDi testMiddleware)
    {
        _logger = logger;
        _testMiddleware = testMiddleware;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        _logger.LogInformation("FactoryMiddleware Invoke");
        _logger.LogInformation($"FactoryMiddleware _testMiddlewareDi: {_testMiddleware.Id}");
        await next(context);
    }
}
app.UseAuthorization();
app.Use(async (context, next) =>
{
    context.Request.Headers.Add("TestMiddlewareAdd", "Abc");
    await next.Invoke();
});
app.UseMiddleware<AMiddleware>();
app.UseMiddleware<FactoryMiddleware>();
app.MapControllers();

app.Run();

需要注意的是,这种方式必须把中间件注册到依赖注入容器中,否则会出现以下错误:
image.png
注册注入之后,我们再次启动服务,并测试请求。

builder.Services.AddScoped<FactoryMiddleware>();

image.png
一切顺利执行。

基于约定的中间件和基于工厂的中间件区别

基于约定的中间件无法通过构造函数注入Scope生命周期的服务,只能通过Invoke方法的参数进行注入。
基于工厂的中间件只能通过构造函数添加注入,Invoke无法注入(因为是基于IMiddleware接口的实现)。

基于约定的中间件无需手动注册进依赖注入容器。
基于工厂的中间件必须注册进依赖注入容器,且生命周期注册为作用域或瞬态服务。

基于约定的中间件生命周期为单例
基于工厂的中间件生命周期为作用域

中间件顺序

中间既然是一种管道的模式,那么必然和顺序有关系,管道前面的中间件先执行,后面的中间件后执行。
那么这个顺序会带来哪种影响呢?
这里盗官方文档图,下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。

这里UseCors 和 UseStaticFiles 顺序是最容易看出影响的。
若是UseStaticFiles在UseCors之前调用,则检索静态文件时,不会检查是否跨站点调用。所有静态文件可以直接检索。
若是相反,则在跨站检索静态文件时,则会优先检查站点是否跨域,若是跨域则无法检索静态文件。

由此我们可以想到,当我们需要做一些前置校验的中间件时,可以把中间件顺序放在前面,校验不通过直接终止后续请求,可以提高应用的响应效率。

欢迎进群催更。

posted @ 2023-07-26 15:25  饭勺oO  阅读(1177)  评论(1编辑  收藏  举报