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

如何在.NET Core Web API中获取真实下载时长(而非请求时长)

解决.NET Core Web API追踪NB-IoT设备真实下载耗时的问题

普通中间件只能统计请求处理的耗时(即服务器准备好响应数据的时间),但NB-IoT设备带宽极低,实际数据传输耗时远超过请求处理时间,这时候需要针对文件传输的实际阶段计时,以下是两种可行方案:

方案一:自定义FileResult拦截传输过程

通过继承FileResult并重写ExecuteResultAsync方法,在实际发送文件数据的前后记录时间,因为该方法负责将文件内容写入响应流,能准确覆盖传输耗时。

自定义TimedFileResult实现

public class TimedFileResult : FileResult
{
    private readonly ILogger<TimedFileResult> _logger;
    private readonly FileResult _innerResult;

    public TimedFileResult(FileResult innerResult, ILogger<TimedFileResult> logger) 
        : base(innerResult.ContentType)
    {
        _innerResult = innerResult;
        _logger = logger;
        // 复制原始FileResult的属性
        FileDownloadName = innerResult.FileDownloadName;
        EntityTag = innerResult.EntityTag;
        LastModified = innerResult.LastModified;
        EnableRangeProcessing = innerResult.EnableRangeProcessing;
    }

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        var startTime = DateTime.UtcNow;
        try
        {
            await _innerResult.ExecuteResultAsync(context);
        }
        finally
        {
            var elapsed = DateTime.UtcNow - startTime;
            _logger.LogInformation("文件下载完成,总耗时:{ElapsedMs}ms,文件大小:{FileSize}KB", 
                elapsed.TotalMilliseconds, GetFileSize());
        }
    }

    private long GetFileSize()
    {
        // 根据不同FileResult类型获取文件大小
        if (_innerResult is PhysicalFileResult physicalResult)
        {
            return new FileInfo(physicalResult.FileName).Length / 1024;
        }
        if (_innerResult is FileStreamResult streamResult)
        {
            return streamResult.FileStream.Length / 1024;
        }
        return 0;
    }
}

在控制器中使用

[ApiController]
[Route("api/download")]
public class DownloadController : ControllerBase
{
    private readonly ILogger<TimedFileResult> _logger;

    public DownloadController(ILogger<TimedFileResult> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Download()
    {
        var filePath = "你的文件路径";
        var originalResult = PhysicalFile(filePath, "application/octet-stream", "download.bin");
        return new TimedFileResult(originalResult, _logger);
    }
}

方案二:通过响应流包装统计传输时长

在中间件中替换响应的Body流为自定义的追踪流,记录写入流的总字节数和耗时,这种方式不需要修改控制器代码,更通用。

实现追踪流和中间件

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

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

    public async Task InvokeAsync(HttpContext context)
    {
        var originalBody = context.Response.Body;
        var startTime = DateTime.UtcNow;
        // 根据你的下载接口路径判断是否需要追踪
        var isDownload = context.Request.Path.StartsWithSegments("/api/download");

        try
        {
            if (isDownload)
            {
                using var trackingStream = new TrackingStream(originalBody);
                context.Response.Body = trackingStream;
                await _next(context);
                // 确保所有数据都已写入客户端
                await trackingStream.FlushAsync();
                var elapsed = DateTime.UtcNow - startTime;
                _logger.LogInformation("下载完成,总耗时:{ElapsedMs}ms,发送字节数:{Bytes}", 
                    elapsed.TotalMilliseconds, trackingStream.BytesWritten);
            }
            else
            {
                await _next(context);
            }
        }
        finally
        {
            context.Response.Body = originalBody;
        }
    }
}

public class TrackingStream : Stream
{
    private readonly Stream _innerStream;
    public long BytesWritten { get; private set; }

    public TrackingStream(Stream innerStream) => _innerStream = 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 Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);
    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)
    {
        BytesWritten += count;
        _innerStream.Write(buffer, offset, count);
    }

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

注册中间件

在Program.cs中,将中间件注册到管道(注意顺序,需放在UseRouting和UseEndpoints之间):

var app = builder.Build();

// 其他中间件...
app.UseMiddleware<DownloadTimingMiddleware>();

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

app.Run();

关键说明

普通中间件无法统计传输耗时的原因是:ASP.NET Core中间件管道在控制器返回结果后就完成了Invoke流程,实际的文件数据传输是由框架在管道外部异步处理的,因此中间件的计时仅覆盖到响应准备完成的阶段,无法包含数据通过TCP传输到客户端的时间。

内容的提问来源于stack exchange,提问作者Michael Eckhart-Wöllkart

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 18:47:07