using System.Buffers;
using System.IO.Pipelines;

namespace Dpz.Core.WebApi.Middleware;

/// <summary>
/// 记录 HTTP 请求参数
/// </summary>
/// <param name="next"></param>
/// <param name="diagnosticContext"></param>
public class HttpRequestRecord(RequestDelegate next, IDiagnosticContext diagnosticContext)
{
    /// <summary>
    ///
    /// </summary>
    /// <param name="httpContext"></param>
    public async Task InvokeAsync(HttpContext httpContext)
    {
        await EnrichFromRequestAsync(httpContext.Request);

        await next.Invoke(httpContext);
    }

    private async Task EnrichFromRequestAsync(HttpRequest request)
    {
        if (request.Query is { Count: > 0 })
        {
            diagnosticContext.Set("QueryString", request.Query);
        }

        var bodyContent = await GetBodyContentAsync(request.BodyReader);
        if (bodyContent != string.Empty)
        {
            diagnosticContext.Set("RequestBody", bodyContent);
        }
    }

    /// <summary>
    /// 读取 http body
    /// 如果 body 超过 1 &lt;&lt; 10 字节,则不记录 body
    /// </summary>
    /// <param name="reader"></param>
    /// <returns></returns>
    private static async Task<string> GetBodyContentAsync(PipeReader reader)
    {
        var result = new StringBuilder();
        const long maxLength = 1 << 10;
        long totalLength = 0;

        while (true)
        {
            var readResult = await reader.ReadAsync();
            var buffer = readResult.Buffer;
            // 如果 buffer 长度已经大于等于 maxLength,直接截取并返回
            if (buffer.Length >= maxLength)
            {
                var span = buffer.Slice(0, maxLength);
                AppendString(result, span);
                result.Append("...");
                reader.AdvanceTo(buffer.Start, buffer.GetPosition(maxLength));
                //await reader.CompleteAsync();
                break;
            }

            totalLength += buffer.Length;

            if (readResult.IsCompleted && buffer.Length > 0)
            {
                AppendString(result, in buffer);
            }

            reader.AdvanceTo(buffer.Start, buffer.End);
            if (totalLength >= maxLength)
            {
                result.Append("...");
                break;
            }

            if (readResult.IsCompleted)
            {
                break;
            }
        }

        return result.ToString();
    }

    private static void AppendString(
        StringBuilder result,
        in ReadOnlySequence<byte> readOnlySequence
    )
    {
        var span = readOnlySequence.IsSingleSegment
            ? readOnlySequence.First.Span
            : readOnlySequence.ToArray().AsSpan();
        result.Append(Encoding.UTF8.GetString(span));
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个ASP.NET Core中间件类,用于记录HTTP请求的参数信息。让我详细解释一下这个代码:

类的整体功能

HttpRequestRecord 是一个中间件,主要用于:

  • 记录HTTP请求的查询字符串参数
  • 记录HTTP请求的Body内容
  • 将这些信息通过诊断上下文进行记录(通常用于日志记录)

主要组件分析

1. 构造函数

public class HttpRequestRecord(RequestDelegate next, IDiagnosticContext diagnosticContext)

使用了C# 12的主构造函数语法,接收:

  • next:下一个中间件的委托
  • diagnosticContext:诊断上下文,用于设置日志属性

2. InvokeAsync方法

public async Task InvokeAsync(HttpContext httpContext)

这是中间件的入口点:

  • 先调用 EnrichFromRequestAsync 提取请求信息
  • 然后调用管道中的下一个中间件

3. EnrichFromRequestAsync方法

负责提取请求信息:

  • 查询字符串:如果存在查询参数,将其记录到诊断上下文
  • 请求体:读取HTTP请求的Body内容并记录

4. GetBodyContentAsync方法(核心逻辑)

这是最复杂的方法,用于安全地读取HTTP请求体:

性能优化特性:

  • 使用 PipeReader 进行高效的流式读取
  • 设置最大读取长度限制(1024字节,即 1 << 10
  • 避免内存溢出攻击

读取逻辑:

while (true)
{
    var readResult = await reader.ReadAsync();
    var buffer = readResult.Buffer;
    
    // 如果单次读取就超过限制,截取前1024字节
    if (buffer.Length >= maxLength)
    {
        var span = buffer.Slice(0, maxLength);
        AppendString(result, span);
        result.Append("...");
        // 标记已处理的位置
        reader.AdvanceTo(buffer.Start, buffer.GetPosition(maxLength));
        break;
    }
    
    // 累计总长度检查
    totalLength += buffer.Length;
    
    // 如果读取完成且有内容,追加到结果
    if (readResult.IsCompleted && buffer.Length > 0)
    {
        AppendString(result, in buffer);
    }
    
    // 标记缓冲区已处理
    reader.AdvanceTo(buffer.Start, buffer.End);
    
    // 总长度超限检查
    if (totalLength >= maxLength)
    {
        result.Append("...");
        break;
    }
    
    if (readResult.IsCompleted)
    {
        break;
    }
}

5. AppendString方法

高效的字节到字符串转换:

private static void AppendString(StringBuilder result, in ReadOnlySequence<byte> readOnlySequence)
{
    // 优化:如果是单段内存,直接使用Span;否则转为数组
    var span = readOnlySequence.IsSingleSegment
        ? readOnlySequence.First.Span
        : readOnlySequence.ToArray().AsSpan();
    result.Append(Encoding.UTF8.GetString(span));
}

设计亮点

  1. 内存安全:限制读取长度,防止大文件攻击
  2. 性能优化:使用PipeReader和ReadOnlySequence,避免不必要的内存拷贝
  3. 流式处理:支持大文件的分段读取
  4. 编码处理:正确处理UTF-8编码的请求体
  5. 优雅降级:超长内容用"..."标记截断

使用场景

这个中间件通常用于:

  • API调用日志记录
  • 请求参数审计
  • 调试和问题排查
  • 性能监控

需要注意的是,读取请求体可能会影响后续中间件对Body的访问,在实际使用时需要考虑这个因素。

评论加载中...