79144975

Date: 2024-10-31 13:49:27
Score: 1.5
Natty:
Report link

I've created my own version of this, which does not rely on external dependencies (like HtmlAgilityPack). This also removes the unsupported <see cref="..." /> but keeps the value inside of it. I don't know which is faster (and/or better) performance-wise though.

public static class XmlCleaner
{
    public static string RemoveUnsupportedCref(string xml)
    {
        //<see cref="P:abc" />
        //<see cref="T:abc" />
        //<see cref="F:abc" />
        //etc.
    
        //Filter only on valid xml input
        if (string.IsNullOrEmpty(xml) || xml.Contains('<') == false) { return xml; }
    
        //Explanation: creates three groups.
        //group 1: all text in front of '<see cref="<<randomAlphabeticCharacterGoesHere>>:'
        //group 2: all text after the match of '<see cref="<<randomAlphabeticCharacterGoesHere>>:' UNTIL there is a match with '" />'
        //group 3: all text after '" />'
        //Then, merges group1, group2 and group3 together. This effectively removes '<see cref="X: " />' but keeps the value in between the " and ".
        xml = Regex.Replace(xml, "(.*)<see cref=\"[A-Za-z]:(.*)\" \\/>(.*)", "$1$2$3");
    
        return xml;
    }
}

That is basically it. However, I have more things I want from Swagger (like enums as strings, external .xml files with comments in them from dependencies that I want to have included, etc), so there is some additional code. Perhaps some of you might find this helpful.

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc(
        "MyAppAPIDocumentation",
        new OpenApiInfo() { Title = "MyApp API", Version = "1" });

    //Prevent schemas with the same type name (duplicates) from crashing Swagger
    options.CustomSchemaIds(type => type.ToString());

    //Will sort the schemas and their parameters alphabetically
    options.DocumentFilter<SwaggerHelper.DocumentSorter>();

    //Will show enums as strings
    options.SchemaFilter<SwaggerHelper.ShowEnumsAsStrings>();

    var dir = new DirectoryInfo(AppContext.BaseDirectory);
    foreach (var fi in dir.EnumerateFiles("*.xml"))
    {
        var doc = XDocument.Load(fi.FullName);
        
        //Removes unsupported <see cref=""/> statements
        var xml = SwaggerHelper.XmlCleaner.RemoveUnsupportedCref(doc.ToString());
        doc = XDocument.Parse(xml);
        
        options.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
        //Adds associated Xml information to each enum
        options.SchemaFilter<SwaggerHelper.XmlCleaner.RemoveUnsupportedCref>(doc);
    }
}

//Adds support for converting strings to enums
builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

The SwaggerHelper class is as follows:

public static class SwaggerHelper
{
    /*
     * FROM: https://stackoverflow.com/questions/61507662/sorting-the-schemas-portion-of-a-swagger-page-using-swashbuckle/62639027#62639027
     */
    /// <summary>
    /// Sorts the schemas and associated Xml documentation files.
    /// </summary>
    public class SortSchemas : IDocumentFilter
    {
        // Implements IDocumentFilter.Apply().
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            if (swaggerDoc == null) { return; }

            //Re-order the schemas alphabetically
            swaggerDoc.Components.Schemas = swaggerDoc.Components.Schemas
                .OrderBy(kvp => kvp.Key, StringComparer.InvariantCulture)
                .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

            //Re-order the properties per schema alphabetically
            foreach (var schema in swaggerDoc.Components.Schemas)
            {
                schema.Value.Properties = schema.Value.Properties
                    .OrderBy(kvp => kvp.Key, StringComparer.InvariantCulture)
                    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
            }
        }
    }

    /*
     * FROM: https://stackoverflow.com/questions/36452468/swagger-ui-web-api-documentation-present-enums-as-strings/61906056#61906056
     */
    /// <summary>
    /// Shows enums as strings in the generated Swagger output.
    /// </summary>
    public class ShowEnumsAsStrings : ISchemaFilter
    {
        public void Apply(OpenApiSchema model, SchemaFilterContext context)
        {
            if (context.Type.IsEnum)
            {
                model.Enum.Clear();
                Enum.GetNames(context.Type)
                    .ToList()
                    .ForEach(n =>
                    {
                        model.Enum.Add(new OpenApiString(n));
                        model.Type = "string";
                        model.Format = null;
                    });
            }
        }
    }

    /*
     * FROM: https://stackoverflow.com/questions/53282170/swaggerui-not-display-enum-summary-description-c-sharp-net-core/69089035#69089035
     */
    /// <summary>
    /// Swagger schema filter to modify description of enum types so they
    /// show the Xml documentation attached to each member of the enum.
    /// </summary>
    public class AddXmlCommentsToEnums : ISchemaFilter
    {
        private readonly XDocument xmlComments;
        private readonly string assemblyName;

        /// <summary>
        /// Initialize schema filter.
        /// </summary>
        /// <param name="xmlComments">Document containing XML docs for enum members.</param>
        public AddXmlCommentsToEnums(XDocument xmlComments)
        {
            this.xmlComments = xmlComments;
            this.assemblyName = DetermineAssembly(xmlComments);
        }

        /// <summary>
        /// Pre-amble to use before the enum items
        /// </summary>
        public static string Prefix { get; set; } = "<p>Possible values:</p>";

        /// <summary>
        /// Format to use, 0 : value, 1: Name, 2: Description
        /// </summary>
        public static string Format { get; set; } = "<b>{0} - {1}</b>: {2}";

        /// <summary>
        /// Apply this schema filter.
        /// </summary>
        /// <param name="schema">Target schema object.</param>
        /// <param name="context">Schema filter context.</param>
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            var type = context.Type;

            // Only process enums and...
            if (!type.IsEnum)
            {
                return;
            }

            // ...only the comments defined in their origin assembly
            if (type.Assembly.GetName().Name != assemblyName)
            {
                return;
            }
            var sb = new StringBuilder(schema.Description);

            if (!string.IsNullOrEmpty(Prefix))
            {
                sb.AppendLine(Prefix);
            }

            sb.AppendLine("<ul>");

            // TODO: Handle flags better e.g. Hex formatting
            foreach (var name in Enum.GetValues(type))
            {
                // Allows for large enums
                var value = Convert.ToInt64(name);
                var fullName = $"F:{type.FullName}.{name}";

                var description = xmlComments.XPathEvaluate(
                    $"normalize-space(//member[@name = '{fullName}']/summary/text())"
                ) as string;

                sb.AppendLine(string.Format("<li>" + Format + "</li>", value, name, description));
            }

            sb.AppendLine("</ul>");

            schema.Description = sb.ToString();
        }

        private string DetermineAssembly(XDocument doc)
        {
            var name = ((IEnumerable)doc.XPathEvaluate("/doc/assembly")).Cast<XElement>().ToList().FirstOrDefault();

            return name?.Value;
        }
    }

    /// <summary>
    /// Cleans Xml documentation files.
    /// </summary>
    public static class XmlCleaner
    {
        public static string RemoveUnsupportedCref(string xml)
        {
            //<see cref="P:abc" />
            //<see cref="T:abc" />
            //<see cref="F:abc" />
            //etc.

            //Filter only on valid xml input
            if (string.IsNullOrEmpty(xml) || xml.Contains('<') == false) { return xml; }

            //Explanation: creates three groups.
            //group 1: all text in front of '<see cref="<<randomAlphabeticCharacterGoesHere>>:'
            //group 2: all text after the match of '<see cref="<<randomAlphabeticCharacterGoesHere>>:' UNTIL there is a match with '" />'
            //group 3: all text after '" />'
            //Then, merges group1, group2 and group3 together. This effectively removes '<see cref="X: " />' but keeps the value in between the " and ".
            xml = Regex.Replace(xml, "(.*)<see cref=\"[A-Za-z]:(.*)\" \\/>(.*)", "$1$2$3");

            return xml;
        }
    }
}
Reasons:
  • Blacklisted phrase (1): stackoverflow
  • RegEx Blacklisted phrase (1): I want
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Andre_IRIS