From 98b7950b1c2cf1d8d21938f29bb6ec046cf57cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Sun, 22 Sep 2024 21:39:13 +0200 Subject: [PATCH] Do not fill the RequestBody description with the first parameter of a FromForm request, and document the Properties instead --- .../XmlCommentsRequestBodyFilter.cs | 150 +++++++++++++----- ...lidSwaggerJson_Basic_DotNet_6.verified.txt | 23 ++- ...lidSwaggerJson_Basic_DotNet_8.verified.txt | 23 ++- .../XmlCommentsRequestBodyFilterTests.cs | 27 +++- .../Controllers/FromFormParamsController.cs | 19 ++- 5 files changed, 190 insertions(+), 52 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs index acbc751abf..dbb1721e7c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs @@ -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 { @@ -13,7 +13,6 @@ public class XmlCommentsRequestBodyFilter : IRequestBodyFilter public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc)) { } - internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary xmlDocMembers) { _xmlDocMembers = xmlDocMembers; @@ -21,48 +20,98 @@ internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary 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); + return; + } + var parameterInfo = bodyParameterDescription.ParameterInfo(); + if (parameterInfo is not null) + { + ApplyParamTagsForBody(requestBody, context, parameterInfo); + return; + } } - - 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 == 0) + { + return; + } - var parameterInfo = parameterDescription.ParameterInfo(); - if (parameterInfo is not null) - { - ApplyParamTags(requestBody, context, parameterInfo); + foreach (var formParameter in context.FormParameterDescriptions) + { + if (formParameter.PropertyInfo() is not null || formParameter.Name is null) + { + continue; + } + + var parameterFromForm = formParameter.ParameterInfo(); + if (parameterFromForm is null) + { + continue; + } + + foreach (var item in requestBody.Content.Values) + { + if ((item?.Schema?.Properties?.TryGetValue(formParameter.Name, out var value) ?? false) + || (item?.Schema?.Properties?.TryGetValue(formParameter.Name.ToCamelCase(), out value) ?? false)) + { + 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) + if (exampleNode is null) { - return; + return (summary, null); + } + + return (summary, exampleNode.ToString()); + + } + + private void ApplyPropertyTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo) + { + var (summary, example) = GetPropertyTags(propertyInfo); + + if (summary is not null) + { + requestBody.Description = summary; } - var example = exampleNode.ToString(); + if (requestBody.Content?.Count is 0) + { + return; + } foreach (var mediaType in requestBody.Content.Values) { @@ -70,40 +119,59 @@ private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilter } } - 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) + { + return (null, null); + } + + 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 = XmlCommentsTextHelper.Humanize(paramNode.InnerXml); + requestBody.Description = summary; + } + + if (requestBody.Content?.Count is 0) + { + return; + } - var example = paramNode.GetAttribute("example"); - if (!string.IsNullOrEmpty(example)) + if (!string.IsNullOrEmpty(example)) + { + foreach (var mediaType in requestBody.Content.Values) { - foreach (var mediaType in requestBody.Content.Values) - { - mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example); - } + mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example); } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_6.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_6.verified.txt index ce88e0c243..46eecdff42 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_6.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_6.verified.txt @@ -571,6 +571,7 @@ tags: [ FromFormParams ], + summary: Form parameters with description, requestBody: { content: { application/x-www-form-urlencoded: { @@ -578,14 +579,26 @@ 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 } } }, @@ -595,6 +608,12 @@ }, phoneNumbers: { style: form + }, + formFile: { + style: form + }, + text: { + style: form } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_8.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_8.verified.txt index fbae1d65df..03d3f2a383 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_8.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_Basic_DotNet_8.verified.txt @@ -571,6 +571,7 @@ tags: [ FromFormParams ], + summary: Form parameters with description, requestBody: { content: { application/x-www-form-urlencoded: { @@ -578,14 +579,26 @@ 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 } } }, @@ -595,6 +608,12 @@ }, phoneNumbers: { style: form + }, + formFile: { + style: form + }, + text: { + style: form } } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsRequestBodyFilterTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsRequestBodyFilterTests.cs index ef0b6e43bf..eddfd34133 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsRequestBodyFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsRequestBodyFilterTests.cs @@ -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; @@ -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 { - ["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string" } } + ["multipart/form-data"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string", + Properties = new Dictionary() + { + [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() diff --git a/test/WebSites/Basic/Controllers/FromFormParamsController.cs b/test/WebSites/Basic/Controllers/FromFormParamsController.cs index 4a94f9ed8b..d323b1766a 100644 --- a/test/WebSites/Basic/Controllers/FromFormParamsController.cs +++ b/test/WebSites/Basic/Controllers/FromFormParamsController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; @@ -6,9 +7,17 @@ namespace Basic.Controllers { public class FromFormParamsController { + /// + /// Form parameters with description + /// + /// Description for whole object + /// Description for file + /// Description for Text + /// + /// [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(); } @@ -22,8 +31,16 @@ public IActionResult PostFormWithIgnoredProperties([FromForm] RegistrationFormWi public class RegistrationForm { + /// + /// Summary for Name + /// + /// MyName public string Name { get; set; } + /// + /// Sumary for PhoneNumbers + /// + public IEnumerable PhoneNumbers { get; set; } }