Skip to content

Commit

Permalink
Fix issues with request body parameters (#3076)
Browse files Browse the repository at this point in the history
Do not fill the RequestBody description with the first parameter of a FromForm request, and document the Properties instead.
  • Loading branch information
jgarciadelanoceda authored Sep 23, 2024
1 parent 8b0e896 commit c3fd84b
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
Expand All @@ -21,90 +21,155 @@ internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary<string, XPathNavigator

public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
{
var parameterDescription =
context.BodyParameterDescription ??
context.FormParameterDescriptions.FirstOrDefault((p) => p is not null);
var bodyParameterDescription = context.BodyParameterDescription;

if (parameterDescription is null)
if (bodyParameterDescription is not null)
{
return;
var propertyInfo = bodyParameterDescription.PropertyInfo();
if (propertyInfo is not null)
{
ApplyPropertyTagsForBody(requestBody, context, propertyInfo);
}
else
{
var parameterInfo = bodyParameterDescription.ParameterInfo();
if (parameterInfo is not null)
{
ApplyParamTagsForBody(requestBody, context, parameterInfo);
}
}
}

var propertyInfo = parameterDescription.PropertyInfo();
if (propertyInfo is not null)
else
{
ApplyPropertyTags(requestBody, context, propertyInfo);
return;
}
var numberOfFromForm = context.FormParameterDescriptions?.Count() ?? 0;
if (requestBody.Content?.Count is 0 || numberOfFromForm is 0)
{
return;
}

var parameterInfo = parameterDescription.ParameterInfo();
if (parameterInfo is not null)
{
ApplyParamTags(requestBody, context, parameterInfo);
foreach (var formParameter in context.FormParameterDescriptions)
{
if (formParameter.Name is null || formParameter.PropertyInfo() is not null)
{
continue;
}

var parameterFromForm = formParameter.ParameterInfo();
if (parameterFromForm is null)
{
continue;
}

foreach (var item in requestBody.Content.Values)
{
if (item?.Schema?.Properties is { } properties
&& (properties.TryGetValue(formParameter.Name, out var value) || properties.TryGetValue(formParameter.Name.ToCamelCase(), out value)))
{
var (summary, example) = GetParamTags(parameterFromForm);
value.Description = summary;
if (!string.IsNullOrEmpty(example))
{
value.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, value, example);
}
}
}
}
}
}

private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo)
private (string summary, string example) GetPropertyTags(PropertyInfo propertyInfo)
{
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo);
if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode))
{
return (null, null);
}

if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return;

string summary = null;
var summaryNode = propertyNode.SelectFirstChild("summary");
if (summaryNode is not null)
{
requestBody.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
}

var exampleNode = propertyNode.SelectFirstChild("example");
if (exampleNode is null || requestBody.Content?.Count is 0)

return (summary, exampleNode?.ToString());
}

private void ApplyPropertyTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo)
{
var (summary, example) = GetPropertyTags(propertyInfo);

if (summary is not null)
{
return;
requestBody.Description = summary;
}

var example = exampleNode.ToString();
if (requestBody.Content?.Count is 0)
{
return;
}

foreach (var mediaType in requestBody.Content.Values)
{
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
}
}

private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo)
private (string summary, string example) GetParamTags(ParameterInfo parameterInfo)
{
if (parameterInfo.Member is not MethodInfo methodInfo)
{
return;
return (null, null);
}

// If method is from a constructed generic type, look for comments from the generic type method
var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType
? methodInfo.GetUnderlyingGenericTypeMethod()
: methodInfo;

if (targetMethod is null)
{
return;
return (null, null);
}

var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);

if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return;
if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode))
{
return (null, null);
}

var paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", parameterInfo.Name);

if (paramNode is not null)
if (paramNode is null)
{
requestBody.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);
return (null, null);
}

var example = paramNode.GetAttribute("example");
if (!string.IsNullOrEmpty(example))
{
foreach (var mediaType in requestBody.Content.Values)
{
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
}
}
var summary = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);
var example = paramNode.GetAttribute("example");

return (summary, example);
}

private void ApplyParamTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo)
{
var (summary, example) = GetParamTags(parameterInfo);

if (summary is not null)
{
requestBody.Description = summary;
}

if (requestBody.Content?.Count is 0 || string.IsNullOrEmpty(example))
{
return;
}

foreach (var mediaType in requestBody.Content.Values)
{
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,21 +571,34 @@
tags: [
FromFormParams
],
summary: Form parameters with description,
requestBody: {
content: {
application/x-www-form-urlencoded: {
schema: {
type: object,
properties: {
name: {
type: string
type: string,
description: Summary for Name,
example: MyName
},
phoneNumbers: {
type: array,
items: {
type: integer,
format: int32
}
},
description: Sumary for PhoneNumbers
},
formFile: {
type: string,
description: Description for file,
format: binary
},
text: {
type: string,
description: Description for Text
}
}
},
Expand All @@ -595,6 +608,12 @@
},
phoneNumbers: {
style: form
},
formFile: {
style: form
},
text: {
style: form
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,21 +571,34 @@
tags: [
FromFormParams
],
summary: Form parameters with description,
requestBody: {
content: {
application/x-www-form-urlencoded: {
schema: {
type: object,
properties: {
name: {
type: string
type: string,
description: Summary for Name,
example: MyName
},
phoneNumbers: {
type: array,
items: {
type: integer,
format: int32
}
},
description: Sumary for PhoneNumbers
},
formFile: {
type: string,
description: Description for file,
format: binary
},
text: {
type: string,
description: Description for Text
}
}
},
Expand All @@ -595,6 +608,12 @@
},
phoneNumbers: {
style: form
},
formFile: {
style: form
},
text: {
style: form
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Xml.XPath;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.TestSupport;
using Xunit;
Expand Down Expand Up @@ -137,25 +138,39 @@ public void Apply_SetsDescription_ForParameterFromBody()
[Fact]
public void Apply_SetsDescription_ForParameterFromForm()
{
var parameterInfo = typeof(FakeControllerWithXmlComments)
.GetMethod(nameof(FakeControllerWithXmlComments.PostForm))
.GetParameters()[0];

var requestBody = new OpenApiRequestBody
{
Content = new Dictionary<string, OpenApiMediaType>
{
["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string" } }
["multipart/form-data"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "string",
Properties = new Dictionary<string, OpenApiSchema>()
{
[parameterInfo.Name] = new()
}
},
}
}
};
var parameterInfo = typeof(FakeControllerWithXmlComments)
.GetMethod(nameof(FakeControllerWithXmlComments.PostForm))
.GetParameters()[0];

var bodyParameterDescription = new ApiParameterDescription
{
ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo }
ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo },
Name = parameterInfo.Name,
Source = BindingSource.Form
};
var filterContext = new RequestBodyFilterContext(null, [bodyParameterDescription], null, null);

Subject().Apply(requestBody, filterContext);

Assert.Equal("Parameter from form body", requestBody.Description);
Assert.Equal("Parameter from form body", requestBody.Content["multipart/form-data"].Schema.Properties[parameterInfo.Name].Description);
}

private static XmlCommentsRequestBodyFilter Subject()
Expand Down
18 changes: 17 additions & 1 deletion test/WebSites/Basic/Controllers/FromFormParamsController.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace Basic.Controllers
{
public class FromFormParamsController
{
/// <summary>
/// Form parameters with description
/// </summary>
/// <param name="form">Description for whole object</param>
/// <param name="formFile">Description for file</param>
/// <param name="text">Description for Text</param>
/// <returns></returns>
/// <exception cref="System.NotImplementedException"></exception>
[HttpPost("registrations")]
[Consumes("application/x-www-form-urlencoded")]
public IActionResult PostForm([FromForm] RegistrationForm form)
public IActionResult PostForm([FromForm] RegistrationForm form, IFormFile formFile, [FromForm] string text)
{
throw new System.NotImplementedException();
}
Expand All @@ -22,8 +31,15 @@ public IActionResult PostFormWithIgnoredProperties([FromForm] RegistrationFormWi

public class RegistrationForm
{
/// <summary>
/// Summary for Name
/// </summary>
/// <example>MyName</example>
public string Name { get; set; }

/// <summary>
/// Sumary for PhoneNumbers
/// </summary>
public IEnumerable<int> PhoneNumbers { get; set; }
}

Expand Down

0 comments on commit c3fd84b

Please sign in to comment.