65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 NSwag (通过 ASP.NET Boilerplate) 解决文件下载和恐龙问题

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2019 年 7 月 30 日

CPOL

4分钟阅读

viewsIcon

13536

使用 NSwag 通过 ASP.NET Boilerplate 解决文件下载和恐龙问题

严格来说,是大约 2.4 亿年前的恐龙首次解决了从 Web 服务器下载文件的问题。那么,使用现代技术栈和自动生成的客户端代理来做这件事应该很容易,对吧?

嵌入自 Getty Images

可悲的是,我几个月来一直忍受着这个针对基本问题的粗糙的解决方法,因为使我轻松处理常见操作的技术组合,却让不常用的操作变得困难。而且,有时候,当生活艰难时,你会放弃并写一些糟糕的东西来教训生活。让它把柠檬退回去。

本周,我通过正确地解决了这个问题,赢得了第二回合。纯粹的喜悦,我告诉你。我只是想分享一下。

各位人类同胞:准备好欢庆吧。

问题

我的技术栈如下:

  • ASP.NET Core - 用于后端
  • Angular 7 - 用于前端(需要自定义 CORS 策略,稍后详述)
  • Swashbuckle - 公开动态生成的 swagger json 文件
  • NSwag - 消耗 swagger 文件并生成客户端代理

它之所以是这样,是因为我使用了这个优秀的框架,称为 ASAP.Net Boilerplate (也可以看看这个惊人的 ASP.NET Boilerplate 概述,然后订阅,制作它的人一定是天才)。但无论如何,你绝对应该使用这个技术栈,因为这四种技术是诸神预定的通往永恒幸福的道路。这是事实,佛陀说过的,自己去查查。

另外,NSwag 生成的 API 客户端代理简直棒了——节省了大量时间和精力。除非,你试图在 TypeScript 中通过按钮点击下载动态生成的 Excel 文件并触发下载。

一种天真的解决方案

经过短暂的网页搜索,你可能会被误导(Apparently, a real word, but not what you think),并写一个像这样的 ASP.NET 控制器,使用 EPPlus

[Route("api/[controller]")]
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<void> {

哦不。不,void 的 Observable 是行不通的。它需要返回一些东西,任何东西。显然,我需要更明确地在控制器中指定返回类型

public ActionResult<FileContentResult> Download(string fileName) { ... }

那 Swagger 呢?

"/api/ProductFiles/{filename}.xlsx": {
   "post": {
       "tags": ["ProductFiles"],
           "operationId": "ApiProductFilesByFilename}.xlsxPost",
           "consumes": [],
           "produces": ["text/plain", "application/json", "text/json"],
      ...

       "200": {
               "description": "Success",
                   "schema": {
                   "$ref": "#/definitions/FileContentResult"
               }
           }

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

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

该死!指定 FileContentResult 作为返回类型呢?失败。又回到了 void

哦,你好 ProducesResponseType 属性。

[HttpPost]
[Route("{filename}.xlsx")]
[ProducesResponseType(typeof(FileContentResult), (int)HttpStatusCode.OK)]
 
public ActionResult Download(string fileName)

Swagger,你现在喜欢我了吗?是的。NSwag?是的!服务器端运行时,你爱我了吗?是的。最终,NSwag,如果你友好和甜蜜,你会把那个甜美的 FileContentResult 还给我吗?

ERROR SyntaxError: Unexpected token P in JSON at position 0

blobToText() 函数里面?!

不!!!!!!!!!!!!!!!!!!!!!

😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡😡

!!!!!!!!!!!!!!!!!!!!!!!!

我放弃了

这太糟糕了。blobToText()?可恶。在与之搏斗的过程中,我甚至遇到了那些红色的 CORS 错误,现在花了几个小时搏斗之后我竟然无法重现了。我只知道,如果你看到 CORS 错误,不要理会 [EnableCors],仔细阅读日志,很可能是别的问题。

那是在大约六个月前。我花了这么长时间才平静下来。我向自那时以来我与之互动过的所有人道歉,为我一直以来的咆哮表示歉意。

当时,我通过添加一个隐藏的 form 标签、一个 ngNoForm、一个 target="_blank" 和一堆隐藏的 input 来解决了这个问题。我不知道我当时是怎么睡着的。

但我其实已经很接近了,并且通过坚持不懈找到了通往启蒙的道路。

少抱怨,多解决

好吧,好吧,我拖延得太久了。在一个美好的谷歌搜索日,我偶然发现了解决方案,即在 *startup.cs* 中告诉 Swashbuckle 将所有 FileContentResult 实例映射为“file”。

services.AddSwaggerGen(options => 
{ options.MapType<filecontentresult>(() => 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<FileResponse> {

这让我能够写出这个漂亮的小东西

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)

然后 NSwag 会生成一个 ProductFileParamsDto 并将其作为参数。太棒了!所有代码都包含在一个整洁的 pull request 中供查阅。

结论

我真的认为这个问题是恐龙灭绝的原因。但现在,希望,只要有一些运气,你就不会和它们有一样的命运。

© . All rights reserved.