ASP.NET Core 中间件管道
大约 9 分钟约 2584 字
ASP.NET Core 中间件管道
简介
中间件(Middleware)是 ASP.NET Core 请求处理模型的核心。它决定一个 HTTP 请求如何进入系统、如何经过认证、路由、日志、异常处理、限流、缓存等环节,最终再把响应返回给客户端。理解中间件,不只是会写 app.Use(...),更重要的是要理解它在整个请求生命周期中的位置、顺序、短路机制以及与路由、终结点和异常处理的关系。
特点
实现
中间件执行模型
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
_logger.LogInformation("Request start: {Method} {Path}", context.Request.Method, context.Request.Path);
await _next(context);
sw.Stop();
_logger.LogInformation(
"Request end: {Method} {Path} => {StatusCode} in {Elapsed} ms",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<RequestTimingMiddleware>();
app.MapGet("/ping", () => Results.Ok("pong"));
app.Run();中间件执行顺序可以理解成:
Request --> M1 --> M2 --> M3 --> Endpoint --> M3 --> M2 --> M1 --> Responseapp.Use / app.Run / app.Map 的区别
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("Use before");
await next();
Console.WriteLine("Use after");
});
app.Map("/admin", adminApp =>
{
adminApp.Run(async context =>
{
await context.Response.WriteAsync("admin area");
});
});
app.Run(async context =>
{
await context.Response.WriteAsync("fallback endpoint");
});三者区别:
- Use:可继续传递给下一个中间件
- Run:终结当前分支,不再继续传递
- Map:按路径分支出子管道// 条件分支:UseWhen
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
apiApp =>
{
apiApp.Use(async (context, next) =>
{
context.Response.Headers["X-API"] = "true";
await next();
});
});自定义中间件的常见写法
// 方式1:内联中间件
app.Use(async (context, next) =>
{
context.Response.Headers["X-App-Name"] = "SunnyFan";
await next();
});// 方式2:约定式中间件(最常用)
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString("N");
context.Items["CorrelationId"] = correlationId;
context.Response.Headers["X-Correlation-Id"] = correlationId;
await _next(context);
}
}// 方式3:IMiddleware(适合从 DI 获取作用域服务)
public class TenantMiddleware : IMiddleware
{
private readonly ITenantContext _tenantContext;
public TenantMiddleware(ITenantContext tenantContext)
{
_tenantContext = tenantContext;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantId))
{
_tenantContext.Set(tenantId.ToString());
}
await next(context);
}
}
builder.Services.AddScoped<TenantMiddleware>();
app.UseMiddleware<TenantMiddleware>();常见系统中间件组合顺序
var app = builder.Build();
app.UseExceptionHandler("/error"); // 先兜住后面的异常
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("DefaultPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();顺序为什么重要:
- Authentication 必须在 Authorization 之前
- UseRouting 通常要在依赖路由元数据的中间件之前
- ExceptionHandler 要尽量靠前,才能捕获更多异常
- StaticFiles 放得太后,可能多走很多无意义中间件全局异常处理示例
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 (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
code = 500,
message = "服务器内部错误",
traceId = context.TraceIdentifier
});
}
}
}中间件与终结点的边界
app.MapGet("/orders/{id:int}", (int id) => Results.Ok(new { id }))
.RequireAuthorization();中间件更适合:
- 跨所有请求的共性逻辑
- 不依赖具体业务对象的处理
- 日志、异常、鉴权、限流、请求头处理
终结点 / Controller 更适合:
- 与具体业务数据和业务规则强相关的逻辑请求限流中间件
// 基于 IP 的简单限流中间件
public class RateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RateLimitMiddleware> _logger;
private readonly ConcurrentDictionary<string, RateLimitCounter> _counters = new();
public RateLimitMiddleware(RequestDelegate next, ILogger<RateLimitMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var path = context.Request.Path;
var key = $"{ip}:{path}";
var counter = _counters.AddOrUpdate(key,
_ => new RateLimitCounter(DateTime.UtcNow, 0),
(_, existing) => existing);
lock (counter)
{
if (counter.WindowStart < DateTime.UtcNow.AddMinutes(-1))
{
counter.WindowStart = DateTime.UtcNow;
counter.Count = 0;
}
counter.Count++;
if (counter.Count > 100) // 每分钟 100 次
{
context.Response.StatusCode = 429;
context.Response.Headers["Retry-After"] = "60";
_logger.LogWarning("Rate limit exceeded for {Ip} on {Path}", ip, path);
return;
}
}
await _next(context);
}
private class RateLimitCounter
{
public DateTime WindowStart { get; set; }
public int Count { get; set; }
public RateLimitCounter(DateTime windowStart, int count)
{
WindowStart = windowStart;
Count = count;
}
}
}请求体读取中间件
// 请求体缓冲中间件(允许多次读取请求体)
public class RequestBodyBufferMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestBodyBufferMiddleware> _logger;
public RequestBodyBufferMiddleware(RequestDelegate next, ILogger<RequestBodyBufferMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 只对需要记录请求体的路径启用
if (context.Request.Method is "POST" or "PUT" or "PATCH"
&& context.Request.Path.StartsWithSegments("/api"))
{
context.Request.EnableBuffering();
using var reader = new StreamReader(
context.Request.Body,
Encoding.UTF8,
leaveOpen: true);
var body = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
// 记录请求体(注意脱敏)
if (body.Length < 10 * 1024) // 只记录小于 10KB 的请求体
{
_logger.LogInformation("Request body for {Path}: {Body}", context.Request.Path, body);
}
}
await _next(context);
}
}
// 使用注意事项:
// - EnableBuffering 会将请求体读入内存,大文件上传场景不适合
// - 读取后必须重置 Position = 0,否则下游无法读取
// - 敏感字段(密码、Token)应在日志中脱敏响应压缩中间件
// ASP.NET Core 内置响应压缩
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
{
"application/json",
"text/json",
"application/xml"
});
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest; // 生产环境可用 Optimal
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
app.UseResponseCompression();
// 压缩效果对比:
// 原始 JSON: 45KB
// Gzip: 8KB(约 82% 压缩率)
// Brotli: 6KB(约 87% 压缩率)
// 注意:压缩本身有 CPU 开销,小响应不建议压缩健康检查中间件
// 健康检查配置
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("database")
.AddRedis(redisConnectionString, "redis")
.AddUrlGroup(new Uri("https://api.external-service.com/health"), "external-api")
.AddCheck<CustomHealthCheck>("custom-check");
// 自定义健康检查
public class CustomHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
public CustomHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync("https://api.example.com/ping", cancellationToken);
if (response.IsSuccessStatusCode)
return HealthCheckResult.Healthy("External API is available");
return HealthCheckResult.Degraded($"External API returned {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("External API check failed", ex);
}
}
}
// 映射健康检查端点
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // 只检查进程是否存活
});
// 健康检查响应格式:
// {
// "status": "Healthy",
// "checks": {
// "database": { "status": "Healthy" },
// "redis": { "status": "Healthy" },
// "external-api": { "status": "Degraded", "description": "..." }
// }
// }中间件性能注意事项
// 性能要点:
// 1. 中间件数量要控制 — 每个中间件增加约 0.01-0.1ms 延迟
// 2. 避免在中间件中做阻塞操作 — 使用 async/await
// 3. 避免在中间件中做重复工作 — 利用 HttpContext.Items 缓存
// 示例:避免重复解析 Token
public class TokenParseMiddleware
{
private readonly RequestDelegate _next;
public TokenParseMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault();
if (token != null && !context.Items.ContainsKey("UserId"))
{
var userId = ParseUserIdFromToken(token);
context.Items["UserId"] = userId;
}
await _next(context);
}
private string ParseUserIdFromToken(string token) => "user-001";
}
// 4. 使用 IMiddleware 而非约定式中间件的场景:
// - 需要从 DI 获取 Scoped 服务
// - 需要精确控制生命周期
// - 需要在单元测试中 Mock
// 5. 中间件执行顺序的完整推荐
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // 开发环境异常页面
}
else
{
app.UseExceptionHandler("/error"); // 生产环境异常处理
app.UseHsts(); // HTTP 严格传输安全
}
app.UseHttpsRedirection(); // HTTPS 重定向
app.UseStaticFiles(); // 静态文件
app.UseResponseCompression(); // 响应压缩
app.UseSerilogRequestLogging(); // 请求日志
app.UseCorrelationId(); // 关联 ID
app.UseRateLimiter(); // 限流
app.UseRouting(); // 路由
app.UseCors("DefaultPolicy"); // CORS
app.UseAuthentication(); // 认证
app.UseAuthorization(); // 授权
app.MapControllers(); // 映射控制器
app.Run();优点
缺点
总结
ASP.NET Core 中间件最重要的不是“会写一个类”,而是能否正确理解请求管道的职责边界和顺序规则。真正做项目时,应优先把共性横切逻辑放在中间件里,把强业务逻辑留在终结点 / 服务层,才能让整个请求链路更清晰、可观测、可维护。
关键知识点
- 中间件的顺序本身就是业务语义的一部分。
Use、Run、Map的行为边界必须分清。- 中间件适合横切能力,不适合承载复杂业务分支。
- 路由、认证、鉴权、异常处理都要和终结点协同设计。
项目落地视角
- API 网关、后台系统、BFF 服务都依赖清晰的管道设计。
- 统一异常处理、CorrelationId、请求日志通常优先做成中间件。
- 多租户、请求头注入、审计链路也常放在中间件层。
- 高并发系统要关注中间件数量和执行代价,避免无意义堆叠。
常见误区
- 所有逻辑都往中间件里塞,最后中间件变成业务中心。
- Authentication / Authorization 顺序搞反。
UseRouting、MapControllers、MapGet之间关系没搞清。- 中间件里直接吞掉异常或改写响应,导致下游很难排障。
进阶路线
- 深入理解 ASP.NET Core 请求管道和 Endpoint Routing 实现原理。
- 学习限流、缓存、CORS、压缩、认证中间件的组合方式。
- 对照源码理解
UseMiddleware<T>的执行机制。 - 把中间件与 ProblemDetails、日志、OpenTelemetry 一起协同治理。
适用场景
- Web API、MVC、Minimal API 项目。
- 需要统一处理请求日志、鉴权、异常、租户上下文的系统。
- 中大型服务端项目的请求链路组织。
- 网关、BFF、后台服务等多种 ASP.NET Core 应用。
落地建议
- 先画清请求链路,再决定中间件顺序。
- 所有全局性横切逻辑优先考虑是否应该做成中间件。
- 中间件要尽量聚焦单一职责,避免大而全。
- 对关键中间件补充日志、traceId 和压测验证。
排错清单
- 行为异常时,先看中间件注册顺序。
- 鉴权失效时,先看
UseAuthentication()/UseAuthorization()是否正确放置。 - 某些请求没有命中逻辑时,检查是否被更前面的中间件短路了。
- 响应被提前写出时,检查中间件是否错误终止了请求。
