ASP.NET Core控制器端点如何发送>2GB的大文件
大文件(>2GB)流式下载解决方案
当前架构中,所有环节均将完整文件加载到内存(MemoryStream或byte[]),导致大文件触发OutOfMemory异常。以下是各环节的修改方案,实现流式传输,彻底移除内存中加载完整文件的逻辑:
1. 后端API控制器:直接返回Blob流
不再将Blob内容复制到MemoryStream,而是直接返回流,由ASP.NET Core自动处理分块传输:
[HttpGet("objects/archives/{archiveName}")] public async Task<IActionResult> GetArchive(Guid tenantId, [FromRoute] string archiveName) { using (Infrastructure.Extensions.LoggerExtensions.BeginScope( this.logger, LoggingScopeField.TenantId(tenantId))) { try { var path = new BlobStorageRepoPathBuilder() .WithId(tenantId) .WithPath("archives") .WithId(this.userInfo.UserId) .WithPath($"{archiveName}.zip") .Build(); var blob = await this.repo.DownloadAsync(path); // 直接返回Blob流,指定MIME类型和下载文件名 return File(blob.Stream, "application/zip", $"{archiveName}.zip"); } catch (Exception ex) { this.logger.LogError(ex, "Error occurred during report generation."); return this.BadRequest("Error occurred during report generation. Please check the logs."); } } }
2. BFF层:流式转发后端响应
修改BFF的服务调用和控制器逻辑,避免将整个文件加载为byte[],直接转发后端的响应流:
修改ObjectsServiceClient的GetArchive方法
public async Task<Stream> GetArchive(string archiveName) { var url = $@"{this.options.BackendUrl}" .AppendPathSegments("api", "tenants", $"{this.tenantInfo.TenantId}", "objects", "archives", archiveName) .SetQueryParam("api-version", "1.0") .ToString(); // 仅读取响应头,不等待完整响应体下载 var response = await _httpClient.GetAsync(new Uri(url), HttpCompletionOption.ResponseHeadersRead); await response.EnsureSuccessStatusCodeExtendedAsync(); // 返回原始响应流 return await response.Content.ReadAsStreamAsync(); }
修改BFF的API控制器
[HttpGet("archives/{archiveName}")] public async Task<IActionResult> GetArchive([FromRoute] string archiveName) { try { var stream = await this._objectsServiceClient.GetArchive(archiveName); // 转发流到前端,指定MIME类型和文件名 return File(stream, "application/zip", $"{archiveName}.zip"); } catch (Exception ex) { _logger.LogError(ex, $"Error while retrieving archive."); return BadRequest(ex.Message); } }
3. Angular前端:处理Blob并触发下载
修改HTTP请求配置,接收二进制Blob数据,通过浏览器原生逻辑触发下载,避免前端加载完整文件:
public getArchive(archiveName: string): Observable<Blob> { return this.http.get( `${this.runtimeConfig.backendApiPath}api/tenants/${this.auth.profile.tid}/objects/archives/${archiveName}`, { headers: new HttpHeaders({ Authorization: `Bearer ${this.auth.accessToken}` }), responseType: 'blob' // 指定接收二进制Blob }); } // 调用下载的方法 downloadArchive(archiveName: string): void { this.getArchive(archiveName).subscribe({ next: (blob) => { // 创建临时下载链接 const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = `${archiveName}.zip`; document.body.appendChild(link); link.click(); // 清理资源 window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(link); }, error: (err) => { console.error('下载失败', err); // 添加自定义错误提示逻辑 } }); }
核心优化点
- 后端API:直接返回Blob存储的原始流,完全避免内存中加载文件
- BFF层:使用
HttpCompletionOption.ResponseHeadersRead实现流式请求,仅转发流不缓存 - 前端:接收Blob数据,通过浏览器原生下载逻辑处理,不占用前端内存
内容的提问来源于stack exchange,提问作者Julian
相关产品推荐
相关产品推荐

