自定义中间件开发
大约 13 分钟约 3913 字
自定义中间件开发
简介
ASP.NET Core 的请求处理管道由一系列中间件(Middleware)串联而成,每个中间件都有机会检查、修改请求和响应。理解中间件的两种编写模式(约定式和工厂式)、管道分支与短路机制、异常传播规则以及性能考量,是构建高质量 Web 应用的基础能力。自定义中间件是实现日志记录、异常处理、认证授权、请求限流、链路追踪等横切关注点的主要手段。
特点
中间件管道模型
管道执行流程
请求进入 → Middleware1 → Middleware2 → Middleware3 → Endpoint
↓ ↓ ↓
响应修改 ← 响应修改 ← 响应修改 ← 处理器返回
具体流程(洋葱模型):
app.UseMiddleware<A>(); // 最外层
app.UseMiddleware<B>();
app.UseMiddleware<C>(); // 最内层
请求:
A.OnRequest → B.OnRequest → C.OnRequest → Endpoint
响应:
Endpoint → C.OnResponse → B.OnResponse → A.OnResponse
每个中间件的执行模式:
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// === 请求处理阶段(从外到内) ===
// 修改请求头、读取请求体、设置上下文数据...
await next(context); // 调用下一个中间件
// === 响应处理阶段(从内到外) ===
// 修改响应头、写入响应体、记录日志...
}完整管道示例
var app = builder.Build();
// 推荐的中间件顺序
app.UseExceptionHandler("/error"); // 1. 异常处理(最外层,捕获所有异常)
app.UseHsts(); // 2. HTTP Strict Transport Security
app.UseHttpsRedirection(); // 3. HTTPS 重定向
app.UseResponseCompression(); // 4. 响应压缩
app.UseStaticFiles(); // 5. 静态文件
app.UseRequestTiming(); // 6. 请求计时(自定义)
app.UseCorrelationId(); // 7. 关联 ID(自定义)
app.UseRouting(); // 8. 路由匹配
app.UseCors(); // 9. CORS
app.UseAuthentication(); // 10. 认证
app.UseAuthorization(); // 11. 授权
app.UseResponseCaching(); // 12. 响应缓存
app.UseEndpoints(endpoints => // 13. 端点执行
{
endpoints.MapControllers();
endpoints.MapGrpcService<MyService>();
});约定式中间件
基础实现
// 约定式中间件:必须满足以下约定
// 1. 构造函数接受 RequestDelegate 参数
// 2. 有一个名为 InvokeAsync 或 Invoke 的公开方法
// 3. 该方法接受 HttpContext 参数,返回 Task
// 请求计时中间件
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 stopwatch = Stopwatch.StartNew();
var method = context.Request.Method;
var path = context.Request.Path;
try
{
await _next(context); // 调用下一个中间件
}
finally
{
stopwatch.Stop();
var statusCode = context.Response.StatusCode;
var elapsed = stopwatch.ElapsedMilliseconds;
_logger.LogInformation(
"{Method} {Path} → {StatusCode} ({ElapsedMs}ms)",
method, path, statusCode, elapsed);
// 添加响应头(方便客户端和网关读取)
context.Response.Headers["X-Response-Time"] = $"{elapsed}ms";
}
}
}
// 注册约定式中间件
// 方式 1:直接使用 UseMiddleware
app.UseMiddleware<RequestTimingMiddleware>();
// 方式 2:封装为扩展方法(推荐)
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}
// 使用
app.UseRequestTiming();约定式中间件注入额外依赖
// 约定式中间件的构造函数只能注入:
// - RequestDelegate(必须,第一个参数)
// - Singleton 和 Transient 服务(通过 DI 自动注入)
// 如果需要 Scoped 服务,必须通过 InvokeAsync 参数注入
public class TenantMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TenantMiddleware> _logger;
// 构造函数:只能注入 Singleton/Transient
public TenantMiddleware(RequestDelegate next, ILogger<TenantMiddleware> logger)
{
_next = next;
_logger = logger;
}
// InvokeAsync:可以注入 Scoped 服务(从 HttpContext.RequestServices 获取)
public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new { error = "缺少租户 ID" });
return; // 短路管道
}
// Scoped 服务使用
tenantService.SetCurrentTenant(tenantId);
context.Items["TenantId"] = tenantId;
_logger.LogInformation("租户 {TenantId} 的请求", tenantId);
await _next(context);
}
}
app.UseMiddleware<TenantMiddleware>();关联 ID 中间件
// 为每个请求生成唯一的关联 ID,贯穿整个请求链路
public class CorrelationIdMiddleware
{
private const string CorrelationIdHeader = "X-Correlation-ID";
private readonly RequestDelegate _next;
private readonly ILogger<CorrelationIdMiddleware> _logger;
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 优先使用客户端传入的 Correlation ID
var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
?? Guid.NewGuid().ToString("N");
// 设置到响应头(返回给客户端)
context.Response.Headers[CorrelationIdHeader] = correlationId;
// 存入 HttpContext.Items(后续中间件和服务可以访问)
context.Items["CorrelationId"] = correlationId;
// 设置到日志上下文(自动附加到所有日志)
using (LogContext.PushProperty("CorrelationId", correlationId))
{
_logger.LogInformation("请求开始: {CorrelationId}", correlationId);
await _next(context);
_logger.LogInformation("请求结束: {CorrelationId}", correlationId);
}
}
}
public static class CorrelationIdMiddlewareExtensions
{
public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CorrelationIdMiddleware>();
}
}
app.UseCorrelationId();工厂式中间件(IMiddleware)
基础实现
// IMiddleware 接口:
// - 由 DI 容器管理生命周期(可以注册为 Scoped)
// - 适合需要 Scoped 服务作为构造函数依赖的场景
// - 性能略低于约定式(每次请求都会从 DI 容器解析)
// 异常处理中间件
public class ApiExceptionMiddleware : IMiddleware
{
private readonly ILogger<ApiExceptionMiddleware> _logger;
private readonly IWebHostEnvironment _env;
// 构造函数可以注入任何生命周期的服务
public ApiExceptionMiddleware(
ILogger<ApiExceptionMiddleware> logger,
IWebHostEnvironment env)
{
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var correlationId = context.Items["CorrelationId"]?.ToString();
_logger.LogError(exception,
"未处理的异常 [{CorrelationId}]: {Message}",
correlationId, exception.Message);
var statusCode = exception switch
{
NotFoundException => StatusCodes.Status404NotFound,
ValidationException => StatusCodes.Status422UnprocessableEntity,
UnauthorizedAccessException => StatusCodes.Status403Forbidden,
_ => StatusCodes.Status500InternalServerError
};
var response = new
{
error = new
{
code = statusCode,
message = _env.IsDevelopment() ? exception.Message : "服务器内部错误",
details = _env.IsDevelopment() ? exception.StackTrace : null,
correlationId
}
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(response);
}
}
// 注册工厂式中间件
builder.Services.AddScoped<ApiExceptionMiddleware>(); // 必须注册到 DI
app.UseMiddleware<ApiExceptionMiddleware>();约定式 vs 工厂式对比
约定式 工厂式 (IMiddleware)
────────────────────────────────────────────────────────────────────
定义方式 类 + InvokeAsync 实现 IMiddleware 接口
生命周期 每次请求创建新实例 由 DI 容器管理
DI 支持 构造函数: Singleton/Transient 构造函数: 所有生命周期
InvokeAsync: Scoped (可注册为 Scoped)
性能 高(无 DI 开销) 略低(每次从 DI 解析)
使用场景 大多数中间件 需要 Scoped 构造函数依赖
日志、计时、关联 ID 数据库上下文、租户服务
选择建议:
1. 大多数情况使用约定式(性能更好)
2. 只在确实需要 Scoped 构造函数依赖时使用工厂式
3. 如果 Scoped 服务可以通过 InvokeAsync 参数注入,优先用约定式管道分支与条件
UseWhen — 条件分支(合并回主管道)
// UseWhen:条件为 true 时执行分支中间件,之后继续主管道
// 分支中间件执行完后,请求会回到 UseWhen 之后的位置
// API 专用中间件
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
apiApp =>
{
apiApp.UseMiddleware<ApiRateLimitMiddleware>();
apiApp.UseMiddleware<ApiResponseWrapperMiddleware>();
});
// 管理端点专用中间件
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/admin"),
adminApp =>
{
adminApp.UseMiddleware<AdminAuditLogMiddleware>();
});
// WebSocket 路径专用中间件
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/ws"),
wsApp =>
{
wsApp.UseMiddleware<WebSocketAuthenticationMiddleware>();
});
// 注意:UseWhen 内的中间件共享同一个 HttpContext
// 分支中间件设置的 Items/Features 在主管道中仍然可用Map — 终端分支(不合并回主管道)
// Map:匹配的路径使用独立管道,不回到主管道
// Map 后的中间件是终端的,不会执行主管道中 Map 之后的中间件
// 健康检查端点
app.Map("/health", healthApp =>
{
healthApp.Run(async context =>
{
await context.Response.WriteAsJsonAsync(new
{
status = "healthy",
timestamp = DateTime.UtcNow
});
});
});
// 管理端点(完全独立的管道)
app.Map("/admin", adminApp =>
{
adminApp.UseMiddleware<AdminAuthMiddleware>();
adminApp.UseMiddleware<AdminAuditMiddleware>();
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin Dashboard");
});
});
// MapWhen — 基于条件的终端分支
app.MapWhen(
context => context.Request.Headers.ContainsKey("X-Debug"),
debugApp =>
{
debugApp.UseMiddleware<DebugMiddleware>();
debugApp.Run(async context =>
{
await context.Response.WriteAsync("Debug Mode");
});
});UseWhen vs Map 对比
UseWhen:
┌─ UseWhen(predicate) ─┐
│ Middleware A │ ← 条件为 true 时执行
│ Middleware B │
└──────────────────────┘
↓ (继续主管道)
Middleware C
Endpoint
Map:
┌─ Map("/api") ─┐
│ Middleware A │ ← 终端分支,不回到主管道
│ Endpoint │
└────────────────┘
Middleware C ← 不执行
Endpoint ← 不执行实用中间件示例
请求/响应日志中间件
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
// 不记录的路径前缀(减少噪音)
private static readonly string[] SkippedPaths = { "/health", "/metrics", "/favicon.ico" };
public RequestResponseLoggingMiddleware(
RequestDelegate next,
ILogger<RequestResponseLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value ?? "";
// 跳过健康检查等路径
if (SkippedPaths.Any(p => path.StartsWith(p)))
{
await _next(context);
return;
}
// 记录请求信息
_logger.LogInformation(
"→ {Method} {Path}{Query} [{Scheme}]",
context.Request.Method,
path,
context.Request.QueryString,
context.Request.Scheme);
// 记录请求头(敏感信息脱敏)
if (_logger.IsEnabled(LogLevel.Debug))
{
foreach (var (key, value) in context.Request.Headers)
{
var safeValue = IsSensitiveHeader(key) ? "***" : value.ToString();
_logger.LogDebug(" 请求头: {Key}: {Value}", key, safeValue);
}
}
// 读取请求体(注意:读取后必须重置流位置)
if (context.Request.Body.CanRead && context.Request.ContentType?.Contains("json") == true)
{
context.Request.EnableBuffering();
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
context.Request.Body.Position = 0;
if (body.Length < 2000) // 避免记录过大的请求体
{
_logger.LogDebug(" 请求体: {Body}", body);
}
}
await _next(context);
// 记录响应状态
var statusCode = context.Response.StatusCode;
var logLevel = statusCode >= 500 ? LogLevel.Error
: statusCode >= 400 ? LogLevel.Warning
: LogLevel.Information;
_logger.Log(logLevel, "← {Method} {Path} → {StatusCode}", context.Request.Method, path, statusCode);
}
private static bool IsSensitiveHeader(string headerName)
{
return headerName.Equals("Authorization", StringComparison.OrdinalIgnoreCase)
|| headerName.Equals("Cookie", StringComparison.OrdinalIgnoreCase)
|| headerName.StartsWith("X-Api-", StringComparison.OrdinalIgnoreCase);
}
}API 响应包装中间件
// 统一 API 响应格式
public class ApiResponseWrapperMiddleware
{
private readonly RequestDelegate _next;
public ApiResponseWrapperMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// 只包装 API 路径
if (!context.Request.Path.StartsWithSegments("/api"))
{
await _next(context);
return;
}
// 替换响应流(捕获原始响应)
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
// 重置流位置,读取响应内容
responseBody.Position = 0;
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
responseBody.Position = 0;
// 如果已经是包装格式,不再包装
if (context.Response.StatusCode == StatusCodes.Status204NoContent ||
responseText.TrimStart().StartsWith("{\"code\""))
{
await responseBody.CopyToAsync(originalBodyStream);
return;
}
// 包装响应
var wrappedResponse = new
{
code = context.Response.StatusCode,
message = GetStatusMessage(context.Response.StatusCode),
data = string.IsNullOrWhiteSpace(responseText) ? null : JsonSerializer.Deserialize<object>(responseText),
timestamp = DateTime.UtcNow
};
context.Response.ContentType = "application/json";
await JsonSerializer.SerializeAsync(originalBodyStream, wrappedResponse);
}
private static string GetStatusMessage(int statusCode) => statusCode switch
{
200 => "success",
201 => "created",
204 => "no content",
400 => "bad request",
401 => "unauthorized",
403 => "forbidden",
404 => "not found",
422 => "validation error",
500 => "internal server error",
_ => "unknown"
};
}性能考量
中间件性能优化
// 1. 避免在中间件中做阻塞操作
// ❌ 错误:同步阻塞
public async Task InvokeAsync(HttpContext context)
{
Thread.Sleep(100); // 阻塞线程池线程
await _next(context);
}
// ✅ 正确:异步操作
public async Task InvokeAsync(HttpContext context)
{
await Task.Delay(100); // 不阻塞线程
await _next(context);
}
// 2. 使用对象池减少 GC 压力
public class PooledBufferMiddleware
{
private readonly RequestDelegate _next;
private static readonly ObjectPool<StringBuilder> _pool =
StringBuilderPool.Shared;
public async Task InvokeAsync(HttpContext context)
{
var sb = _pool.Get();
try
{
sb.Append("Request: ");
sb.Append(context.Request.Path);
// 使用 sb...
await _next(context);
}
finally
{
sb.Clear();
_pool.Return(sb);
}
}
}
// 3. 避免在热路径上使用反射
// ❌ 每次请求都用反射
var type = context.Items["Handler"]?.GetType();
var method = type?.GetMethod("Execute");
// ✅ 编译时确定类型
if (context.Items["Handler"] is IHandler handler)
{
handler.Execute();
}
// 4. 条件性启用中间件
// 使用 UseWhen 而非 if 判断
// ❌ 在中间件内部判断
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/api"))
{
// API 专用逻辑
}
await _next(context);
}
// ✅ 使用 UseWhen
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), apiApp =>
{
apiApp.UseMiddleware<ApiMiddleware>();
});常见问题
异常处理
// 问题 1:中间件中未捕获的异常导致 500
// 原因:没有 try-catch,异常冒泡到框架默认处理器
// 解决:使用 try-catch 包裹 await _next(context)
// 问题 2:响应头已发送后无法修改状态码
// 原因:响应已经开始发送(如 await _next 已经写入了响应体)
// 解决:在 next 之前修改请求,在 next 之后只读取不修改
// 问题 3:响应体只能写入一次
// 原因:HttpResponse.Body 是单向流
// 解决:替换 Body 流,在中间件中缓存响应
// 问题 4:UseMiddleware 泛型参数找不到
// 原因:类没有满足约定式中间件的要求
// 解决:确保构造函数有 RequestDelegate,有 InvokeAsync 方法
// 问题 5:Scoped 服务在 Singleton 中注入失败
// 原因:约定式中间件的构造函数不能注入 Scoped 服务
// 解决:通过 InvokeAsync 参数注入,或改用 IMiddleware优点
缺点
总结
自定义中间件有两种编写模式:约定式(构造函数 + InvokeAsync)适合大多数场景,性能更好;工厂式(实现 IMiddleware 接口)适合需要 Scoped 构造函数依赖的场景。中间件遵循洋葱模型,await next(context) 之前处理请求,之后处理响应。UseWhen 创建条件分支(执行后回到主管道),Map 创建终端分支(不回到主管道)。管道顺序至关重要:异常处理最外层,认证/授权在路由之后。性能优化:避免阻塞操作、使用对象池减少 GC、热路径避免反射。建议将中间件封装为扩展方法 app.UseXxx(),提高代码可读性。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 框架能力的真正重点是它在请求链路中的位置和对上下游的影响。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 画清执行顺序、入参来源、失败返回和日志记录点。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 知道 API 名称,却不知道它应该放在请求链路的哪个位置。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐协议选型、网关治理、端点可观测性和契约演进策略。
适用场景
- 当你准备把《自定义中间件开发》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《自定义中间件开发》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《自定义中间件开发》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《自定义中间件开发》最大的收益和代价分别是什么?
