请求管道详解
大约 10 分钟约 2918 字
请求管道详解
简介
ASP.NET Core 请求管道由一系列中间件组成,每个 HTTP 请求按顺序经过每个中间件。理解请求管道的工作原理是构建自定义中间件和排查问题的基础。请求管道是 ASP.NET Core 的核心架构概念,所有功能(认证、授权、CORS、缓存、路由等)都通过中间件实现。
请求管道的执行模型
请求管道是一个"洋葱"模型:
客户端请求 → [中间件 1] → [中间件 2] → [中间件 3] → [端点处理]
客户端响应 ← [中间件 1] ← [中间件 2] ← [中间件 3] ← [端点处理]
具体示例:
GET /api/users → [异常处理] → [HSTS] → [HTTPS重定向] → [静态文件]
→ [CORS] → [认证] → [授权] → [输出缓存] → [路由]
→ [控制器] → [返回响应]
← [输出缓存] ← [授权] ← [认证] ← [CORS]
← [静态文件] ← [HTTPS] ← [HSTS] ← [异常处理]
关键概念:
- 每个中间件都可以决定是否调用下一个中间件(next)
- 请求和响应经过中间件的顺序相反
- 中间件可以短路管道(不调用 next,直接返回响应)
- 中间件可以在请求前和响应后都执行逻辑特点
管道结构
标准中间件顺序
var app = builder.Build();
// ============================================
// 标准中间件注册顺序(推荐)
// ============================================
// 1. 异常处理(最外层 — 捕获所有异常)
app.UseExceptionHandler("/error");
// 2. HSTS(仅 HTTPS 环境)
app.UseHsts();
// 3. HTTPS 重定向
app.UseHttpsRedirection();
// 4. 静态文件(在 CORS 和认证之前)
app.UseStaticFiles();
// 5. CORS(在认证和授权之前)
app.UseCors("CorsPolicy");
// 6. 认证(在授权之前)
app.UseAuthentication();
// 7. 授权(在端点路由之前)
app.UseAuthorization();
// 8. 输出缓存(在路由之前)
app.UseOutputCache();
// 9. 限流
app.UseRateLimiter();
// 10. 端点路由(映射控制器/Minimal API)
app.MapControllers();
app.MapHub<ChatHub>("/hubs/chat");
// 请求流向:
// Request → ExceptionHandler → Hsts → Https → Static → CORS
// → AuthN → AuthZ → OutputCache → RateLimiter → Controller
// Response ← (反序返回)中间件顺序决策表
中间件 必须在...之前注册 原因
---------------------------------------------------------------
ExceptionHandler 无 捕获所有异常
Hsts 无 安全头
HttpsRedirection 无 重定向
StaticFiles 无 快速返回静态文件
Cors Authentication 预检请求需要通过 CORS
Authentication Authorization 先认证后授权
Authorization MapControllers 先授权后路由
OutputCache UseRouting 缓存需要知道路由
RateLimiter MapControllers 限流需要路由信息
UseRouting UseEndpoints 路由匹配Use / Run / Map
// ============================================
// Use — 注册中间件(可调用 next 继续管道)
// ============================================
app.Use(async (context, next) =>
{
// 请求阶段(前半)
Console.WriteLine($"请求: {context.Request.Method} {context.Request.Path}");
// 调用下一个中间件
await next(context);
// 响应阶段(后半)
Console.WriteLine($"响应: {context.Response.StatusCode}");
});
// ============================================
// Run — 终端中间件(短路,不调用 next)
// ============================================
app.Run(async context =>
{
await context.Response.WriteAsync("管道终止 — 这是最终响应");
});
// ============================================
// Map — 路径分支(创建子管道)
// ============================================
app.Map("/admin", adminApp =>
{
// /admin 路径下的独立管道
adminApp.Use(async (context, next) =>
{
context.Items["IsAdmin"] = true;
await next(context);
});
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin 面板");
});
});
// ============================================
// MapWhen — 条件分支
// ============================================
app.MapWhen(
context => context.Request.Headers.ContainsKey("X-Mobile"),
mobileApp =>
{
mobileApp.Use(async (context, next) =>
{
context.Items["IsMobile"] = true;
await next(context);
});
mobileApp.Run(async context =>
{
await context.Response.WriteAsync("移动端响应");
});
});
// ============================================
// MapGet / MapPost — Minimal API 端点
// ============================================
app.MapGet("/api/health", () => Results.Ok(new { status = "healthy" }));
app.MapPost("/api/orders", (Order order) => Results.Created($"/api/orders/{order.Id}", order));自定义中间件
约定式中间件
// ============================================
// 约定式中间件 — 构造函数注入单例依赖
// ============================================
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
var requestId = context.TraceIdentifier;
// 请求阶段
_logger.LogInformation(
"[{RequestId}] {Method} {Path}{QueryString} 开始",
requestId,
context.Request.Method,
context.Request.Path,
context.Request.QueryString);
try
{
await _next(context);
}
finally
{
sw.Stop();
// 响应阶段
_logger.LogInformation(
"[{RequestId}] {Method} {Path} => {StatusCode} ({ElapsedMs}ms)",
requestId,
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
}
// 扩展方法 — 简化注册
public static class RequestLoggingExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
{
return app.UseMiddleware<RequestLoggingMiddleware>();
}
}
// 使用
app.UseRequestLogging();工厂式中间件(支持 Scoped 依赖)
// ============================================
// IMiddleware 接口 — 每个请求创建新实例
// ============================================
public class TenantMiddleware : IMiddleware
{
private readonly ITenantService _tenantService;
private readonly ILogger<TenantMiddleware> _logger;
// 支持 Scoped 依赖注入
public TenantMiddleware(ITenantService tenantService, ILogger<TenantMiddleware> logger)
{
_tenantService = tenantService;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (!string.IsNullOrEmpty(tenantId))
{
var tenant = await _tenantService.GetTenantAsync(tenantId);
if (tenant != null)
{
context.Items["Tenant"] = tenant;
_logger.LogDebug("租户已解析: {TenantId}", tenantId);
}
}
await next(context);
}
}
// 注册为 Scoped 服务(重要!)
builder.Services.AddScoped<TenantMiddleware>();
// 使用
app.UseMiddleware<TenantMiddleware>();响应压缩中间件
// ============================================
// 响应压缩中间件示例
// ============================================
public class ResponseCompressionMiddleware
{
private readonly RequestDelegate _next;
public ResponseCompressionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 检查是否支持压缩
var acceptEncoding = context.Request.Headers["Accept-Encoding"].ToString();
if (!acceptEncoding.Contains("gzip") && !acceptEncoding.Contains("br"))
{
await _next(context);
return;
}
// 替换响应体流
var originalBodyStream = context.Response.Body;
using var compressionStream = new MemoryStream();
context.Response.Body = compressionStream;
await _next(context);
// 压缩响应
compressionStream.Seek(0, SeekOrigin.Begin);
context.Response.Headers["Content-Encoding"] =
acceptEncoding.Contains("br") ? "br" : "gzip";
// 实际压缩逻辑...
context.Response.Body = originalBodyStream;
await compressionStream.CopyToAsync(originalBodyStream);
}
}常用中间件实现
请求 ID 中间件
public class CorrelationIdMiddleware
{
private const string HeaderName = "X-Correlation-Id";
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 优先从请求头获取,否则生成新的
var correlationId = context.Request.Headers[HeaderName].FirstOrDefault()
?? Guid.NewGuid().ToString("N")[..16];
// 设置到 TraceIdentifier(会自动写入日志)
context.TraceIdentifier = correlationId;
// 写入响应头
context.Response.OnStarting(() =>
{
context.Response.Headers[HeaderName] = correlationId;
return Task.CompletedTask;
});
// 也可以写入 Activity
Activity.Current?.SetTag("correlation_id", correlationId);
await _next(context);
}
}
app.UseMiddleware<CorrelationIdMiddleware>();请求计时中间件
public class RequestTimingMiddleware
{
private const string HeaderName = "X-Response-Time";
private readonly RequestDelegate _next;
public RequestTimingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await _next(context);
sw.Stop();
var elapsedMs = sw.ElapsedMilliseconds;
// 写入响应头
context.Response.Headers[HeaderName] = $"{elapsedMs}ms";
// 记录到 Activity
Activity.Current?.SetTag("response_time_ms", elapsedMs);
}
}中间件 vs Filter 对比
| 特性 | 中间件 | Filter |
|---|---|---|
| 作用范围 | 所有请求 | 特定控制器/Action |
| 执行时机 | 路由之前 | 路由之后 |
| 访问路由信息 | 有限 | 完整 |
| 依赖注入 | 构造函数注入 | 支持所有 DI |
| 适用场景 | 全局关注点 | 业务逻辑 |
| 典型用途 | 日志、CORS、限流、缓存 | 验证、权限、异常处理 |
| 执行顺序 | 注册顺序 | Order 属性 |
高级中间件模式
请求上下文传播中间件
/// <summary>
/// 请求上下文中间件 — 在整个请求管道中共享上下文信息
/// </summary>
public class RequestContextMiddleware
{
private readonly RequestDelegate _next;
public RequestContextMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 从请求头提取上下文信息
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
var userId = context.User?.FindFirst("sub")?.Value;
var clientIp = context.Connection.RemoteIpAddress?.ToString();
var userAgent = context.Request.Headers["User-Agent"].ToString();
// 存储到 HttpContext.Items(管道中所有中间件可访问)
context.Items["RequestContext"] = new RequestContext
{
RequestId = context.TraceIdentifier,
TenantId = tenantId ?? "default",
UserId = userId ?? "anonymous",
ClientIp = clientIp ?? "unknown",
UserAgent = userAgent,
StartTime = Stopwatch.GetTimestamp(),
};
await _next(context);
}
}
public record RequestContext
{
public string RequestId { get; init; } = "";
public string TenantId { get; init; } = "";
public string UserId { get; init; } = "";
public string ClientIp { get; init; } = "";
public string UserAgent { get; init; } = "";
public long StartTime { get; init; }
}
// 扩展方法 — 简化其他中间件获取上下文
public static class RequestContextExtensions
{
public static RequestContext GetRequestContext(this HttpContext context)
{
return context.Items["RequestContext"] as RequestContext
?? throw new InvalidOperationException("RequestContext not found");
}
}
// 在任意中间件或控制器中使用
// var ctx = HttpContext.GetRequestContext();
// var tenantId = ctx.TenantId;限流中间件实现
/// <summary>
/// 简单的固定窗口限流中间件
/// </summary>
public class RateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RateLimitMiddleware> _logger;
private readonly int _maxRequests;
private readonly TimeSpan _window;
private readonly Dictionary<string, RequestCounter> _counters = new();
private readonly object _lock = new();
public RateLimitMiddleware(
RequestDelegate next,
ILogger<RateLimitMiddleware> logger,
int maxRequests = 100,
int windowSeconds = 60)
{
_next = next;
_logger = logger;
_maxRequests = maxRequests;
_window = TimeSpan.FromSeconds(windowSeconds);
}
public async Task InvokeAsync(HttpContext context)
{
var clientKey = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
if (IsRateLimited(clientKey))
{
_logger.LogWarning("限流: 客户端 {ClientKey} 超过限制", clientKey);
context.Response.StatusCode = 429;
context.Response.Headers["Retry-After"] = _window.TotalSeconds.ToString();
await context.Response.WriteAsJsonAsync(new
{
error = "请求过于频繁,请稍后重试",
retryAfter = _window.TotalSeconds
});
return;
}
await _next(context);
}
private bool IsRateLimited(string clientKey)
{
lock (_lock)
{
var now = DateTime.UtcNow;
if (!_counters.TryGetValue(clientKey, out var counter))
{
_counters[clientKey] = new RequestCounter { Count = 1, WindowStart = now };
return false;
}
// 重置窗口
if (now - counter.WindowStart > _window)
{
counter.Count = 1;
counter.WindowStart = now;
return false;
}
counter.Count++;
return counter.Count > _maxRequests;
}
}
private class RequestCounter
{
public int Count { get; set; }
public DateTime WindowStart { get; set; }
}
}
// 注册
app.UseMiddleware<RateLimitMiddleware>(maxRequests: 100, windowSeconds: 60);健康检查端点自定义
// Program.cs — 自定义健康检查
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("database")
.AddRedis(builder.Configuration.GetConnectionString("Redis")!, "redis")
.AddRabbitMQ(builder.Configuration.GetConnectionString("RabbitMQ")!, "rabbitmq");
// 自定义健康检查响应
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds,
}),
totalDuration = report.TotalDuration.TotalMilliseconds,
};
await context.Response.WriteAsJsonAsync(result);
}
});优点
缺点
总结
请求管道核心:中间件按注册顺序执行,请求进、响应出(U 型管道)。Use 注册中间件并调用 next、Run 终端短路、Map 路径分支。自定义中间件推荐约定式(构造函数注入单例依赖)。工厂式(IMiddleware)支持 Scoped 依赖。中间件适合全局关注点(日志/CORS/限流),Filter 适合业务逻辑(认证/验证)。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《请求管道详解》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《请求管道详解》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《请求管道详解》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《请求管道详解》最大的收益和代价分别是什么?
