Swashbuckle.WebApi icon indicating copy to clipboard operation
Swashbuckle.WebApi copied to clipboard

Swashbuckle doesn't work properly with inheritdoc

Open iamkarlson opened this issue 7 years ago • 21 comments

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.

iamkarlson avatar Feb 05 '17 07:02 iamkarlson

yeah, nothing changed!

RoCore avatar Feb 27 '18 10:02 RoCore

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.

Bidthedog avatar Apr 25 '18 15:04 Bidthedog

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

spelltwister avatar Jan 31 '19 18:01 spelltwister

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>

Kikimora avatar Mar 13 '19 02:03 Kikimora

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 avatar May 06 '19 14:05 anhaehne

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

anyone managed to get Swagger to load inheritdoc ?

namtab00 avatar May 10 '19 19:05 namtab00

  1. dotnet tool -g install InheritDoc

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

dotnet tool install -g InheritDocTool

jeffman avatar Jul 19 '19 15:07 jeffman

@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

hf-kklein avatar Apr 27 '20 06:04 hf-kklein

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 avatar Mar 25 '21 17:03 julealgon

@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.

ericmutta avatar Apr 17 '21 12:04 ericmutta

pretty easily

People say this to anything even if it's 56 lines of code..

Arclight3 avatar Apr 05 '22 20:04 Arclight3

@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.

Unfortunately, this assumes that <inheritdoc cref=""/> is used rather than a simple <inheritdoc/>, which the latter implicitly takes the documentation from the child element - be it from an interface, or from a base class. Any workarounds?

kikaragyozov avatar Apr 28 '22 13:04 kikaragyozov

@SpiritBob: Unfortunately, this assumes that is used rather than a simple , which the latter implicitly takes the documentation from the child element - be it from an interface, or from a base class. Any workarounds?

You would have to "dereference the pointer" similar to the example I showed, but with some modifications to locate the <member> element containing the docs from the base class and copy those XML elements to the <member> element for the child class. Example: the base class XML docs may look like this:

        <member name="M:MyNameSpace.MyBaseClass.MyMethodFoo">
            <summary>
            Does blah blah.
            </summary>
        </member>

The child class will then use <inheritdoc> and look something like this:

        <member name="M:MyNameSpace.MyChildClass.MyMethodFoo">
            <inheritdoc/>
        </member>

The task is then to (1) use XPath to find all <member> elements that contain an <inheritdoc/> element, (2) get the value from the name attribute (which is M:MyNameSpace.MyChildClass.MyMethodFoo in the above example), (3) map that value to the name in the base class (which is M:MyNameSpace.MyBaseClass.MyMethodFoo in the above example), (4) use the mapped name and XPath to locate the <member> element containing docs for the base class, (5) copy XML nodes from the base class docs and replace them into the <member> element for the child class which originally contained the <inheritdoc/> element.

The tricky part is mapping from M:MyNameSpace.MyChildClass.MyMethodFoo to M:MyNameSpace.MyBaseClass.MyMethodFoo in order to locate the base class docs. The rest (using XPath, replacing XML nodes, etc) is shown in the code I gave earlier.

Hopefully that points you in the right direction!

ericmutta avatar Apr 28 '22 19:04 ericmutta

@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.

This does work, but all xmldoc references (see / seealso) get stripped, I presume because they are brackets...

Does anyone know a way to force SwaggerDocGen to render them, at least as pure text, if not resolved as fully qualified references ?

namtab00 avatar Oct 14 '22 13:10 namtab00

InheritDocSchemaFilter

This solution is probably outdated for the current Swagger UI generator version. I cannot seem to find the namespaces for XmlCommentsMemberNameHelper and XmlCommentsTextHelper.Humanize returns string instead of Microsoft.OpenApi.Any.IOpenApiAny type

and

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>

This seemingly does nothing for me (even when I installed .NET Core 3.1 which it requires to run and has shown a warning in build output).

janseris avatar Dec 04 '22 17:12 janseris

@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.

This has no effect other than it changes the ordering of the controllers (endpoint groups) displayed in the Swagger UI.

janseris avatar Dec 04 '22 17:12 janseris

Here's my version of the workaround by @anhaehne, working for me with Swashbuckle.AspNetCore 6.5.0 as of today, ymmv:

public class SwaggerXmlSchemaFilter : ISchemaFilter
{
    private const string SUMMARY_TAG = "summary";
    private const string EXAMPLE_TAG = "example";
    private readonly List<XPathDocument> _documents;
    private readonly Dictionary<string, string> _inheritedDocs;

    public SwaggerXmlSchemaFilter(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);
    } /

    private static string GetMemberNameForType(Type type)
        => $"T:{type.FullName}";

    private static string GetMemberNameForMember(MemberInfo member)
        => $"{(member is PropertyInfo ? "P" : "F")}:{(member.DeclaringType ?? member.ReflectedType).FullName}.{member.Name}";

    /// <inheritdoc />
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        var memberName = GetMemberNameForType(context.Type);
        var sources = GetPossibleSources(context.Type);

        if (string.IsNullOrEmpty(schema.Description) && _inheritedDocs.TryGetValue(memberName, out var cref))
        {
            // use explicit source if provided (by <inheritdoc cref="source" />)
            if (!string.IsNullOrEmpty(cref))
            {
                var crefTarget = sources.SingleOrDefault(t => GetMemberNameForType(t) == cref);
                if (crefTarget != null)
                    sources = new List<Type> { crefTarget };
            }

            foreach (var source in sources)
            {
                var sourceXmlNode = GetMemberXmlNode(GetMemberNameForType(source));
                var summaryNode = sourceXmlNode?.SelectSingleNode(SUMMARY_TAG);

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

        if (schema.Properties == null)
            return;

        foreach (var entry in schema.Properties)
        {
            var propertyName = entry.Key.ToTitleCase();
            var property = context.Type.GetProperty(propertyName);
            if (property != null)
            {
                var propertySchema = entry.Value;
                var propertyMemberName = GetMemberNameForMember(property);
                if (string.IsNullOrEmpty(propertySchema.Description) && _inheritedDocs.TryGetValue(propertyMemberName, out cref))
                {
                    foreach (var source in sources)
                    {
                        var sourceProperty = source.GetProperty(propertyName);
                        if (sourceProperty != null)
                        {
                            var sourceXmlNode = GetMemberXmlNode(GetMemberNameForMember(sourceProperty));
                            var summaryNode = sourceXmlNode?.SelectSingleNode(SUMMARY_TAG);

                            if (summaryNode != null)
                            {
                                propertySchema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    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();

        return targets;
    }

    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;
    }
}

fverhoef avatar Mar 21 '23 10:03 fverhoef

I created a working fix, based on the version from @fverhoef: https://gist.github.com/drasive/872fdf9f23fe37471b66fad2ee80bb71 Tested with Swashbuckle.AspNetCore v6.5.0 on .NET 7.0.

I have made the following changes:

  • Fix: Example tag is inherited as well
  • Fix: Properties can be found regardless of name capitalization
  • Fix: It compiles
  • Refactoring: Code cleanup

drasive avatar Aug 08 '23 21:08 drasive

hi you can use SauceControl.InheritDoc

mdrezak avatar Oct 08 '23 16:10 mdrezak

I created a working fix, based on the version from @fverhoef: https://gist.github.com/drasive/872fdf9f23fe37471b66fad2ee80bb71 Tested with Swashbuckle.AspNetCore v6.5.0 on .NET 7.0.

Sadly, this did not work for me with .NET 8.0.

Mark-Good avatar Dec 18 '23 19:12 Mark-Good

Any update on this ?

@domaindrivendev

Reza-Noei avatar Mar 13 '24 08:03 Reza-Noei