Skip to content

Commit

Permalink
Fixes an issue where IFormFile parameters are incorrectly generated w…
Browse files Browse the repository at this point in the history
…hen WithOpenApi() extension method is used. domaindrivendev#2625 and #3
  • Loading branch information
Havunen committed Feb 21, 2024
1 parent b1d1bba commit fd2825a
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -290,15 +290,32 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api
{
foreach (var content in requestContentTypes)
{
var requestParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.IsFromBody() || desc.IsFromForm());
if (requestParameter is not null)
var fromBodyParam = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.IsFromBody());
if (fromBodyParam is not null)
{
content.Schema = GenerateSchema(
requestParameter.Type,
fromBodyParam.Type,
schemaRepository,
requestParameter.PropertyInfo(),
requestParameter.ParameterInfo());
fromBodyParam.PropertyInfo(),
fromBodyParam.ParameterInfo());
}
else
{
var fromFormParam = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromForm());

if (fromFormParam.Any())
{
content.Schema = GenerateSchemaFromFormParameters(
fromFormParam,
schemaRepository
);
content.Encoding = content.Schema.Properties.ToDictionary(
entry => entry.Key,
entry => new OpenApiEncoding { Style = ParameterStyle.Form }
);
}
}

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;

Expand Down Expand Up @@ -79,5 +80,11 @@ public void VoidActionWithProducesAttribute()
{
throw new NotImplementedException();
}

[Consumes("multipart/form-data")]
public void ActionWithIFormFile(IFormFile file)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -830,11 +830,11 @@ public void GetSwagger_SetsResponseContentTypesFromAttribute_IfActionHasProduces
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
supportedResponseTypes: new []
supportedResponseTypes: new[]
{
new ApiResponseType
{
ApiResponseFormats = new [] { new ApiResponseFormat { MediaType = "application/json" } },
ApiResponseFormats = new[] { new ApiResponseFormat { MediaType = "application/json" } },
StatusCode = 200,
}
})
Expand All @@ -847,6 +847,106 @@ public void GetSwagger_SetsResponseContentTypesFromAttribute_IfActionHasProduces
Assert.Equal(new[] { "application/someMediaType" }, operation.Responses["200"].Content.Keys);
}

[Fact]
public void GetSwagger_IFormFile_HasSpecial_Handling()
{
var subject = Subject(
apiDescriptions: new[]
{
ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionWithIFormFile),
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions: new []
{
new ApiParameterDescription()
{
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(IFormFile)),
Name = "file",
Source = BindingSource.FormFile,
Type = typeof(IFormFile)
}
}
)
}
);

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

var operation = document.Paths["/resource"].Operations[OperationType.Post];

Assert.Single(operation.RequestBody.Content);
Assert.Equal("multipart/form-data", operation.RequestBody.Content.Keys.Single());
var mediaType = operation.RequestBody.Content["multipart/form-data"];
Assert.Equal("object", mediaType.Schema.Type);
Assert.Equal("string", mediaType.Schema.Properties["file"].Type);
Assert.Equal("binary", mediaType.Schema.Properties["file"].Format);
Assert.Single(mediaType.Encoding);
Assert.Equal("file", mediaType.Encoding.Keys.Single());
Assert.Equal(ParameterStyle.Form, mediaType.Encoding["file"].Style);
}

[Fact]
public void GetSwagger_MetaData_IFormFile_HasSpecial_Handling()
{
var apiDesc = ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionWithIFormFile),
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions: new[]
{
new ApiParameterDescription()
{
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(IFormFile)),
Name = "file",
Source = BindingSource.FormFile,
Type = typeof(IFormFile)
}
}
);
apiDesc.ActionDescriptor.EndpointMetadata = new List<object>()
{
new OpenApiOperation
{
RequestBody = new OpenApiRequestBody
{
Content = new Dictionary<string, OpenApiMediaType>
{
["multipart/form-data"] = new OpenApiMediaType
{
// These values are null
Schema = null,
Encoding = null
}
}
}
}
};

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

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

var operation = document.Paths["/resource"].Operations[OperationType.Post];

Assert.Single(operation.RequestBody.Content);
Assert.Equal("multipart/form-data", operation.RequestBody.Content.Keys.Single());
var mediaType = operation.RequestBody.Content["multipart/form-data"];
Assert.Equal("object", mediaType.Schema.Type);
Assert.Equal("string", mediaType.Schema.Properties["file"].Type);
Assert.Equal("binary", mediaType.Schema.Properties["file"].Format);
Assert.Single(mediaType.Encoding);
Assert.Equal("file", mediaType.Encoding.Keys.Single());
Assert.Equal(ParameterStyle.Form, mediaType.Encoding["file"].Style);
}

[Fact]
public void GetSwagger_ThrowsUnknownSwaggerDocumentException_IfProvidedDocumentNameNotRegistered()
{
Expand Down
22 changes: 16 additions & 6 deletions test/WebSites/WebApplication1/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,22 @@
new(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
};

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/api/upload", (IFormFile file) =>
{
return TypedResults.Ok(file.FileName);
});

app.MapPost("/api/upload_with_openapi", (IFormFile file) =>
{
return TypedResults.Ok(file.FileName);
})
.WithOpenApi(operation =>
{
operation.Summary = "summary";
operation.Description = "description";

return operation;
});

app.Run();

Expand Down
4 changes: 4 additions & 0 deletions test/WebSites/WebApplication1/WebApplication1.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\DotSwashbuckle.AspNetCore.Annotations\DotSwashbuckle.AspNetCore.Annotations.csproj" />
<ProjectReference Include="..\..\..\src\DotSwashbuckle.AspNetCore.SwaggerUI\DotSwashbuckle.AspNetCore.SwaggerUI.csproj" />
Expand Down

0 comments on commit fd2825a

Please sign in to comment.