如何在.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
相关产品推荐
相关产品推荐

