Reputation: 2702
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
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...’
Upvotes: 0
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
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
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
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