Search code examples
swashbuckle.aspnetcore

Include xml remarks in SwaggerUI Asp.Net Core API


Remarks are rendered for action results. However, only summaries are rendered for class definitions and parameters. Is it possible to include the XML remarks in the UI? Given the following class:

    public class Customer
    { 
        /// <summary>
        /// This is the ID
        /// </summary>
        public int CustomerId { get; set; }

        /// <summary>
        /// This is the name
        /// </summary>
        /// <remarks>Remarks are not rendered anywhere</remarks>
        public string Name { get; set; }
    }

When rendering schemas, the remarks are not rendered enter image description here

The same applies for the Parameters section given the following class:

    public class QueryOptions
    {
        /// <summary>
        /// The name to query.
        /// </summary>
        /// <remarks>
        /// Remarks are not rendered
        /// </remarks>
        public string? Name { get; set; }

        /// <summary>
        /// The ID to query
        /// </summary>
        /// <remarks>
        /// Remarks are not rendered
        /// </remarks>
        public int? Id { get; set; }

    }

enter image description here

Is it possible to include remarks using document or schema filters to obtain remarks and append them to the description? I'm using version 6.5.0 of Swashbuckle.AspNetCore


Solution

  • My solution was to copy and modify the source XmlComment[...]Filter.cs files. In looking at the source code for IncludeXmlComments(), I noticed that all it did was register the filters.

    public static void IncludeXmlComments(
                this SwaggerGenOptions swaggerGenOptions,
                Func<XPathDocument> xmlDocFactory,
                bool includeControllerXmlComments = false)
            {
                var xmlDoc = xmlDocFactory();
                swaggerGenOptions.ParameterFilter<XmlCommentsParameterFilter>(xmlDoc);
                swaggerGenOptions.RequestBodyFilter<XmlCommentsRequestBodyFilter>(xmlDoc);
                swaggerGenOptions.OperationFilter<XmlCommentsOperationFilter>(xmlDoc);
                swaggerGenOptions.SchemaFilter<XmlCommentsSchemaFilter>(xmlDoc);
    
                if (includeControllerXmlComments)
                    swaggerGenOptions.DocumentFilter<XmlCommentsDocumentFilter>(xmlDoc);
            }
    

    It was just a matter of me not using the pre-defined filters and just registering my local modified copies instead within Program.cs. I also added support for external XML files as well.

        var apiXmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var apiXmlPath = Path.Combine(AppContext.BaseDirectory, apiXmlFilename);
    
        var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly).ToList();
        foreach (string xmlFilePath in xmlFiles)
        {
            if (string.Compare(apiXmlPath, xmlFilePath, true) == 0)
            {
                // These filters only apply to this application's XML document
                options.DocumentFilter<XmlCommentsDocumentFilter>(xmlFilePath);
                options.OperationFilter<XmlCommentsOperationFilter>(xmlFilePath);
            }
    
            options.ParameterFilter<XmlCommentsParameterFilter>(xmlFilePath);
            options.RequestBodyFilter<XmlCommentsRequestBodyFilter>(xmlFilePath);
            options.SchemaFilter<XmlCommentsSchemaFilter>(xmlFilePath);
        }
    

    As an example, here is my modified schema filter where I append remarks to the summary:

    
        public class XmlCommentsSchemaFilter : ISchemaFilter
        {
            private const string SummaryTag = "summary";
            private const string RemarksTag = "remarks";
            private const string ExampleTag = "example";
            private readonly XPathNavigator _xmlNavigator;
    
            public XmlCommentsSchemaFilter(string xmlFilePath)
            {
                var xmlDoc = new XPathDocument(xmlFilePath);
                _xmlNavigator = xmlDoc.CreateNavigator();
            }
    
            public void Apply(OpenApiSchema schema, SchemaFilterContext context)
            {
                if (!string.IsNullOrWhiteSpace(schema.Description))
                    return;
    
                ApplyTypeTags(schema, context.Type);
    
                if (context.MemberInfo != null)
                {
                    ApplyMemberTags(schema, context);
                }
            }
    
            private void ApplyTypeTags(OpenApiSchema schema, Type type)
            {
                var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type);
                var typeSummaryNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{typeMemberName}']/summary");
    
                if (typeSummaryNode != null)
                {
                    schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml);
                }
            }
    
            private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context)
            {
                var fieldOrPropertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.MemberInfo);
                var fieldOrPropertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{fieldOrPropertyMemberName}']");
    
                if (fieldOrPropertyNode == null) return;
    
                var summaryNode = fieldOrPropertyNode.SelectSingleNode(SummaryTag);
                var remarksNode = fieldOrPropertyNode.SelectSingleNode(RemarksTag);
    
                if (summaryNode != null)
                {
                    var summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
                    var remarks = remarksNode != null ? $"<br/><em>{XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)}</em>" : "";
    
                    schema.Description = $"{summary}{remarks}";
                }
    
                var exampleNode = fieldOrPropertyNode.SelectSingleNode(ExampleTag);
                if (exampleNode != null)
                {
                    var exampleAsJson = (schema.ResolveType(context.SchemaRepository) == "string") && !exampleNode.Value.Equals("null")
                         ? $"\"{exampleNode.ToString()}\""
                         : exampleNode.ToString();
    
                    schema.Example = OpenApiAnyFactory.CreateFromJson(exampleAsJson);
                }
            }
        }
    

    Hopefully someone else finds this helpful.