ASP.NET Core Middleware抽丝剥茧
一. 中间件的概念和数据结构
ASP.NET Core Middleware是在ASP.NET Core处理管道中处理特定业务逻辑的组件。
ASP.NET Core处理管道由一系列请求委托组成,一环接一环的调用特定的中间件。
上图示例:
处理管道包含四个中间件,每个中间件都包含后续中间件执行动作的引用(next),同时每个中间件在交棒之前和交棒之后可以自行参与针对HttpContxt
的业务处理。
通过上面的分析,中间件其实具备两个特征:
入参:下一个中间件的执行委托 RequestDelegate
(public delegate Task RequestDelegate(HttpContext context);)输出:特定中间件的业务处理动作:因为中间件是处理管道中预设的处理逻辑,所以这个动作其实也是一个委托 RequestDelegate
所以.NET Core用Func
数据结构表示中间件是合理的。
二. Middleware的定义方式
ASP.NETCore 提供了很多内置的中间件,帮助我们完成基础通用的业务逻辑。
有两种自定义中间件的方式:
1. Factory-based Middleware
基于工厂模式的中间件有如下优点:
在每个客户端请求时激活实例 (injection of scoped services) 实现IMiddleware接口: 强类型
// 该接口只有一个固定的函数
public Task InvokeAsync(HttpContext context,RequestDelegate next);
public class FactoryActivatedMiddleware : IMiddleware
{
private readonly ILogger _logger;
public FactoryActivatedMiddleware(ILoggerFactory logger) //
{
_logger = logger.CreateLogger();
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// TODO logic handler
_logger.LogInformation("测试");
await next(context);
// TODO logic handler
}
}
使用工厂模式的中间件,构造函数参数由依赖注入(DI)填充;
在[使用UseMiddleware()注册中间件]时不允许显式传参。
源码在https://github.com/dotnet/aspnetcore/blob/v5.0.1/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs第56行。
2. Conventional-based Middleware
顾名思义,基于约定的中间件类有一些特定约定:
具有RequestDelegate类型参数的公共构造函数 名称为Invoke或InvokeAsync的公共方法, 此方法必须 ①返回Task ② 方法第一个参数是HttpContext
public class ConventionalMiddleware
{
private readonly RequestDelegate _next;
public ConventionalMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, AppDbContext db)
{
var keyValue = context.Request.Query["key"];
if (!string.IsNullOrWhiteSpace(keyValue))
{
db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "ConventionalMiddleware",
Value = keyValue
});
await db.SaveChangesAsync();
}
await _next(context);
}
}
构造函数和Invoke/InvokeAsync的其他参数由依赖注入(DI)填充;
基于约定的中间件,在[使用UseMiddleware()注册中间件]时允许显式传参。
三. 注册中间件的算法分析
app.UseMiddleware
内部使用app.Use(Func
注册中间件,
返回值还是IApplicationBuilder
, 故具备链式注册的能力。
算法类似于基础链表, 只是指针指向的不是节点,而是后续节点的字段。
//--------节选自 Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder--------
private readonly IList> _components = new List>();
public IApplicationBuilder Use(Func middleware)
{
this._components.Add(middleware);
return this;
}
public RequestDelegate Build()
{
RequestDelegate app = context =>
{
// If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
// This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
var endpoint = context.GetEndpoint();
var endpointRequestDelegate = endpoint?.RequestDelegate;
if (endpointRequestDelegate != null)
{
var message =
$"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
$"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
$"routing.";
throw new InvalidOperationException(message);
}
context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
};
foreach (var component in _components.Reverse())
{
app = component(app);
}
return app;
}
通过以上代码我们可以看出:
注册中间件的过程实际上,是给一个Type为List
> 的集合依次添加元素的过程; 中间件的数据结构:(input)后置中间件的执行函数指针(以委托RequestDelegate表示),(output)当前中间件的执行函数指针(以委托RequestDelegate表示);
通过
build
方法将集合打通为链表,链表就是我们常说的[处理管道]。
build方法是在web托管服务
GenericWebHostService
开始启动的时候被调用。源码在https://github.com/dotnet/aspnetcore/blob/master/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs 105 行。
附:非标准中间件的用法
短路中间件、 分叉中间件、条件中间件
Use方法是一个注册中间件的简便写法 Run方法是一个约定,一些中间件使用Run方法来完成管道的结尾
Map扩展方法:Path满足指定条件,将会执行分叉管道
MapWhen方法:HttpContext满足条件,将会执行分叉管道,相比Map有更灵活的匹配功能
UseWhen方法:HttpContext满足条件则插入中间件
关注并星标我们
更多干货及最佳实践分享