mcpField
D
dotnet-xml-docs

dotnet-xml-docs

github.com/wshaddix开发8683 次安装35 周前更新
代码仓库

关于 dotnet-xml-docs

面向 .NET 库开发者的 XML 文档注释技能,介绍标签语法、编写约定和 MSBuild 文档生成设置。还涵盖警告处理、IDE 补全及注释生成工具。

主要功能

  1. 01标准与高级 XML 标签
  2. 02启用 XML 文档生成
  3. 03CS1591 警告处理
  4. 04公共 API 文档约定
  5. 05IDE 注释骨架生成
  6. 06IntelliSense 文档显示

适用场景

  1. 01为公共 NuGet API 编写注释
  2. 02配置构建时生成 XML 文件
  3. 03管理缺少文档的编译警告
  4. 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&lt;IWidgetService, WidgetService&gt;();
///
/// // 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 包,请遵循以下规范:

  1. 每个公共类型都必须有 <summary> ——这是 IntelliSense 显示有用信息的最低要求
  2. 每个公共方法参数都必须有 <param> ——说明参数的用途、有效范围和对 null 的处理方式
  3. 每个可能引发异常的方法都必须使用 <exception> 记录异常 ——调用方需要知道要捕获哪些异常
  4. 返回值必须有 <returns> ——说明返回值的语义,尤其是可能为 null 的返回值
  5. 属性应有 <value> ——说明属性的含义及其默认值
  6. 泛型类型参数必须有 <typeparam> ——说明约束和预期用法
  7. 复杂 API 应包含 <example> ——展示实际用法,而不是简单的一行示例
  8. 使用 <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 常见问题

  1. 公共库始终启用 <GenerateDocumentationFile>——否则 NuGet 使用者将无法获得 IntelliSense 文档。将其添加到 Directory.Build.props,以便应用于解决方案中的所有项目。

  2. 对接口实现和重写使用 <inheritdoc />——不要在接口及其实现之间重复文档文本。重复内容会导致维护偏差。

  3. 不要在公共 NuGet 包中全局抑制 CS1591——通过 <NoWarn>CS1591</NoWarn> 全局抑制会隐藏所有缺失文档的警告。对于有意不编写文档的类型,请使用针对单个文件的 #pragma 抑制,或将内部类型设为真正的 internal。

  4. 所有类型引用都使用 <see cref="..."/>,不要只写类型名称——<see cref="Widget"/> 支持 IDE 导航,并会在构建时验证。裸文本“Widget”不会成为链接,而且在类型重命名后可能过时。

  5. 文档文本中使用 <see langword="null"/>,不要直接写 null——这样在 IntelliSense 和 API 文档网站中会以正确格式显示。true、false 和其他 C# 关键字也同样适用。

  6. <inheritdoc> 在构建时解析,而非设计时解析——某些较旧的 IDE 版本可能会在工具提示中对 <inheritdoc> 显示“未找到文档”。生成的 XML 文件和 API 文档网站中的文档会正确解析。

  7. XML 文档注释的正文中,泛型类型语法必须使用 &lt; 和 &gt;——但 <see cref="..."/> 会自动处理泛型。请使用 <see cref="List{T}"/>(花括号),不要使用 <see cref="List&lt;T&gt;"/>。

  8. 在 <code> 块中,尖括号要使用 &lt; 和 &gt;——XML 文档注释属于 XML,因此代码示例中的 < 和 > 必须转义。也可以使用 <![CDATA[...]]> 来避免转义。

  9. 不要根据 XML 注释生成 API 文档网站——API 文档网站生成(DocFX、OpenAPI-as-docs)属于 [skill:dotnet-api-docs]。本技能仅涉及 XML 注释的编写。

  10. 使用一条标准句子记录取消令牌——所有 CancellationToken 参数均使用“A token to cancel the asynchronous operation.”。不要过度描述取消模式。

评论

支持 Markdown