关于 dotnet-xml-docs
面向 .NET 库开发者的 XML 文档注释技能,介绍标签语法、编写约定和 MSBuild 文档生成设置。还涵盖警告处理、IDE 补全及注释生成工具。
主要功能
- 01标准与高级 XML 标签
- 02启用 XML 文档生成
- 03CS1591 警告处理
- 04公共 API 文档约定
- 05IDE 注释骨架生成
- 06IntelliSense 文档显示
适用场景
- 01为公共 NuGet API 编写注释
- 02配置构建时生成 XML 文件
- 03管理缺少文档的编译警告
- 04在 IDE 中生成并完善注释
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-xml-docs安装后在 Claude Code 中输入/dotnet-xml-docs确认已加载。
详细介绍
dotnet-xml-docs
.NET 的 XML 文档注释:所有标准标签(<summary>、<param>、<returns>、<exception>、<remarks>、<example>、<value>、<typeparam>、<typeparamref>、<paramref>)、高级标签(用于接口和基类继承的 <inheritdoc>、<see cref="..."/>、<seealso>、<c> 和 <code>)、通过 <GenerateDocumentationFile> MSBuild 属性启用 XML 文档生成、针对内部 API 的警告抑制策略(CS1591、<NoWarn>、InternalsVisibleTo)、面向公共 NuGet 库的 XML 文档约定、自动生成工具(IDE 快速修复 /// 触发方式、GhostDoc 风格模式),以及在 IDE 工具提示和自动补全中显示 XML 文档的 IntelliSense 集成。
**版本假设:**基准版本为 .NET 8.0+。XML 文档注释是 C# 语言功能,适用于所有 .NET 版本。<GenerateDocumentationFile> MSBuild 属性适用于 .NET SDK 6+。自 C# 9.0 / .NET 5+ 起,<inheritdoc> 得到全面支持。
**范围边界:**此技能负责 XML 文档注释的编写——包括用于生成 XML 文档文件的语法、约定和 MSBuild 配置。根据 XML 注释生成 API 文档站点(DocFX、Starlight)由 [skill:dotnet-api-docs] 负责。通用 C# 编码约定(命名、格式)由 [skill:dotnet-csharp-coding-standards] 负责。
**范围之外:**根据 XML 注释生成 API 文档站点(DocFX 配置、以 OpenAPI 作为文档、文档与代码同步)——参见 [skill:dotnet-api-docs]。通用 C# 编码约定和命名标准——参见 [skill:dotnet-csharp-coding-standards]。文档站点的 CI/CD 部署——参见 [skill:dotnet-gha-deploy]。
交叉引用:[skill:dotnet-api-docs] 介绍根据 XML 注释生成下游 API 文档,[skill:dotnet-csharp-coding-standards] 介绍通用 C# 编码约定,[skill:dotnet-gha-deploy] 介绍文档站点部署。
启用 XML 文档生成
MSBuild 配置
在项目或 Directory.Build.props 中启用 XML 文档文件生成:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
这会在生成程序集时,在其旁边生成一个 .xml 文件(例如,在 MyLibrary.dll 旁生成 MyLibrary.xml)。NuGet 打包会自动将此 XML 文件包含在包中,从而为包使用者启用 IntelliSense。
针对内部 API 的警告抑制
启用 GenerateDocumentationFile 后,编译器会对所有缺少 XML 文档注释的公共成员发出 CS1591 警告。对于面向内部的代码,可选择性地抑制警告:
选项 1:在整个项目中全局抑制(不建议用于公共库):
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
选项 2:使用 pragma 指令按文件抑制(推荐用于可见性混合的程序集):
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public class InternalServiceHelper
{
// This type is internal-facing despite being public
// (e.g., exposed for testing via InternalsVisibleTo)
}
#pragma warning restore CS1591
选项 3:使用 InternalsVisibleTo,并让内部类型保持真正的内部可见性:
// In AssemblyInfo.cs or a Properties file
[assembly: InternalsVisibleTo("MyLibrary.Tests")]
// Mark internal-facing types as internal instead of public
internal class ServiceHelper
{
// No CS1591 warning -- internal types are not documented
}
选项 4:将缺少文档视为错误,适用于公共库(最严格):
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<WarningsAsErrors>$(WarningsAsErrors);CS1591</WarningsAsErrors>
</PropertyGroup>
这会强制为每个公共成员编写文档。对于使用者依赖 IntelliSense 文档的 NuGet 包,可采用此选项。
标准 XML 文档标签
<summary>
描述类型或成员。这是会显示在 IntelliSense 工具提示中的主要标签。
/// <summary>
/// Provides methods for managing widgets in the system.
/// Widget operations are thread-safe and support cancellation.
/// </summary>
public class WidgetService
{
}
<param>
说明方法参数。
/// <summary>
/// Creates a new widget with the specified name and optional category.
/// </summary>
/// <param name="name">The display name for the widget. Must not be null or whitespace.</param>
/// <param name="category">
/// The category to assign. When <see langword="null"/>, the default category is used.
/// </param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
public async Task<Widget> CreateWidgetAsync(
string name,
string? category = null,
CancellationToken cancellationToken = default)
{
}
<returns>
说明返回值。
/// <summary>
/// Finds a widget by its unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the widget.</param>
/// <returns>
/// The widget if found; otherwise, <see langword="null"/>.
/// </returns>
public async Task<Widget?> FindByIdAsync(Guid id)
{
}
<exception>
说明可能引发的异常。
/// <summary>
/// Updates the widget's name.
/// </summary>
/// <param name="id">The widget identifier.</param>
/// <param name="newName">The new name to assign.</param>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="newName"/> is null or whitespace.
/// </exception>
/// <exception cref="KeyNotFoundException">
/// Thrown when no widget with the specified <paramref name="id"/> exists.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the widget is in a read-only state and cannot be modified.
/// </exception>
public async Task UpdateNameAsync(Guid id, string newName)
{
}
<remarks>
提供摘要之外的其他背景信息。可用于实现说明、使用指南和注意事项。
/// <summary>
/// Computes the hash of the widget's content for change detection.
/// </summary>
/// <remarks>
/// <para>
/// The hash is computed using SHA-256 over the UTF-8 encoded content.
/// Results are deterministic across platforms and .NET versions.
/// </para>
/// <para>
/// This method is thread-safe. The returned hash is a lowercase hex string
/// without any prefix (e.g., "a1b2c3..." not "0xa1b2c3...").
/// </para>
/// </remarks>
/// <returns>A 64-character lowercase hexadecimal hash string.</returns>
public string ComputeContentHash()
{
}
<example>
提供展示用法的代码示例。对于公共库 API 至关重要。
/// <summary>
/// Creates a new widget and persists it to the repository.
/// </summary>
/// <example>
/// <code>
/// var service = new WidgetService(repository, logger);
///
/// var widget = await service.CreateWidgetAsync(
/// "Dashboard Widget",
/// category: "Analytics",
/// cancellationToken: ct);
///
/// Console.WriteLine($"Created widget: {widget.Id}");
/// </code>
/// </example>
public async Task<Widget> CreateWidgetAsync(
string name,
string? category = null,
CancellationToken cancellationToken = default)
{
}
<value>
说明属性的值。类似于 <returns>,但用于属性。
/// <summary>
/// Gets the current status of the widget.
/// </summary>
/// <value>
/// The widget status. Defaults to <see cref="WidgetStatus.Draft"/> for new widgets.
/// </value>
public WidgetStatus Status { get; private set; }
<typeparam>
说明泛型类型参数。
/// <summary>
/// A repository that provides CRUD operations for entities.
/// </summary>
/// <typeparam name="TEntity">
/// The entity type. Must implement <see cref="IEntity"/> and have a parameterless constructor.
/// </typeparam>
/// <typeparam name="TKey">
/// The type of the entity's primary key. Typically <see cref="Guid"/> or <see cref="int"/>.
/// </typeparam>
public interface IRepository<TEntity, TKey>
where TEntity : class, IEntity
where TKey : struct
{
}
高级标签
<inheritdoc>
从基类或接口继承文档。在实现接口或重写虚成员时,可避免重复编写文档。
接口实现:
public interface IWidgetService
{
/// <summary>
/// Creates a new widget with the specified name.
/// </summary>
/// <param name="name">The display name for the widget.</param>
/// <returns>The created widget.</returns>
Task<Widget> CreateWidgetAsync(string name);
}
public class WidgetService : IWidgetService
{
/// <inheritdoc />
public async Task<Widget> CreateWidgetAsync(string name)
{
// Implementation inherits all documentation from the interface
}
}
基类重写:
public abstract class RepositoryBase<T>
{
/// <summary>
/// Retrieves an entity by its unique identifier.
/// </summary>
/// <param name="id">The entity identifier.</param>
/// <returns>The entity if found; otherwise, <see langword="null"/>.</returns>
public abstract Task<T?> GetByIdAsync(Guid id);
}
public class OrderRepository : RepositoryBase<Order>
{
/// <inheritdoc />
public override async Task<Order?> GetByIdAsync(Guid id)
{
// Inherits summary, param, and returns docs from base class
}
}
使用 cref 选择性继承:
/// <inheritdoc cref="IWidgetService.CreateWidgetAsync(string)"/>
/// <remarks>
/// This implementation validates the name and assigns a default category
/// before persisting to the database.
/// </remarks>
public async Task<Widget> CreateWidgetAsync(string name)
{
}
使用路径筛选器继承:
/// <inheritdoc cref="IWidgetService.CreateWidgetAsync(string)" path="/summary"/>
/// <param name="name">Custom param doc that overrides the interface.</param>
public async Task<Widget> CreateWidgetAsync(string name)
{
}
<see> 和 <seealso>
创建内联引用和“另请参阅”链接。
/// <summary>
/// Validates the widget against the rules defined in <see cref="WidgetValidator"/>.
/// Use <see cref="WidgetService.CreateWidgetAsync(string)"/> to create validated widgets.
/// </summary>
/// <seealso cref="WidgetValidator"/>
/// <seealso cref="IValidationResult"/>
/// <seealso href="https://docs.mylib.dev/validation">Validation Guide</seealso>
public ValidationResult Validate(Widget widget)
{
}
<see> 的变体:
/// <summary>
/// Returns <see langword="true"/> if the widget is active,
/// <see langword="false"/> otherwise.
/// Use <see cref="Activate"/> to change the state.
/// The value is stored as a <see cref="bool"/>.
/// </summary>
public bool IsActive { get; }
<c> 和 <code>
在文档中设置内联代码和代码块的格式。
/// <summary>
/// Parses a widget name from the format <c>"category:name"</c>.
/// </summary>
/// <remarks>
/// The expected input format:
/// <code>
/// "analytics:page-views" // category = "analytics", name = "page-views"
/// "default:my-widget" // category = "default", name = "my-widget"
/// "my-widget" // category = null, name = "my-widget" (no colon)
/// </code>
/// </remarks>
public (string? Category, string Name) ParseWidgetName(string input)
{
}
<paramref> 和 <typeparamref>
在文档文本中引用参数和类型参数。
/// <summary>
/// Converts <paramref name="entity"/> to a DTO of type <typeparamref name="TDto"/>.
/// If <paramref name="entity"/> is <see langword="null"/>, returns the default value
/// of <typeparamref name="TDto"/>.
/// </summary>
/// <typeparam name="TDto">The target DTO type.</typeparam>
/// <param name="entity">The source entity to convert.</param>
public TDto? ToDto<TDto>(object? entity)
{
}
<list>
在文档中创建项目符号列表、编号列表或表格列表。
/// <summary>
/// Applies the specified transformation to the widget.
/// </summary>
/// <remarks>
/// Supported transformations:
/// <list type="bullet">
/// <item><description>Resize -- changes the widget dimensions</description></item>
/// <item><description>Recolor -- applies a new color scheme</description></item>
/// <item><description>Reposition -- moves the widget to new coordinates</description></item>
/// </list>
/// </remarks>
public void ApplyTransformation(Widget widget, Transformation transform)
{
}
完整的 XML 文档示例
展示所有标签如何配合使用的完整类示例:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MyLibrary;
/// <summary>
/// Manages the lifecycle of widgets including creation, retrieval, update, and deletion.
/// All operations are thread-safe and support cancellation via <see cref="CancellationToken"/>.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="WidgetService"/> requires a registered <see cref="IWidgetRepository"/>
/// and <see cref="ILogger{TCategoryName}"/> via dependency injection.
/// </para>
/// <para>
/// Widget names must be unique within a category. Attempting to create a widget with a
/// duplicate name in the same category throws <see cref="InvalidOperationException"/>.
/// </para>
/// </remarks>
/// <example>
/// Register and use the service:
/// <code>
/// // Registration
/// builder.Services.AddScoped<IWidgetService, WidgetService>();
///
/// // Usage
/// var widget = await widgetService.CreateWidgetAsync(
/// "My Widget",
/// category: "Dashboard");
/// </code>
/// </example>
/// <seealso cref="IWidgetRepository"/>
/// <seealso cref="Widget"/>
public class WidgetService : IWidgetService
{
private readonly IWidgetRepository _repository;
private readonly ILogger<WidgetService> _logger;
/// <summary>
/// Initializes a new instance of <see cref="WidgetService"/>.
/// </summary>
/// <param name="repository">The widget data repository.</param>
/// <param name="logger">The logger for diagnostic output.</param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="repository"/> or <paramref name="logger"/> is
/// <see langword="null"/>.
/// </exception>
public WidgetService(IWidgetRepository repository, ILogger<WidgetService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Gets the total number of widgets in the system.
/// </summary>
/// <value>A non-negative integer representing the total widget count.</value>
public int TotalCount => _repository.Count;
/// <inheritdoc />
public async Task<Widget> CreateWidgetAsync(
string name,
string? category = null,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
_logger.LogInformation("Creating widget {Name} in category {Category}", name, category);
var widget = new Widget
{
Id = Guid.NewGuid(),
Name = name,
Category = category ?? "default",
CreatedAt = DateTimeOffset.UtcNow,
Status = WidgetStatus.Draft
};
await _repository.AddAsync(widget, cancellationToken);
return widget;
}
/// <summary>
/// Retrieves all widgets matching the specified filter criteria.
/// </summary>
/// <param name="filter">
/// The filter to apply. Use <see cref="WidgetFilter.None"/> for unfiltered results.
/// </param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>
/// A read-only list of widgets matching the filter. Returns an empty list if no
/// widgets match the criteria.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="filter"/> is <see langword="null"/>.
/// </exception>
public async Task<IReadOnlyList<Widget>> ListAsync(
WidgetFilter filter,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
return await _repository.FindAsync(filter, cancellationToken);
}
}
公共库的 XML 文档规范
文档要求
对于供其他开发者使用的 NuGet 包,请遵循以下规范:
- 每个公共类型都必须有
<summary>——这是 IntelliSense 显示有用信息的最低要求 - 每个公共方法参数都必须有
<param>——说明参数的用途、有效范围和对 null 的处理方式 - 每个可能引发异常的方法都必须使用
<exception>记录异常 ——调用方需要知道要捕获哪些异常 - 返回值必须有
<returns>——说明返回值的语义,尤其是可能为 null 的返回值 - 属性应有
<value>——说明属性的含义及其默认值 - 泛型类型参数必须有
<typeparam>——说明约束和预期用法 - 复杂 API 应包含
<example>——展示实际用法,而不是简单的一行示例 - 使用
<remarks>说明注意事项和线程行为 ——记录不适合放在摘要中的重要背景信息
写作风格
- 使用第三人称陈述句:“获取小部件名称”,而不是“获取小部件名称!”或“此方法获取小部件名称”
<summary>以动词开头:“创建……”“检索……”“验证……”“计算……”- 避免重复显而易见的信息:对于名为
Name的属性,不要写“获取 Name 属性” - 记录行为,而非实现:“如果有缓存值,则返回该值”,而不是“检查字典并返回该值”
- 所有类型和成员引用都使用
<see cref="..."/>——便于 IDE 导航和重构 - 使用
<see langword="null"/>、<see langword="true"/>、<see langword="false"/>,而不是直接写null、true、false
CancellationToken 规范
统一 CancellationToken 参数的文档说明:
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
对于标准的 CancellationToken cancellationToken = default 模式,这一句描述已足够。不要过度说明取消的含义——调用方了解这一模式。
自动生成工具
IDE 快速修复生成
所有主流 .NET IDE 都会在成员上方输入 /// 时生成 XML 文档框架:
Visual Studio / VS Code(C# Dev Kit):
- 在方法、类或属性上方输入
/// - IDE 会生成
<summary>框架,并根据成员签名预先填入<param>、<returns>、<typeparam>标签
JetBrains Rider:
- 输入
///以触发相同的框架生成 - Rider 还会提示缺少 XML 文档,并提供快速修复选项来添加文档
GhostDoc 风格模式
GhostDoc 和类似工具会使用命名约定启发式规则,根据成员名称生成文档文本:
GetWidgetById会生成“根据标识符获取小部件”IsActive会生成“获取一个值,用于指示此实例是否处于活动状态”CreateWidgetAsync会生成“异步创建小部件”
这些自动生成的描述只是起点。务必检查并改进生成的文本,补充特定领域的背景信息、参数约束、异常条件和示例。
EditorConfig 集成
通过 .editorconfig 强制执行 XML 文档要求:
# .editorconfig
[*.cs]
# Require XML documentation for public members
dotnet_diagnostic.CS1591.severity = warning
# For public libraries, treat as error:
# dotnet_diagnostic.CS1591.severity = error
IntelliSense 集成
XML 文档如何在 IDE 中显示
XML 文档注释是 IntelliSense 工具提示的主要来源:
- 悬停提示: 将鼠标悬停在类型或成员上时显示
<summary>文本 - 参数提示: 方法调用期间,在参数信息弹出窗口中显示
<param>文本 - 补全列表: 在自动补全下拉列表中显示
<summary> - 快速信息: 在展开的文档面板中显示
<returns>、<exception>和<remarks> - 签名帮助: 显示参数名称和类型,以及
<param>描述
NuGet 包 IntelliSense
启用 <GenerateDocumentationFile> 并将包发布到 NuGet 后,XML 文件会包含在包中。使用者无需访问源代码即可获得完整的 IntelliSense 支持:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
无需额外配置 NuGet 打包;当 GenerateDocumentationFile 为 true 时,dotnet pack 会自动包含 XML 文档文件。
Source Link 集成
将 XML 文档与 Source Link 结合,获得最佳开发体验:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
同时启用 XML 文档和 Source Link 后,使用者可以通过悬停查看文档,并跳转到原始源代码。
Agent 常见问题
-
公共库始终启用
<GenerateDocumentationFile>——否则 NuGet 使用者将无法获得 IntelliSense 文档。将其添加到Directory.Build.props,以便应用于解决方案中的所有项目。 -
对接口实现和重写使用
<inheritdoc />——不要在接口及其实现之间重复文档文本。重复内容会导致维护偏差。 -
不要在公共 NuGet 包中全局抑制 CS1591——通过
<NoWarn>CS1591</NoWarn>全局抑制会隐藏所有缺失文档的警告。对于有意不编写文档的类型,请使用针对单个文件的#pragma抑制,或将内部类型设为真正的internal。 -
所有类型引用都使用
<see cref="..."/>,不要只写类型名称——<see cref="Widget"/>支持 IDE 导航,并会在构建时验证。裸文本“Widget”不会成为链接,而且在类型重命名后可能过时。 -
文档文本中使用
<see langword="null"/>,不要直接写null——这样在 IntelliSense 和 API 文档网站中会以正确格式显示。true、false和其他 C# 关键字也同样适用。 -
<inheritdoc>在构建时解析,而非设计时解析——某些较旧的 IDE 版本可能会在工具提示中对<inheritdoc>显示“未找到文档”。生成的 XML 文件和 API 文档网站中的文档会正确解析。 -
XML 文档注释的正文中,泛型类型语法必须使用
<和>——但<see cref="..."/>会自动处理泛型。请使用<see cref="List{T}"/>(花括号),不要使用<see cref="List<T>"/>。 -
在
<code>块中,尖括号要使用<和>——XML 文档注释属于 XML,因此代码示例中的<和>必须转义。也可以使用<![CDATA[...]]>来避免转义。 -
不要根据 XML 注释生成 API 文档网站——API 文档网站生成(DocFX、OpenAPI-as-docs)属于 [skill:dotnet-api-docs]。本技能仅涉及 XML 注释的编写。
-
使用一条标准句子记录取消令牌——所有
CancellationToken参数均使用“A token to cancel the asynchronous operation.”。不要过度描述取消模式。