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

如何在Azure托管的ASP.NET Core中记录响应总大小

如何在ASP.NET Core中记录响应总大小(含头部+压缩后体)

嘿,这个需求我之前做类似成本核算的时候刚好研究过,给你梳理下靠谱的实现方案和关键细节,帮你准确统计出站响应的总大小。

首先先明确你几个疑问的答案:

  • HttpContext.Response.Body.Length 不包含响应头部,而且如果启用了响应压缩,这个值要么是压缩前的原始大小,要么根本没被正确设置(比如流式响应场景),所以直接用它来统计总大小是不准确的。
  • 要统计真实的出站数据大小,必须同时计算响应头部的字节数和实际发送的响应体字节数(包括压缩后的)。

核心思路

我们需要包装ASP.NET Core的响应流,统计所有写入到客户端的字节数;同时手动计算响应头部的字节数,两者相加就是总出站大小。这个方案能兼容响应压缩、流式响应等所有场景。

代码实现

1. 自定义计数流和中间件

先写一个用来统计写入字节数的包装流,再写中间件替换响应流并计算总大小:

public class ResponseSizeLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ResponseSizeLoggingMiddleware> _logger;

    public ResponseSizeLoggingMiddleware(RequestDelegate next, ILogger<ResponseSizeLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 获取原始的响应体特性
        var originalResponseBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
        if (originalResponseBodyFeature == null)
        {
            await _next(context);
            return;
        }

        // 用计数流包装原始响应流
        var countingStream = new CountingStream(originalResponseBodyFeature.Stream);
        context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(countingStream));

        try
        {
            // 让后续中间件/控制器处理请求
            await _next(context);

            // 确保响应写入完成
            await originalResponseBodyFeature.CompleteAsync();

            // 计算响应头部的字节大小
            var headersSize = CalculateHeadersSize(context.Response.Headers);
            // 总大小 = 头部 + 体
            var totalResponseSize = headersSize + countingStream.TotalBytesWritten;

            // 这里可以根据需求过滤特定功能的请求,比如Blob下载路径
            if (context.Request.Path.StartsWithSegments("/api/blob/download"))
            {
                _logger.LogInformation("Blob download response size: {TotalSize} bytes (Headers: {HeadersSize}, Body: {BodySize})",
                    totalResponseSize, headersSize, countingStream.TotalBytesWritten);
                // 把数据存入数据库/监控系统,用于成本核算
            }
        }
        finally
        {
            // 恢复原始响应体特性
            context.Features.Set(originalResponseBodyFeature);
        }
    }

    private long CalculateHeadersSize(IHeaderDictionary headers)
    {
        if (headers == null || !headers.Any())
            return 0;

        // HTTP头部格式:每个键值对为 "Key: Value\r\n",最后加"\r\n"表示头部结束
        var headersString = string.Join("\r\n", headers.Select(kv => $"{kv.Key}: {kv.Value}")) + "\r\n\r\n";
        // HTTP头部默认用UTF-8编码计算字节数
        return System.Text.Encoding.UTF8.GetByteCount(headersString);
    }
}

// 自定义计数流,统计写入的字节数
public class CountingStream : Stream
{
    private readonly Stream _innerStream;
    public long TotalBytesWritten { get; private set; }

    public CountingStream(Stream innerStream)
    {
        _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream));
    }

    public override bool CanRead => _innerStream.CanRead;
    public override bool CanSeek => _innerStream.CanSeek;
    public override bool CanWrite => _innerStream.CanWrite;
    public override long Length => _innerStream.Length;
    public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }

    public override void Flush() => _innerStream.Flush();
    public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
    public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
    public override void SetLength(long value) => _innerStream.SetLength(value);

    public override void Write(byte[] buffer, int offset, int count)
    {
        TotalBytesWritten += count;
        _innerStream.Write(buffer, offset, count);
    }

    public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        TotalBytesWritten += count;
        await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            _innerStream.Dispose();
        base.Dispose(disposing);
    }
}

2. 注册中间件

在Program.cs中注册这个中间件,注意顺序要放在任何可能修改响应体的中间件之后(比如UseResponseCompression、UseStaticFiles),但要在UseEndpoints之前:

var builder = WebApplication.CreateBuilder(args);

// 添加响应压缩服务(如果需要)
builder.Services.AddResponseCompression();
builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
// 如果启用了响应压缩,要先注册它
app.UseResponseCompression();

app.UseRouting();
app.UseAuthorization();

// 注册我们的响应大小记录中间件
app.UseMiddleware<ResponseSizeLoggingMiddleware>();

app.MapControllers();

app.Run();

关键细节说明

  • 压缩后的体大小统计:这个方案通过包装原始响应流,不管后续中间件(比如响应压缩)怎么处理,所有写入到客户端的字节都会被计数,所以拿到的是真实的压缩后体大小。
  • 响应头部的计算:HTTP头部也是出站流量的一部分,Azure会统计这部分字节,所以必须把它加上。我们通过将头部转换成标准HTTP格式的字符串,再计算字节数,这个方式是准确的。
  • 特定功能过滤:你可以通过context.Request.Path、查询参数或者自定义请求标识,只记录Blob下载相关的响应,避免不必要的统计。
  • 流式响应兼容:如果你的Blob下载是用流式传输(比如直接写Blob流到响应),这个计数流依然能准确统计所有发送的字节,没有问题。

注意事项

  • 性能开销:这个中间件的性能影响极小,只是做了流的包装和字节计数,不会对响应速度造成明显影响。
  • Azure成本核算:Azure的出站流量是按实际发送的字节数收费的,这个方案统计的总大小和Azure的计费逻辑一致,可以直接用来核算成本。

内容的提问来源于stack exchange,提问作者Adam Stapleton

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 06:35:01