Click here to Skip to main content
15,033,650 members
Articles / Web Development / ASP.NET / ASP.NET Core
Article
Posted 30 Jul 2019

Stats

6.7K views
6 bookmarked

Fighting File Downloads and Dinosaurs with NSwag (via ASP.NET Boilerplate)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
30 Jul 2019CPOL4 min read
Fighting File Downloads and Dinosaurs with NSwag via ASP.NET Boilerplate

Technically, it was the dinosaurs, approximately 240 million years ago, that first solved downloading files from web servers. So doing it with a modern tech stack with an auto-generated client-side proxy should be easy, right?

Embed from Getty Images

Sadly, I've lived with this embarrassing hack to a rudimentary problem for months because a confluence of technologies that make my life easy for common actions make it hard for infrequent ones. And sometimes, when life is hard, you give up and write something godawful to teach life a lesson. Make it take the lemons back.

This week, I won round 2 by solving the problem correctly. Pure joy, I'm tellin' ya. I just had to share.

Fellow humanoids: prepare to rejoice.

The Problem

My tech stack looks like this:

  • ASP.NET Core - for back end
  • Angular 7 - for front end (it requires a custom CORS policy, more on that later)
  • Swashbuckle - exposes a dynamically generated swagger json file
  • NSwag - consumes the swagger file and generates a client proxy

It happens to look like that because I use this excellent framework called ASAP.Net Boilerplate (also check out this amazing ASP.NET Boilerplate Overview, then subscribe, the guy who produced it must be a genius). But whatever, you should totally use that stack anyway because those four technologies were preordained by the Gods as a path to eternal bliss. That's a fact, the Buddha said it, go look it up.

Also, the API client proxy that NSwag generates is totes amazing -- saves a huge amount of time and energy. Unless, it turns out, you're trying to download a dynamically generated Excel file in TypeScript on button click and trigger a download.

A Naive Solution

After a brief web search, one couldn't be blamed for nuggetting (a real word, apparently, but not what you think) EPPlus and writing an ASP.NET controller like this:

C#
[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();
       }
   }
}

I took the approach above and naively expected Swashbuckle to generate a reasonable swagger.json file. It generated this:

"/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"
           }
       }
   }
},

See the problem? You're clearly smarter than me. I ran NSwag and it generated this:

C#
export class ApiServiceProxy {
   productFiles(fileName: string): Observable<void> {

Oh no. No, Observable of void, is not going to work. It needs to return something, anything. Clearly, I needed to be more explicit about the return type in the controller:

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

And Swagger?

JavaScript
"/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"
               }
           }

Perfect! Swagger says a FileContentResult is the result and NSwag generates the exact code I was hoping for. Everything looks peachy ... until you run it and the server says:

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

Gah! And what about specifying FileContentResult as the return type? Fail. It's back to void.

Ohai ProducesResponseType attribute.

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

Swagger, do you like me now? Yes. NSwag? Yes! Serverside runtime you love me right? Yup. Finally NSwag, you'll give me back that sweet FileContentResult if I'm friendly and sweet?

ERROR SyntaxError: Unexpected token P in JSON at position 0

inside the blobToText() function?!

NOOOOOOOOOOOOOOOOOO

😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑😑

OOOOOOOOOOOOOOOOOO!

I Give Up

It was a disaster. blobToText()? Grr. At some point, while fighting it, I was even getting these red herring CORS errors that I can't reproduce now that I spent hours fighting. All I know is if you see CORS errors, don't bother with [EnableCors], just read the logs closely, it's probably something else.

That was about six months ago. It's taken me that long to calm down. To everyone I've interacted with since, I do apologize for the perpetual yelling.

At the time, I solved it by adding a hidden form tag, an ngNoForm, a target="_blank", and a bunch of hidden inputs. I don't know how I slept at night.

But I was actually pretty close and with persistence found the path to enlightenment.

Less Complaining, More Solution

Ok, ok, I've dragged this on long enough. On a good googlefu day, I stumbled on the solution of telling Swashbuckle to map all instances of FileContentResult with "file" in startup.cs:

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

That generates this swagger file: "/api/ProductFiles/{filename}.xlsx":

XML
{ "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" } } } } }

Type: file, yes of course. Solved problems are always so simple. Which NSwag turns into this function:

C#
productFiles(fileName: string): Observable<FileResponse> {

Which allows me to write this fancy little thang:

C#
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(); 
    }); 
} 

So pretty, right?! And it even works!! What's even awesomer is if you add additional parameters like:

C#
public ActionResult Download(string fileName, [FromBody]ProductFileParamsDto paramsDto)

Then NSwag generates a ProductFileParamsDto and makes it a parameter. Fantabulous! All the code is available in a nice tidy pull request for perusal.

Conclusion

I really think this issue is why the dinosaurs left. But now hopefully, with some luck, you won't share their fate.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Lee P Richardson
Web Developer
United States United States
Lee is a prolific writer, speaker, and video producer on .Net and open source topics. He has published over 100 posts to his personal blog (https://www.leerichardson.com) that have received more than half a million views since 2007. His "Code Hour" YouTube channel (https://youtube.com/leerichardson200) has attracted nearly 1,000 subscribers who have collectively consumed over 5,900 hours of his content. StackOverflow ranks him as a top 2% contributor. He has published 25 articles to CodeProject with an average article rating of 4.96/5. Throughout his 20 year software development consulting career in the DC area he has spoken scores of times at code camps, conferences, and user groups. He created the Siren of Shame (https://sirenofshame.com), and is a Solution Samurai at InfernoRed (http://infernoredtech.com). He is active on twitter where you can reach him @lprichar (https://twitter.com/lprichar).

Comments and Discussions

 
QuestionUse NSwag for spec generation Pin
Rico Suter14-Jan-20 12:52
MemberRico Suter14-Jan-20 12:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.