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





5.00/5 (4投票s)
使用 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 中供查阅。
结论
我真的认为这个问题是恐龙灭绝的原因。但现在,希望,只要有一些运气,你就不会和它们有一样的命运。