From 90f302d9bf0e1285a7f4c3cc2bab32fddc777fbb Mon Sep 17 00:00:00 2001 From: mus65 Date: Tue, 27 Aug 2024 11:01:54 +0200 Subject: [PATCH] Improve IncludeXmlComments performance (2) (#3044) * Improve IncludeXmlComments performance Co-authored-by: steven.darby * Add braces Co-authored-by: Martin Costello * Use ??= operator Co-authored-by: Martin Costello * 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 Co-authored-by: Martin Costello --- .../SwaggerGenOptionsExtensions.cs | 12 +++-- .../XmlComments/XPathNavigatorExtensions.cs | 33 +++++++++++++ .../XmlComments/XmlCommentsDocumentFilter.cs | 37 +++++++------- .../XmlComments/XmlCommentsDocumentHelper.cs | 24 ++++++++++ .../XmlComments/XmlCommentsOperationFilter.cs | 39 +++++++++------ .../XmlComments/XmlCommentsParameterFilter.cs | 30 +++++++----- .../XmlCommentsRequestBodyFilter.cs | 33 +++++++------ .../XmlComments/XmlCommentsSchemaFilter.cs | 48 +++++++++++-------- .../XmlCommentsDocumentFilterTests.cs | 33 +++++++++++++ 9 files changed, 206 insertions(+), 83 deletions(-) create mode 100644 src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XPathNavigatorExtensions.cs create mode 100644 src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentHelper.cs diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs index 37144900a5..65ffc38307 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs @@ -704,13 +704,15 @@ public static void IncludeXmlComments( bool includeControllerXmlComments = false) { var xmlDoc = xmlDocFactory(); - swaggerGenOptions.ParameterFilter(xmlDoc); - swaggerGenOptions.RequestBodyFilter(xmlDoc); - swaggerGenOptions.OperationFilter(xmlDoc); - swaggerGenOptions.SchemaFilter(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(xmlDoc, swaggerGenOptions.SwaggerGeneratorOptions); + swaggerGenOptions.AddDocumentFilterInstance(new XmlCommentsDocumentFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); } /// diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XPathNavigatorExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XPathNavigatorExtensions.cs new file mode 100644 index 0000000000..99c2de51fb --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XPathNavigatorExtensions.cs @@ -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() + .FirstOrDefault(); + } + + internal static XPathNavigator SelectFirstChildWithAttribute(this XPathNavigator navigator, string childName, string attributeName, string attributeValue) + { + return navigator.SelectChildren(childName, EmptyNamespace) + ?.OfType() + .FirstOrDefault(n => n.GetAttribute(attributeName, EmptyNamespace) == attributeValue); + } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs index 35685136a5..0c2cbabcf6 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs @@ -9,10 +9,9 @@ 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 _xmlDocMembers; private readonly SwaggerGeneratorOptions _options; public XmlCommentsDocumentFilter(XPathDocument xmlDoc) @@ -20,9 +19,13 @@ public XmlCommentsDocumentFilter(XPathDocument xmlDoc) { } - public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) + public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), options) { - _xmlNavigator = xmlDoc.CreateNavigator(); + } + + internal XmlCommentsDocumentFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + { + _xmlDocMembers = xmlDocMembers; _options = options; } @@ -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(); + + swaggerDoc.Tags.Add(new OpenApiTag { - if (swaggerDoc.Tags == null) - swaggerDoc.Tags = new List(); - - swaggerDoc.Tags.Add(new OpenApiTag - { - Name = nameAndType.Key, - Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml) - }); - } + Name = nameAndType.Key, + Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml) + }); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentHelper.cs new file mode 100644 index 0000000000..c6e70270a8 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentHelper.cs @@ -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 CreateMemberDictionary(XPathDocument xmlDoc) + { + var members = xmlDoc.CreateNavigator() + .SelectFirstChild("doc") + ?.SelectFirstChild("members") + ?.SelectChildren("member") + ?.OfType(); + + if (members == null) + { + return new Dictionary(); + } + + return members.ToDictionary(memberNode => memberNode.GetAttribute("name")); + } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs index 5af72d42c1..cf18c61ea7 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs @@ -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 _xmlDocMembers; - public XmlCommentsOperationFilter(XPathDocument xmlDoc) + public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc)) { - _xmlNavigator = xmlDoc.CreateNavigator(); + } + + internal XmlCommentsOperationFilter(IReadOnlyDictionary xmlDocMembers) + { + _xmlDocMembers = xmlDocMembers; } public void Apply(OpenApiOperation operation, OperationFilterContext context) @@ -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); } @@ -59,10 +66,12 @@ 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(); + 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); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs index 6eb17fa94d..07caf2044d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs @@ -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 _xmlDocMembers; - public XmlCommentsParameterFilter(XPathDocument xmlDoc) + public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc)) { - _xmlNavigator = xmlDoc.CreateNavigator(); + } + + internal XmlCommentsParameterFilter(IReadOnlyDictionary xmlDocMembers) + { + _xmlDocMembers = xmlDocMembers; } public void Apply(OpenApiParameter parameter, ParameterFilterContext context) @@ -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()); @@ -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); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs index 48d07a0684..acbc751abf 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs @@ -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 _xmlDocMembers; + + public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc)) + { + } - public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) + internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary xmlDocMembers) { - _xmlNavigator = xmlDoc.CreateNavigator(); + _xmlDocMembers = xmlDocMembers; } public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) @@ -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; @@ -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) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs index a246657d2c..67c7377c8b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs @@ -1,16 +1,21 @@ -using System; +using Microsoft.OpenApi.Models; +using System; +using System.Collections.Generic; using System.Xml.XPath; -using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.SwaggerGen { public class XmlCommentsSchemaFilter : ISchemaFilter { - private readonly XPathNavigator _xmlNavigator; + private readonly IReadOnlyDictionary _xmlDocMembers; - public XmlCommentsSchemaFilter(XPathDocument xmlDoc) + public XmlCommentsSchemaFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc)) { - _xmlNavigator = xmlDoc.CreateNavigator(); + } + + internal XmlCommentsSchemaFilter(IReadOnlyDictionary xmlDocMembers) + { + _xmlDocMembers = xmlDocMembers; } public void Apply(OpenApiSchema schema, SchemaFilterContext context) @@ -26,7 +31,10 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) private void ApplyTypeTags(OpenApiSchema schema, Type type) { var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type); - var typeSummaryNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{typeMemberName}']/summary"); + + if (!_xmlDocMembers.TryGetValue(typeMemberName, out var memberNode)) return; + + var typeSummaryNode = memberNode.SelectFirstChild("summary"); if (typeSummaryNode != null) { @@ -37,32 +45,34 @@ private void ApplyTypeTags(OpenApiSchema schema, Type type) private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context) { var fieldOrPropertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.MemberInfo); - var fieldOrPropertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{fieldOrPropertyMemberName}']"); var recordTypeName = XmlCommentsNodeNameHelper.GetMemberNameForType(context.MemberInfo.DeclaringType); - var recordDefaultConstructorProperty = - _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{recordTypeName}']/param[@name='{context.MemberInfo.Name}']"); - if (recordDefaultConstructorProperty != null) + if (_xmlDocMembers.TryGetValue(recordTypeName, out var recordTypeNode)) { - var summaryNode = recordDefaultConstructorProperty.Value; - if (summaryNode != null) - schema.Description = XmlCommentsTextHelper.Humanize(summaryNode); + XPathNavigator recordDefaultConstructorProperty = recordTypeNode.SelectFirstChildWithAttribute("param", "name", context.MemberInfo.Name); - var example = recordDefaultConstructorProperty.GetAttribute("example", string.Empty); - if (!string.IsNullOrEmpty(example)) + if (recordDefaultConstructorProperty != null) { - TrySetExample(schema, context, example); + var summaryNode = recordDefaultConstructorProperty.Value; + if (summaryNode != null) + schema.Description = XmlCommentsTextHelper.Humanize(summaryNode); + + var example = recordDefaultConstructorProperty.GetAttribute("example"); + if (!string.IsNullOrEmpty(example)) + { + TrySetExample(schema, context, example); + } } } - if (fieldOrPropertyNode != null) + if (_xmlDocMembers.TryGetValue(fieldOrPropertyMemberName, out var fieldOrPropertyNode)) { - var summaryNode = fieldOrPropertyNode.SelectSingleNode("summary"); + var summaryNode = fieldOrPropertyNode.SelectFirstChild("summary"); if (summaryNode != null) schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); - var exampleNode = fieldOrPropertyNode.SelectSingleNode("example"); + var exampleNode = fieldOrPropertyNode.SelectFirstChild("example"); TrySetExample(schema, context, exampleNode?.Value); } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs index 5fe5a2421b..3480385890 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs @@ -47,6 +47,39 @@ public void Apply_SetsTagDescription_FromControllerSummaryTags() Assert.Equal("Summary for FakeControllerWithXmlComments", tag.Description); } + [Fact] + public void Apply_SetsTagDescription_FromControllerSummaryTags_OneControllerWithoutDescription() + { + var document = new OpenApiDocument(); + var filterContext = new DocumentFilterContext( + new[] + { + new ApiDescription + { + ActionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(FakeController).GetTypeInfo(), + ControllerName = nameof(FakeController) + } + }, + new ApiDescription + { + ActionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(FakeControllerWithXmlComments).GetTypeInfo(), + ControllerName = nameof(FakeControllerWithXmlComments) + } + } + }, + null, + null); + + Subject().Apply(document, filterContext); + + var tag = Assert.Single(document.Tags); + Assert.Equal("Summary for FakeControllerWithXmlComments", tag.Description); + } + private static XmlCommentsDocumentFilter Subject() { using (var xmlComments = File.OpenText($"{typeof(FakeControllerWithXmlComments).Assembly.GetName().Name}.xml"))