Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for <ineritdoc cref="" /> #2687

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System.Xml.XPath;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.XPath;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using System;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
Expand Down Expand Up @@ -31,24 +31,16 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
foreach (var nameAndType in controllerNamesAndTypes)
{
var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(nameAndType.Value);
var typeNode = _xmlNavigator.SelectSingleNode(string.Format(MemberXPath, memberName));
var summaryNode = _xmlNavigator.SelectSingleNodeRecursive(memberName, SummaryTag);
if (summaryNode == null) continue;
swaggerDoc.Tags ??= new List<OpenApiTag>();

if (typeNode != null)
swaggerDoc.Tags.Add(new OpenApiTag
{
var summaryNode = typeNode.SelectSingleNode(SummaryTag);
if (summaryNode != null)
{
if (swaggerDoc.Tags == null)
swaggerDoc.Tags = new List<OpenApiTag>();

swaggerDoc.Tags.Add(new OpenApiTag
{
Name = nameAndType.Key,
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml)
});
}
}
Name = nameAndType.Key,
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml)
});
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsOperationFilter : IOperationFilter
{
private const string SummaryTag = "summary";
private const string RemarksTag = "remarks";
private const string ResponseTag = "response";
private readonly XPathNavigator _xmlNavigator;

public XmlCommentsOperationFilter(XPathDocument xmlDoc)
Expand All @@ -32,27 +35,26 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
private void ApplyControllerTags(OpenApiOperation operation, Type controllerType)
{
var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType);
var responseNodes = _xmlNavigator.Select($"/doc/members/member[@name='{typeMemberName}']/response");
var responseNodes = _xmlNavigator.SelectNodeRecursive(typeMemberName, ResponseTag);
if (responseNodes == null) return;
ApplyResponseTags(operation, responseNodes);
}

private void ApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo)
{
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo);
var methodNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{methodMemberName}']");

if (methodNode == null) return;

var summaryNode = methodNode.SelectSingleNode("summary");
var summaryNode = _xmlNavigator.SelectSingleNodeRecursive(methodMemberName, SummaryTag);
if (summaryNode != null)
operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);

var remarksNode = methodNode.SelectSingleNode("remarks");
var remarksNode = _xmlNavigator.SelectSingleNodeRecursive(methodMemberName, RemarksTag);
if (remarksNode != null)
operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml);

var responseNodes = methodNode.Select("response");
ApplyResponseTags(operation, responseNodes);
var responseNodes = _xmlNavigator.SelectNodeRecursive(methodMemberName, ResponseTag);
if (responseNodes != null)
ApplyResponseTags(operation, responseNodes);
}

private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsParameterFilter : IParameterFilter
{
private XPathNavigator _xmlNavigator;
private const string SummaryTag = "summary";
private const string ExampleTag = "example";
private readonly XPathNavigator _xmlNavigator;

public XmlCommentsParameterFilter(XPathDocument xmlDoc)
{
Expand All @@ -16,13 +18,37 @@ public XmlCommentsParameterFilter(XPathDocument xmlDoc)
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (context.PropertyInfo != null)
{
ApplyPropertyTags(parameter, context);
}
else if (context.ParameterInfo != null)
{
ApplyParamTags(parameter, context);
}
else if (context.ParameterInfo != null) ApplyParamTags(parameter, context);
}

private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext context)
{
if (!(context.ParameterInfo.Member is 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;

var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
var paramNode =
_xmlNavigator.SelectSingleNodeRecursive(methodMemberName,
$"param[@name='{context.ParameterInfo.Name}']");

if (paramNode == null) return;
parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);

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

var exampleAsJson = parameter.Schema?.ResolveType(context.SchemaRepository) == "string"
? $"\"{example}\""
: example;

parameter.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
}

private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContext context)
Expand All @@ -32,51 +58,21 @@ private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContex

if (propertyNode == null) return;

var summaryNode = propertyNode.SelectSingleNode("summary");
var summaryNode = _xmlNavigator.SelectSingleNodeRecursive(propertyMemberName, SummaryTag);
if (summaryNode != null)
{
parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
parameter.Schema.Description = null; // no need to duplicate
}

var exampleNode = propertyNode.SelectSingleNode("example");
var exampleNode = _xmlNavigator.SelectSingleNodeRecursive(propertyMemberName, ExampleTag);
if (exampleNode == null) return;

var exampleAsJson = (parameter.Schema?.ResolveType(context.SchemaRepository) == "string")
? $"\"{exampleNode.ToString()}\""
var exampleAsJson = parameter.Schema?.ResolveType(context.SchemaRepository) == "string"
? $"\"{exampleNode}\""
: exampleNode.ToString();

parameter.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
}

private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext context)
{
if (!(context.ParameterInfo.Member is 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;

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

if (paramNode != null)
{
parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);

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

var exampleAsJson = (parameter.Schema?.ResolveType(context.SchemaRepository) == "string")
? $"\"{example}\""
: example;

parameter.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Xml.XPath;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
/// <summary>
/// Service to handle recursive retrieval of XML comments from an XPathNavigator.
/// </summary>
internal static class XmlCommentsRecursionService
{
private const string MemberXPath = "/doc/members/member[@name='{0}']";

/// <summary>
/// Finds the first node with the specified tag name recursively, starting from the given memberName in the XML
/// document.
/// </summary>
/// <param name="xmlNavigator">The XPathNavigator representing the XML document.</param>
/// <param name="memberName">The name of the member to start the recursive search from.</param>
/// <param name="tag">The tag name to find.</param>
/// <returns>The XPathNavigator representing the found node or null if the node is not found.</returns>
private static XPathNavigator FindNodeRecursive(XPathNavigator xmlNavigator, string memberName, string tag)
{
while (true)
{
// Find the node representing the current memberName in the XML document.
var memberNode = xmlNavigator.SelectSingleNode(string.Format(MemberXPath, memberName));

// Try to find the specified tag node within the current member node.
var node = memberNode?.SelectSingleNode(tag);
if (node != null)
return memberNode;

// If the specified tag node is not found, check if there is an "inheritdoc" tag.
var inheritDocNode = memberNode?.SelectSingleNode("inheritdoc");
if (inheritDocNode == null)
return null;

// If "inheritdoc" tag exists, get the "cref" attribute to find the referenced node in the XML document.
var cref = inheritDocNode.GetAttribute("cref", string.Empty);
if (string.IsNullOrEmpty(cref))
return null;

// Update the memberName to the referenced member and continue the recursive search.
memberName = cref;
}
}

/// <summary>
/// Selects multiple nodes with the specified tag name recursively, starting from the given memberName in the XML
/// document.
/// </summary>
/// <param name="xmlNavigator">The XPathNavigator representing the XML document.</param>
/// <param name="memberName">The name of the member to start the recursive search from.</param>
/// <param name="tag">The tag name to find.</param>
/// <returns>
/// An XPathNodeIterator representing the collection of nodes with the specified tag, or null if the nodes are not
/// found.
/// </returns>
public static XPathNodeIterator SelectNodeRecursive(this XPathNavigator xmlNavigator, string memberName,
string tag)
{
var node = FindNodeRecursive(xmlNavigator, memberName, tag);
return node?.Select(tag);
}

/// <summary>
/// Selects the first node with the specified tag name recursively, starting from the given memberName in the XML
/// document.
/// </summary>
/// <param name="xmlNavigator">The XPathNavigator representing the XML document.</param>
/// <param name="memberName">The name of the member to start the recursive search from.</param>
/// <param name="tag">The tag name to find.</param>
/// <returns>An XPathNavigator representing the found node or null if the node is not found.</returns>
public static XPathNavigator SelectSingleNodeRecursive(this XPathNavigator xmlNavigator, string memberName,
string tag)
{
var node = FindNodeRecursive(xmlNavigator, memberName, tag);
return node?.SelectSingleNode(tag);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsRequestBodyFilter : IRequestBodyFilter
{
private const string SummaryTag = "summary";
private const string ExampleTag = "example";
private readonly XPathNavigator _xmlNavigator;

public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc)
Expand All @@ -30,65 +32,61 @@ public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext conte
if (parameterInfo != null)
{
ApplyParamTags(requestBody, context, parameterInfo);
return;
}
}

private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo)
private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context,
ParameterInfo parameterInfo)
{
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo);
var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']");
if (!(parameterInfo.Member is MethodInfo methodInfo)) return;

if (propertyNode == null) 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;

var summaryNode = propertyNode.SelectSingleNode("summary");
if (summaryNode != null)
requestBody.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
if (targetMethod == null) return;

var exampleNode = propertyNode.SelectSingleNode("example");
if (exampleNode == null) return;
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
var paramNode =
_xmlNavigator.SelectSingleNodeRecursive(methodMemberName, $"param[@name='{parameterInfo.Name}']");

if (paramNode == null) return;
requestBody.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);

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

foreach (var mediaType in requestBody.Content.Values)
{
var exampleAsJson = (mediaType.Schema?.ResolveType(context.SchemaRepository) == "string")
? $"\"{exampleNode.ToString()}\""
: exampleNode.ToString();
var exampleAsJson = mediaType.Schema?.ResolveType(context.SchemaRepository) == "string"
? $"\"{example}\""
: example;

mediaType.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
}
}

private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo)
private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context,
PropertyInfo propertyInfo)
{
if (!(parameterInfo.Member is 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;
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo);

if (targetMethod == null) return;
var summaryNode = _xmlNavigator.SelectSingleNodeRecursive(propertyMemberName, SummaryTag);
if (summaryNode != null)
requestBody.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);

var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
var paramNode = _xmlNavigator.SelectSingleNode(
$"/doc/members/member[@name='{methodMemberName}']/param[@name='{parameterInfo.Name}']");
var exampleNode = _xmlNavigator.SelectSingleNodeRecursive(propertyMemberName, ExampleTag);
if (exampleNode == null) return;

if (paramNode != null)
foreach (var mediaType in requestBody.Content.Values)
{
requestBody.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);

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

foreach (var mediaType in requestBody.Content.Values)
{
var exampleAsJson = (mediaType.Schema?.ResolveType(context.SchemaRepository) == "string")
? $"\"{example}\""
: example;
var exampleAsJson = mediaType.Schema?.ResolveType(context.SchemaRepository) == "string"
? $"\"{exampleNode}\""
: exampleNode.ToString();

mediaType.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
}
mediaType.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
}
}
}
}
}