You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
    }
}

关键要点

  1. 过滤器时机选择:IAsyncResourceFilter的OnResourceExecutionAsync在模型绑定前执行,此时请求体未被读取,能获取完整内容。
  2. 通用请求判断:通过ContentLength判断是否有请求体,无需硬编码POST/PUT等方法,兼容所有带请求体的请求类型。
  3. 编码兼容处理:根据请求的Content-Type头自动获取编码,避免UTF8以外的编码导致乱码。
  4. 流位置重置:读取后必须将Request.Body.Position设为0,否则后续模型绑定会读取到空内容。

注册过滤器

在Program.cs中全局注册该过滤器,即可对所有端点生效:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<RequestLoggingFilter>();
});

也可以在特定控制器/Action上通过[ServiceFilter(typeof(RequestLoggingFilter))]特性单独注册。


内容的提问来源于stack exchange,提问作者Vivek Nuna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 21:15:26