ASP.NET Core的请求处理管道由一个服务器和一组中间件完成,其中服务器负责请求的监听、接收、分发、和最终的响应,中间件则用来完成针对请求的处理。
从概念上讲中间件是一种装配到应用管道以外处理请求和响应的组件。每个组件:

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

3种注册方式

内联中间件

使用 WebApplication.Use(Func<RequestDelegate, RequestDelegate> middleware) 来进行注册。

1
2
3
4
5
6
7
8
9
app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("before");
await next(context);
await context.Response.WriteAsync("after");
};
});

除此之外还有两个两个 Use 扩展方法:

  • IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func, Task> middleware)
  • IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, RequestDelegate, Task> middleware)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //第1种
    app.Use(async (context, func) =>
    {
    await context.Response.WriteAsync("before");
    await func();
    await context.Response.WriteAsync("end");
    });

    //第2种
    app.Use(async (context, next) =>
    {
    await context.Response.WriteAsync("before");
    await next(context);
    await context.Response.WriteAsync("end");
    });

    其实第1种中的 await func() 和第2种中的 await next(context) 是等价的,源码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
    {
    return app.Use(next =>
    {
    return context =>
    {
    Func<Task> simpleNext = () => next(context);
    return middleware(context, simpleNext);
    };
    });
    }

强类型的中间件

如果采用强类型的中间件,则只需要实现 IMiddleware 接口,该接口定义了唯一的 InvokeAsync 方法来处理请求。这个 InvokeAsync 方法定义了两个参数,前者表示当前 HttpContext 上下文对象,后者表示一个 RequestDelegate 委托对象,它也表示后续中间件组成的管道。如果当前中间件需要将请求分发给后续的中间件处理,则只需要调用这个委托对象,否则会停止对请求的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public sealed class MyMiddleware : IMiddleware
{
private readonly OtherService _otherService;

public MyMiddleware(OtherService otherService)
{
_otherService = otherService;
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await context.Response.WriteAsync("before");
await next(context);
await context.Response.WriteAsync("after");
}
}

由于中间件是通过依赖注入方式提供的,所以需要预先对它注册为服务。

1
2
3
4
5
//1.首先注册我们自定义的中间件为作用域服务
builder.Services.AddScoped<MyMiddleware>();

//2.然后再真正的注册中间件,其实还有一个非泛型的方法app.UseMiddleware(typeof(MyMiddleware))
app.UseMiddleware<MyMiddleware>();

当请求过来的时候,我们的中间件工厂 MiddlewareFactory 会从当前服务容器里面去获取我们注册的 MyMiddleware,然后自动调用 InvokeAsync 方法。
注意:无论是泛型版本的或者非泛型版本的 UseMiddleware,它们均提供一个 args 参数(是无法由容器提供或者需要显示指定的参数),因为该参数是为注册“基于约定的中间件”而设计的,当注册一个实现了 IMiddleware 接口的强类型中间件是不能指定该参数的,从下面的部分源码可以看出会直接抛出异常。

1
2
3
4
5
6
7
8
9
if (typeof(IMiddleware).IsAssignableFrom(middleware))
{
if (args.Length > 0)
{
throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
}

return UseMiddlewareInterface(app, middleware);
}

按照约定的中间件

可能我们已经习惯了通过实现某个接口或者继承某个抽象类的扩展方式,其实这种方式有时显得约束过重,不够灵活,基于约定来定义中间件类型更常用。这种定义方式比较自由,因为它不需要实现某个预定义的接口或者继承某个基类,而只需要遵循如下这些约定:

  • 中间件类型需要一个有效的公共实例构造函数,该构造函数必须包含一个 RequestDelegate 类型的参数,当中间件实例被创建时,表示后续的中间件管道的 RequestDelegate 对象将于这个参数进行绑定。构造函数可以包含其他任意参数,RequestDelegate 出现的位置也没有限制。
  • 针对请求的处理实现在一个返回类型为 Task 的 Invoke 方法或者 InvokeAsync 方法中,它们的第一个参数必须为 HttpContext 上下文对象。约定并未对后续的参数进行限制,但由于这些参数是由依赖注入框架提供的,所以相应的服务必须先注册。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MyMiddleware
    {
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
    await context.Response.WriteAsync("before");
    await _next(context);
    await context.Response.WriteAsync("after");
    }
    }
    1
    2
    //注册MyMiddleware.cs
    app.UseMiddleware<MyMiddleware>();
    接下来编写含有参数的MyMiddleware.cs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class MyMiddleware
    {
    private readonly RequestDelegate _next;
    //布尔类型参数
    private readonly bool _global;

    public MyMiddleware(RequestDelegate next, bool global)
    {
    _next = next;
    _global = global;
    }

    public async Task InvokeAsync(HttpContext context)
    {
    await context.Response.WriteAsync("before");
    await _next(context);
    await context.Response.WriteAsync("after");
    }
    }
    1
    2
    //注册MyMiddleware.cs, 这个true参数将会映射到MyMiddleware构造函数中的global
    app.UseMiddleware<MyMiddleware>(true);
    如果存在多个参数的话,如果类型都不一样,在 InvokeAsync 中的顺序倒是无所谓的,保证第一个是 HttpContext 就行了;如果存在连续的类型相同的参数,此时会按照顺序来取;反正最好是按照顺序来取值以避免歧义。

和强类型定义的区别

  • 预定义参数支持不一样:预定义参数可以认为是无法由容器提供或者需要显示指定的参数,强类型方式定义的中间件不支持,而按照约定方式的中间件支持。
  • 生命周期不一样:强类型方式定义的中间件采用的生命周期取决于对应的服务注册,一般是 Scoped ,当然注册成 Transient 一般情况下也是一样的,因为整个请求期间只会自动生成一次,除非你需要手动使用它。而按照约定方式的中间件则总是一个单例对象,并且在应用启动时就已经创建好了。

实现原理

Func<RequestDelegate, RequestDelegate> 委托对象的中间件通过调用 IApplicationBuilder 接口的 Use 方法进行注册。而 RequestDelegate 委托对象的构建体现在 IApplicationBuilder 的 Build 方法上。作为 IApplicationBuilder 默认实现的 ApplicationBuilder,它利用一个 List<Func<RequestDelegate, RequestDelegate>> 对象来保存注册的中间件,所以 Use 方法的实现其实就是往这个集合中放入中间件,然后 Build 方法采用逆序调用这些 Func<RequestDelegate, RequestDelegate> 委托对象便将 RequestDelegate 委托对象构建出来。值得注意的是 Build 方法会在委托链的尾部添加一个额外的中间件,该中间件会将响应状态码设置为404。

1
2
3
4
5
6
7
private readonly List<Func<RequestDelegate, RequestDelegate>> _components = new();

public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_components.Add(middleware);
return this;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
};

for (var c = _components.Count - 1; c >= 0; c--)
{
app = _components[c](app);
}

return app;
}