-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for associating XML Comments with custom tags defined using TagsAttribute #2565
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace Swashbuckle.AspNetCore.SwaggerGen | ||
{ | ||
public static class KeyValuePairExtensions | ||
{ | ||
// Explicit deconstruct required for older .NET frameworks | ||
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value) | ||
{ | ||
key = kvp.Key; | ||
value = kvp.Value; | ||
} | ||
} | ||
Comment on lines
+5
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to only add this polyfill for the TFMs that need it. Also, can it be |
||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -4,6 +4,8 @@ | |||||
using Microsoft.AspNetCore.Mvc.Controllers; | ||||||
using Microsoft.OpenApi.Models; | ||||||
using System; | ||||||
using System.Reflection; | ||||||
using Microsoft.AspNetCore.Http; | ||||||
|
||||||
namespace Swashbuckle.AspNetCore.SwaggerGen | ||||||
{ | ||||||
|
@@ -21,34 +23,63 @@ public XmlCommentsDocumentFilter(XPathDocument xmlDoc) | |||||
|
||||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) | ||||||
{ | ||||||
// Collect (unique) controller names and types in a dictionary | ||||||
var controllerNamesAndTypes = context.ApiDescriptions | ||||||
// Collect (unique) controller names, types and custom tags (defined by the first TagsAttribute value) in a dictionary | ||||||
var controllers = context.ApiDescriptions | ||||||
.Select(apiDesc => apiDesc.ActionDescriptor as ControllerActionDescriptor) | ||||||
.Where(actionDesc => actionDesc != null) | ||||||
.GroupBy(actionDesc => actionDesc.ControllerName) | ||||||
.Select(group => new KeyValuePair<string, Type>(group.Key, group.First().ControllerTypeInfo.AsType())); | ||||||
.Select(group => new KeyValuePair<string, ControllerInfo>(group.Key, GetControllerInfo(group))); | ||||||
|
||||||
foreach (var nameAndType in controllerNamesAndTypes) | ||||||
swaggerDoc.Tags ??= new List<OpenApiTag>(); | ||||||
foreach (var (controllerName, controllerInfo) in controllers) | ||||||
{ | ||||||
var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(nameAndType.Value); | ||||||
var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerInfo.ControllerType); | ||||||
var typeNode = _xmlNavigator.SelectSingleNode(string.Format(MemberXPath, memberName)); | ||||||
|
||||||
if (typeNode != null) | ||||||
var description = GetXmlDescriptionOrNull(typeNode); | ||||||
|
||||||
swaggerDoc.Tags.Add(new OpenApiTag | ||||||
{ | ||||||
var summaryNode = typeNode.SelectSingleNode(SummaryTag); | ||||||
if (summaryNode != null) | ||||||
{ | ||||||
if (swaggerDoc.Tags == null) | ||||||
swaggerDoc.Tags = new List<OpenApiTag>(); | ||||||
|
||||||
swaggerDoc.Tags.Add(new OpenApiTag | ||||||
{ | ||||||
Name = nameAndType.Key, | ||||||
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml) | ||||||
}); | ||||||
} | ||||||
Name = controllerInfo.CustomTagName ?? controllerName, | ||||||
Description = description | ||||||
}); | ||||||
} | ||||||
swaggerDoc.Tags = swaggerDoc.Tags.OrderBy(x => x.Name).ToList(); | ||||||
} | ||||||
|
||||||
private class ControllerInfo | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{ | ||||||
public Type ControllerType { get; set; } | ||||||
public string CustomTagName { get; set; } | ||||||
} | ||||||
|
||||||
private static ControllerInfo GetControllerInfo(IGrouping<string, ControllerActionDescriptor> group) | ||||||
{ | ||||||
var controllerInfo = new ControllerInfo | ||||||
{ | ||||||
ControllerType = group.First().ControllerTypeInfo.AsType() | ||||||
}; | ||||||
|
||||||
#if NET6_0_OR_GREATER | ||||||
controllerInfo.CustomTagName = | ||||||
group.First().MethodInfo.DeclaringType?.GetCustomAttribute<TagsAttribute>()?.Tags[0]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it'll throw an exception if |
||||||
#endif | ||||||
|
||||||
return controllerInfo; | ||||||
} | ||||||
|
||||||
private static string GetXmlDescriptionOrNull(XPathNavigator typeNode) | ||||||
{ | ||||||
if (typeNode != null) | ||||||
{ | ||||||
var summaryNode = typeNode.SelectSingleNode(SummaryTag); | ||||||
if (summaryNode != null) | ||||||
{ | ||||||
return XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); | ||||||
} | ||||||
} | ||||||
|
||||||
return null; | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Swashbuckle.AspNetCore.SwaggerGen.Test | ||
{ | ||
/// <summary> | ||
/// Summary for FakeControllerWithCustomTag | ||
/// </summary> | ||
[Tags("fake controller custom tag")] | ||
public class FakeControllerWithCustomTag | ||
{ | ||
public void ActionAny() | ||
{ } | ||
|
||
public void ActionAnother() | ||
{ } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make
TagsAttribute
andTagActionsBy
<see .... />
references in these two comments.