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