Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,13 +508,12 @@ public void UploadFile([FromForm]string description, [FromForm]DateTime clientDa
> Important note: As per the [ASP.NET Core docs](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1), you're not supposed to decorate `IFormFile` parameters with the `[FromForm]` attribute as the binding source is automatically inferred from the type. In fact, the inferred value is `BindingSource.FormFile` and if you apply the attribute it will be set to `BindingSource.Form` instead, which screws up `ApiExplorer`, the metadata component that ships with ASP.NET Core and is heavily relied on by Swashbuckle. One particular issue here is that SwaggerUI will not treat the parameter as a file and so will not display a file upload button, if you do mistakenly include this attribute.

### Handle File Downloads ###
`ApiExplorer` (the ASP.NET Core metadata component that Swashbuckle is built on) *DOES NOT* surface the `FileResult` type by default and so you need to explicitly tell it to with the `Produces` attribute:
`ApiExplorer` (the ASP.NET Core metadata component that Swashbuckle is built on) *DOES NOT* surface the `FileResult` types by default and so you need to explicitly tell it to with the `ProducesResponseType` attribute (or `Produces` on .NET 5 or older):
```csharp
[HttpGet("{fileName}")]
[Produces("application/octet-stream", Type = typeof(FileResult))]
public FileResult GetFile(string fileName)
[ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK, "image/jpeg")]
public FileStreamResult GetFile(string fileName)
```
If you want the swagger-ui to display a "Download file" link, you're operation will need to return a **Content-Type of "application/octet-stream"** or a **Content-Disposition of "attachement"**.

### Include Descriptions from XML Comments ###

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,15 +602,18 @@ private OpenApiResponse GenerateResponse(
Description = description,
Content = responseContentTypes.ToDictionary(
contentType => contentType,
contentType => CreateResponseMediaType(apiResponseType.ModelMetadata, schemaRepository)
contentType => CreateResponseMediaType(apiResponseType.ModelMetadata?.ModelType ?? apiResponseType.Type, schemaRepository)
)
};
}

private IEnumerable<string> InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType)
{
// If there's no associated model, return an empty list (i.e. no content)
if (apiResponseType.ModelMetadata == null) return Enumerable.Empty<string>();
// If there's no associated model type, return an empty list (i.e. no content)
if (apiResponseType.ModelMetadata == null && (apiResponseType.Type == null || apiResponseType.Type == typeof(void)))
{
return Enumerable.Empty<string>();
}

// If there's content types explicitly specified via ProducesAttribute, use them
var explicitContentTypes = apiDescription.CustomAttributes().OfType<ProducesAttribute>()
Expand All @@ -627,11 +630,11 @@ private IEnumerable<string> InferResponseContentTypes(ApiDescription apiDescript
return Enumerable.Empty<string>();
}

private OpenApiMediaType CreateResponseMediaType(ModelMetadata modelMetadata, SchemaRepository schemaRespository)
private OpenApiMediaType CreateResponseMediaType(Type modelType, SchemaRepository schemaRespository)
{
return new OpenApiMediaType
{
Schema = GenerateSchema(modelMetadata.ModelType, schemaRespository)
Schema = GenerateSchema(modelType, schemaRespository)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public int ActionWithProducesAttribute()
throw new NotImplementedException();
}

[ProducesResponseType(typeof(FileContentResult), 200, "application/zip")]
public FileContentResult ActionWithFileResult()
{
throw new NotImplementedException();
}

[SwaggerIgnore]
public void ActionWithSwaggerIgnoreAttribute()
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using System.Reflection;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
Expand Down Expand Up @@ -968,6 +969,40 @@ public void GetSwagger_GeneratesResponses_ForSupportedResponseTypes()
Assert.Empty(responseDefault.Content.Keys);
}

[Fact]
public void GetSwagger_SetsResponseContentType_WhenActionHasFileResult()
{
var apiDescription = ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionWithFileResult),
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
supportedResponseTypes: new[]
{
new ApiResponseType
{
ApiResponseFormats = new [] { new ApiResponseFormat { MediaType = "application/zip" } },
StatusCode = 200,
Type = typeof(FileContentResult)
}
});

// ASP.NET Core sets ModelMetadata to null for FileResults
apiDescription.SupportedResponseTypes[0].ModelMetadata = null;

var subject = Subject(
apiDescriptions: new[] { apiDescription }
);

var document = subject.GetSwagger("v1");

var operation = document.Paths["/resource"].Operations[OperationType.Post];
var content = operation.Responses["200"].Content.FirstOrDefault();
Assert.Equal("application/zip", content.Key);
Assert.Equal("binary", content.Value.Schema.Format);
Assert.Equal("string", content.Value.Schema.Type);
}

[Fact]
public void GetSwagger_SetsResponseContentTypesFromAttribute_IfActionHasProducesAttribute()
{
Expand Down
12 changes: 9 additions & 3 deletions test/WebSites/Basic/Controllers/FilesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ public IActionResult PostFormWithFile([FromForm]FormWithFile formWithFile)
}

[HttpGet("{name}")]
[Produces("application/octet-stream", Type = typeof(FileResult))]
#if NET6_0_OR_GREATER
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK, "text/plain", "application/zip")]
#else
[Produces("text/plain", "application/zip", Type = typeof(FileResult))]
#endif
public FileResult GetFile(string name)
{
var stream = new MemoryStream();
Expand All @@ -37,7 +41,9 @@ public FileResult GetFile(string name)
writer.Flush();
stream.Position = 0;

return File(stream, "application/octet-stream", name);
var contentType = name.EndsWith(".zip", StringComparison.InvariantCultureIgnoreCase) ? "application/zip" : "text/plain";

return File(stream, contentType, name);
}
}

Expand All @@ -47,4 +53,4 @@ public class FormWithFile

public IFormFile File { get; set; }
}
}
}