C# · #csharp#aspnetcore#middleware

ASP.NET Core 中间件管道深入解析

2018.07.11 C# 3 min 1.3k
// 目录 · contents

ASP.NET Core 的中间件管道是整个框架的核心,理解它对于排查请求处理问题非常关键。相比 ASP.NET MVC 5 的 HttpModule/HttpHandler,新的管道模型更加直观和灵活。

1. 管道执行模型

中间件是一个嵌套的委托链,每个中间件可以选择是否调用 next 来把请求传递给下一个:

1
2
3
Request → MW1 → MW2 → MW3 → [Handler]

Response ← MW1 ← MW2 ← MW3 ←
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
// 最简单的中间件
app.Use(async (context, next) =>
{
// 请求阶段(进入管道)
Console.WriteLine("MW1 before");

await next(); // 调用下一个中间件

// 响应阶段(从管道返回)
Console.WriteLine("MW1 after");
});

app.Use(async (context, next) =>
{
Console.WriteLine("MW2 before");
await next();
Console.WriteLine("MW2 after");
});

// 终端中间件(不调用 next)
app.Run(async context =>
{
Console.WriteLine("Terminal handler");
await context.Response.WriteAsync("Hello World");
});

// 输出顺序:
// MW1 before → MW2 before → Terminal handler → MW2 after → MW1 after

2. 自定义中间件类

对于复杂的中间件,建议封装成独立类:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;

// 中间件实例是单例!只能在构造函数注入 Singleton 服务
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}

// Scoped/Transient 服务通过 InvokeAsync 参数注入(每次请求调用)
public async Task InvokeAsync(HttpContext context, IUserContext userContext)
{
var sw = Stopwatch.StartNew();
var requestId = context.TraceIdentifier;

_logger.LogInformation(
"Request [{RequestId}] {Method} {Path} started by {User}",
requestId,
context.Request.Method,
context.Request.Path,
userContext.UserId);

try
{
await _next(context);
}
finally
{
sw.Stop();
_logger.LogInformation(
"Request [{RequestId}] completed {StatusCode} in {ElapsedMs}ms",
requestId,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
}

// 扩展方法(使用更优雅)
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
=> app.UseMiddleware<RequestLoggingMiddleware>();
}

// 使用
app.UseRequestLogging();

注意:中间件实例本身是单例(整个应用生命周期只创建一次),所以构造函数里只能注入 Singleton 服务。需要 Scoped 服务时,通过 InvokeAsync 方法参数注入。

3. 短路中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 健康检查:直接返回,不进入后续管道
app.Use(async (context, next) =>
{
if (context.Request.Path == "/health")
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync("OK");
return; // 不调用 next,管道在此短路
}
await next();
});

// 更好的写法:使用内置 UseWhen 条件分支
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
apiApp =>
{
apiApp.UseAuthentication();
apiApp.UseAuthorization();
});

4. 全局异常处理中间件

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;

public GlobalExceptionMiddleware(RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
_logger.LogWarning("Validation failed: {Message}", ex.Message);
context.Response.StatusCode = 400;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(new { error = ex.Message }));
}
catch (NotFoundException ex)
{
context.Response.StatusCode = 404;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(new { error = ex.Message }));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(new { error = "Internal server error" }));
}
}
}

5. 中间件顺序非常重要

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
29
30
31
32
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// 1. 异常处理必须最外层(最先注册),才能捕获所有异常
app.UseMiddleware<GlobalExceptionMiddleware>();

// 2. HTTPS 重定向
app.UseHttpsRedirection();

// 3. 静态文件(在认证之前,不需要鉴权)
app.UseStaticFiles();

// 4. 路由
app.UseRouting();

// 5. CORS(必须在认证/授权之后,路由之后)
app.UseCors("DefaultPolicy");

// 6. 认证(识别用户)
app.UseAuthentication();

// 7. 授权(检查权限)—— 必须在认证之后
app.UseAuthorization();

// 8. 自定义业务中间件
app.UseRequestLogging();

// 9. 终端:MVC 路由匹配
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

顺序错了会导致诡异的问题:比如 UseAuthorizationUseAuthentication 之前,所有请求都会 401。

踩坑记录

有一次我们的 CORS 配置一直不生效,OPTIONS 预检请求返回 403。排查了很久才发现:UseCors 放在了 UseAuthorization 之后,导致预检请求先被授权中间件拦截了。

浏览器的 OPTIONS 请求没有携带认证 token(这是标准行为),所以授权中间件直接拒绝,CORS 头根本没有机会被添加。

正确的顺序:UseRoutingUseCorsUseAuthenticationUseAuthorization。OPTIONS 请求需要在授权检查之前就被 CORS 中间件处理并返回。

总结

  • 中间件是洋葱模型:请求进入时按注册顺序执行,响应返回时逆序执行
  • 中间件实例是单例,Scoped 服务通过 InvokeAsync 参数注入
  • 顺序至关重要:异常处理最外层,CORS 在授权之前,静态文件在认证之前
  • UseWhen/MapWhen 用于条件分支,避免在每个中间件里写 if 判断
  • 全局异常处理中间件是最佳实践,统一错误响应格式
作者 · authorzt
发布 · date2018-07-11
篇幅 · length1.3k 字 · 3 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论