Last active
August 10, 2023 13:01
-
-
Save drasive/872fdf9f23fe37471b66fad2ee80bb71 to your computer and use it in GitHub Desktop.
.NET Swagger - Members Inherit Documentation From Base Member
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class Helpers | |
{ | |
public static XPathNavigator? GetMemberXmlNode(List<XPathDocument> documents, string memberName) | |
{ | |
var path = $"/doc/members/member[@name='{memberName}']"; | |
foreach (var document in documents) | |
{ | |
var node = document.CreateNavigator().SelectSingleNode(path); | |
if (node != null) | |
{ | |
return node; | |
} | |
} | |
return null; | |
} | |
public static string GetMemberNameForType(Type type) | |
=> $"T:{type.FullName}"; | |
public static string GetMemberNameForMember(MemberInfo member) | |
=> $"{(member is PropertyInfo ? "P" : "F")}:{(member.DeclaringType ?? member.ReflectedType).FullName}.{member.Name}"; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services.AddSwaggerGen(options => options.SchemaFilter<SwaggerInheritDocSchemaFilter>(options)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// For members that inherit their documentation, this filter will apply the documentation from the base member to the Swagger documentation. | |
/// Without it, no documentation is applied to members that inherit their documentation. | |
/// "cref" Attributes are not (yet) supported. | |
/// </summary> | |
/// <remarks> | |
/// Based on https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/1000#issuecomment-1477606163 (accessed 2023-08-08) | |
/// </remarks> | |
public class SwaggerInheritDocSchemaFilter : ISchemaFilter | |
{ | |
private readonly List<XPathDocument> _documents; | |
private readonly HashSet<string> _membersWithInheritedDocs; | |
public SwaggerInheritDocSchemaFilter(SwaggerGenOptions options) | |
{ | |
_documents = options.SchemaFilterDescriptors.Where(x => x.Type == typeof(XmlCommentsSchemaFilter)) | |
.Select(x => x.Arguments.Single()) | |
.Cast<XPathDocument>() | |
.ToList(); | |
_membersWithInheritedDocs = _documents.SelectMany(document => | |
{ | |
var inheritedElements = new List<string>(); | |
foreach (XPathNavigator member in document.CreateNavigator().Select("doc/members/member/inheritdoc")) | |
{ | |
member.MoveToParent(); | |
inheritedElements.Add(member.GetAttribute("name", "")); | |
} | |
return inheritedElements; | |
}) | |
.ToHashSet(); | |
} | |
public void Apply(OpenApiSchema schema, SchemaFilterContext context) | |
{ | |
var memberName = Helpers.GetMemberNameForType(context.Type); | |
var sources = GetPossibleSources(context.Type); | |
if (string.IsNullOrEmpty(schema.Description) && _membersWithInheritedDocs.Contains(memberName)) | |
{ | |
foreach (var source in sources) | |
{ | |
var sourceXmlNode = Helpers.GetMemberXmlNode(_documents, Helpers.GetMemberNameForType(source)); | |
var summaryNode = sourceXmlNode?.SelectSingleNode("summary"); | |
if (summaryNode != null) | |
{ | |
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); | |
} | |
var exampleNode = sourceXmlNode?.SelectSingleNode("example"); | |
if (exampleNode != null) | |
{ | |
schema.Example = new OpenApiString(XmlCommentsTextHelper.Humanize(exampleNode.InnerXml)); | |
} | |
} | |
} | |
if (schema.Properties == null) | |
{ | |
return; | |
} | |
foreach (var entry in schema.Properties) | |
{ | |
var propertyName = entry.Key; | |
var property = context.Type.GetProperty(entry.Key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); | |
if (property == null) | |
{ | |
continue; | |
} | |
var propertySchema = entry.Value; | |
var propertyMemberName = Helpers.GetMemberNameForMember(property); | |
if (!string.IsNullOrEmpty(propertySchema.Description) || !_membersWithInheritedDocs.Contains(propertyMemberName)) | |
{ | |
continue; | |
} | |
foreach (var source in sources) | |
{ | |
var sourceProperty = source.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); | |
if (sourceProperty == null) | |
{ | |
continue; | |
} | |
var sourceXmlNode = Helpers.GetMemberXmlNode(_documents, Helpers.GetMemberNameForMember(sourceProperty)); | |
var summaryNode = sourceXmlNode?.SelectSingleNode("summary"); | |
if (summaryNode != null) | |
{ | |
propertySchema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); | |
} | |
var exampleNode = sourceXmlNode?.SelectSingleNode("example"); | |
if (exampleNode != null) | |
{ | |
propertySchema.Example = new OpenApiString(XmlCommentsTextHelper.Humanize(exampleNode.InnerXml)); | |
} | |
} | |
} | |
} | |
private static List<Type> GetPossibleSources(Type type) | |
{ | |
var targets = type.GetInterfaces().ToList(); | |
var baseType = type.BaseType; | |
while (baseType != typeof(object) && baseType != null) | |
{ | |
targets.Add(baseType); | |
baseType = baseType.BaseType; | |
} | |
targets.Reverse(); // Put most specific types first | |
return targets; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment