Skip to content

Commit

Permalink
fix: Fix IncludeXmlCommentsWithRemarks
Browse files Browse the repository at this point in the history
  • Loading branch information
unchase committed Aug 26, 2021
1 parent 00572c2 commit 14f2d7d
Show file tree
Hide file tree
Showing 14 changed files with 591 additions and 190 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

These are the changes to each version that has been released on the [nuget](https://www.nuget.org/packages/Unchase.Swashbuckle.AspNetCore.Extensions/).

## v2.6.3 `2021-08-26`

- [x] Fix `IncludeXmlCommentsWithRemarks`

## v2.6.1 `2021-08-26`

- [x] Update nuget-dependencies: update Swashbuckle.AspNetCore to [v6.1.5](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases/tag/v6.1.5)
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,6 @@ public void ConfigureServices(IServiceCollection services)
// use it if you want to hide Paths and Definitions from OpenApi documentation correctly
options.UseAllOfToExtendReferenceSchemas();

// if you want to add xml comments from inheritdocs (from summary and remarks) into the swagger documentation, add:
// you can exclude remarks for concrete types
options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true, excludedTypes: typeof(string));

// if you want to add xml comments from summary and remarks into the swagger documentation, first of all add:
// you can exclude remarks for concrete types
var xmlFilePath = Path.Combine(AppContext.BaseDirectory, "WebApi3.1-Swashbuckle.xml");
Expand All @@ -118,6 +114,10 @@ public void ConfigureServices(IServiceCollection services)
// or add without remarks
//options.IncludeXmlComments(xmlFilePath);
// if you want to add xml comments from inheritdocs (from summary and remarks) into the swagger documentation, add:
// you can exclude remarks for concrete types
options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true, excludedTypes: typeof(string));

// Add filters to fix enums
// use by default:
//options.AddEnumsWithValuesFixFilters();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Xml.XPath;
Expand Down Expand Up @@ -188,8 +189,35 @@ public static SwaggerGenOptions IncludeXmlCommentsFromInheritDocs(
bool includeRemarks = false,
params Type[] excludedTypes)
{
var documents = swaggerGenOptions.SchemaFilterDescriptors.Where(x => x.Type == typeof(XmlCommentsSchemaFilter))
.Select(x => x.Arguments.Single())
.Cast<XPathDocument>()
.ToList();
var inheritedDocs = documents.SelectMany(
doc =>
{
var inheritedElements = new List<(string Name, string Cref)>();
foreach (XPathNavigator member in doc.CreateNavigator().Select("doc/members/member/inheritdoc"))
{
string cref = member.GetAttribute("cref", string.Empty);
member.MoveToParent();
string parentCref = member.GetAttribute("cref", string.Empty);
if (!string.IsNullOrWhiteSpace(parentCref))
{
cref = parentCref;
}

inheritedElements.Add((member.GetAttribute("name", string.Empty), cref));
}

return inheritedElements;
})
.ToDictionary(x => x.Name, x => x.Cref);

var distinctExcludedTypes = excludedTypes?.Distinct().ToArray();
swaggerGenOptions.SchemaFilter<InheritDocSchemaFilter>(swaggerGenOptions, includeRemarks, distinctExcludedTypes);
swaggerGenOptions.ParameterFilter<InheritDocParameterFilter>(documents, inheritedDocs, includeRemarks, distinctExcludedTypes);
swaggerGenOptions.RequestBodyFilter<InheritDocRequestBodyFilter>(documents, inheritedDocs, includeRemarks, distinctExcludedTypes);
swaggerGenOptions.SchemaFilter<InheritDocSchemaFilter>(documents, inheritedDocs, includeRemarks, distinctExcludedTypes);
return swaggerGenOptions;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.XPath;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Unchase.Swashbuckle.AspNetCore.Extensions.Extensions
{
internal static class XmlCommentsExtensions
{
private const string SummaryTag = "summary";
private const string RemarksTag = "remarks";
private const string ExampleTag = "example";

internal static Type GetTargetRecursive(this Type type, Dictionary<string, string> inheritedDocs, string cref)
{
var target = GetTarget(type, cref);

if (target == null)
{
return null;
}

string targetMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(target);

if (inheritedDocs.ContainsKey(targetMemberName))
{
return GetTarget(target, inheritedDocs[targetMemberName]);
}

return target;
}

private static Type GetTarget(Type type, string cref)
{
var targets = type.GetInterfaces();
if (type.BaseType != typeof(object))
{
targets = targets.Append(type.BaseType).ToArray();
}

// Try to find the target, if one is declared.
if (!string.IsNullOrEmpty(cref))
{
var crefTarget = targets.SingleOrDefault(t => XmlCommentsNodeNameHelper.GetMemberNameForType(t) == cref);

if (crefTarget != null)
{
return crefTarget;
}
}

// We use the last since that will be our base class or the "nearest" implemented interface.
return targets.LastOrDefault();
}

internal static MemberInfo GetTargetRecursive(this MemberInfo memberInfo, Dictionary<string, string> inheritedDocs, string cref)
{
var target = GetTarget(memberInfo, cref);

if (target == null)
{
return null;
}

string targetMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target);

if (inheritedDocs.ContainsKey(targetMemberName))
{
return GetTarget(target, inheritedDocs[targetMemberName]);
}

return target;
}

private static MemberInfo GetTarget(MemberInfo memberInfo, string cref)
{
var type = memberInfo.DeclaringType ?? memberInfo.ReflectedType;

if (type == null)
{
return null;
}

// Find all matching members in all interfaces and the base class.
var targets = type.GetInterfaces()
.Append(type.BaseType)
.SelectMany(
x => x.FindMembers(
memberInfo.MemberType,
BindingFlags.Instance | BindingFlags.Public,
(info, _) => info.Name == memberInfo.Name,
null))
.ToList();

// Try to find the target, if one is declared.
if (!string.IsNullOrEmpty(cref))
{
var crefTarget = targets.SingleOrDefault(t => XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(t) == cref);

if (crefTarget != null)
{
return crefTarget;
}
}

// We use the last since that will be our base class or the "nearest" implemented interface.
return targets.LastOrDefault();
}

internal static void ApplyPropertyComments(
this OpenApiSchema schema,
MemberInfo memberInfo,
List<XPathDocument> documents,
Dictionary<string, string> inheritedDocs,
bool includeRemarks = false,
params Type[] excludedTypes)
{
string memberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(memberInfo);

if (!inheritedDocs.ContainsKey(memberName))
{
return;
}

if (excludedTypes.Any() && excludedTypes.ToList()
.Contains(((PropertyInfo)memberInfo).PropertyType))
{
return;
}

string cref = inheritedDocs[memberName];
var target = memberInfo.GetTargetRecursive(inheritedDocs, cref);
if (target == null)
{
return;
}

var targetXmlNode = GetMemberXmlNode(XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target), documents);

if (targetXmlNode == null)
{
return;
}

var summaryNode = targetXmlNode.SelectSingleNode(SummaryTag);
if (summaryNode != null)
{
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);

if (includeRemarks)
{
var remarksNode = targetXmlNode.SelectSingleNode(RemarksTag);
if (remarksNode != null && !string.IsNullOrWhiteSpace(remarksNode.InnerXml))
{
schema.Description += $" ({XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)})";
}
}
}

var exampleNode = targetXmlNode.SelectSingleNode(ExampleTag);
if (exampleNode != null)
{
schema.Example = new OpenApiString(XmlCommentsTextHelper.Humanize(exampleNode.InnerXml));
}
}

internal static XPathNavigator GetMemberXmlNode(string memberName, List<XPathDocument> documents)
{
string path = $"/doc/members/member[@name='{memberName}']";

foreach (var document in documents)
{
var node = document.CreateNavigator().SelectSingleNode(path);

if (node != null)
{
return node;
}
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Unchase.Swashbuckle.AspNetCore.Extensions.Extensions;

namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters
{
/// <summary>
/// Adds documentation to parameters that is provided by the &lt;inhertidoc /&gt; tag.
/// </summary>
/// <seealso cref="IParameterFilter" />
internal class InheritDocParameterFilter : IParameterFilter
{
#region Fields

private const string SummaryTag = "summary";
private const string RemarksTag = "remarks";
private const string ExampleTag = "example";
private readonly bool _includeRemarks;
private readonly List<XPathDocument> _documents;
private readonly Dictionary<string, string> _inheritedDocs;
private readonly Type[] _excludedTypes;

#endregion

#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="InheritDocParameterFilter" /> class.
/// </summary>
/// <param name="inheritedDocs">Dictionary with inheritdoc in form of name-cref.</param>
/// <param name="includeRemarks">Include remarks from inheritdoc XML comments.</param>
/// <param name="documents">List of <see cref="XPathDocument"/>.</param>
public InheritDocParameterFilter(List<XPathDocument> documents, Dictionary<string, string> inheritedDocs, bool includeRemarks = false)
: this(documents, inheritedDocs, includeRemarks, Array.Empty<Type>())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="InheritDocParameterFilter" /> class.
/// </summary>
/// <param name="inheritedDocs">Dictionary with inheritdoc in form of name-cref.</param>
/// <param name="includeRemarks">Include remarks from inheritdoc XML comments.</param>
/// <param name="documents">List of <see cref="XPathDocument"/>.</param>
/// <param name="excludedTypes">Excluded types.</param>
public InheritDocParameterFilter(List<XPathDocument> documents, Dictionary<string, string> inheritedDocs, bool includeRemarks = false, params Type[] excludedTypes)
{
_includeRemarks = includeRemarks;
_excludedTypes = excludedTypes;
_documents = documents;
_inheritedDocs = inheritedDocs;
}

#endregion

#region Methods

/// <summary>
/// Apply filter.
/// </summary>
/// <param name="parameter"><see cref="OpenApiParameter"/>.</param>
/// <param name="context"><see cref="ParameterFilterContext"/>.</param>
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (context.ApiParameterDescription.PropertyInfo() == null)
{
return;
}

if (_excludedTypes.Any() && _excludedTypes.ToList().Contains(context.ApiParameterDescription.PropertyInfo().DeclaringType))
{
return;
}

// Try to apply a description for inherited types.
string parameterMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.ApiParameterDescription.PropertyInfo());
if (string.IsNullOrEmpty(parameter.Description) && _inheritedDocs.ContainsKey(parameterMemberName))
{
string cref = _inheritedDocs[parameterMemberName];
var target = context.ApiParameterDescription.PropertyInfo().GetTargetRecursive(_inheritedDocs, cref);

var targetXmlNode = XmlCommentsExtensions.GetMemberXmlNode(XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target), _documents);
var summaryNode = targetXmlNode?.SelectSingleNode(SummaryTag);

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

if (_includeRemarks)
{
var remarksNode = targetXmlNode.SelectSingleNode(RemarksTag);
if (remarksNode != null && !string.IsNullOrWhiteSpace(remarksNode.InnerXml))
{
parameter.Description += $" ({XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)})";
}
}
}
}
}

#endregion
}
}
Loading

0 comments on commit 14f2d7d

Please sign in to comment.