关于 dotnet-api-surface-validation
为 .NET 库维护者提供公共 API 表面验证工作流,涵盖源码跟踪、反射快照测试和 CI 兼容性检查。帮助团队审查并显式批准 API 变更。
主要功能
- 01用 PublicApiAnalyzers 跟踪已发布与未发布 API
- 02用 Verify 对程序集 API 做快照测试
- 03用 ApiCompat 在 CI 检查 API 差异
- 04支持多目标框架的独立 API 清单
适用场景
- 01开发期间发现未登记的公共成员
- 02发布时审查并归档 API 变更
- 03在拉取请求中阻止未经批准的兼容性变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-api-surface-validation安装后在 Claude Code 中输入/dotnet-api-surface-validation确认已加载。
详细介绍
dotnet-api-surface-validation
用于验证和跟踪 .NET 库公共 API 表面的工具和工作流。涵盖三种互补方式:使用 PublicApiAnalyzers 通过文本文件跟踪已发布和未发布的 API,并提供 Roslyn 诊断;使用 Verify 快照模式 基于反射进行 API 表面快照测试;以及使用 ApiCompat CI 强制检查,根据 API 表面变更阻止或放行拉取请求。
版本假设: 以 .NET 8.0+ 为基准。PublicApiAnalyzers 3.3+(随 Microsoft.CodeAnalysis.Analyzers 一同提供,或使用独立的 Microsoft.CodeAnalysis.PublicApiAnalyzers)。ApiCompat 工具包含在 .NET 8+ SDK 中。
范围之外: 二进制兼容性与源代码兼容性规则、类型转发器、SemVer 影响——请参阅 [skill:dotnet-library-api-compat]。NuGet 打包、EnablePackageValidation 基础知识及抑制文件机制——请参阅 [skill:dotnet-nuget-authoring] 和 [skill:dotnet-multi-targeting]。Verify 库基础知识(设置、清理、转换器)——请参阅 [skill:dotnet-snapshot-testing]。常规 Roslyn 分析器配置(EditorConfig、严重性级别)——请参阅 [skill:dotnet-roslyn-analyzers]。HTTP API 版本控制——请参阅 [skill:dotnet-api-versioning]。
交叉引用:[skill:dotnet-library-api-compat] 介绍二进制兼容性与源代码兼容性规则,[skill:dotnet-nuget-authoring] 介绍 EnablePackageValidation 和 NuGet SemVer,[skill:dotnet-multi-targeting] 介绍多 TFM 的 ApiCompat 工具机制,[skill:dotnet-snapshot-testing] 介绍 Verify 基础知识,[skill:dotnet-roslyn-analyzers] 介绍常规分析器配置,[skill:dotnet-api-versioning] 介绍 HTTP API 版本控制。
PublicApiAnalyzers
PublicApiAnalyzers 会跟踪提交到源代码管理的文本文件中的每个公共 API 成员。分析器要求新 API 先经过明确的“未发布”阶段,然后才能标记为“已发布”,从而防止意外公开 API,以及未记录的 API 表面变更。
设置
安装分析器包:
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.*" PrivateAssets="all" />
</ItemGroup>
在项目根目录(与 .csproj 相邻)创建两个跟踪文件:
MyLib/
MyLib.csproj
PublicAPI.Shipped.txt # APIs shipped in released versions
PublicAPI.Unshipped.txt # APIs added since last release
两个文件都必须存在,即使内容为空也一样。每个文件都必须包含标头注释:
#nullable enable
#nullable enable 标头会告知分析器跟踪 API 签名中的可空注释。若没有此标头,分析器会忽略可空上下文差异。
诊断规则
| 规则 | 严重性 | 含义 |
|---|---|---|
| RS0016 | 警告 | 公共 API 成员未在 API 跟踪文件中声明 |
| RS0017 | 警告 | 公共 API 成员已移除,但仍保留在跟踪文件中 |
| RS0024 | 警告 | 公共 API 成员的可空注释不正确 |
| RS0025 | 警告 | 已标记为已发布的公共 API 符号签名已发生变化 |
| RS0026 | 警告 | 新增公共 API,但未在 PublicAPI.Unshipped.txt 中添加条目 |
| RS0036 | 警告 | API 文件缺少 #nullable enable 标头 |
| RS0037 | 警告 | 已声明公共 API,但源码中不存在 |
RS0016 是最常见的诊断。添加新的 public 或 protected 成员时,RS0016 会持续触发,直到你将该成员的签名添加到 PublicAPI.Unshipped.txt 中。在 IDE 中使用代码修复(灯泡图标)自动添加条目。
移除或重命名 public 成员,但跟踪文件中仍保留旧签名时,会触发 RS0017。从相应文件中删除过时的行。
文件格式
跟踪文件中的每一行都使用其文档注释 ID 格式表示一个公共 API 符号:
#nullable enable
MyLib.Widget
MyLib.Widget.Widget() -> void
MyLib.Widget.Name.get -> string!
MyLib.Widget.Name.set -> void
MyLib.Widget.Calculate(int count) -> decimal
MyLib.Widget.CalculateAsync(int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<decimal>!
MyLib.IWidgetFactory
MyLib.IWidgetFactory.Create(string! name) -> MyLib.Widget!
MyLib.WidgetOptions
MyLib.WidgetOptions.WidgetOptions() -> void
MyLib.WidgetOptions.MaxRetries.get -> int
MyLib.WidgetOptions.MaxRetries.set -> void
关键格式规则:
!后缀表示启用了可空上下文时的非可空引用类型?后缀表示可空引用类型或可空值类型- 构造函数使用类型名称(例如,
Widget.Widget() -> void) - 属性会展开为
.get和.set条目 - 签名中包含默认参数值
已发布/未发布生命周期
跨发布周期的工作流:
开发期间(版本发布之间):
- 在源代码中添加新的公共 API 成员
- 触发 RS0016——该成员尚未跟踪
- 使用代码修复或手动将其添加到
PublicAPI.Unshipped.txt - RS0016 警告消失
发布时:
- 将
PublicAPI.Unshipped.txt中的所有条目移至PublicAPI.Shipped.txt - 将
PublicAPI.Unshipped.txt清空,仅保留#nullable enable标头 - 将这两个文件作为发布 PR 的一部分提交
- 为此次发布打标签
移除先前已发布的 API 时(主版本):
- 从源代码中移除该成员
- 从
PublicAPI.Shipped.txt中移除该条目 - RS0017 警告消失(如果曾触发)
- 在发布说明中记录此次移除
移除尚未发布的 API 时(发布前):
- 从源代码中移除该成员
- 从
PublicAPI.Unshipped.txt中移除该条目 - 不影响 SemVer——该 API 从未发布
多 TFM 项目
对于多目标项目,如果不同目标的 API 表面存在差异,PublicApiAnalyzers 支持按 TFM 跟踪文件:
MyLib/
MyLib.csproj
PublicAPI.Shipped.txt # Shared across all TFMs
PublicAPI.Unshipped.txt # Shared across all TFMs
PublicAPI.Shipped.net8.0.txt # net8.0-specific APIs
PublicAPI.Unshipped.net8.0.txt # net8.0-specific APIs
PublicAPI.Shipped.net10.0.txt # net10.0-specific APIs
PublicAPI.Unshipped.net10.0.txt # net10.0-specific APIs
共享文件包含所有 TFM 通用的 API。TFM 专属文件包含仅在相应目标上存在的 API。分析器会在构建时将它们合并。
要启用按 TFM 区分的文件,请在 .csproj 中添加:
<PropertyGroup>
<RoslynPublicApiPerTfm>true</RoslynPublicApiPerTfm>
</PropertyGroup>
有关多 TFM 打包机制,请参阅 [skill:dotnet-multi-targeting]。
集成到 CI
PublicApiAnalyzers 会作为标准构建的一部分运行。要在 CI 中强制执行,请确保将 RS 系列规则的警告视为错误:
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);RS0016;RS0017;RS0036;RS0037</WarningsAsErrors>
</PropertyGroup>
这样,任何未声明的公共 API 更改都会阻止 CI 构建通过。开发人员必须先明确更新跟踪文件,才能通过构建。
验证 API 表面快照模式
使用 Verify 库对程序集的整个公共 API 表面进行快照测试。此方法通过反射枚举所有公共类型和成员,生成可读的快照并将其提交到源代码管理中;每次测试运行时都会进行比对。公共 API 表面的任何更改都会导致测试失败,直到快照得到明确批准。
此模式与 PublicApiAnalyzers 相辅相成——分析器在项目内的构建时捕获更改,而 Verify 快照则从已编译程序集使用者的角度捕获更改。
有关 Verify 基础知识(设置、清理、转换器、差异工具集成、CI 配置),请参阅 [skill:dotnet-snapshot-testing]。
提取公共 API 表面
创建一个辅助方法,通过反射检查程序集,生成稳定且经过排序的所有公共类型及其成员的表示:
using System.Reflection;
using System.Text;
public static class PublicApiExtractor
{
public static string GetPublicApi(Assembly assembly)
{
var sb = new StringBuilder();
var publicTypes = assembly
.GetTypes()
.Where(t => t.IsPublic || t.IsNestedPublic)
.OrderBy(t => t.FullName, StringComparer.Ordinal);
foreach (var type in publicTypes)
{
AppendType(sb, type);
}
return sb.ToString();
}
private static void AppendType(StringBuilder sb, Type type)
{
var kind = type switch
{
{ IsEnum: true } => "enum",
{ IsValueType: true } => "struct",
{ IsInterface: true } => "interface",
{ IsAbstract: true, IsSealed: true } => "static class",
{ IsAbstract: true } => "abstract class",
{ IsSealed: true } => "sealed class",
_ => "class"
};
sb.AppendLine($"{kind} {type.FullName}");
var members = type
.GetMembers(BindingFlags.Public | BindingFlags.Instance
| BindingFlags.Static | BindingFlags.DeclaredOnly)
.OrderBy(m => m.MemberType)
.ThenBy(m => m.Name, StringComparer.Ordinal)
.ThenBy(m => m.ToString(), StringComparer.Ordinal);
foreach (var member in members)
{
sb.AppendLine($" {FormatMember(member)}");
}
sb.AppendLine();
}
private static string FormatMember(MemberInfo member) =>
member switch
{
ConstructorInfo c => $".ctor({FormatParameters(c.GetParameters())})",
MethodInfo m when !m.IsSpecialName =>
$"{m.ReturnType.Name} {m.Name}({FormatParameters(m.GetParameters())})",
PropertyInfo p => $"{p.PropertyType.Name} {p.Name} {{ {GetAccessors(p)} }}",
FieldInfo f => $"{f.FieldType.Name} {f.Name}",
EventInfo e => $"event {e.EventHandlerType?.Name} {e.Name}",
_ => member.ToString() ?? string.Empty
};
private static string FormatParameters(ParameterInfo[] parameters) =>
string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"));
private static string GetAccessors(PropertyInfo prop)
{
var parts = new List<string>();
if (prop.GetMethod?.IsPublic == true) parts.Add("get;");
if (prop.SetMethod?.IsPublic == true) parts.Add("set;");
return string.Join(" ", parts);
}
}
编写快照测试
[UsesVerify]
public class PublicApiSurfaceTests
{
[Fact]
public Task PublicApi_ShouldMatchApprovedSurface()
{
var assembly = typeof(Widget).Assembly;
var publicApi = PublicApiExtractor.GetPublicApi(assembly);
return Verify(publicApi);
}
}
首次运行时,会创建一个包含完整公共 API 列表的 .verified.txt 文件。后续运行会将当前 API 表面与已批准的快照进行比对。公共成员的任何新增、移除或修改都会导致测试失败,并显示清晰的差异。
审查 API 表面更改
当快照测试失败时:
- Verify 会生成一个
.received.txt文件,展示新的 API 表面 - 比较
.received.txt和.verified.txt,审查更改 - 如果更改是有意的,请使用
verify accept接受新快照 - 如果更改是意外的,请还原代码更改
这样就创建了一个代码审查检查点:每项公共 API 更改都必须由审查拉取请求中快照差异的人员明确批准。
与 PublicApiAnalyzers 结合使用
这两种方法各有用途:
| 关注点 | PublicApiAnalyzers | Verify 快照 |
|---|---|---|
| 检测时机 | 构建时(IDE 内) | 测试时(编译后) |
| 粒度 | 每个成员的签名 | 整个程序集的 API 表面 |
| 可空注解 | 通过 #nullable enable 跟踪 | 需要显式反射 |
| 审批工作流 | 编辑文本文件(已发布/未发布) | 接受快照差异 |
| 多 TFM | 每个 TFM 对应独立文件 | 每个 TFM 对应独立测试目标 |
| CI 门禁 | 将警告视为错误 | 测试失败 |
若要最大限度地覆盖 API 变更,请同时使用两者:PublicApiAnalyzers 可在开发过程中捕获变更,而 Verify 快照则可在测试套件中提供端到端的程序集级验证。
ApiCompat CI 强制检查
ApiCompat 会比较两个程序集(或将基准 NuGet 包与当前构建进行比较),并报告 API 差异。将其集成到 CI 后,它会根据 API 表面变更拦截拉取请求——任何破坏性变更都会导致构建错误,作者必须明确确认该变更。
关于 EnablePackageValidation 基础知识和抑制文件机制,请参阅 [skill:dotnet-nuget-authoring] 和 [skill:dotnet-multi-targeting]。
在 CI 中进行包验证
最简单的强制检查方式是在执行 dotnet pack 时使用 EnablePackageValidation:
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>
在 CI 流水线中,dotnet pack 会自动运行包验证:
# GitHub Actions -- gate PRs on API compatibility
name: API Compatibility Check
on:
pull_request:
paths:
- 'src/**'
- '*.props'
- '*.targets'
jobs:
api-compat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Pack with API validation
run: dotnet pack --configuration Release --no-build
# EnablePackageValidation runs during pack and fails
# the build if breaking changes are detected
使用独立的 ApiCompat 工具比较程序集
如果需要在不打包的情况下比较程序集(例如,将功能分支的构建与主分支的构建进行比较),请使用独立的 ApiCompat 工具:
# GitHub Actions -- compare assemblies directly
name: API Diff Check
on:
pull_request:
jobs:
api-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Install ApiCompat tool
run: dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
- name: Build current branch
run: dotnet build src/MyLib/MyLib.csproj -c Release -o artifacts/current
- name: Build baseline (main branch)
run: |
git stash
git checkout origin/main -- src/MyLib/
dotnet build src/MyLib/MyLib.csproj -c Release -o artifacts/baseline
git checkout - -- src/MyLib/
git stash pop || true
- name: Compare APIs
run: |
apicompat --left-assembly artifacts/baseline/MyLib.dll \
--right-assembly artifacts/current/MyLib.dll
为 API 变更标记 PR
结合使用 ApiCompat 和 PR 标记,让评审者能够清楚地看到 API 变更:
- name: Check for API changes
id: api-check
continue-on-error: true
run: |
apicompat --left-assembly artifacts/baseline/MyLib.dll \
--right-assembly artifacts/current/MyLib.dll 2>&1 | tee api-diff.txt
echo "has_changes=$([[ -s api-diff.txt ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Label PR with API changes
if: steps.api-check.outputs.has_changes == 'true'
run: gh pr edit "${{ github.event.pull_request.number }}" --add-label "api-change"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
处理有意进行的破坏性变更
如果有意进行破坏性变更(新主版本),请生成抑制文件:
dotnet pack /p:GenerateCompatibilitySuppressionFile=true
这会在项目目录中创建 CompatibilitySuppressions.xml。如果文件存放在其他位置,请显式引用:
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
注意:ApiCompatSuppressionFile 是 ItemGroup 项,而不是 PropertyGroup 属性。使用 PropertyGroup 语法不会生效,也不会报错。
抑制文件会记录已接受的特定破坏性变更:
<?xml version="1.0" encoding="utf-8"?>
<Suppressions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:MyLib.Widget.Calculate</Target>
<Left>lib/net8.0/MyLib.dll</Left>
<Right>lib/net8.0/MyLib.dll</Right>
</Suppression>
</Suppressions>
将抑制文件提交到源代码管理。评审者可以检查文件,确认破坏性变更已有记录且经过有意批准。
在 CI 中强制检查 PublicApiAnalyzers 文件
将 PublicApiAnalyzers 的警告视为错误,并结合 CI 步骤验证跟踪文件不是过时文件:
- name: Build with API tracking enforcement
run: dotnet build -c Release /p:TreatWarningsAsErrors=true /warnaserror:RS0016,RS0017,RS0036,RS0037
- name: Verify PublicAPI files are committed
run: |
if git diff --name-only | grep -q 'PublicAPI'; then
echo "::error::PublicAPI tracking files have uncommitted changes"
git diff -- '**/PublicAPI.*.txt'
exit 1
fi
多库单体仓库强制检查
对于包含多个库的仓库,请在解决方案级别应用 API 验证:
<Project>
<PropertyGroup Condition="'$(IsPackable)' == 'true'">
<EnablePackageValidation>true</EnablePackageValidation>
<WarningsAsErrors>$(WarningsAsErrors);RS0016;RS0017;RS0036;RS0037</WarningsAsErrors>
</PropertyGroup>
<ItemGroup Condition="'$(IsPackable)' == 'true'">
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers"
Version="3.3.*" PrivateAssets="all" />
</ItemGroup>
</Project>
这样可确保仓库中的每个可打包项目都启用了 PublicApiAnalyzers 和包验证,而无需重复配置。
Agent 注意事项
- 切勿忘记创建
PublicAPI.Shipped.txt和PublicAPI.Unshipped.txt—— PublicApiAnalyzers 要求这两个文件都存在,即使文件为空。缺少文件会导致每个公共成员都出现 RS0037 警告。 - 切勿从 PublicAPI 跟踪文件中省略
#nullable enable头部 —— 如果缺少该头部(RS0036),分析器会忽略可空注释的差异,导致启用了可空性的库遗漏真实的 API 表面变更。 - 切勿将
ApiCompatSuppressionFile放在 PropertyGroup 中 —— 它是一个 ItemGroup 项(<ApiCompatSuppressionFile Include="..." />)。PropertyGroup 语法会被静默忽略,抑制功能也不会生效。 - 开发过程中切勿将条目从
PublicAPI.Unshipped.txt移到PublicAPI.Shipped.txt—— 只在发布时移动条目。过早标记为已发布,会导致无法干净地撤销尚未发布的 API 添加。 - 切勿将 Verify API 表面快照作为唯一的验证机制 —— 它在测试时运行,晚于编译。使用 PublicApiAnalyzers 获取即时的构建时反馈,并使用 ApiCompat 进行基线比较;将 Verify 快照作为额外的安全保障。
- 切勿在 CI ApiCompat 工作流中硬编码特定于 TFM 的路径 —— 使用 MSBuild 输出路径变量,或将 TFM 参数化,以避免添加或更改 TFM 时导致工作流失效。
- 切勿使用
<NoWarn>全局抑制 RS0016 —— 这会静默禁用所有公共 API 跟踪。应将缺失的 API 条目添加到跟踪文件中。如果某个 API 有意设为内部,但必须为public(例如,用作InternalsVisibleTo的替代方案),请使用[EditorBrowsable(EditorBrowsableState.Never)],并将其添加到跟踪文件中。 - 切勿使用
GenerateCompatibilitySuppressionFile=true生成抑制文件后就不检查它 —— 该文件可能会抑制超出预期的变更。提交前务必检查生成的 XML。
前置条件
- .NET 8.0+ SDK
Microsoft.CodeAnalysis.PublicApiAnalyzersNuGet 包(用于 RS0016/RS0017 诊断)EnablePackageValidationMSBuild 属性(用于在dotnet pack期间进行基线 API 比较)Microsoft.DotNet.ApiCompat.Tool(可选,用于在dotnet pack之外独立比较程序集)- Verify 测试库和测试框架集成包(用于 API 表面快照测试)——设置说明请参阅 [skill:dotnet-snapshot-testing]
- 了解二进制兼容性与源代码兼容性规则——请参阅 [skill:dotnet-library-api-compat]