Skip to content

Commit

Permalink
Improve IncludeXmlComments performance
Browse files Browse the repository at this point in the history
Co-authored-by: steven.darby <steven.darby@tribalgroup.com>
  • Loading branch information
mus65 and steven.darby committed Aug 25, 2024
1 parent 16e2a94 commit 21d0a75
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -541,13 +541,15 @@ public static void IncludeXmlComments(
bool includeControllerXmlComments = false)
{
var xmlDoc = xmlDocFactory();
swaggerGenOptions.ParameterFilter<XmlCommentsParameterFilter>(xmlDoc);
swaggerGenOptions.RequestBodyFilter<XmlCommentsRequestBodyFilter>(xmlDoc);
swaggerGenOptions.OperationFilter<XmlCommentsOperationFilter>(xmlDoc);
swaggerGenOptions.SchemaFilter<XmlCommentsSchemaFilter>(xmlDoc);
var xmlDocMembers = XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc);

swaggerGenOptions.AddParameterFilterInstance(new XmlCommentsParameterFilter(xmlDocMembers));
swaggerGenOptions.AddRequestBodyFilterInstance(new XmlCommentsRequestBodyFilter(xmlDocMembers));
swaggerGenOptions.AddOperationFilterInstance(new XmlCommentsOperationFilter(xmlDocMembers));
swaggerGenOptions.AddSchemaFilterInstance(new XmlCommentsSchemaFilter(xmlDocMembers));

if (includeControllerXmlComments)
swaggerGenOptions.DocumentFilter<XmlCommentsDocumentFilter>(xmlDoc, swaggerGenOptions.SwaggerGeneratorOptions);
swaggerGenOptions.AddDocumentFilterInstance(new XmlCommentsDocumentFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Linq;
using System.Xml.XPath;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
internal static class XPathNavigatorExtensions
{
internal static XPathNavigator SelectFirstChild(this XPathNavigator navigator, string name)
{
return navigator.SelectChildren(name, "")
?.Cast<XPathNavigator>()
.FirstOrDefault();
}

internal static XPathNavigator SelectFirstChildWithAttribute(this XPathNavigator navigator, string childName, string attributeName, string attributeValue)
{
return navigator.SelectChildren(childName, "")
?.Cast<XPathNavigator>()
.FirstOrDefault(n => n.GetAttribute(attributeName, "") == attributeValue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,23 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsDocumentFilter : IDocumentFilter
{
private const string MemberXPath = "/doc/members/member[@name='{0}']";
private const string SummaryTag = "summary";

private readonly XPathNavigator _xmlNavigator;
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
private readonly SwaggerGeneratorOptions _options;

public XmlCommentsDocumentFilter(XPathDocument xmlDoc)
: this(xmlDoc, null)
{
}

public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options)
public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc), options)
{
_xmlNavigator = xmlDoc.CreateNavigator();
}

internal XmlCommentsDocumentFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers, SwaggerGeneratorOptions options)
{
_xmlDocMembers = xmlDocMembers;
_options = options;
}

Expand All @@ -38,22 +41,20 @@ 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));

if (typeNode != null)
if (!_xmlDocMembers.TryGetValue(memberName, out var typeNode)) continue;

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

swaggerDoc.Tags.Add(new OpenApiTag
{
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)
});
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.XPath;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
internal static class XmlCommentsDocumentHelper
{
internal static IReadOnlyDictionary<string, XPathNavigator> GetMemberDictionary(XPathDocument xmlDoc)
{
var members = xmlDoc.CreateNavigator()
.SelectFirstChild("doc")
?.SelectFirstChild("members")
?.SelectChildren("member", "")
?.OfType<XPathNavigator>();

if (members == null)
{
return new Dictionary<string, XPathNavigator>();
}

return members.ToDictionary(memberNode => memberNode.GetAttribute("name", ""));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
using System;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsOperationFilter : IOperationFilter
{
private readonly XPathNavigator _xmlNavigator;
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;

public XmlCommentsOperationFilter(XPathDocument xmlDoc)
public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc))
{
_xmlNavigator = xmlDoc.CreateNavigator();
}

internal XmlCommentsOperationFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
{
_xmlDocMembers = xmlDocMembers;
}

public void Apply(OpenApiOperation operation, OperationFilterContext context)
Expand All @@ -32,26 +37,28 @@ 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");

if (!_xmlDocMembers.TryGetValue(typeMemberName, out var methodNode)) return;

var responseNodes = methodNode.SelectChildren("response", "");
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;
if (!_xmlDocMembers.TryGetValue(methodMemberName, out var methodNode)) return;

var summaryNode = methodNode.SelectSingleNode("summary");
var summaryNode = methodNode.SelectFirstChild("summary");
if (summaryNode != null)
operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);

var remarksNode = methodNode.SelectSingleNode("remarks");
var remarksNode = methodNode.SelectFirstChild("remarks");
if (remarksNode != null)
operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml);

var responseNodes = methodNode.Select("response");
var responseNodes = methodNode.SelectChildren("response", "");
ApplyResponseTags(operation, responseNodes);
}

Expand All @@ -60,9 +67,11 @@ private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator res
while (responseNodes.MoveNext())
{
var code = responseNodes.Current.GetAttribute("code", "");
var response = operation.Responses.TryGetValue(code, out var operationResponse)
? operationResponse
: operation.Responses[code] = new OpenApiResponse();
if (!operation.Responses.TryGetValue(code, out var response))
{
response = new OpenApiResponse();
operation.Responses[code] = response;
}

response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsParameterFilter : IParameterFilter
{
private XPathNavigator _xmlNavigator;
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;

public XmlCommentsParameterFilter(XPathDocument xmlDoc)
public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc))
{
_xmlNavigator = xmlDoc.CreateNavigator();
}

internal XmlCommentsParameterFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
{
_xmlDocMembers = xmlDocMembers;
}

public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
Expand All @@ -28,18 +33,17 @@ public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContext context)
{
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.PropertyInfo);
var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']");

if (propertyNode == null) return;
if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return;

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

var exampleNode = propertyNode.SelectSingleNode("example");
var exampleNode = propertyNode.SelectFirstChild("example");
if (exampleNode == null) return;

parameter.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, parameter.Schema, exampleNode.ToString());
Expand All @@ -57,8 +61,10 @@ private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext c
if (targetMethod == null) return;

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

if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return;

XPathNavigator paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", context.ParameterInfo.Name);

if (paramNode != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
using System.Linq;
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
{
public class XmlCommentsRequestBodyFilter : IRequestBodyFilter
{
private readonly XPathNavigator _xmlNavigator;
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;

public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc))
{
}

public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc)
internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
{
_xmlNavigator = xmlDoc.CreateNavigator();
_xmlDocMembers = xmlDocMembers;
}

public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
Expand Down Expand Up @@ -42,20 +47,16 @@ public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext conte
private void ApplyPropertyTags(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo)
{
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo);
var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']");

if (propertyNode is null)
{
return;
}
if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return;

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

var exampleNode = propertyNode.SelectSingleNode("example");
var exampleNode = propertyNode.SelectFirstChild("example");
if (exampleNode is null || requestBody.Content?.Count is 0)
{
return;
Expand Down Expand Up @@ -87,8 +88,10 @@ private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterCon
}

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

if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return;

var paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", parameterInfo.Name);

if (paramNode is not null)
{
Expand Down
Loading

0 comments on commit 21d0a75

Please sign in to comment.