Skip to content

Commit

Permalink
Fix missing form parameter XML documentation
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
martincostello committed Aug 18, 2024
1 parent 1be5040 commit f4ca2cd
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Linq;
using System.Reflection;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;

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

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

if (bodyParameterDescription == null) return;
if (parameterDescription is null)
{
return;
}

var propertyInfo = bodyParameterDescription.PropertyInfo();
if (propertyInfo != null)
var propertyInfo = parameterDescription.PropertyInfo();
if (propertyInfo is not null)
{
ApplyPropertyTags(requestBody, context, propertyInfo);
return;
}

var parameterInfo = bodyParameterDescription.ParameterInfo();
if (parameterInfo != null)
var parameterInfo = parameterDescription.ParameterInfo();
if (parameterInfo is not null)
{
ApplyParamTags(requestBody, context, parameterInfo);
return;
}
}

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

if (propertyNode == null) return;
if (propertyNode is null)
{
return;
}

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

var exampleNode = propertyNode.SelectSingleNode("example");
if (exampleNode == null) return;
if (exampleNode is null || requestBody.Content?.Count is 0)
{
return;
}

var example = exampleNode.ToString();

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

private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo)
{
if (!(parameterInfo.Member is MethodInfo methodInfo)) return;
if (parameterInfo.Member is not MethodInfo methodInfo)
{
return;
}

// 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 == null) return;
if (targetMethod is null)
{
return;
}

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

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

var example = paramNode.GetAttribute("example", "");
if (string.IsNullOrEmpty(example)) return;

foreach (var mediaType in requestBody.Content.Values)
if (!string.IsNullOrEmpty(example))
{
mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example);
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
@@ -1,54 +1,48 @@
using Swashbuckle.AspNetCore.TestSupport;
using System;
using Microsoft.AspNetCore.Mvc;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test
namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

/// <summary>
/// Summary for FakeControllerWithXmlComments
/// </summary>
/// <response code="default">Description for default response</response>
public class FakeControllerWithXmlComments
{
/// <summary>
/// Summary for FakeControllerWithXmlComments
/// Summary for ActionWithSummaryAndRemarksTags
/// </summary>
/// <response code="default">Description for default response</response>
public class FakeControllerWithXmlComments
/// <remarks>
/// Remarks for ActionWithSummaryAndRemarksTags
/// </remarks>
public void ActionWithSummaryAndRemarksTags()
{
/// <summary>
/// Summary for ActionWithSummaryAndRemarksTags
/// </summary>
/// <remarks>
/// Remarks for ActionWithSummaryAndRemarksTags
/// </remarks>
public void ActionWithSummaryAndRemarksTags()
{ }

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

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

/// <response code="200">Description for 200 response</response>
/// <response code="400">Description for 400 response</response>
public void ActionWithResponseTags()
{
}

/// <response code="200">Description for 200 response</response>
/// <response code="400">Description for 400 response</response>
public void ActionWithResponseTags()
{ }
/// <summary>
/// An action with a JSON body
/// </summary>
/// <param name="name">Parameter from JSON body</param>
public void PostBody([FromBody] string name)
{
}

///// <param name="boolParam" example="true"></param>
///// <param name="intParam" example="27"></param>
///// <param name="longParam" example="4294967296"></param>
///// <param name="floatParam" example="1.23"></param>
///// <param name="doubleParam" example="1.25"></param>
///// <param name="enumParam" example="2"></param>
///// <param name="guidParam" example="1edab3d2-311a-4782-9ec9-a70d0478b82f"></param>
///// <param name="stringParam" example="Example for StringProperty"></param>
///// <param name="badExampleIntParam" example="goodbye"></param>
//public void ActionWithExampleParams(
// bool boolParam,
// int intParam,
// long longParam,
// float floatParam,
// double doubleParam,
// IntEnum enumParam,
// Guid guidParam,
// string stringParam,
// int badExampleIntParam)
//{ }
/// <summary>
/// An action with a form body
/// </summary>
/// <param name="name">Parameter from form body</param>
public void PostForm([FromForm] string name)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Xml.XPath;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Xunit;
using Swashbuckle.AspNetCore.TestSupport;
using System.Collections.Generic;
using Xunit;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test
{
Expand Down Expand Up @@ -109,12 +109,59 @@ public void Apply_SetsDescriptionAndExample_FromUriTypePropertySummaryAndExample
Assert.NotNull(requestBody.Content["application/json"].Example);
Assert.Equal("\"https://test.com/a?b=1&c=2\"", requestBody.Content["application/json"].Example.ToJson());
}
private XmlCommentsRequestBodyFilter Subject()

[Fact]
public void Apply_SetsDescription_ForParameterFromBody()
{
var requestBody = new OpenApiRequestBody
{
Content = new Dictionary<string, OpenApiMediaType>
{
["application/json"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string" } }
}
};
var parameterInfo = typeof(FakeControllerWithXmlComments)
.GetMethod(nameof(FakeControllerWithXmlComments.PostBody))
.GetParameters()[0];
var bodyParameterDescription = new ApiParameterDescription
{
ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo }
};
var filterContext = new RequestBodyFilterContext(bodyParameterDescription, null, null, null);

Subject().Apply(requestBody, filterContext);

Assert.Equal("Parameter from JSON body", requestBody.Description);
}

[Fact]
public void Apply_SetsDescription_ForParameterFromForm()
{
using (var xmlComments = File.OpenText(typeof(FakeControllerWithXmlComments).Assembly.GetName().Name + ".xml"))
var requestBody = new OpenApiRequestBody
{
return new XmlCommentsRequestBodyFilter(new XPathDocument(xmlComments));
}
Content = new Dictionary<string, OpenApiMediaType>
{
["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string" } }
}
};
var parameterInfo = typeof(FakeControllerWithXmlComments)
.GetMethod(nameof(FakeControllerWithXmlComments.PostForm))
.GetParameters()[0];
var bodyParameterDescription = new ApiParameterDescription
{
ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo }
};
var filterContext = new RequestBodyFilterContext(null, [bodyParameterDescription], null, null);

Subject().Apply(requestBody, filterContext);

Assert.Equal("Parameter from form body", requestBody.Description);
}

private static XmlCommentsRequestBodyFilter Subject()
{
using var xmlComments = File.OpenText(typeof(FakeControllerWithXmlComments).Assembly.GetName().Name + ".xml");
return new XmlCommentsRequestBodyFilter(new XPathDocument(xmlComments));
}
}
}

0 comments on commit f4ca2cd

Please sign in to comment.