结论:用中间件记请求日志,request.body 和 response.body 必须启用缓冲并重置流位置,否则控制器拿不到数据、客户端收不到响应;日志内容必须采样 + 脱敏,否则一上线就拖垮吞吐量。

直接说结论:用中间件记请求日志,Request.Body 和 Response.Body 必须启用缓冲并重置流位置,否则控制器拿不到数据、客户端收不到响应;日志内容必须采样 + 脱敏,否则一上线就拖垮吞吐量。
为什么不能在 Controller 里记完整请求日志
Controller 日志天然漏掉大量关键路径:模型绑定失败的 400、CORS 拒绝、认证失败的 401、路由未匹配的 404 —— 这些请求根本进不了 Controller。中间件是唯一能统一捕获这些状态的层级。另外,HttpContext.RequestServices 在中间件里才稳定可用,Controller 里拿 IUserRepository 之类作用域服务容易跨请求复用出错。
怎么安全读取 Request.Body 和 Response.Body
默认情况下,Request.Body 只能读一次,Response.Body 是只写流。不处理好,要么 Controller 报 System.InvalidOperationException: Synchronous operations are disallowed,要么前端白屏。
- 对请求体:调用
context.Request.EnableBuffering()(.NET 5+),再用StreamReader或JsonDocument.Parse解析,完事后必须context.Request.Body.Position = 0 - 对响应体:先用
var originalBody = context.Response.Body保存原流,再设context.Response.Body = new MemoryStream(),等await _next(context)执行完,把内存流内容拷回originalBody,最后别忘context.Response.Body.Position = 0 - 大文件上传场景慎用缓冲:启用
EnableBuffering()会把整个 body 加载进内存,可能触发OutOfMemoryException
日志字段选哪些才真正有用
全量记录所有请求,等于主动放弃可观测性。/health、/metrics、.js、.css 这类路径打满日志,只会掩盖真实业务问题。
- 必记字段:
context.Request.Method、context.Request.Path、context.Response.StatusCode、耗时(用Stopwatch测)、context.TraceIdentifier - 按需记 body:仅对 POST/PUT 且路径匹配业务 API(如
/api/orders)才解析,且必须脱敏password、token、idCard等字段 - 结构化日志优于字符串拼接:用
logger.LogInformation("Req {Method} {Path} {StatusCode} {ElapsedMs}", method, path, code, elapsedMs),别写"Req " + method + " " + path - 生产环境开启采样:比如只记录
context.Response.StatusCode >= 400的请求,或用随机数控制 5% 采样率
中间件注册和 InvokeAsync 容易踩的坑
注册顺序错、await _next(context) 漏掉、方法签名不对——这三处任一出错,日志中间件不是不生效,就是让整个应用卡死或返回空响应。
-
InvokeAsync必须是public async Task InvokeAsync(HttpContext context),参数名不能改,不能少,不能多,不能是Invoke同步方法 - 必须用
app.UseMiddleware<requestloggingmiddleware>()</requestloggingmiddleware>注册,别用app.Use(...)匿名委托——后者无法注入ILogger<requestloggingmiddleware></requestloggingmiddleware> -
await _next(context)必须显式 await,写成_next(context)或await _next都会中断管道 - 读取
context.Response.StatusCode要在await _next(context)之后;修改它(比如设为 401)必须在这之前,否则抛InvalidOperationException: Headers are read-only
最易被忽略的是 Request.Body.Position = 0 的重置时机:它必须在 await _next(context) 之前执行,且不能放在 try/catch 外层——异常发生时位置可能已偏移,得在 catch 块里也补一次重置。










