Skip to content

Commit f4ca2cd

Browse files
Fix missing form parameter XML documentation
- Fix form parameters not being annotated with XML documentation. - Refactor filter to use modern C# and be slightly more efficient. - Remove commented-out code in test controller. Resolves #3018.
1 parent 1be5040 commit f4ca2cd

File tree

3 files changed

+133
-70
lines changed

3 files changed

+133
-70
lines changed

src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Reflection;
1+
using System.Linq;
2+
using System.Reflection;
23
using System.Xml.XPath;
34
using Microsoft.OpenApi.Models;
45

@@ -15,22 +16,26 @@ public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc)
1516

1617
public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
1718
{
18-
var bodyParameterDescription = context.BodyParameterDescription;
19+
var parameterDescription =
20+
context.BodyParameterDescription ??
21+
context.FormParameterDescriptions.FirstOrDefault((p) => p is not null);
1922

20-
if (bodyParameterDescription == null) return;
23+
if (parameterDescription is null)
24+
{
25+
return;
26+
}
2127

22-
var propertyInfo = bodyParameterDescription.PropertyInfo();
23-
if (propertyInfo != null)
28+
var propertyInfo = parameterDescription.PropertyInfo();
29+
if (propertyInfo is not null)
2430
{
2531
ApplyPropertyTags(requestBody, context, propertyInfo);
2632
return;
2733
}
2834

29-
var parameterInfo = bodyParameterDescription.ParameterInfo();
30-
if (parameterInfo != null)
35+
var parameterInfo = parameterDescription.ParameterInfo();
36+
if (parameterInfo is not null)
3137
{
3238
ApplyParamTags(requestBody, context, parameterInfo);
33-
return;
3439
}
3540
}
3641

@@ -39,46 +44,63 @@ private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilter
3944
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo);
4045
var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']");
4146

42-
if (propertyNode == null) return;
47+
if (propertyNode is null)
48+
{
49+
return;
50+
}
4351

4452
var summaryNode = propertyNode.SelectSingleNode("summary");
45-
if (summaryNode != null)
53+
if (summaryNode is not null)
54+
{
4655
requestBody.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
56+
}
4757

4858
var exampleNode = propertyNode.SelectSingleNode("example");
49-
if (exampleNode == null) return;
59+
if (exampleNode is null || requestBody.Content?.Count is 0)
60+
{
61+
return;
62+
}
63+
64+
var example = exampleNode.ToString();
5065

5166
foreach (var mediaType in requestBody.Content.Values)
5267
{
53-
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, exampleNode.ToString());
68+
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
5469
}
5570
}
5671

5772
private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo)
5873
{
59-
if (!(parameterInfo.Member is MethodInfo methodInfo)) return;
74+
if (parameterInfo.Member is not MethodInfo methodInfo)
75+
{
76+
return;
77+
}
6078

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

66-
if (targetMethod == null) return;
84+
if (targetMethod is null)
85+
{
86+
return;
87+
}
6788

6889
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
6990
var paramNode = _xmlNavigator.SelectSingleNode(
7091
$"/doc/members/member[@name='{methodMemberName}']/param[@name='{parameterInfo.Name}']");
7192

72-
if (paramNode != null)
93+
if (paramNode is not null)
7394
{
7495
requestBody.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);
7596

7697
var example = paramNode.GetAttribute("example", "");
77-
if (string.IsNullOrEmpty(example)) return;
78-
79-
foreach (var mediaType in requestBody.Content.Values)
98+
if (!string.IsNullOrEmpty(example))
8099
{
81-
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
100+
foreach (var mediaType in requestBody.Content.Values)
101+
{
102+
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
103+
}
82104
}
83105
}
84106
}
Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,48 @@
1-
using Swashbuckle.AspNetCore.TestSupport;
2-
using System;
1+
using Microsoft.AspNetCore.Mvc;
32

4-
namespace Swashbuckle.AspNetCore.SwaggerGen.Test
3+
namespace Swashbuckle.AspNetCore.SwaggerGen.Test;
4+
5+
/// <summary>
6+
/// Summary for FakeControllerWithXmlComments
7+
/// </summary>
8+
/// <response code="default">Description for default response</response>
9+
public class FakeControllerWithXmlComments
510
{
611
/// <summary>
7-
/// Summary for FakeControllerWithXmlComments
12+
/// Summary for ActionWithSummaryAndRemarksTags
813
/// </summary>
9-
/// <response code="default">Description for default response</response>
10-
public class FakeControllerWithXmlComments
14+
/// <remarks>
15+
/// Remarks for ActionWithSummaryAndRemarksTags
16+
/// </remarks>
17+
public void ActionWithSummaryAndRemarksTags()
1118
{
12-
/// <summary>
13-
/// Summary for ActionWithSummaryAndRemarksTags
14-
/// </summary>
15-
/// <remarks>
16-
/// Remarks for ActionWithSummaryAndRemarksTags
17-
/// </remarks>
18-
public void ActionWithSummaryAndRemarksTags()
19-
{ }
20-
21-
/// <param name="param1" example="Example for &quot;param1&quot;">Description for param1</param>
22-
/// <param name="param2" example="http://test.com/?param1=1&amp;param2=2">Description for param2</param>
23-
public void ActionWithParamTags(string param1, string param2)
24-
{ }
19+
}
2520

21+
/// <param name="param1" example="Example for &quot;param1&quot;">Description for param1</param>
22+
/// <param name="param2" example="http://test.com/?param1=1&amp;param2=2">Description for param2</param>
23+
public void ActionWithParamTags(string param1, string param2)
24+
{
25+
}
2626

27+
/// <response code="200">Description for 200 response</response>
28+
/// <response code="400">Description for 400 response</response>
29+
public void ActionWithResponseTags()
30+
{
31+
}
2732

28-
/// <response code="200">Description for 200 response</response>
29-
/// <response code="400">Description for 400 response</response>
30-
public void ActionWithResponseTags()
31-
{ }
33+
/// <summary>
34+
/// An action with a JSON body
35+
/// </summary>
36+
/// <param name="name">Parameter from JSON body</param>
37+
public void PostBody([FromBody] string name)
38+
{
39+
}
3240

33-
///// <param name="boolParam" example="true"></param>
34-
///// <param name="intParam" example="27"></param>
35-
///// <param name="longParam" example="4294967296"></param>
36-
///// <param name="floatParam" example="1.23"></param>
37-
///// <param name="doubleParam" example="1.25"></param>
38-
///// <param name="enumParam" example="2"></param>
39-
///// <param name="guidParam" example="1edab3d2-311a-4782-9ec9-a70d0478b82f"></param>
40-
///// <param name="stringParam" example="Example for StringProperty"></param>
41-
///// <param name="badExampleIntParam" example="goodbye"></param>
42-
//public void ActionWithExampleParams(
43-
// bool boolParam,
44-
// int intParam,
45-
// long longParam,
46-
// float floatParam,
47-
// double doubleParam,
48-
// IntEnum enumParam,
49-
// Guid guidParam,
50-
// string stringParam,
51-
// int badExampleIntParam)
52-
//{ }
41+
/// <summary>
42+
/// An action with a form body
43+
/// </summary>
44+
/// <param name="name">Parameter from form body</param>
45+
public void PostForm([FromForm] string name)
46+
{
5347
}
54-
}
48+
}

test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsRequestBodyFilterTests.cs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
23
using System.Xml.XPath;
34
using Microsoft.AspNetCore.Mvc.ApiExplorer;
45
using Microsoft.AspNetCore.Mvc.Controllers;
56
using Microsoft.OpenApi.Models;
6-
using Xunit;
77
using Swashbuckle.AspNetCore.TestSupport;
8-
using System.Collections.Generic;
8+
using Xunit;
99

1010
namespace Swashbuckle.AspNetCore.SwaggerGen.Test
1111
{
@@ -109,12 +109,59 @@ public void Apply_SetsDescriptionAndExample_FromUriTypePropertySummaryAndExample
109109
Assert.NotNull(requestBody.Content["application/json"].Example);
110110
Assert.Equal("\"https://test.com/a?b=1&c=2\"", requestBody.Content["application/json"].Example.ToJson());
111111
}
112-
private XmlCommentsRequestBodyFilter Subject()
112+
113+
[Fact]
114+
public void Apply_SetsDescription_ForParameterFromBody()
115+
{
116+
var requestBody = new OpenApiRequestBody
117+
{
118+
Content = new Dictionary<string, OpenApiMediaType>
119+
{
120+
["application/json"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string" } }
121+
}
122+
};
123+
var parameterInfo = typeof(FakeControllerWithXmlComments)
124+
.GetMethod(nameof(FakeControllerWithXmlComments.PostBody))
125+
.GetParameters()[0];
126+
var bodyParameterDescription = new ApiParameterDescription
127+
{
128+
ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo }
129+
};
130+
var filterContext = new RequestBodyFilterContext(bodyParameterDescription, null, null, null);
131+
132+
Subject().Apply(requestBody, filterContext);
133+
134+
Assert.Equal("Parameter from JSON body", requestBody.Description);
135+
}
136+
137+
[Fact]
138+
public void Apply_SetsDescription_ForParameterFromForm()
113139
{
114-
using (var xmlComments = File.OpenText(typeof(FakeControllerWithXmlComments).Assembly.GetName().Name + ".xml"))
140+
var requestBody = new OpenApiRequestBody
115141
{
116-
return new XmlCommentsRequestBodyFilter(new XPathDocument(xmlComments));
117-
}
142+
Content = new Dictionary<string, OpenApiMediaType>
143+
{
144+
["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string" } }
145+
}
146+
};
147+
var parameterInfo = typeof(FakeControllerWithXmlComments)
148+
.GetMethod(nameof(FakeControllerWithXmlComments.PostForm))
149+
.GetParameters()[0];
150+
var bodyParameterDescription = new ApiParameterDescription
151+
{
152+
ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo }
153+
};
154+
var filterContext = new RequestBodyFilterContext(null, [bodyParameterDescription], null, null);
155+
156+
Subject().Apply(requestBody, filterContext);
157+
158+
Assert.Equal("Parameter from form body", requestBody.Description);
159+
}
160+
161+
private static XmlCommentsRequestBodyFilter Subject()
162+
{
163+
using var xmlComments = File.OpenText(typeof(FakeControllerWithXmlComments).Assembly.GetName().Name + ".xml");
164+
return new XmlCommentsRequestBodyFilter(new XPathDocument(xmlComments));
118165
}
119166
}
120167
}

0 commit comments

Comments
 (0)