Skip to content
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

Swashbuckle doesn't work properly with inheritdoc #1000

Open
iamkarlson opened this issue Feb 5, 2017 · 22 comments
Open

Swashbuckle doesn't work properly with inheritdoc #1000

iamkarlson opened this issue Feb 5, 2017 · 22 comments

Comments

@iamkarlson
Copy link

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.

@RoCore
Copy link

RoCore commented Feb 27, 2018

yeah, nothing changed!

@Bidthedog
Copy link

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.

@spelltwister
Copy link

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

@Kikimora
Copy link

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>

@anhaehne
Copy link

anhaehne commented May 6, 2019

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

@namtab00
Copy link

namtab00 commented May 10, 2019

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

anyone managed to get Swagger to load inheritdoc ?

@jeffman
Copy link

jeffman commented Jul 19, 2019

  1. dotnet tool -g install InheritDoc

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

dotnet tool install -g InheritDocTool

@hf-kklein
Copy link

@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

@julealgon
Copy link

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.

@ericmutta
Copy link

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

@Arclight3
Copy link

pretty easily

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

@kikaragyozov
Copy link

kikaragyozov commented Apr 28, 2022

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

@ericmutta
Copy link

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

@namtab00
Copy link

namtab00 commented Oct 14, 2022

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

@janseris
Copy link

janseris commented Dec 4, 2022

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
Copy link

janseris commented Dec 4, 2022

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

@fverhoef
Copy link

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

@drasive
Copy link

drasive commented Aug 8, 2023

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

@mdrezak
Copy link

mdrezak commented Oct 8, 2023

hi you can use SauceControl.InheritDoc

@Mark-Good
Copy link

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.

@Reza-Noei
Copy link

Reza-Noei commented Mar 13, 2024

Any update on this ?

@domaindrivendev

@smetlov
Copy link

smetlov commented Oct 21, 2024

Anyone? 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests