技术栈解决Excel文件下载问题

在现代技术栈中,自动生成客户端代理应该是一件简单的事情,对吧?不幸的是,由于多种技术的融合,使得一些不常见的操作变得困难。有时候,当生活变得艰难时,会放弃并写一些糟糕的东西来教训生活。本周,通过正确解决问题赢得了第二轮,纯粹的快乐,必须分享。

问题描述

技术栈如下:

  • ASP.NET Core- 后端
  • Angular7 - 前端(需要自定义CORS策略)
  • Swashbuckle- 暴露动态生成的swagger json文件
  • NSwag - 消费swagger文件并生成客户端代理

这种技术栈之所以看起来如此,是因为使用了一种名为ASAP.Net Boilerplate的优秀框架。但无论如何,应该完全使用这个技术栈,因为这四种技术是上帝预设的通往永恒幸福的路径。

天真的解决方案

经过简短的网络搜索,人们可能会错误地认为使用EPPlus并编写如下ASP.NET控制器:

public class ProductFilesController : AbpController { [HttpPost] [Route("{filename}.xlsx")] public ActionResult Download(string fileName) { var fileMemoryStream = GenerateReportAndWriteToMemoryStream(); return File(fileMemoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName + ".xlsx"); } private byte[] GenerateReportAndWriteToMemoryStream() { using (ExcelPackage package = new ExcelPackage()) { ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("Data"); worksheet.Cells[1, 1].Value = "Hello World"; return package.GetAsByteArray(); } } }

采取了上述方法,并天真地期望Swashbuckle生成一个合理的swagger.json文件。它生成了这个:

{ "/api/ProductFiles/{filename}.xlsx": { "post": { "tags": ["ProductFiles"], "operationId": "ApiProductFilesByFilename}.xlsxPost", "consumes": [], "produces": [], "parameters": [{ "name": "fileName", "in": "path", "required": true, "type": "string" }], "responses": { "200": { "description": "Success" } } } } }

问题很明显,比聪明。运行了NSwag,它生成了这个:

export class ApiServiceProxy { productFiles(fileName: string): Observable { } }

哦不。不,Observable of void,不会起作用。它需要返回一些东西,任何东西。显然,需要在控制器中更明确地指定返回类型:

public ActionResult Download(string fileName) { ... }

Swagger呢?JavaScript:

{ "/api/ProductFiles/{filename}.xlsx": { "post": { "tags": ["ProductFiles"], "operationId": "ApiProductFilesByFilename}.xlsxPost", "consumes": [], "produces": ["text/plain", "application/json", "text/json"], "parameters": [{ "name": "fileName", "in": "path", "required": true, "type": "string" }], "responses": { "200": { "description": "Success", "schema": { "$ref": "#/definitions/FileContentResult" } } } } } }

完美!Swagger说FileContentResult是结果,NSwag生成了期望的确切代码。一切看起来都很完美...直到运行它,服务器说:

System.ArgumentException: Invalid type parameter 'Microsoft.AspNetCore.Mvc.FileContentResult' specified for 'ActionResult'.

啊!指定FileContentResult作为返回类型?失败。它回到了void。

放弃

这是一场灾难。blobToText()?Grr。在与它斗争的过程中,甚至得到了这些无法再现的CORS错误。所知道的是,如果看到CORS错误,不要费心去[EnableCors],只需仔细阅读日志,它可能是别的东西。

解决方案

好的,好的,已经拖延太久了。在一个好的googlefu日子里,偶然发现了解决方案,告诉Swashbuckle将所有FileContentResult实例映射为"file":

services.AddSwaggerGen(options => { options.MapType(() => new Schema { Type = "file" }); });

这生成了这个swagger文件:

{ "/api/ProductFiles/{filename}.xlsx": { "post": { "tags": ["ProductFiles"], "operationId": "ApiProductFilesByFilename}.xlsxPost", "consumes": [], "produces": ["text/plain", "application/json", "text/json"], "parameters": [{ "name": "fileName", "in": "path", "required": true, "type": "string" }], "responses": { "200": { "description": "Success", "schema": { "type": "file" } } } } } }

类型:文件,当然。解决的问题总是如此简单。NSwag将其转换为这个函数:

productFiles(fileName: string): Observable { }

这让可以写出这个花哨的小东西:

public download() { const fileName = moment().format('YYYY-MM-DD'); this.apiServiceProxy.productFiles(fileName) .subscribe(fileResponse => { const a = document.createElement('a'); a.href = URL.createObjectURL(fileResponse.data); a.download = fileName + '.xlsx'; a.click(); }); }

如此漂亮,对吧?!它甚至有效!!如果添加额外的参数,如:

public ActionResult Download(string fileName, [FromBody]ProductFileParamsDto paramsDto)
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485