diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs index 2239084555..e03819bea7 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs @@ -436,7 +436,7 @@ public static void IncludeXmlComments( swaggerGenOptions.SchemaFilter(xmlDoc); if (includeControllerXmlComments) - swaggerGenOptions.DocumentFilter(xmlDoc); + swaggerGenOptions.DocumentFilter(xmlDoc, swaggerGenOptions.SwaggerGeneratorOptions); } /// diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs index 2ce004eebc..35685136a5 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs @@ -13,20 +13,27 @@ public class XmlCommentsDocumentFilter : IDocumentFilter private const string SummaryTag = "summary"; private readonly XPathNavigator _xmlNavigator; + private readonly SwaggerGeneratorOptions _options; public XmlCommentsDocumentFilter(XPathDocument xmlDoc) + : this(xmlDoc, null) + { + } + + public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) { _xmlNavigator = xmlDoc.CreateNavigator(); + _options = options; } public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { // Collect (unique) controller names and types in a dictionary var controllerNamesAndTypes = context.ApiDescriptions - .Select(apiDesc => apiDesc.ActionDescriptor as ControllerActionDescriptor) - .Where(actionDesc => actionDesc != null) - .GroupBy(actionDesc => actionDesc.ControllerName) - .Select(group => new KeyValuePair(group.Key, group.First().ControllerTypeInfo.AsType())); + .Select(apiDesc => new { ApiDesc = apiDesc, ActionDesc = apiDesc.ActionDescriptor as ControllerActionDescriptor }) + .Where(x => x.ActionDesc != null) + .GroupBy(x => _options?.TagsSelector(x.ApiDesc).FirstOrDefault() ?? x.ActionDesc.ControllerName) + .Select(group => new KeyValuePair(group.Key, group.First().ActionDesc.ControllerTypeInfo.AsType())); foreach (var nameAndType in controllerNamesAndTypes) { diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs index 4f3b28a3ad..c92fab0425 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsDocumentFilterTests.cs @@ -1,8 +1,12 @@ using System.Xml.XPath; +using System.Collections.Generic; using System.Reflection; using System.IO; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.OpenApi.Models; using Xunit; using Swashbuckle.AspNetCore.TestSupport; @@ -51,5 +55,114 @@ private static XmlCommentsDocumentFilter Subject() return new XmlCommentsDocumentFilter(new XPathDocument(xmlComments)); } } + + [Fact] + public void Uses_Proper_Tag_Name() + { + var expectedTagName = "AliasControllerWithXmlComments"; + var options = new SwaggerGeneratorOptions(); + var document = new OpenApiDocument(); + var filterContext = new DocumentFilterContext( + new[] + { + new ApiDescription + { + ActionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(FakeControllerWithXmlComments).GetTypeInfo(), + ControllerName = nameof(FakeControllerWithXmlComments), + RouteValues = new Dictionary { { "controller", expectedTagName } } + } + }, + new ApiDescription + { + ActionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(FakeControllerWithXmlComments).GetTypeInfo(), + ControllerName = nameof(FakeControllerWithXmlComments), + RouteValues = new Dictionary { { "controller", expectedTagName } } + } + } + }, + null, + null); + + Subject(options).Apply(document, filterContext); + + var tag = Assert.Single(document.Tags); + Assert.Equal(expectedTagName, tag.Name); + } + + [Fact] + public void Uses_Proper_Tag_Name_With_Custom_TagSelector() + { + var expectedTagName = "AliasControllerWithXmlComments"; + var options = new SwaggerGeneratorOptions { TagsSelector = apiDesc => new[] { expectedTagName } }; + var document = new OpenApiDocument(); + var filterContext = new DocumentFilterContext( + new[] + { + new ApiDescription + { + ActionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(FakeControllerWithXmlComments).GetTypeInfo(), + ControllerName = nameof(FakeControllerWithXmlComments), + } + }, + new ApiDescription + { + ActionDescriptor = new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(FakeControllerWithXmlComments).GetTypeInfo(), + ControllerName = nameof(FakeControllerWithXmlComments), + } + } + }, + null, + null); + + Subject(options).Apply(document, filterContext); + + var tag = Assert.Single(document.Tags); + Assert.Equal(expectedTagName, tag.Name); + } + + private static XmlCommentsDocumentFilter Subject(SwaggerGeneratorOptions options) + { + using (var xmlComments = File.OpenText($"{typeof(FakeControllerWithXmlComments).Assembly.GetName().Name}.xml")) + { + return new XmlCommentsDocumentFilter(new XPathDocument(xmlComments), options); + } + } + + [Fact] + public void Ensure_IncludeXmlComments_Adds_Filter_To_Options() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSwaggerGen(c => + { + c.IncludeXmlComments( + $"{typeof(FakeControllerWithXmlComments).Assembly.GetName().Name}.xml", + includeControllerXmlComments: true); + }); + + using var provider = services.BuildServiceProvider(); + var options = provider.GetService>().Value; + + Assert.NotNull(options); + Assert.Contains(options.DocumentFilters, x => x is XmlCommentsDocumentFilter); + } + + private sealed class DummyHostEnvironment : IWebHostEnvironment + { + public string WebRootPath { get; set; } + public IFileProvider WebRootFileProvider { get; set; } + public string ApplicationName { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + public string ContentRootPath { get; set; } + public string EnvironmentName { get; set; } + } } }