我有一个 ASP.Net Web API,其中包含整个 XML 文档,包括许多
<see />
标签。
我最近在解决方案中添加了 Swagger(Swashbuckle UI 东西),并注意到它不处理像
<see />
这样的 XML 标签。在网上浏览后,我发现了这个 - https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/57,所以开发人员似乎不想解决这个问题。
有人有什么建议吗?我尝试运行一个小 .exe 将生成的 XML 文件中的所有标签替换为其对象名称(例如
<see cref="MyObject"/>
变为 MyObject
),但这一直搞砸,即使我手动执行此操作时,Swagger 由于某种原因也没有获取更改(XML 是否已加载到内存中的某个位置?)
XML 文档注释生成到一个文件中,其名称与同一文件夹中的程序集相同(通常)。您可以使用 XDocument 和相关类解析该文件,然后使用 schema 过滤器 (
ISchemaFilter
) 在 Swashbuckle 公开的 Open API schema 上设置适当的属性。
我不确定什么架构元素适合“另请参阅”或“相关资源”功能。您需要对此进行调查(可能是 ASP.NET Core 的开放 API 实现中的自定义属性,或者通过将文本附加到描述属性)。
至于
XML 是否已加载到内存中的某个位置?
是的。 Swashbuckle 本身会解析此 XML 以执行其当前执行的所有操作。粗略地说,它将其构建到一个文档对象模型 (DOM) 中,该文档对象模型构建在上述 Microsoft 的 Open API 实现之上。使用模式过滤器将使您能够访问该 DOM。
根据 Kit 的建议,我找到了一种检索和更改文档的方法,但需要一种解析 XML 标签的方法。我发现 this 虽然它指的是 HTML,但也删除了 XML 标签。
因此,按照需要的顺序:
public class XmlDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var path in swaggerDoc.Paths)
{
foreach (var operation in path.Value.Operations)
{
operation.Value.Description = Utilities.RemoveXMLTags(operation.Value.Description);
operation.Value.Summary = Utilities.RemoveXMLTags(operation.Value.Summary);
if (operation.Value.RequestBody != null)
{
operation.Value.RequestBody.Description = Utilities.RemoveXMLTags(operation.Value.RequestBody.Description);
}
foreach (var parameter in operation.Value.Parameters)
{
parameter.Description = Utilities.RemoveXMLTags(parameter.Description);
}
}
}
}
}
services.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc(
"MyAppAPIDocumentation",
new OpenApiInfo() { Title = "MyApp API", Version = "1" });
var xmlDocumentationFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlDocumentationFullPath = Path.Combine(AppContext.BaseDirectory, xmlDocumentationFile);
setupAction.IncludeXmlComments(xmlDocumentationFullPath);
setupAction.DocumentFilter<XmlDocumentFilter>();
});
我创建了自己的 this 版本,它不依赖于外部依赖项(如 HtmlAgilityPack)。这也删除了不受支持的
<see cref="..." />
但保留了其中的值。但我不知道哪个在性能方面更快(和/或更好)。
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;
}
}
基本上就是这样。但是,我想要从 Swagger 获得更多东西(例如作为字符串的枚举、带有我想要包含的依赖项的注释的外部 .xml 文件等),因此还有一些额外的代码。也许你们中的一些人可能会发现这很有帮助。
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());
});
SwaggerHelper类如下:
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;
}
}
}