Skip to content

Instantly share code, notes, and snippets.

@drasive
Last active August 10, 2023 13:01
Show Gist options
  • Save drasive/872fdf9f23fe37471b66fad2ee80bb71 to your computer and use it in GitHub Desktop.
Save drasive/872fdf9f23fe37471b66fad2ee80bb71 to your computer and use it in GitHub Desktop.
.NET Swagger - Members Inherit Documentation From Base Member
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}";
}
services.AddSwaggerGen(options => options.SchemaFilter<SwaggerInheritDocSchemaFilter>(options));
/// <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