ASP.NET Core多端点请求日志:修复ActionFilter空RequestBody问题
ASP.NET Core API 请求体日志记录问题(基于ActionFilter)
我正在开发ASP.NET Core API,现有约1000个端点,每个端点的请求体各不相同。希望记录每个端点的入站请求,但不想逐个修改端点。
尝试了以下代码(仅针对HTTP POST请求),但日志中RequestBody为空字符串,尽管控制器Action方法中能获取到请求对象的值。该代码在单个端点上可行,但面对多种请求类型时需要大量if-else判断,希望避免这种情况,寻求通用解决方案:
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // Log the request details _logger.LogInformation($"Request: {context.HttpContext.Request.Method} {context.HttpContext.Request.Path}"); // Log the request body for POST requests if (context.HttpContext.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase)) { context.HttpContext.Request.EnableBuffering(); // Read the request body using (var reader = new StreamReader(context.HttpContext.Request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true)) { var requestBody = await reader.ReadToEndAsync(); // Reset the position of the request body stream context.HttpContext.Request.Body.Position = 0; _logger.LogInformation($"Request Body: {requestBody}"); } } // Call the action method var resultContext = await next(); // Perform any post-action execution logic if needed }
注:了解可通过中间件实现,但更关注如何修复现有代码或基于ActionFilter解决问题。
问题原因
你的代码在OnActionExecutionAsync阶段读取请求体,但此时ASP.NET Core已经完成了模型绑定——也就是已经把请求体读取并解析成Action的参数了,所以Request.Body的流位置已经处于末尾,即使调用EnableBuffering,直接读取也会得到空字符串。
解决方案:改用IResourceFilter提前读取请求体
要在模型绑定之前读取请求体,需要实现IAsyncResourceFilter,这个过滤器的执行时机早于模型绑定,此时请求体还未被读取。同时可以实现通用逻辑,无需针对不同请求方法写大量if-else。
完整的通用ActionFilter代码
public class RequestLoggingFilter : IAsyncResourceFilter { private readonly ILogger<RequestLoggingFilter> _logger; public RequestLoggingFilter(ILogger<RequestLoggingFilter> logger) { _logger = logger; } public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { var request = context.HttpContext.Request; _logger.LogInformation($"Request: {request.Method} {request.Path}"); // 仅处理有请求体的请求(排除GET、DELETE等无请求体的方法,兼容分块传输场景) if (request.ContentLength > 0 || request.ContentLength == null) { request.EnableBuffering(); // 根据请求头自动匹配编码,避免乱码 var encoding = Encoding.UTF8; if (!string.IsNullOrEmpty(request.ContentType)) { var contentType = new MediaTypeHeaderValue(request.ContentType); encoding = contentType.CharSet != null ? Encoding.GetEncoding(contentType.CharSet) : Encoding.UTF8; } using (var reader = new StreamReader(request.Body, encoding, detectEncodingFromByteOrderMarks: false, leaveOpen: true)) { var requestBody = await reader.ReadToEndAsync(); // 重置流位置,保证后续模型绑定能正常读取 request.Body.Position = 0; if (!string.IsNullOrWhiteSpace(requestBody)) { _logger.LogInformation($"Request Body: {requestBody}"); } } } // 继续执行后续管道(包括模型绑定和Action执行) await next(); } }
关键要点
- 过滤器时机选择:
IAsyncResourceFilter的OnResourceExecutionAsync在模型绑定前执行,此时请求体未被读取,能获取完整内容。 - 通用请求判断:通过
ContentLength判断是否有请求体,无需硬编码POST/PUT等方法,兼容所有带请求体的请求类型。 - 编码兼容处理:根据请求的
Content-Type头自动获取编码,避免UTF8以外的编码导致乱码。 - 流位置重置:读取后必须将
Request.Body.Position设为0,否则后续模型绑定会读取到空内容。
注册过滤器
在Program.cs中全局注册该过滤器,即可对所有端点生效:
builder.Services.AddControllers(options => { options.Filters.Add<RequestLoggingFilter>(); });
也可以在特定控制器/Action上通过[ServiceFilter(typeof(RequestLoggingFilter))]特性单独注册。
内容的提问来源于stack exchange,提问作者Vivek Nuna
相关产品推荐
相关产品推荐

