-
Notifications
You must be signed in to change notification settings - Fork 677
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
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
<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 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 ? |
Thanks! I had to run a slightly different command on my end:
|
@anhaehne : The class you provided does not compile for me. For example because the name of the class |
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 |
@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 |
People say this to anything even if it's 56 lines of code.. |
Unfortunately, this assumes that |
You would have to "dereference the pointer" similar to the example I showed, but with some modifications to locate the <member name="M:MyNameSpace.MyBaseClass.MyMethodFoo">
<summary>
Does blah blah.
</summary>
</member> The child class will then use <member name="M:MyNameSpace.MyChildClass.MyMethodFoo">
<inheritdoc/>
</member> The task is then to (1) use XPath to find all 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! |
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 ? |
This solution is probably outdated for the current Swagger UI generator version. and
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). |
This has no effect other than it changes the ordering of the controllers (endpoint groups) displayed in the Swagger UI. |
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;
}
} |
I created a working fix, based on the version from @fverhoef: https://gist.github.com/drasive/872fdf9f23fe37471b66fad2ee80bb71 I have made the following changes:
|
hi you can use SauceControl.InheritDoc |
Sadly, this did not work for me with .NET 8.0. |
Any update on this ? |
Anyone? 👀 |
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.
The text was updated successfully, but these errors were encountered: