Mr_LinDowsMac
Mr_LinDowsMac

Reputation: 2702

Autorest/Swagger generated code for Web Api controller that returns File

In my ASP.NET Web API application I have a controller like this:

    [RoutePrefix("api/ratings")]
    public class RateCostumerController : ApiController
    { 

        [AllowAnonymous]  
        [Route("Report/GetReport")]  
        [HttpGet]
        public HttpResponseMessage ExportReport([FromUri] string costumer)  

        {  
            var rd = new ReportDocument();  

           /*No relevant code here*/

            var result = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent(ms.ToArray())
            };
            result.Content.Headers.ContentDisposition =
                new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
                {
                    FileName = "Reporte.pdf"
                };
            result.Content.Headers.ContentType =
                new MediaTypeHeaderValue("application/octet-stream");

            return result;
        }
}

So, when I make a simple GET request with a costumer parameter I get a pdf file in browser as response. Some of the response headers:

Content-Disposition : attachment; filename=Reporte.pdf Content-Length : 22331 Content-Type : application/octet-stream

After setting up swagger, generated json metadafile and generated C# code with it in my Xamarin PCL project I tried to consume the service. But it failed because in the generated code is trying to Deserialize json, but is not a json result!

Here it is part of the generated code where it fails:

[...]
var _result = new Microsoft.Rest.HttpOperationResponse<object>();
            _result.Request = _httpRequest;
            _result.Response = _httpResponse;
            // Deserialize Response
            if ((int)_statusCode == 200)
            {
                _responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
                try
                {
                    _result.Body = Microsoft.Rest.Serialization.SafeJsonConvert.DeserializeObject<object>(_responseContent, this.Client.DeserializationSettings);
                }
                catch (Newtonsoft.Json.JsonException ex)
                {
                    _httpRequest.Dispose();
                    if (_httpResponse != null)
                    {
                        _httpResponse.Dispose();
                    }
                    throw new Microsoft.Rest.SerializationException("Unable to deserialize the response.", _responseContent, ex);
                }
            }
            if (_shouldTrace)
            {
                Microsoft.Rest.ServiceClientTracing.Exit(_invocationId, _result);
            }
            return _result;
[...]

When I debugged I figure out that the content of the file is in the body, so deserialization is messing it up. Since is not recommended to edit this generated class file, What I need to change in my API to properly generate the code for application/octet-stream content-response?

Upvotes: 1

Views: 6095

Answers (5)

qq719862911
qq719862911

Reputation: 11

.EnableSwagger(c => 
{
    … 
    c.OperationFilter<FileManagementFilter>();
});
public class FileManagementFilter : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        if (operation.operationId.ToLower().IndexOf("_download") >= 0)
        {
            operation.produces = new[] { "application/octet-stream" };
            operation.responses["200"].schema = new Schema { type = "file", description = "Download file" };
        }
    }
}
[ResponseType(typeof(HttpResponseMessage))]
//[SwaggerResponse(HttpStatusCode.OK, Type = typeof(byte[]))]
[HttpGet, Route("DownloadItemFile")]
public HttpResponseMessage DownloadItemFile(int itemId, string fileName)
{
    var result = … 
    return result;
}

Note: Action name must ‘Download...’

enter image description here

Upvotes: 0

Shadam
Shadam

Reputation: 1122

I use the answer of @Petr Štipek and globalize it :

public void Apply(Operation operation, OperationFilterContext context)
{
    Type type = context.MethodInfo.ReturnType;

    if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
    {
        type = type.GetGenericArguments()[0];
    }

    if (typeof(FileResult).IsAssignableFrom(type))
    {
        Response response = operation.Responses["200"];
        operation.Responses["200"] = new Response
        {
            Description = string.IsNullOrWhiteSpace(response?.Description) ? "Success" : response.Description,
            Schema = new Schema { Type = "file" }
        };
        operation.Produces.Clear();
        operation.Produces.Add(MediaTypeNames.Application.Octet);
    }
}

Upvotes: 0

Petr Stipek
Petr Stipek

Reputation: 91

For Swashbuckle version 4 works for me to create filter:

public class FileDownloadOperation : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        var rt = context.MethodInfo.ReturnType;
        if (rt == typeof(Stream) || 
            rt == typeof(Task<Stream>) || 
            rt == typeof(FileStreamResult) || 
            rt == typeof(Task<FileStreamResult>))
        {
            operation.Responses["200"] = new Response
            {
                Description = "Success", Schema = new Schema {Type = "file"}
            };
            operation.Produces.Clear();
            operation.Produces.Add("application/octet-stream");
        }
    }
}

Assign that into swagger generator

services.AddSwaggerGen(c =>
            {
                ...
                c.OperationFilter<FileDownloadOperation>();
            });

And then just have simple controllers:

[HttpGet("{fileId}")]
public async Task<FileStreamResult> GetMyFile(int fileId)
{
    var result = await _fileService.GetFile(fileId);
    return File(result.Stream, result.ContentType, result.FileName);
}

Upvotes: 2

DiederikTiemstra
DiederikTiemstra

Reputation: 355

The generated code treats the output of your method as json because the wrong type is written to the swagger.json (probable .... #/definitions/....). It should contain "type": "file"

You can manipulate the output with the SwaggerGen options.

If your method looks like this:

    [Produces("application/pdf")]
    [ProducesResponseType(200, Type = typeof(Stream))]
    public IActionResult Download()
    {           
        Stream yourFileStream = null; //get file contents here
        return new FileStreamResult(yourFileStream , new MediaTypeHeaderValue("application/pdf"))
        {
            FileDownloadName = filename
        };
    }

And in your startup where the Swagger generation is setup configure the mapping between the Type you are returning and the Type you want to appear in your Swagger file

     services.AddSwaggerGen(
            options =>
            {                   
                options.MapType<System.IO.Stream>(() => new Schema { Type = "file" });
            });

Then your generated code looks like this:

public async Task<HttpOperationResponse<System.IO.Stream>> DownloadWithHttpMessagesAsync()

Upvotes: 3

Mr_LinDowsMac
Mr_LinDowsMac

Reputation: 2702

Creating a custom filter that returns a file:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public sealed class SwaggerFileResponseAttribute : SwaggerResponseAttribute
    {
        public SwaggerFileResponseAttribute(HttpStatusCode statusCode) : base(statusCode)
        {
        }

        public SwaggerFileResponseAttribute(HttpStatusCode statusCode, string description = null, Type type = null)  : base(statusCode, description, type)
        {
        }
        public SwaggerFileResponseAttribute(int statusCode) : base(statusCode)
        {
        }

        public SwaggerFileResponseAttribute(int statusCode, string description = null, Type type = null) : base(statusCode, description, type)
        {
        }
    }

And also this custom ResponseTypeFilter class:

public sealed class UpdateFileResponseTypeFilter : IOperationFilter
    {
        public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
        {
            if (apiDescription.GetControllerAndActionAttributes<SwaggerResponseRemoveDefaultsAttribute>().Any())
            {
                operation.responses.Clear();
            }
            var responseAttributes = apiDescription.GetControllerAndActionAttributes<SwaggerFileResponseAttribute>()
                .OrderBy(attr => attr.StatusCode);

            foreach (var attr in responseAttributes)
            {
                var statusCode = attr.StatusCode.ToString();

                Schema responseSchema = new Schema { format = "byte", type = "file" };

                operation.produces.Clear();
                operation.produces.Add("application/octet-stream");

                operation.responses[statusCode] = new Response
                {
                    description = attr.Description ?? InferDescriptionFrom(statusCode),
                    schema = responseSchema
                };
            }
        }

        private string InferDescriptionFrom(string statusCode)
        {
            HttpStatusCode enumValue;
            if (Enum.TryParse(statusCode, true, out enumValue))
            {
                return enumValue.ToString();
            }
            return null;
        }
    }

Then register it in SwaggerConfig file:

c.OperationFilter<UpdateFileResponseTypeFilter>();

To use this filter just add it in every action controller like this:

 [Route("Report/GetReport/{folio}")]
        [SwaggerFileResponse(HttpStatusCode.OK, "File Response")]
        [HttpGet]
        public HttpResponseMessage ExportReport(string folio)
        {
...

So, when swagger generates json metadata, autorest will properly create a method that returns a Task < Microsoft.Rest.HttpOperationResponse< System.IO.Stream > >

Upvotes: 4

Related Questions