中间件介绍
在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()之前添加上述中间件。
可以看到,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();
启动项目发出请求。可以看到下图结果:
需要注意的是,这里的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>();
当我们需要注入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都是不一样的。
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();
需要注意的是,这种方式必须把中间件注册到依赖注入容器中,否则会出现以下错误:
注册注入之后,我们再次启动服务,并测试请求。
builder.Services.AddScoped<FactoryMiddleware>();
一切顺利执行。
基于约定的中间件和基于工厂的中间件区别
基于约定的中间件无法通过构造函数注入Scope生命周期的服务,只能通过Invoke方法的参数进行注入。
基于工厂的中间件只能通过构造函数添加注入,Invoke无法注入(因为是基于IMiddleware接口的实现)。
基于约定的中间件无需手动注册进依赖注入容器。
基于工厂的中间件必须注册进依赖注入容器,且生命周期注册为作用域或瞬态服务。
基于约定的中间件生命周期为单例
基于工厂的中间件生命周期为作用域
中间件顺序
中间既然是一种管道的模式,那么必然和顺序有关系,管道前面的中间件先执行,后面的中间件后执行。
那么这个顺序会带来哪种影响呢?
这里盗官方文档图,下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。
这里UseCors 和 UseStaticFiles 顺序是最容易看出影响的。
若是UseStaticFiles在UseCors之前调用,则检索静态文件时,不会检查是否跨站点调用。所有静态文件可以直接检索。
若是相反,则在跨站检索静态文件时,则会优先检查站点是否跨域,若是跨域则无法检索静态文件。
由此我们可以想到,当我们需要做一些前置校验的中间件时,可以把中间件顺序放在前面,校验不通过直接终止后续请求,可以提高应用的响应效率。
欢迎进群催更。