24

I try to download a file via an angular 2 typescript client. The link generated in Swagger UI works fine, but the generated typescript client does not.

The controller looks like this:

    [HttpGet("export")]
    [SwaggerResponse((int) HttpStatusCode.OK, Type = typeof(FileContentResult))]
    [ProducesResponseType(typeof(FileResult), (int) HttpStatusCode.OK)]
    [Produces("text/csv")]
    public virtual FileResult Export(int Id, string fileType, CsvFormat format, bool includeHeader)
    {
        .
        .
        .
        FileStreamResult file = new FileStreamResult(s, "text/csv");
        file.FileDownloadName = ts.Name + "." + fileType;

        return file;
    }

Swagger UI:Swagger UI Download

The generated typescript client looks like this. As you can see the responseText is set but never returned. What am I missing?

protected processRestTimeSeriesExportGet(response: Response): Observable<void> {
    const status = response.status; 

    if (status === 200) {
        const responseText = response.text();
        return Observable.of<void>(<any>null);
    } else if (status !== 200 && status !== 204) {
        const responseText = response.text();
        return throwException("An unexpected server error occurred.", status, responseText);
    }
    return Observable.of<void>(<any>null);
}

Best regards

lama
  • 362
  • 1
  • 2
  • 9
  • ok, first problem is resolved. now the typescript client tries to parse the incoming FileStreamResult. let resultData200 = responseText === "" ? null : JSON.parse(responseText, this.jsonParseReviver); Is it possible to get the FileStreamResult object without parsing? – lama May 08 '17 at 12:54
  • The response.schema.type must be file, then the file download logic (returning a blob obj) is generated... – Rico Suter May 11 '17 at 11:52
  • Did you try SwaggerResponse with FileResult instead of FileContentResult? – Rico Suter May 11 '17 at 11:53
  • @RicoSuter thanks for the tip, the response schema was not set. The problem was not caused by nswag though, but rather by swashbuckle. we use swashbuckle to generate our swagger.json, and the schema was not correct. – lama Nov 13 '17 at 15:57
  • ah ok, btw: you can also generate the spec with nswag instead of swashbuckle... – Rico Suter Nov 13 '17 at 16:00
  • So in our case, we just needed to manually set the schema for the 200 response for FileResults to a new schema of type "file". – lama Nov 13 '17 at 16:03
  • ah, thanks, did not know that! – lama Nov 13 '17 at 16:04
  • 1
    See https://github.com/RSuter/NSwag/wiki/WebApiToSwaggerGenerator – Rico Suter Nov 13 '17 at 16:17
  • 1
    Is this solved, but no answer is posted? – Eivind Gussiås Løkseth Dec 18 '17 at 13:28

4 Answers4

28

Eric Gontier's solution works great for Swashbuckle 4 and NSwag 12. If you've upgraded to swashbuckle 5 and thus OpenApi 3 and NSwag 13, then the solution is different. Instead you'll need a custom operation filter, and an reusable attribute to indicate the content-type result:

Custom attribute

/// <summary>
/// Indicates swashbuckle should expose the result of the method as a file in open api (see https://swagger.io/docs/specification/describing-responses/)
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class FileResultContentTypeAttribute : Attribute
{
    public FileResultContentTypeAttribute(string contentType)
    {
        ContentType = contentType;
    }

    /// <summary>
    /// Content type of the file e.g. image/png
    /// </summary>
    public string ContentType { get; }
}

Operation filter

public class FileResultContentTypeOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var requestAttribute = context.MethodInfo.GetCustomAttributes(typeof(FileResultContentTypeAttribute), false)
            .Cast<FileResultContentTypeAttribute>()
            .FirstOrDefault();

        if (requestAttribute == null) return;

        operation.Responses.Clear();
        operation.Responses.Add("200", new OpenApiResponse
        {
            Content = new Dictionary<string, OpenApiMediaType>
            {
                {
                    requestAttribute.ContentType, new OpenApiMediaType
                    {
                        Schema = new OpenApiSchema
                        {
                            Type = "string",
                            Format = "binary"
                        }
                    }
                }
            }
        });
    }
}

Startup.cs

services.AddSwaggerGen(options =>
{
    ...
    options.OperationFilter<FileResultContentTypeOperationFilter>();
}

Sample Controller

Then annotate your controller with the attribute.

[HttpPost]
[Route("{fileName}.csv")]
[FileResultContentType("text/csv")]
public async Task<ActionResult> Generate(string fileName, [FromBody]MyDto myDto)
{
    var fileMemoryStream = GetCsvAsBytes(myDto);
    return File(fileMemoryStream,
        "text/csv", fileName + ".csv");
}
Lee Richardson
  • 8,331
  • 6
  • 42
  • 65
  • 1
    Thx a lot!!! I would give you more thumbs up if it was possible!!! – dotnet-provoke Apr 23 '20 at 10:28
  • note: Description is also required. in the operation.Responses.Add, I added this next to Context: Description = "Success", – John-Luke Laue May 26 '20 at 20:38
  • 3
    Up-vote, works for for following stack : Swashbuckle.AspNetCore 5.5.1 + netCore 3.1 + Angular 9.1 + NSwag command line tool for .NET Core NetCore21, toolchain v13.6.2.0 (NJsonSchema v10.1.23.0 (Newtonsoft.Json v11.0.0.0)) – Edgars Pivovarenoks Jul 08 '20 at 17:18
  • I had a FileContentResult but that wouldn't work without a content-disposition header, the following works: `Response.Headers.Add("Content-Disposition", $"attachment; filename={fileName}");` – Rubenisme Jul 29 '20 at 10:19
  • The 3rd parameter in the `return File(stream, contentType, fileName)` method is mandatory! – fharreau Feb 24 '21 at 16:41
  • This solution does NOT work for Nswag.... what was specifically asked – ThexBasic May 27 '22 at 08:25
9

Found the response of this problem :

In startup add:

services.AddSwaggerGen(options =>
{   
    options.MapType<FileResult>(() =>
    {
        return new Microsoft.OpenApi.Models.OpenApiSchema
        {
            Type = "string",
            Format = "binary",
        };
    });
}

And for your controller:

[HttpPost]
[SwaggerResponse(200, typeof(FileContentResult))]
[ProducesResponseType(typeof(FileContentResult), 200)]
public async Task<FileResult> MyMethod(Viewmodel vm)
{
    // ...
}

Or in Minimal API style

app.MapGet("/download", () => { ... }).Produces<FileStreamResult>()

A late response but for people who has the same problem ...

2xRon
  • 25
  • 6
Eric Gontier
  • 131
  • 1
  • 5
  • options.MapType does not exist anymore. Seems like it was replaced with options.TypeMappers `(ICollection)` – ThexBasic May 27 '22 at 08:22
3

In the API, Required Nuget packages:

1. Microsoft.AspNetCore.StaticFiles // To determine MimeType
2. NSwag.Annotations // To map the return type of API with Angular Service Generated by NSwag

Search for the pacakges in Nuget and install them.

Then In Startup.cs,

services.AddSwaggerGen(options =>
{
    // Swagger Configurations
    options.MapType<FileContentResult>(() => new Schema
    {
        Type = "file"
    });
});

Now add a method to get the MimeType of file

private string GetMimeType(string fileName)
{
    var provider = new FileExtensionContentTypeProvider();
    string contentType;
    if (!provider.TryGetContentType(fileName, out contentType))
    {
        contentType = "application/octet-stream";
    }
    return contentType;
} 

Now Add a method to download file

[SwaggerResponse(200, typeof(FileContentResult))]
[ProducesResponseType(typeof(FileContentResult), 200)]
public FileContentResult DownloadDocument(string fileName)
{ 
    // _environment => IHostingEnvironment Instance
    var filepath = Path.Combine($"{this._environment.WebRootPath}\\path-to\\filename}");

    var mimeType = this.GetMimeType(filename);

    // Checks if file exists 
    var fileBytes = File.ReadAllBytes(filepath);
    return new FileContentResult(fileBytes, mimeType)
    {
        FileDownloadName = filename
    };
}

Now the downloadFile method in angular service generated by NSwag will return Observable. To Consume the service, first install file-saver using npm i file-saver. Then import it in component
import { saveAs } from 'file-saver';

downloadDocument = (filename: string): void => {
    this._service.downloadDocument(filename).subscribe((res) => {
      saveAs(res.data, 'filename');
    });
  };

This will download file.

20B2
  • 2,011
  • 17
  • 30
0

The solution of @20B2 is working well, but instead of using

() => new Schema

You should use:

() => new OpenApiSchema