ASP.NET Core Middleware

中间件的概念和数据结构

  • ASP.NET Core Middleware是在ASP.NET Core处理管道中处理特定业务逻辑的组件
    • ASP.NET Core处理管道由一系列请求委托组成,一环接一环的调用特定的中间件

Middleware - 图1

  • 上图示例:

    • 处理管道包含四个中间件,每个中间件都包含后续中间件执行动作的引用(next),同时每个中间件在交棒之前和交棒之后可以自行参与针对HttpContxt的业务处理。
  • 通过上面的分析,中间件其实具备两个特征:

  • 入参:下一个中间件的执行委托RequestDelegate (public delegate Task RequestDelegate(HttpContext context);)
  • 输出:特定中间件的业务处理动作:因为中间件是处理管道中预设的处理逻辑,所以这个动作其实也是一个委托RequestDelegate
  • 所以.NET Core用Func<RequestDelegate,RequestDelegate> 数据结构表示中间件是合理的。

Middleware的定义方式

有两种自定义中间件的方式:

1. Factory-based Middleware

  • 基于工厂模式的中间件有如下优点:

在每个客户端请求时激活实例 (injection of scoped services) 实现IMiddleware接口: 强类型

  1. // 该接口只有一个固定的函数
  2. public Task InvokeAsync(HttpContext context,RequestDelegate next);
  3. public class FactoryActivatedMiddleware : IMiddleware
  4. {
  5. private readonly ILogger _logger;
  6. public FactoryActivatedMiddleware(ILoggerFactory logger) //
  7. {
  8. _logger = logger.CreateLogger<FactoryActivatedMiddleware>();
  9. }
  10. public async Task InvokeAsync(HttpContext context, RequestDelegate next)
  11. {
  12. // TODO logic handler
  13. _logger.LogInformation("测试");
  14. await next(context);
  15. // TODO logic handler
  16. }
  17. }
  • 使用工厂模式的中间件,构造函数参数由依赖注入(DI)填充;
  • 在[使用UseMiddleware()注册中间件]时不允许显式传参。

Middleware - 图2

2. Conventional-based Middleware

顾名思义,基于约定的中间件类有一些特定约定:

具有RequestDelegate类型参数的公共构造函数 名称为Invoke或InvokeAsync的公共方法, 此方法必须 ①返回Task ② 方法第一个参数是HttpContext

  1. public class ConventionalMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4. public ConventionalMiddleware(RequestDelegate next)
  5. {
  6. _next = next;
  7. }
  8. public async Task InvokeAsync(HttpContext context, AppDbContext db)
  9. {
  10. var keyValue = context.Request.Query["key"];
  11. if (!string.IsNullOrWhiteSpace(keyValue))
  12. {
  13. db.Add(new Request()
  14. {
  15. DT = DateTime.UtcNow,
  16. MiddlewareActivation = "ConventionalMiddleware",
  17. Value = keyValue
  18. });
  19. await db.SaveChangesAsync();
  20. }
  21. await _next(context);
  22. }
  23. }

构造函数和Invoke/InvokeAsync的其他参数由依赖注入(DI)填充; 基于约定的中间件,在[使用UseMiddleware()注册中间件]时允许显式传参。

注册中间件的算法分析

  • app.UseMiddleware<TMiddleware>()内部使用app.Use(Func<RequestDelegate,RequestDelegate> middleware)注册中间件,
  • 返回值还是IApplicationBuilder, 故具备链式注册的能力。

算法类似于基础链表, 只是指针指向的不是节点,而是后续节点的字段。

Middleware - 图3

  1. //--------节选自 Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder--------
  2. private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
  3. public IApplicationBuilder Use(Func<RequestDelegate,RequestDelegate> middleware)
  4. {
  5. this._components.Add(middleware);
  6. return this;
  7. }
  8. public RequestDelegate Build()
  9. {
  10. RequestDelegate app = context =>
  11. {
  12. // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
  13. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
  14. var endpoint = context.GetEndpoint();
  15. var endpointRequestDelegate = endpoint?.RequestDelegate;
  16. if (endpointRequestDelegate != null)
  17. {
  18. var message =
  19. $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
  20. $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
  21. $"routing.";
  22. throw new InvalidOperationException(message);
  23. }
  24. context.Response.StatusCode = StatusCodes.Status404NotFound;
  25. return Task.CompletedTask;
  26. };
  27. foreach (var component in _components.Reverse())
  28. {
  29. app = component(app);
  30. }
  31. return app;
  32. }
  • 通过以上代码我们可以看出:

注册中间件的过程实际上,是给一个Type为List<Func<RequestDelegate, RequestDelegate>>的集合依次添加元素的过程;

中间件的数据结构:(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满足条件则插入中间件

Middleware - 图4