Skip to content

Commit 37bdd9d

Browse files
mus65steven.darby
andcommitted
Improve IncludeXmlComments performance
Co-authored-by: steven.darby <[email protected]>
1 parent 16e2a94 commit 37bdd9d

File tree

9 files changed

+191
-80
lines changed

9 files changed

+191
-80
lines changed

src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -541,13 +541,15 @@ public static void IncludeXmlComments(
541541
bool includeControllerXmlComments = false)
542542
{
543543
var xmlDoc = xmlDocFactory();
544-
swaggerGenOptions.ParameterFilter<XmlCommentsParameterFilter>(xmlDoc);
545-
swaggerGenOptions.RequestBodyFilter<XmlCommentsRequestBodyFilter>(xmlDoc);
546-
swaggerGenOptions.OperationFilter<XmlCommentsOperationFilter>(xmlDoc);
547-
swaggerGenOptions.SchemaFilter<XmlCommentsSchemaFilter>(xmlDoc);
544+
var xmlDocMembers = XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc);
545+
546+
swaggerGenOptions.AddParameterFilterInstance(new XmlCommentsParameterFilter(xmlDocMembers));
547+
swaggerGenOptions.AddRequestBodyFilterInstance(new XmlCommentsRequestBodyFilter(xmlDocMembers));
548+
swaggerGenOptions.AddOperationFilterInstance(new XmlCommentsOperationFilter(xmlDocMembers));
549+
swaggerGenOptions.AddSchemaFilterInstance(new XmlCommentsSchemaFilter(xmlDocMembers));
548550

549551
if (includeControllerXmlComments)
550-
swaggerGenOptions.DocumentFilter<XmlCommentsDocumentFilter>(xmlDoc, swaggerGenOptions.SwaggerGeneratorOptions);
552+
swaggerGenOptions.AddDocumentFilterInstance(new XmlCommentsDocumentFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));
551553
}
552554

553555
/// <summary>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Linq;
2+
using System.Xml.XPath;
3+
4+
namespace Swashbuckle.AspNetCore.SwaggerGen
5+
{
6+
internal static class XPathNavigatorExtensions
7+
{
8+
internal static XPathNavigator SelectFirstChild(this XPathNavigator navigator, string name)
9+
{
10+
return navigator.SelectChildren(name, "")
11+
?.Cast<XPathNavigator>()
12+
.FirstOrDefault();
13+
}
14+
15+
internal static XPathNavigator SelectFirstChildWithAttribute(this XPathNavigator navigator, string childName, string attributeName, string attributeValue)
16+
{
17+
return navigator.SelectChildren(childName, "")
18+
?.Cast<XPathNavigator>()
19+
.FirstOrDefault(n => n.GetAttribute(attributeName, "") == attributeValue);
20+
}
21+
}
22+
}

src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,23 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
99
{
1010
public class XmlCommentsDocumentFilter : IDocumentFilter
1111
{
12-
private const string MemberXPath = "/doc/members/member[@name='{0}']";
1312
private const string SummaryTag = "summary";
1413

15-
private readonly XPathNavigator _xmlNavigator;
14+
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
1615
private readonly SwaggerGeneratorOptions _options;
1716

1817
public XmlCommentsDocumentFilter(XPathDocument xmlDoc)
1918
: this(xmlDoc, null)
2019
{
2120
}
2221

23-
public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options)
22+
public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc), options)
2423
{
25-
_xmlNavigator = xmlDoc.CreateNavigator();
24+
}
25+
26+
internal XmlCommentsDocumentFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers, SwaggerGeneratorOptions options)
27+
{
28+
_xmlDocMembers = xmlDocMembers;
2629
_options = options;
2730
}
2831

@@ -38,22 +41,20 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
3841
foreach (var nameAndType in controllerNamesAndTypes)
3942
{
4043
var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(nameAndType.Value);
41-
var typeNode = _xmlNavigator.SelectSingleNode(string.Format(MemberXPath, memberName));
4244

43-
if (typeNode != null)
45+
if (!_xmlDocMembers.TryGetValue(memberName, out var typeNode)) continue;
46+
47+
var summaryNode = typeNode.SelectFirstChild(SummaryTag);
48+
if (summaryNode != null)
4449
{
45-
var summaryNode = typeNode.SelectSingleNode(SummaryTag);
46-
if (summaryNode != null)
50+
if (swaggerDoc.Tags == null)
51+
swaggerDoc.Tags = new List<OpenApiTag>();
52+
53+
swaggerDoc.Tags.Add(new OpenApiTag
4754
{
48-
if (swaggerDoc.Tags == null)
49-
swaggerDoc.Tags = new List<OpenApiTag>();
50-
51-
swaggerDoc.Tags.Add(new OpenApiTag
52-
{
53-
Name = nameAndType.Key,
54-
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml)
55-
});
56-
}
55+
Name = nameAndType.Key,
56+
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml)
57+
});
5758
}
5859
}
5960
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Xml.XPath;
4+
5+
namespace Swashbuckle.AspNetCore.SwaggerGen
6+
{
7+
internal static class XmlCommentsDocumentHelper
8+
{
9+
internal static IReadOnlyDictionary<string, XPathNavigator> GetMemberDictionary(XPathDocument xmlDoc)
10+
{
11+
var members = xmlDoc.CreateNavigator()
12+
.SelectFirstChild("doc")
13+
?.SelectFirstChild("members")
14+
?.SelectChildren("member", "")
15+
?.OfType<XPathNavigator>();
16+
17+
if (members == null)
18+
{
19+
return new Dictionary<string, XPathNavigator>();
20+
}
21+
22+
return members.ToDictionary(memberNode => memberNode.GetAttribute("name", ""));
23+
}
24+
}
25+
}

src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
using System;
1+
using Microsoft.OpenApi.Models;
2+
using System;
3+
using System.Collections.Generic;
24
using System.Reflection;
35
using System.Xml.XPath;
4-
using Microsoft.OpenApi.Models;
56

67
namespace Swashbuckle.AspNetCore.SwaggerGen
78
{
89
public class XmlCommentsOperationFilter : IOperationFilter
910
{
10-
private readonly XPathNavigator _xmlNavigator;
11+
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
1112

12-
public XmlCommentsOperationFilter(XPathDocument xmlDoc)
13+
public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc))
1314
{
14-
_xmlNavigator = xmlDoc.CreateNavigator();
15+
}
16+
17+
internal XmlCommentsOperationFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
18+
{
19+
_xmlDocMembers = xmlDocMembers;
1520
}
1621

1722
public void Apply(OpenApiOperation operation, OperationFilterContext context)
@@ -32,26 +37,28 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
3237
private void ApplyControllerTags(OpenApiOperation operation, Type controllerType)
3338
{
3439
var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType);
35-
var responseNodes = _xmlNavigator.Select($"/doc/members/member[@name='{typeMemberName}']/response");
40+
41+
if (!_xmlDocMembers.TryGetValue(typeMemberName, out var methodNode)) return;
42+
43+
var responseNodes = methodNode.SelectChildren("response", "");
3644
ApplyResponseTags(operation, responseNodes);
3745
}
3846

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

44-
if (methodNode == null) return;
51+
if (!_xmlDocMembers.TryGetValue(methodMemberName, out var methodNode)) return;
4552

46-
var summaryNode = methodNode.SelectSingleNode("summary");
53+
var summaryNode = methodNode.SelectFirstChild("summary");
4754
if (summaryNode != null)
4855
operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
4956

50-
var remarksNode = methodNode.SelectSingleNode("remarks");
57+
var remarksNode = methodNode.SelectFirstChild("remarks");
5158
if (remarksNode != null)
5259
operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml);
5360

54-
var responseNodes = methodNode.Select("response");
61+
var responseNodes = methodNode.SelectChildren("response", "");
5562
ApplyResponseTags(operation, responseNodes);
5663
}
5764

@@ -60,9 +67,11 @@ private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator res
6067
while (responseNodes.MoveNext())
6168
{
6269
var code = responseNodes.Current.GetAttribute("code", "");
63-
var response = operation.Responses.TryGetValue(code, out var operationResponse)
64-
? operationResponse
65-
: operation.Responses[code] = new OpenApiResponse();
70+
if (!operation.Responses.TryGetValue(code, out var response))
71+
{
72+
response = new OpenApiResponse();
73+
operation.Responses[code] = response;
74+
}
6675

6776
response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml);
6877
}

src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
using System.Reflection;
1+
using Microsoft.OpenApi.Models;
2+
using System.Collections.Generic;
3+
using System.Reflection;
24
using System.Xml.XPath;
3-
using Microsoft.OpenApi.Models;
45

56
namespace Swashbuckle.AspNetCore.SwaggerGen
67
{
78
public class XmlCommentsParameterFilter : IParameterFilter
89
{
9-
private XPathNavigator _xmlNavigator;
10+
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
1011

11-
public XmlCommentsParameterFilter(XPathDocument xmlDoc)
12+
public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc))
1213
{
13-
_xmlNavigator = xmlDoc.CreateNavigator();
14+
}
15+
16+
internal XmlCommentsParameterFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
17+
{
18+
_xmlDocMembers = xmlDocMembers;
1419
}
1520

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

33-
if (propertyNode == null) return;
37+
if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return;
3438

35-
var summaryNode = propertyNode.SelectSingleNode("summary");
39+
var summaryNode = propertyNode.SelectFirstChild("summary");
3640
if (summaryNode != null)
3741
{
3842
parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
3943
parameter.Schema.Description = null; // no need to duplicate
4044
}
4145

42-
var exampleNode = propertyNode.SelectSingleNode("example");
46+
var exampleNode = propertyNode.SelectFirstChild("example");
4347
if (exampleNode == null) return;
4448

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

5963
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
60-
var paramNode = _xmlNavigator.SelectSingleNode(
61-
$"/doc/members/member[@name='{methodMemberName}']/param[@name='{context.ParameterInfo.Name}']");
64+
65+
if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return;
66+
67+
XPathNavigator paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", context.ParameterInfo.Name);
6268

6369
if (paramNode != null)
6470
{

src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
using System.Linq;
1+
using Microsoft.OpenApi.Models;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Reflection;
35
using System.Xml.XPath;
4-
using Microsoft.OpenApi.Models;
56

67
namespace Swashbuckle.AspNetCore.SwaggerGen
78
{
89
public class XmlCommentsRequestBodyFilter : IRequestBodyFilter
910
{
10-
private readonly XPathNavigator _xmlNavigator;
11+
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
12+
13+
public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.GetMemberDictionary(xmlDoc))
14+
{
15+
}
1116

12-
public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc)
17+
internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
1318
{
14-
_xmlNavigator = xmlDoc.CreateNavigator();
19+
_xmlDocMembers = xmlDocMembers;
1520
}
1621

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

47-
if (propertyNode is null)
48-
{
49-
return;
50-
}
51+
if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return;
5152

52-
var summaryNode = propertyNode.SelectSingleNode("summary");
53+
var summaryNode = propertyNode.SelectFirstChild("summary");
5354
if (summaryNode is not null)
5455
{
5556
requestBody.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
5657
}
5758

58-
var exampleNode = propertyNode.SelectSingleNode("example");
59+
var exampleNode = propertyNode.SelectFirstChild("example");
5960
if (exampleNode is null || requestBody.Content?.Count is 0)
6061
{
6162
return;
@@ -87,8 +88,10 @@ private void ApplyParamTags(OpenApiRequestBody requestBody, RequestBodyFilterCon
8788
}
8889

8990
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
90-
var paramNode = _xmlNavigator.SelectSingleNode(
91-
$"/doc/members/member[@name='{methodMemberName}']/param[@name='{parameterInfo.Name}']");
91+
92+
if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return;
93+
94+
var paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", parameterInfo.Name);
9295

9396
if (paramNode is not null)
9497
{

0 commit comments

Comments
 (0)