What is the correct way to download a file via the NSwag Code Generator (angular 2 typescript)
Asked Answered
E

4

25

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

Evaporation answered 8/5, 2017 at 9:34 Comment(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?Evaporation
The response.schema.type must be file, then the file download logic (returning a blob obj) is generated...Dundalk
Did you try SwaggerResponse with FileResult instead of FileContentResult?Dundalk
@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.Evaporation
ah ok, btw: you can also generate the spec with nswag instead of swashbuckle...Dundalk
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".Evaporation
ah, thanks, did not know that!Evaporation
See github.com/RSuter/NSwag/wiki/WebApiToSwaggerGeneratorDundalk
Is this solved, but no answer is posted?Dansby
S
29

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");
}
Schurman answered 30/10, 2019 at 21:46 Comment(5)
note: Description is also required. in the operation.Responses.Add, I added this next to Context: Description = "Success",Gober
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))Gestation
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}");Hoyle
The 3rd parameter in the return File(stream, contentType, fileName) method is mandatory!Ignatius
This solution does NOT work for Nswag.... what was specifically askedArchiplasm
H
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 ...

Hester answered 11/12, 2018 at 10:39 Comment(1)
options.MapType<T> does not exist anymore. Seems like it was replaced with options.TypeMappers (ICollection<ITypeMapper>)Archiplasm
S
4

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.

Sunlit answered 19/3, 2019 at 18:0 Comment(2)
Are you passing file name with extension like <fileName>.<csv>.Meshuga
options.MapType<T> does not exist anymore. Seems like it was replaced with options.TypeMappers (ICollection<ITypeMapper>)Archiplasm
D
0

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

() => new Schema

You should use:

() => new OpenApiSchema
Disproportion answered 23/12, 2019 at 15:17 Comment(1)
In NSwag.Core >= 13.16 OpenApiSchema is an EnumArchiplasm

© 2022 - 2024 — McMap. All rights reserved.