Skip to content

Commit

Permalink
Improve IncludeXmlComments performance (2) (domaindrivendev#3044)
Browse files Browse the repository at this point in the history
* Improve IncludeXmlComments performance

Co-authored-by: steven.darby <[email protected]>

* Add braces

Co-authored-by: Martin Costello <[email protected]>

* Use ??= operator

Co-authored-by: Martin Costello <[email protected]>

* use file scoped namespaces

* use constant for empty XML namespace

and add SelectChild/GetAttribute extensions for
consistency.

* Rename GetMemberDictionary to CreateMemberDictionary

* use concrete Dictionary type

* use file-scoped namespace

* revert file-scoped namespace

didn't mean to touch this file.

* use OfType instead of Cast

---------

Co-authored-by: steven.darby <[email protected]>
Co-authored-by: Martin Costello <[email protected]>
  • Loading branch information
3 people authored Aug 27, 2024
1 parent d358e7b commit 90f302d
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -704,13 +704,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.CreateMemberDictionary(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,33 @@
using System.Linq;
using System.Xml.XPath;

namespace Swashbuckle.AspNetCore.SwaggerGen;

internal static class XPathNavigatorExtensions
{
private const string EmptyNamespace = "";

internal static XPathNodeIterator SelectChildren(this XPathNavigator navigator, string name)
{
return navigator.SelectChildren(name, EmptyNamespace);
}

internal static string GetAttribute(this XPathNavigator navigator, string name)
{
return navigator.GetAttribute(name, EmptyNamespace);
}

internal static XPathNavigator SelectFirstChild(this XPathNavigator navigator, string name)
{
return navigator.SelectChildren(name, EmptyNamespace)
?.OfType<XPathNavigator>()
.FirstOrDefault();
}

internal static XPathNavigator SelectFirstChildWithAttribute(this XPathNavigator navigator, string childName, string attributeName, string attributeValue)
{
return navigator.SelectChildren(childName, EmptyNamespace)
?.OfType<XPathNavigator>()
.FirstOrDefault(n => n.GetAttribute(attributeName, EmptyNamespace) == 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.CreateMemberDictionary(xmlDoc), options)
{
_xmlNavigator = xmlDoc.CreateNavigator();
}

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

Expand All @@ -38,22 +41,22 @@ 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))
{
var summaryNode = typeNode.SelectSingleNode(SummaryTag);
if (summaryNode != null)
continue;
}

var summaryNode = typeNode.SelectFirstChild(SummaryTag);
if (summaryNode != 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,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.XPath;

namespace Swashbuckle.AspNetCore.SwaggerGen;

internal static class XmlCommentsDocumentHelper
{
internal static Dictionary<string, XPathNavigator> CreateMemberDictionary(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.CreateMemberDictionary(xmlDoc))
{
_xmlNavigator = xmlDoc.CreateNavigator();
}

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

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

private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes)
{
while (responseNodes.MoveNext())
{
var code = responseNodes.Current.GetAttribute("code", "");
var response = operation.Responses.TryGetValue(code, out var operationResponse)
? operationResponse
: operation.Responses[code] = new OpenApiResponse();
var code = responseNodes.Current.GetAttribute("code");
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.CreateMemberDictionary(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,14 +61,16 @@ 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)
{
parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);

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

parameter.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, parameter.Schema, example);
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.CreateMemberDictionary(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,14 +88,16 @@ 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)
{
requestBody.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);

var example = paramNode.GetAttribute("example", "");
var example = paramNode.GetAttribute("example");
if (!string.IsNullOrEmpty(example))
{
foreach (var mediaType in requestBody.Content.Values)
Expand Down
Loading

0 comments on commit 90f302d

Please sign in to comment.