From fcc049b92a22f585ab0d8ce3b56ada4dc18c6dcd Mon Sep 17 00:00:00 2001 From: Chebotov Nikolay Date: Thu, 15 Oct 2020 13:58:42 +0300 Subject: [PATCH] Fix some bugs in `IncludeXmlCommentsWithRemarks` option; Add `IncludeXmlCommentsFromInheritDocs` option to add xml comments from inheritdocs (from summary and remarks) into the swagger documentation. --- CHANGELOG.md | 5 + README.md | 104 +++++++ Unchase.Swashbuckle.AspNetCore.Extensions.sln | 1 + appveyor.yml | 2 +- assets/addXmlCommentsFromInheritdoc.png | Bin 0 -> 20884 bytes .../Extensions/SwaggerGenOptionsExtensions.cs | 20 +- .../Filters/InheritDocSchemaFilter.cs | 259 ++++++++++++++++++ .../XmlCommentsWithRemarksDocumentFilter.cs | 5 +- .../XmlCommentsWithRemarksParameterFilter.cs | 5 +- ...XmlCommentsWithRemarksRequestBodyFilter.cs | 5 +- .../XmlCommentsWithRemarksSchemaFilter.cs | 8 +- ...e.Swashbuckle.AspNetCore.Extensions.csproj | 6 +- ...hase.Swashbuckle.AspNetCore.Extensions.xml | 31 +++ .../Controllers/TodoController.cs | 29 +- .../Models/IInheritDocClass.cs | 19 ++ .../Models/IInheritDocCommon.cs | 24 ++ .../Models/InheritDocClass.cs | 15 + .../Models/InheritEnum.cs | 19 ++ test/WebApi3.1-Swashbuckle/Startup.cs | 9 +- .../WebApi3.1-Swashbuckle.xml | 75 +++++ 20 files changed, 616 insertions(+), 25 deletions(-) create mode 100644 assets/addXmlCommentsFromInheritdoc.png create mode 100644 src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/InheritDocSchemaFilter.cs create mode 100644 test/WebApi3.1-Swashbuckle/Models/IInheritDocClass.cs create mode 100644 test/WebApi3.1-Swashbuckle/Models/IInheritDocCommon.cs create mode 100644 test/WebApi3.1-Swashbuckle/Models/InheritDocClass.cs create mode 100644 test/WebApi3.1-Swashbuckle/Models/InheritEnum.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5ec97..89f7cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ These are the changes to each version that has been released on the [nuget](https://www.nuget.org/packages/Unchase.Swashbuckle.AspNetCore.Extensions/). +## v2.5.0 `2020-10-15` + +- [x] Fix some bugs in `IncludeXmlCommentsWithRemarks` option +- [x] Add `IncludeXmlCommentsFromInheritDocs` option to add xml comments from inheritdocs (from summary and remarks) into the swagger documentation + ## v2.4.1 `2020-10-13` - [x] Add `params Type[]` parameters to `IncludeXmlCommentsWithRemarks` option to exclude remarks for concrete types diff --git a/README.md b/README.md index 14567cf..1670ce3 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ public void ConfigureServices(IServiceCollection services) // use it if you want to hide Paths and Definitions from OpenApi documentation correctly options.UseAllOfToExtendReferenceSchemas(); + // if you want to add xml comments from inheritdocs (from summary and remarks) into the swagger documentation, add: + // you can exclude remarks for concrete types + options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true, excludedTypes: typeof(string)); + // if you want to add xml comments from summary and remarks into the swagger documentation, first of all add: // you can exclude remarks for concrete types var xmlFilePath = Path.Combine(AppContext.BaseDirectory, "WebApi3.1-Swashbuckle.xml"); @@ -352,6 +356,29 @@ public void ConfigureServices(IServiceCollection services) } ``` +7. **Add xml comments from <inheritdoc/> (from summary and remarks) into the swagger documentation**: + +- Since [v2.5.0](https://github.com/unchase/Unchase.Swashbuckle.AspNetCore.Extensions/releases/tag/v2.5.0) in the _ConfigureServices_ method of _Startup.cs_, inside your `AddSwaggerGen` call, add `IncludeXmlCommentsFromInheritDocs` option: + +```csharp +// This method gets called by the runtime. Use this method to add services to the container. +public void ConfigureServices(IServiceCollection services) +{ + ... + + services.AddSwaggerGen(options => + { + ... + + // add xml comments from inheritdocs (from summary and remarks) into the swagger documentation, add: + // with excluding concrete types + options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true, excludedTypes: typeof(string)); + + ... + }); +} +``` + ## Builds status |Status|Value| @@ -600,6 +627,83 @@ public enum SecondInnerEnum } ``` +### Add xml comments from <inheritdoc/> (from summary and remarks) into the swagger documentation + +![Add xml comments from ingeritdoc](assets/addXmlCommentsFromInheritdoc.png) + +For code: + +```csharp +/// +public class InheritDocClass : IInheritDocClass +{ + /// + public string Name { get; set; } + + /// + public string Common { get; set; } + + /// + public InheritEnum InheritEnum { get; set; } +} + +/// +/// InheritDocClass - inheritdoc +/// +/// +/// InheritDocClass remarks - inheritdoc +/// +public interface IInheritDocClass : IInheritDocCommon +{ + /// + /// Name - inheritdoc + /// + /// + /// Name remarks - inheritdoc + /// + public string Name { get; set; } +} + +/// +/// IInheritDocCommon interface +/// +/// +/// IInheritDocCommon interface remarks +/// +public interface IInheritDocCommon +{ + /// + /// Common - inheritdoc (inner) + /// + /// + /// Common remarks - inheritdoc (inner) + /// + public string Common { get; set; } + + /// + /// InheritEnum - inheritdoc (inner) + /// + public InheritEnum InheritEnum { get; set; } +} + +/// +/// Inherit enum - enum +/// +/// +/// Inherit enum remarks - enum +/// +public enum InheritEnum +{ + /// + /// Inherit enum Value + /// + /// + /// Inherit enum Value remarks + /// + Value = 0 +} +``` + ## HowTos - [ ] Add HowTos in a future diff --git a/Unchase.Swashbuckle.AspNetCore.Extensions.sln b/Unchase.Swashbuckle.AspNetCore.Extensions.sln index 151f138..0112527 100644 --- a/Unchase.Swashbuckle.AspNetCore.Extensions.sln +++ b/Unchase.Swashbuckle.AspNetCore.Extensions.sln @@ -10,6 +10,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{145A03BF-E60E-41F6-BD89-457F940F6C9B}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore + assets\addXmlCommentsFromInheritdoc.png = assets\addXmlCommentsFromInheritdoc.png assets\addXmlCommentsFromSummaryAndRemarks.png = assets\addXmlCommentsFromSummaryAndRemarks.png assets\appendActionCountIntoSwaggerTag.png = assets\appendActionCountIntoSwaggerTag.png appveyor.yml = appveyor.yml diff --git a/appveyor.yml b/appveyor.yml index 53ac2d4..c784d07 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.4.{build} +version: 2.5.{build} pull_requests: do_not_increment_build_number: true skip_tags: true diff --git a/assets/addXmlCommentsFromInheritdoc.png b/assets/addXmlCommentsFromInheritdoc.png new file mode 100644 index 0000000000000000000000000000000000000000..a723df3fe24874d8f0a27274f87c0b990e7731ba GIT binary patch literal 20884 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, distinctExcludedTypes); @@ -137,6 +137,24 @@ public static SwaggerGenOptions IncludeXmlCommentsWithRemarks( return swaggerGenOptions.IncludeXmlCommentsWithRemarks(() => new XPathDocument(filePath), includeControllerXmlComments, excludedTypes); } + /// + /// Inject human-friendly descriptions for Schemas and it's Parameters based on <inheritdoc/> XML Comments (from summary and remarks). + /// + /// . + /// + /// Flag to indicate to include remarks from XML comments. + /// + /// Types for which remarks will be excluded. + public static SwaggerGenOptions IncludeXmlCommentsFromInheritDocs( + this SwaggerGenOptions swaggerGenOptions, + bool includeRemarks = false, + params Type[] excludedTypes) + { + var distinctExcludedTypes = excludedTypes?.Distinct().ToArray(); + swaggerGenOptions.SchemaFilter(swaggerGenOptions, includeRemarks, distinctExcludedTypes); + return swaggerGenOptions; + } + #endregion } } \ No newline at end of file diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/InheritDocSchemaFilter.cs b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/InheritDocSchemaFilter.cs new file mode 100644 index 0000000..92267a4 --- /dev/null +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/InheritDocSchemaFilter.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.XPath; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters +{ + /// + /// Adds documentation that is provided by the <inhertidoc /> tag. + /// + /// + internal class InheritDocSchemaFilter : ISchemaFilter + { + #region Fields + + private const string SummaryTag = "summary"; + private const string RemarksTag = "remarks"; + private const string ExampleTag = "example"; + private readonly bool _includeRemarks; + private readonly List _documents; + private readonly Dictionary _inheritedDocs; + private readonly Type[] _excludedTypes; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// . + /// Include remarks from inheritdoc XML comments. + /// Excluded types. + public InheritDocSchemaFilter(SwaggerGenOptions options, bool includeRemarks = false, params Type[] excludedTypes) + { + _includeRemarks = includeRemarks; + _excludedTypes = excludedTypes; + _documents = options.SchemaFilterDescriptors.Where(x => x.Type == typeof(XmlCommentsSchemaFilter)) + .Select(x => x.Arguments.Single()) + .Cast() + .ToList(); + + _inheritedDocs = _documents.SelectMany( + doc => + { + var inheritedElements = new List<(string, string)>(); + foreach (XPathNavigator member in doc.CreateNavigator().Select("doc/members/member/inheritdoc")) + { + var cref = member.GetAttribute("cref", ""); + member.MoveToParent(); + var parentCref = member.GetAttribute("cref", ""); + if (!string.IsNullOrWhiteSpace(parentCref)) + cref = parentCref; + inheritedElements.Add((member.GetAttribute("name", ""), cref)); + } + + return inheritedElements; + }) + .ToDictionary(x => x.Item1, x => x.Item2); + } + + #endregion + + #region Methods + + /// + /// Apply filter. + /// + /// . + /// . + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (_excludedTypes.Any() && _excludedTypes.ToList().Contains(context.Type)) + { + return; + } + + // Try to apply a description for inherited types. + var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(context.Type); + if (string.IsNullOrEmpty(schema.Description) && _inheritedDocs.ContainsKey(memberName)) + { + var cref = _inheritedDocs[memberName]; + var target = GetTargetRecursive(context.Type, cref); + + var targetXmlNode = GetMemberXmlNode(XmlCommentsNodeNameHelper.GetMemberNameForType(target)); + var summaryNode = targetXmlNode?.SelectSingleNode(SummaryTag); + + if (summaryNode != null) + { + schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); + + if (_includeRemarks) + { + var remarksNode = targetXmlNode.SelectSingleNode(RemarksTag); + if (remarksNode != null && !string.IsNullOrWhiteSpace(remarksNode.InnerXml)) + { + schema.Description += $" ({XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)})"; + } + } + } + } + + if (schema.Properties == null) + return; + + // Add the summary and examples for the properties. + foreach (var entry in schema.Properties) + { + var memberInfo = ((TypeInfo) context.Type).DeclaredMembers?.FirstOrDefault(p => p.Name.Equals(entry.Key, StringComparison.OrdinalIgnoreCase)); + if (memberInfo != null) + { + ApplyPropertyComments(entry.Value, memberInfo); + } + } + } + + 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 => XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(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 => XmlCommentsNodeNameHelper.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(OpenApiSchema propertySchema, MemberInfo memberInfo) + { + var memberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(memberInfo); + + if (!_inheritedDocs.ContainsKey(memberName)) + return; + + if (_excludedTypes.Any() && _excludedTypes.ToList() + .Contains(((PropertyInfo)memberInfo).PropertyType)) + { + return; + } + + var cref = _inheritedDocs[memberName]; + var target = GetTargetRecursive(memberInfo, cref); + + var targetXmlNode = GetMemberXmlNode(XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target)); + + if (targetXmlNode == null) + return; + + var summaryNode = targetXmlNode.SelectSingleNode(SummaryTag); + if (summaryNode != null) + { + propertySchema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); + + if (_includeRemarks) + { + var remarksNode = targetXmlNode.SelectSingleNode(RemarksTag); + if (remarksNode != null && !string.IsNullOrWhiteSpace(remarksNode.InnerXml)) + { + propertySchema.Description += $" ({XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)})"; + } + } + } + + var exampleNode = targetXmlNode.SelectSingleNode(ExampleTag); + if (exampleNode != null) + propertySchema.Example = new OpenApiString(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 = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(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 = XmlCommentsNodeNameHelper.GetMemberNameForType(target); + + if (_inheritedDocs.ContainsKey(targetMemberName)) + return GetTarget(target, _inheritedDocs[targetMemberName]); + + return target; + } + + #endregion + } +} diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksDocumentFilter.cs b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksDocumentFilter.cs index 0b24586..4a410ab 100644 --- a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksDocumentFilter.cs +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksDocumentFilter.cs @@ -11,7 +11,7 @@ namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters /// /// Inject human-friendly remarks to descriptions for Document's Tags based on XML Comment files. /// - public class XmlCommentsWithRemarksDocumentFilter : IDocumentFilter + internal class XmlCommentsWithRemarksDocumentFilter : IDocumentFilter { #region Fields @@ -56,8 +56,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) foreach (var nameAndType in controllerNamesAndTypes) { - if (_excludedTypes.ToList().Select(t => t.FullName) - .Contains(nameAndType.Value.FullName)) + if (_excludedTypes.Any() && _excludedTypes.ToList().Contains(nameAndType.Value)) { continue; } diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksParameterFilter.cs b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksParameterFilter.cs index ce2d133..991fa5c 100644 --- a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksParameterFilter.cs +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksParameterFilter.cs @@ -10,7 +10,7 @@ namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters /// /// Inject human-friendly remarks to descriptions for Parameters based on XML Comment files. /// - public class XmlCommentsWithRemarksParameterFilter : IParameterFilter + internal class XmlCommentsWithRemarksParameterFilter : IParameterFilter { #region Fields @@ -53,8 +53,7 @@ public void Apply(OpenApiParameter parameter, ParameterFilterContext context) private void ApplyPropertyTags(OpenApiParameter parameter, PropertyInfo propertyInfo) { - if (propertyInfo.DeclaringType != null && _excludedTypes.ToList().Select(t => t.FullName) - .Contains(propertyInfo.DeclaringType?.FullName)) + if (propertyInfo.DeclaringType != null && _excludedTypes.Any() && _excludedTypes.ToList().Contains(propertyInfo.DeclaringType)) { return; } diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksRequestBodyFilter.cs b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksRequestBodyFilter.cs index 3f4266d..d88a4e3 100644 --- a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksRequestBodyFilter.cs +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksRequestBodyFilter.cs @@ -10,7 +10,7 @@ namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters /// /// Inject human-friendly remarks to descriptions for RequestBodies based on XML Comment files. /// - public class XmlCommentsWithRemarksRequestBodyFilter : IRequestBodyFilter + internal class XmlCommentsWithRemarksRequestBodyFilter : IRequestBodyFilter { #region Fields @@ -59,8 +59,7 @@ public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext conte private void ApplyPropertyTags(OpenApiRequestBody requestBody, PropertyInfo propertyInfo) { - if (propertyInfo.DeclaringType != null && _excludedTypes.ToList().Select(t => t.FullName) - .Contains(propertyInfo.DeclaringType?.FullName)) + if (propertyInfo.DeclaringType != null && _excludedTypes.Any() && _excludedTypes.ToList().Contains(propertyInfo.DeclaringType)) { return; } diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksSchemaFilter.cs b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksSchemaFilter.cs index c66f442..7a5e12a 100644 --- a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksSchemaFilter.cs +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/XmlCommentsWithRemarksSchemaFilter.cs @@ -10,7 +10,7 @@ namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters /// /// Inject human-friendly remarks to descriptions for Schemas based on XML Comment files. /// - public class XmlCommentsWithRemarksSchemaFilter : ISchemaFilter + internal class XmlCommentsWithRemarksSchemaFilter : ISchemaFilter { #region Fields @@ -59,8 +59,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) private void ApplyTypeTags(OpenApiSchema schema, Type type) { - if (_excludedTypes.ToList().Select(t => t.FullName) - .Contains(type.FullName)) + if (_excludedTypes.Any() && _excludedTypes.ToList().Contains(type)) { return; } @@ -80,8 +79,7 @@ private void ApplyTypeTags(OpenApiSchema schema, Type type) private void ApplyFieldOrPropertyTags(OpenApiSchema schema, MemberInfo fieldOrPropertyInfo) { - if (fieldOrPropertyInfo.DeclaringType != null && _excludedTypes.ToList().Select(t => t.FullName) - .Contains(fieldOrPropertyInfo.DeclaringType?.FullName)) + if (fieldOrPropertyInfo.DeclaringType != null && _excludedTypes.Any() && _excludedTypes.ToList().Contains(fieldOrPropertyInfo.DeclaringType)) { return; } diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.csproj b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.csproj index f5ccaeb..24b95df 100644 --- a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.csproj +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.csproj @@ -14,9 +14,9 @@ 7.3 https://github.com/unchase/Unchase.Swashbuckle.AspNetCore.Extensions/blob/master/assets/icon.png?raw=true - 2.4.1 - 2.4.1.0 - 2.4.1.0 + 2.5.0 + 2.5.0.0 + 2.5.0.0 false Unchase.Swashbuckle.AspNetCore.Extensions.xml diff --git a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.xml b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.xml index 69c9947..c42c625 100644 --- a/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.xml +++ b/src/Unchase.Swashbuckle.AspNetCore.Extensions/Unchase.Swashbuckle.AspNetCore.Extensions.xml @@ -127,6 +127,16 @@ Types for which remarks will be excluded. + + + Inject human-friendly descriptions for Schemas and it's Parameters based on <inheritdoc/> XML Comments (from summary and remarks). + + . + + Flag to indicate to include remarks from XML comments. + + Types for which remarks will be excluded. + factory. @@ -315,6 +325,27 @@ . . + + + Adds documentation that is provided by the <inhertidoc /> tag. + + + + + + Initializes a new instance of the class. + + . + Include remarks from inheritdoc XML comments. + Excluded types. + + + + Apply filter. + + . + . + Document filter for ordering tags by name in OpenApi document. diff --git a/test/WebApi3.1-Swashbuckle/Controllers/TodoController.cs b/test/WebApi3.1-Swashbuckle/Controllers/TodoController.cs index e9af970..2cff1f2 100644 --- a/test/WebApi3.1-Swashbuckle/Controllers/TodoController.cs +++ b/test/WebApi3.1-Swashbuckle/Controllers/TodoController.cs @@ -19,7 +19,7 @@ namespace WebApi3._1_Swashbuckle.Controllers [SwaggerTag("Controller for todo")] public class TodoController : ControllerBase { - private static readonly ItodoContext _context = new TodoContext(); + private static readonly ItodoContext Context = new TodoContext(); /// /// Hided action @@ -61,6 +61,27 @@ public ActionResult ComplicatedAction() }); } + /// + /// InheritDoc action + /// + /// + /// InheritDoc action remarks + /// + /// A InheritDoc class. + /// Returns a InheritDoc class. + [HttpGet("inheritdoc")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult InheritDocAction() + { + return Ok(new InheritDocClass + { + Name = "Name", + Common = "Common", + InheritEnum = InheritEnum.Value + }); + } + /// /// Deletes a specific TodoItem /// @@ -72,14 +93,14 @@ public ActionResult ComplicatedAction() [Authorize(Roles = "AcceptedRole")] public IActionResult Delete(long id) { - var todo = _context.Find(id); + var todo = Context.Find(id); if (todo == null) { return NotFound(); } - _context.Remove(todo); + Context.Remove(todo); return NoContent(); } @@ -108,7 +129,7 @@ public IActionResult Delete(long id) [ProducesResponseType(StatusCodes.Status400BadRequest)] public ActionResult Create(TodoItem item) { - _context.Add(item); + Context.Add(item); return new CreatedResult(string.Empty, item); } diff --git a/test/WebApi3.1-Swashbuckle/Models/IInheritDocClass.cs b/test/WebApi3.1-Swashbuckle/Models/IInheritDocClass.cs new file mode 100644 index 0000000..1f97427 --- /dev/null +++ b/test/WebApi3.1-Swashbuckle/Models/IInheritDocClass.cs @@ -0,0 +1,19 @@ +namespace WebApi3._1_Swashbuckle.Models +{ + /// + /// InheritDocClass - inheritdoc + /// + /// + /// InheritDocClass remarks - inheritdoc + /// + public interface IInheritDocClass : IInheritDocCommon + { + /// + /// Name - inheritdoc + /// + /// + /// Name remarks - inheritdoc + /// + public string Name { get; set; } + } +} diff --git a/test/WebApi3.1-Swashbuckle/Models/IInheritDocCommon.cs b/test/WebApi3.1-Swashbuckle/Models/IInheritDocCommon.cs new file mode 100644 index 0000000..e349d5b --- /dev/null +++ b/test/WebApi3.1-Swashbuckle/Models/IInheritDocCommon.cs @@ -0,0 +1,24 @@ +namespace WebApi3._1_Swashbuckle.Models +{ + /// + /// IInheritDocCommon interface + /// + /// + /// IInheritDocCommon interface remarks + /// + public interface IInheritDocCommon + { + /// + /// Common - inheritdoc (inner) + /// + /// + /// Common remarks - inheritdoc (inner) + /// + public string Common { get; set; } + + /// + /// InheritEnum - inheritdoc (inner) + /// + public InheritEnum InheritEnum { get; set; } + } +} diff --git a/test/WebApi3.1-Swashbuckle/Models/InheritDocClass.cs b/test/WebApi3.1-Swashbuckle/Models/InheritDocClass.cs new file mode 100644 index 0000000..6ad59a1 --- /dev/null +++ b/test/WebApi3.1-Swashbuckle/Models/InheritDocClass.cs @@ -0,0 +1,15 @@ +namespace WebApi3._1_Swashbuckle.Models +{ + /// + public class InheritDocClass : IInheritDocClass + { + /// + public string Name { get; set; } + + /// + public string Common { get; set; } + + /// + public InheritEnum InheritEnum { get; set; } + } +} diff --git a/test/WebApi3.1-Swashbuckle/Models/InheritEnum.cs b/test/WebApi3.1-Swashbuckle/Models/InheritEnum.cs new file mode 100644 index 0000000..39633df --- /dev/null +++ b/test/WebApi3.1-Swashbuckle/Models/InheritEnum.cs @@ -0,0 +1,19 @@ +namespace WebApi3._1_Swashbuckle.Models +{ + /// + /// Inherit enum - enum + /// + /// + /// Inherit enum remarks - enum + /// + public enum InheritEnum + { + /// + /// Inherit enum Value + /// + /// + /// Inherit enum Value remarks + /// + Value = 0 + } +} diff --git a/test/WebApi3.1-Swashbuckle/Startup.cs b/test/WebApi3.1-Swashbuckle/Startup.cs index 5940d2e..6b3671c 100644 --- a/test/WebApi3.1-Swashbuckle/Startup.cs +++ b/test/WebApi3.1-Swashbuckle/Startup.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Runtime.InteropServices; using System.Text.Json.Serialization; using Unchase.Swashbuckle.AspNetCore.Extensions.Extensions; using Unchase.Swashbuckle.AspNetCore.Extensions.Filters; @@ -34,18 +35,22 @@ public void ConfigureServices(IServiceCollection services) // use it if you want to hide Paths and Definitions from OpenApi documentation correctly options.UseAllOfToExtendReferenceSchemas(); - #region AddEnumsWithValuesFixFilters + // if you want to add xml comments from inheritdocs (from summary and remarks) into the swagger documentation, add: + // you can exclude remarks for concrete types + options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true, excludedTypes: typeof(string)); // if you want to add xml comments from summary and remarks into the swagger documentation, first of all add: // you can exclude remarks for concrete types var xmlFilePath = Path.Combine(AppContext.BaseDirectory, "WebApi3.1-Swashbuckle.xml"); - options.IncludeXmlCommentsWithRemarks(xmlFilePath, false, + options.IncludeXmlCommentsWithRemarks(filePath: xmlFilePath, includeControllerXmlComments: false, typeof(ComplicatedClass), typeof(InnerEnum)); // or add without remarks //options.IncludeXmlComments(xmlFilePath); + #region AddEnumsWithValuesFixFilters + // Add filters to fix enums // use by default: options.AddEnumsWithValuesFixFilters(); diff --git a/test/WebApi3.1-Swashbuckle/WebApi3.1-Swashbuckle.xml b/test/WebApi3.1-Swashbuckle/WebApi3.1-Swashbuckle.xml index 806df06..cd5726e 100644 --- a/test/WebApi3.1-Swashbuckle/WebApi3.1-Swashbuckle.xml +++ b/test/WebApi3.1-Swashbuckle/WebApi3.1-Swashbuckle.xml @@ -84,6 +84,16 @@ A complicated class. Returns a complicated class. + + + InheritDoc action + + + InheritDoc action remarks + + A InheritDoc class. + Returns a InheritDoc class. + Deletes a specific TodoItem @@ -210,6 +220,71 @@ Second inner enum value remarks + + + InheritDocClass - inheritdoc + + + InheritDocClass remarks - inheritdoc + + + + + Name - inheritdoc + + + Name remarks - inheritdoc + + + + + IInheritDocCommon interface + + + IInheritDocCommon interface remarks + + + + + Common - inheritdoc (inner) + + + Common remarks - inheritdoc (inner) + + + + + InheritEnum - inheritdoc (inner) + + + + + + + + + + + + + + + + + Inherit enum - enum + + + Inherit enum remarks - enum + + + + + Inherit enum Value + + + Inherit enum Value remarks + + Tag for TodoItem