在现代技术栈中,自动生成客户端代理应该是一件简单的事情,对吧?不幸的是,由于多种技术的融合,使得一些不常见的操作变得困难。有时候,当生活变得艰难时,会放弃并写一些糟糕的东西来教训生活。本周,通过正确解决问题赢得了第二轮,纯粹的快乐,必须分享。
技术栈如下:
这种技术栈之所以看起来如此,是因为使用了一种名为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)