Swashbuckle.webapi: Swashbuckle doesn't work properly with inheritdoc

Created on 5 Feb 2017  路  10Comments  路  Source: domaindrivendev/Swashbuckle.WebApi

I have following xmldoc generated:

<member name="P:Gate.GateConfig.Connection"> <inheritdoc /> </member>
And this member is inherited from an interface. Visual Studio shows this properly, but output swagger is generated without information from parent.

Most helpful comment

In the meantime this is how you can workaround the problem

  1. dotnet tool -g install InheritDoc
  2. Add this target to your csproj file and also
  <Target Name="InheritDoc" AfterTargets="PostBuildEvent" Condition="$(GenerateDocumentationFile)">
    <Exec Command="InheritDoc -o" IgnoreExitCode="True" ContinueOnError="true"/>
  </Target>

All 10 comments

yeah, nothing changed!

Just ran into this myself - not actually sure if this is possible, as the generated XML doc doesn't seem to contain any information relating to the inheritance chain. I expect Visual Studio needs to deal with this before Swagger can.

is there any plan to enable this or is this something that just doesn't gel with how it all works?

In the meantime this is how you can workaround the problem

  1. dotnet tool -g install InheritDoc
  2. Add this target to your csproj file and also
  <Target Name="InheritDoc" AfterTargets="PostBuildEvent" Condition="$(GenerateDocumentationFile)">
    <Exec Command="InheritDoc -o" IgnoreExitCode="True" ContinueOnError="true"/>
  </Target>

Edit: This is just for the AspNetCore Version

I wrote a ISchemaFilter to automatically add the summary and example texts to the types and members decorated with an tag.

Add to Swagger:

services.AddSwaggerGen(config => config.SchemaFilter<InheritDocSchemaFilter>(config));

Code:

/// <summary>
/// Adds documentation that is provided by the <inhertidoc /> tag.
/// </summary>
/// <seealso cref="Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter" />
public class InheritDocSchemaFilter : ISchemaFilter
{
    private const string SUMMARY_TAG = "summary";
    private const string EXAMPLE_TAG = "example";
    private readonly List<XPathDocument> _documents;
    private readonly Dictionary<string, string> _inheritedDocs;

    /// <summary>
    /// Initializes a new instance of the <see cref="InheritDocDocumentFilter" /> class.
    /// </summary>
    /// <param name="options">The options.</param>
    public InheritDocDocumentFilter(SwaggerGenOptions options)
    {
        _documents = options.SchemaFilterDescriptors.Where(x => x.Type == typeof(XmlCommentsSchemaFilter))
            .Select(x => x.Arguments.Single())
            .Cast<XPathDocument>()
            .ToList();

        _inheritedDocs = _documents.SelectMany(
                doc =>
                {
                    var inheritedElements = new List<(string, string)>();
                    foreach (XPathNavigator member in doc.CreateNavigator().Select("doc/members/member/inheritdoc"))
                    {
                        member.MoveToParent();
                        inheritedElements.Add((member.GetAttribute("name", ""), member.GetAttribute("cref", "")));
                    }

                    return inheritedElements;
                })
            .ToDictionary(x => x.Item1, x => x.Item2);
    }

    /// <inheritdoc />
    public void Apply(Schema schema, SchemaFilterContext context)
    {
        if (!(context.JsonContract is JsonObjectContract jsonObjectContract))
            return;

        // Try to apply a description for inherited types.
        var memberName = XmlCommentsMemberNameHelper.GetMemberNameForType(context.SystemType);
        if (string.IsNullOrEmpty(schema.Description) && _inheritedDocs.ContainsKey(memberName))
        {
            var cref = _inheritedDocs[memberName];
            var target = GetTargetRecursive(context.SystemType, cref);

            var targetXmlNode = GetMemberXmlNode(XmlCommentsMemberNameHelper.GetMemberNameForType(target));
            var summaryNode = targetXmlNode?.SelectSingleNode(SUMMARY_TAG);

            if (summaryNode != null)
                schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
        }

        if (schema.Properties == null)
            return;

        // Add the summary and examples for the properties.
        foreach (var entry in schema.Properties)
        {
            if (!jsonObjectContract.Properties.Contains(entry.Key))
                continue;

            var jsonProperty = jsonObjectContract.Properties[entry.Key];

            if (TryGetMemberInfo(jsonProperty, out var memberInfo))
                ApplyPropertyComments(entry.Value, memberInfo);
        }
    }

    private static bool TryGetMemberInfo(JsonProperty jsonProperty, out MemberInfo memberInfo)
    {
        if (jsonProperty.UnderlyingName == null)
        {
            memberInfo = null;
            return false;
        }

        var metadataAttribute = jsonProperty.DeclaringType
            .GetCustomAttributes(typeof(ModelMetadataTypeAttribute), true)
            .FirstOrDefault();

        var typeToReflect = metadataAttribute != null
            ? ((ModelMetadataTypeAttribute)metadataAttribute).MetadataType
            : jsonProperty.DeclaringType;

        memberInfo = typeToReflect.GetMember(jsonProperty.UnderlyingName).FirstOrDefault();

        return memberInfo != null;
    }

    private static MemberInfo GetTarget(MemberInfo memberInfo, string cref)
    {
        var type = memberInfo.DeclaringType ?? memberInfo.ReflectedType;

        if (type == null)
            return null;

        // Find all matching members in all interfaces and the base class.
        var targets = type.GetInterfaces()
            .Append(type.BaseType)
            .SelectMany(
                x => x.FindMembers(
                    memberInfo.MemberType,
                    BindingFlags.Instance | BindingFlags.Public,
                    (info, criteria) => info.Name == memberInfo.Name,
                    null))
            .ToList();

        // Try to find the target, if one is declared.
        if (!string.IsNullOrEmpty(cref))
        {
            var crefTarget = targets.SingleOrDefault(t => XmlCommentsMemberNameHelper.GetMemberNameForMember(t) == cref);

            if (crefTarget != null)
                return crefTarget;
        }

        // We use the last since that will be our base class or the "nearest" implemented interface.
        return targets.LastOrDefault();
    }

    private static Type GetTarget(Type type, string cref)
    {
        var targets = type.GetInterfaces();
        if (type.BaseType != typeof(object))
            targets = targets.Append(type.BaseType).ToArray();

        // Try to find the target, if one is declared.
        if (!string.IsNullOrEmpty(cref))
        {
            var crefTarget = targets.SingleOrDefault(t => XmlCommentsMemberNameHelper.GetMemberNameForType(t) == cref);

            if (crefTarget != null)
                return crefTarget;
        }

        // We use the last since that will be our base class or the "nearest" implemented interface.
        return targets.LastOrDefault();
    }

    private void ApplyPropertyComments(Schema propertySchema, MemberInfo memberInfo)
    {
        var memberName = XmlCommentsMemberNameHelper.GetMemberNameForMember(memberInfo);

        if (!_inheritedDocs.ContainsKey(memberName))
            return;

        var cref = _inheritedDocs[memberName];
        var target = GetTargetRecursive(memberInfo, cref);

        var targetXmlNode = GetMemberXmlNode(XmlCommentsMemberNameHelper.GetMemberNameForMember(target));

        if (targetXmlNode == null)
            return;

        var summaryNode = targetXmlNode.SelectSingleNode(SUMMARY_TAG);
        if (summaryNode != null)
            propertySchema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);

        var exampleNode = targetXmlNode.SelectSingleNode(EXAMPLE_TAG);
        if (exampleNode != null)
            propertySchema.Example = XmlCommentsTextHelper.Humanize(exampleNode.InnerXml);
    }

    private XPathNavigator GetMemberXmlNode(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;
    }

    private MemberInfo GetTargetRecursive(MemberInfo memberInfo, string cref)
    {
        var target = GetTarget(memberInfo, cref);

        if (target == null)
            return null;

        var targetMemberName = XmlCommentsMemberNameHelper.GetMemberNameForMember(target);

        if (_inheritedDocs.ContainsKey(targetMemberName))
            return GetTarget(target, _inheritedDocs[targetMemberName]);

        return target;
    }

    private Type GetTargetRecursive(Type type, string cref)
    {
        var target = GetTarget(type, cref);

        if (target == null)
            return null;

        var targetMemberName = XmlCommentsMemberNameHelper.GetMemberNameForType(target);

        if (_inheritedDocs.ContainsKey(targetMemberName))
            return GetTarget(target, _inheritedDocs[targetMemberName]);

        return target;
    }
}

@anhaehne great code, sadly completely incompatible with this ( "not Core" ) version of Swashbuckle...

anyone managed to get Swagger to load inheritdoc ?

  1. dotnet tool -g install InheritDoc

Thanks! I had to run a slightly different command on my end:

dotnet tool install -g InheritDocTool

@anhaehne : The class you provided does not compile for me. For example because the name of the class InheritDocSchemaFilter does not match the constructor name InheritDocDocumentFilter

Are there are news here? It's been over 4 years this has been opened.

I got into this situation today as well while doing some cleanup on some of our models. We have an interface defining a few properties that many DTOs have and changed the classes to use <inheritdoc /> only to find that the text vanished from the schema.

@julealgon I just ran into this issue myself today. Seems like they haven't implemented it because you can do it yourself pretty easily by pre-processing the XML docs before adding them to Swagger. Here is the code I am using (inspired by this):

        void AddXmlDocs() {
          // generate paths for the XML doc files in the assembly's directory.
          var XmlDocPaths = Directory.GetFiles(
            path: AppDomain.CurrentDomain.BaseDirectory, 
            searchPattern: "*.xml"
          );

          // load the XML docs for processing.
          var XmlDocs = (
            from DocPath in XmlDocPaths select XDocument.Load(DocPath)
          ).ToList();

          // need a map for looking up member elements by name.
          var TargetMemberElements = new Dictionary<string, XElement>();

          // add member elements across all XML docs to the look-up table. We want <member> elements
          // that have a 'name' attribute but don't contain an <inheritdoc> child element.
          foreach(var doc in XmlDocs) {
            var members = doc.XPathSelectElements("/doc/members/member[@name and not(inheritdoc)]");

            foreach(var m in members) TargetMemberElements.Add(m.Attribute("name")!.Value, m);
          }

          // for each <member> element that has an <inheritdoc> child element which references another
          // <member> element, replace the <inheritdoc> element with the nodes of the referenced <member>
          // element (effectively this 'dereferences the pointer' which is something Swagger doesn't support).
          foreach(var doc in XmlDocs) {
            var PointerMembers = doc.XPathSelectElements("/doc/members/member[inheritdoc[@cref]]");

            foreach(var PointerMember in PointerMembers) {
              var PointerElement = PointerMember.Element("inheritdoc");
              var TargetMemberName = PointerElement!.Attribute("cref")!.Value;

              if(TargetMemberElements.TryGetValue(TargetMemberName, out var TargetMember))
                PointerElement.ReplaceWith(TargetMember.Nodes());
            }
          }

          // replace all <see> elements with the unqualified member name that they point to (Swagger uses the
          // fully qualified name which makes no sense because the relevant classes and namespaces are not useful
          // when calling an API over HTTP).
          foreach(var doc in XmlDocs) {
            foreach(var SeeElement in doc.XPathSelectElements("//see[@cref]")) {
              var TargetMemberName = SeeElement.Attribute("cref")!.Value;
              var ShortMemberName = TargetMemberName.Substring(TargetMemberName.LastIndexOf('.') + 1);

              if(TargetMemberName.StartsWith("M:")) ShortMemberName += "()";

              SeeElement.ReplaceWith(ShortMemberName);
            }
          }

          // add pre-processed XML docs to Swagger.
          foreach(var doc in XmlDocs)
            ArgOptions.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
        }

The ArgOptions variable refers to an instance of SwaggerGenOptions which you use to add the XML files.

Was this page helpful?
0 / 5 - 0 ratings