dotnet-api-surface-validation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-api-surface-validation
.NET API 表面验证工具与工作流
Tools and workflows for validating and tracking the public API surface of .NET libraries. Covers three complementary approaches: PublicApiAnalyzers for text-file tracking of shipped/unshipped APIs with Roslyn diagnostics, the Verify snapshot pattern for reflection-based API surface snapshot testing, and ApiCompat CI enforcement for gating pull requests on API surface changes.
Version assumptions: .NET 8.0+ baseline. PublicApiAnalyzers 3.3+ (ships with or standalone ). ApiCompat tooling included in .NET 8+ SDK.
Microsoft.CodeAnalysis.AnalyzersMicrosoft.CodeAnalysis.PublicApiAnalyzersOut of scope: Binary vs source compatibility rules, type forwarders, SemVer impact -- see [skill:dotnet-library-api-compat]. NuGet packaging, basics, and suppression file mechanics -- see [skill:dotnet-nuget-authoring] and [skill:dotnet-multi-targeting]. Verify library fundamentals (setup, scrubbing, converters) -- see [skill:dotnet-snapshot-testing]. General Roslyn analyzer configuration (EditorConfig, severity levels) -- see [skill:dotnet-roslyn-analyzers]. HTTP API versioning -- see [skill:dotnet-api-versioning].
EnablePackageValidationCross-references: [skill:dotnet-library-api-compat] for binary/source compatibility rules, [skill:dotnet-nuget-authoring] for and NuGet SemVer, [skill:dotnet-multi-targeting] for multi-TFM ApiCompat tool mechanics, [skill:dotnet-snapshot-testing] for Verify fundamentals, [skill:dotnet-roslyn-analyzers] for general analyzer configuration, [skill:dotnet-api-versioning] for HTTP API versioning.
EnablePackageValidation用于验证和跟踪.NET库公共API表面的工具与工作流。涵盖三种互补方法:PublicApiAnalyzers(通过Roslyn诊断跟踪已发布/未发布API的文本文件方式)、Verify快照模式(基于反射的API表面快照测试),以及ApiCompat CI强制执行(通过校验API表面变更来管控拉取请求)。
版本要求:以.NET 8.0+为基准。PublicApiAnalyzers 3.3+(随一同发布,也可单独安装)。ApiCompat工具已包含在.NET 8+ SDK中。
Microsoft.CodeAnalysis.AnalyzersMicrosoft.CodeAnalysis.PublicApiAnalyzers不涉及范围:二进制与源代码兼容性规则、类型转发器、语义化版本(SemVer)影响——详见[skill:dotnet-library-api-compat]。NuGet打包、基础用法、抑制文件机制——详见[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]。
EnablePackageValidation交叉引用:[skill:dotnet-library-api-compat](二进制/源代码兼容性规则)、[skill:dotnet-nuget-authoring](与NuGet SemVer)、[skill:dotnet-multi-targeting](多TFM ApiCompat工具机制)、[skill:dotnet-snapshot-testing](Verify基础)、[skill:dotnet-roslyn-analyzers](通用分析器配置)、[skill:dotnet-api-versioning](HTTP API版本控制)。
EnablePackageValidationPublicApiAnalyzers
PublicApiAnalyzers
PublicApiAnalyzers tracks every public API member in text files committed to source control. The analyzer enforces that new APIs go through an explicit "unshipped" phase before being marked "shipped," preventing accidental public API exposure and undocumented surface area changes.
PublicApiAnalyzers通过提交到源代码管理的文本文件跟踪每个公共API成员。该分析器强制要求新API在标记为“已发布”前需经过明确的“未发布”阶段,防止意外暴露公共API以及未记录的API表面变更。
Setup
配置
Install the analyzer package:
xml
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.*" PrivateAssets="all" />
</ItemGroup>Create the two tracking files at the project root (adjacent to the ):
.csprojMyLib/
MyLib.csproj
PublicAPI.Shipped.txt # APIs shipped in released versions
PublicAPI.Unshipped.txt # APIs added since last releaseBoth files must exist, even if empty. Each must contain a header comment:
#nullable enableThe header tells the analyzer to track nullable annotations in API signatures. Without it, nullable context differences are ignored.
#nullable enable安装分析器包:
xml
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.*" PrivateAssets="all" />
</ItemGroup>在项目根目录(与同级)创建两个跟踪文件:
.csprojMyLib/
MyLib.csproj
PublicAPI.Shipped.txt # 已发布版本中的API
PublicAPI.Unshipped.txt # 上次发布后新增的API两个文件必须存在,即使为空。每个文件都必须包含头部注释:
#nullable enable#nullable enableDiagnostic Rules
诊断规则
| Rule | Severity | Meaning |
|---|---|---|
| RS0016 | Warning | Public API member not declared in API tracking files |
| RS0017 | Warning | Public API member removed but still in tracking files |
| RS0024 | Warning | Public API member has wrong nullable annotation |
| RS0025 | Warning | Public API symbol marked shipped but has changed signature |
| RS0026 | Warning | New public API added without |
| RS0036 | Warning | API file missing |
| RS0037 | Warning | Public API declared but does not exist in source |
RS0016 is the most common diagnostic. When you add a new or member, RS0016 fires until you add the member's signature to . Use the code fix (lightbulb) in the IDE to automatically add the entry.
publicprotectedPublicAPI.Unshipped.txtRS0017 fires when you remove or rename a member but the old signature still exists in the tracking files. Remove the stale line from the appropriate file.
public| 规则 | 严重级别 | 含义 |
|---|---|---|
| RS0016 | 警告 | 公共API成员未在API跟踪文件中声明 |
| RS0017 | 警告 | 公共API成员已移除但仍存在于跟踪文件中 |
| RS0024 | 警告 | 公共API成员的可空注解错误 |
| RS0025 | 警告 | 标记为已发布的公共API符号签名已变更 |
| RS0026 | 警告 | 新增公共API未添加到 |
| RS0036 | 警告 | API文件缺少 |
| RS0037 | 警告 | 声明的公共API在源代码中不存在 |
RS0016是最常见的诊断。当你添加新的或成员时,RS0016会触发,直到你将成员签名添加到中。可使用IDE中的代码修复(小灯泡图标)自动添加条目。
publicprotectedPublicAPI.Unshipped.txtRS0017会在你移除或重命名成员,但旧签名仍存在于跟踪文件中时触发。从相应文件中删除过时的行即可。
publicFile Format
文件格式
Each line in the tracking files represents one public API symbol using its documentation comment ID format:
#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 -> voidKey formatting rules:
- The suffix denotes a non-nullable reference type in nullable-enabled context
! - The suffix denotes a nullable reference type or nullable value type
? - Constructors use the type name (e.g., )
Widget.Widget() -> void - Properties expand to and
.getentries.set - Default parameter values are included in the signature
跟踪文件中的每一行使用文档注释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 - 签名中包含默认参数值
Shipped/Unshipped Lifecycle
已发布/未发布生命周期
The workflow across release cycles:
During development (between releases):
- Add new public API member to source code
- RS0016 fires -- member not tracked
- Use code fix or manually add to
PublicAPI.Unshipped.txt - RS0016 clears
At release time:
- Move all entries from to
PublicAPI.Unshipped.txtPublicAPI.Shipped.txt - Clear back to just the
PublicAPI.Unshipped.txtheader#nullable enable - Commit both files as part of the release PR
- Tag the release
When removing a previously shipped API (major version):
- Remove the member from source code
- Remove the entry from
PublicAPI.Shipped.txt - RS0017 clears (if it fired)
- Document the removal in release notes
When removing an unshipped API (before release):
- Remove the member from source code
- Remove the entry from
PublicAPI.Unshipped.txt - No SemVer impact -- the API was never released
跨发布周期的工作流:
开发期间(发布间隔期):
- 在源代码中添加新的公共API成员
- RS0016触发——成员未被跟踪
- 使用代码修复或手动添加到
PublicAPI.Unshipped.txt - RS0016警告消除
发布时:
- 将中的所有条目移动到
PublicAPI.Unshipped.txtPublicAPI.Shipped.txt - 将重置为仅保留
PublicAPI.Unshipped.txt头部#nullable enable - 将两个文件作为发布PR的一部分提交
- 标记发布版本
移除已发布API时(主版本升级):
- 从源代码中移除成员
- 从中删除条目
PublicAPI.Shipped.txt - RS0017警告消除(如果已触发)
- 在发布说明中记录移除操作
移除未发布API时(发布前):
- 从源代码中移除成员
- 从中删除条目
PublicAPI.Unshipped.txt - 无SemVer影响——该API从未发布过
Multi-TFM Projects
多TFM项目
For multi-targeted projects, PublicApiAnalyzers supports per-TFM tracking files when the API surface differs across targets:
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 APIsThe shared files contain APIs common to all TFMs. The TFM-specific files contain APIs that only exist on that target. The analyzer merges them at build time.
To enable per-TFM files, add to the :
.csprojxml
<PropertyGroup>
<RoslynPublicApiPerTfm>true</RoslynPublicApiPerTfm>
</PropertyGroup>See [skill:dotnet-multi-targeting] for multi-TFM packaging mechanics.
对于多目标框架(TFM)项目,当不同目标的API表面不同时,PublicApiAnalyzers支持按TFM跟踪文件:
MyLib/
MyLib.csproj
PublicAPI.Shipped.txt # 所有TFM共享的API
PublicAPI.Unshipped.txt # 所有TFM共享的API
PublicAPI.Shipped.net8.0.txt # net8.0专属API
PublicAPI.Unshipped.net8.0.txt # net8.0专属API
PublicAPI.Shipped.net10.0.txt # net10.0专属API
PublicAPI.Unshipped.net10.0.txt # net10.0专属API共享文件包含所有TFM通用的API。TFM专属文件仅包含该目标下存在的API。分析器在构建时会合并这些文件。
要启用按TFM跟踪文件,在中添加:
.csprojxml
<PropertyGroup>
<RoslynPublicApiPerTfm>true</RoslynPublicApiPerTfm>
</PropertyGroup>多TFM打包机制详见[skill:dotnet-multi-targeting]。
Integrating with CI
与CI集成
PublicApiAnalyzers runs as part of the standard build. To enforce it in CI, ensure warnings are treated as errors for the RS-series rules:
xml
<!-- In Directory.Build.props or the library .csproj -->
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);RS0016;RS0017;RS0036;RS0037</WarningsAsErrors>
</PropertyGroup>This gates CI builds on any undeclared public API changes. Developers must explicitly update the tracking files before the build passes.
PublicApiAnalyzers作为标准构建的一部分运行。要在CI中强制执行,需将RS系列规则的警告视为错误:
xml
<!-- 在Directory.Build.props或库的.csproj中 -->
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);RS0016;RS0017;RS0036;RS0037</WarningsAsErrors>
</PropertyGroup>这会将CI构建与任何未声明的公共API变更绑定。开发人员必须在构建通过前明确更新跟踪文件。
Verify API Surface Snapshot Pattern
Verify API表面快照模式
Use the Verify library to snapshot-test the entire public API surface of an assembly. This approach uses reflection to enumerate all public types and members, producing a human-readable snapshot that is committed to source control and compared on every test run. Any change to the public API surface causes a test failure until the snapshot is explicitly approved.
This pattern complements PublicApiAnalyzers -- the analyzer catches changes at build time within the project, while the Verify snapshot catches changes from the perspective of a compiled assembly consumer.
For Verify fundamentals (setup, scrubbing, converters, diff tool integration, CI configuration), see [skill:dotnet-snapshot-testing].
使用Verify库对程序集的整个公共API表面进行快照测试。这种方法使用反射枚举所有公共类型和成员,生成易于阅读的快照并提交到源代码管理,每次测试运行时都会进行比较。公共API表面的任何变更都会导致测试失败,直到快照被明确批准。
此模式与PublicApiAnalyzers互补——分析器在项目构建时捕获变更,而Verify快照从编译后程序集使用者的角度捕获变更。
Verify基础(设置、清理、转换器、差异工具集成、CI配置)详见[skill:dotnet-snapshot-testing]。
Extracting the Public API Surface
提取公共API表面
Create a helper method that reflects over an assembly to produce a stable, sorted representation of all public types and their members:
csharp
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);
}
}创建一个辅助方法,通过反射遍历程序集,生成所有公共类型及其成员的稳定、有序表示:
csharp
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);
}
}Writing the Snapshot Test
编写快照测试
csharp
[UsesVerify]
public class PublicApiSurfaceTests
{
[Fact]
public Task PublicApi_ShouldMatchApprovedSurface()
{
var assembly = typeof(Widget).Assembly;
var publicApi = PublicApiExtractor.GetPublicApi(assembly);
return Verify(publicApi);
}
}On first run, this creates a file containing the full public API listing. Subsequent runs compare the current API surface against the approved snapshot. Any addition, removal, or modification of public members causes a test failure with a clear diff.
.verified.txtcsharp
[UsesVerify]
public class PublicApiSurfaceTests
{
[Fact]
public Task PublicApi_ShouldMatchApprovedSurface()
{
var assembly = typeof(Widget).Assembly;
var publicApi = PublicApiExtractor.GetPublicApi(assembly);
return Verify(publicApi);
}
}首次运行时,会创建一个包含完整公共API列表的文件。后续运行会将当前API表面与已批准的快照进行比较。公共成员的任何添加、删除或修改都会导致测试失败,并显示清晰的差异。
.verified.txtReviewing API Surface Changes
审核API表面变更
When the snapshot test fails:
- Verify generates a file showing the new API surface
.received.txt - Diff the against
.received.txtto review changes.verified.txt - If the changes are intentional, accept the new snapshot with
verify accept - If the changes are accidental, revert the code changes
This creates a code-review checkpoint where every public API change must be explicitly approved by someone reviewing the snapshot diff in the pull request.
当快照测试失败时:
- Verify会生成一个显示新API表面的文件
.received.txt - 对比与
.received.txt以查看变更.verified.txt - 如果变更是有意的,使用接受新快照
verify accept - 如果变更是意外的,回滚代码变更
这创建了一个代码审核检查点,每个公共API变更都必须由审核拉取请求中快照差异的人员明确批准。
Combining with PublicApiAnalyzers
与PublicApiAnalyzers结合使用
The two approaches serve different purposes:
| Concern | PublicApiAnalyzers | Verify Snapshot |
|---|---|---|
| Detection timing | Build time (in-IDE) | Test time (post-compile) |
| Granularity | Per-member signatures | Assembly-wide surface |
| Nullable annotations | Tracked via | Requires explicit reflection |
| Approval workflow | Edit text files (shipped/unshipped) | Accept snapshot diffs |
| Multi-TFM | Per-TFM files | Per-TFM test targets |
| CI gating | Warnings-as-errors | Test failures |
Use both for maximum coverage: PublicApiAnalyzers catches changes during development, while Verify snapshots provide an end-to-end assembly-level validation in the test suite.
两种方法各有侧重:
| 关注点 | PublicApiAnalyzers | Verify快照 |
|---|---|---|
| 检测时机 | 构建时(IDE内) | 测试时(编译后) |
| 粒度 | 按成员签名 | 程序集级表面 |
| 可空注解 | 通过 | 需要显式反射处理 |
| 批准流程 | 编辑文本文件(已发布/未发布) | 接受快照差异 |
| 多TFM支持 | 按TFM文件 | 按TFM测试目标 |
| CI管控 | 警告视为错误 | 测试失败 |
同时使用两种方法以获得最大覆盖范围:PublicApiAnalyzers在开发过程中捕获变更,而Verify快照在测试套件中提供端到端的程序集级验证。
ApiCompat CI Enforcement
ApiCompat CI强制执行
ApiCompat compares two assemblies (or a baseline NuGet package against the current build) and reports API differences. When integrated into CI, it gates pull requests on API surface changes -- any breaking change produces a build error that the author must explicitly acknowledge.
For basics and suppression file mechanics, see [skill:dotnet-nuget-authoring] and [skill:dotnet-multi-targeting].
EnablePackageValidationApiCompat比较两个程序集(或基线NuGet包与当前构建)并报告API差异。集成到CI后,它会通过API表面变更管控拉取请求——任何破坏性变更都会产生构建错误,作者必须明确确认。
EnablePackageValidationPackage Validation in CI
CI中的包验证
The simplest enforcement uses during :
EnablePackageValidationdotnet packxml
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>In a CI pipeline, runs package validation automatically:
dotnet packyaml
undefined最简单的强制执行方式是在期间使用:
dotnet packEnablePackageValidationxml
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>在CI流水线中,会自动运行包验证:
dotnet packyaml
undefinedGitHub Actions -- gate PRs on API compatibility
GitHub Actions -- 通过API兼容性管控PR
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 detectedundefinedname: 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: 还原依赖
run: dotnet restore
- name: 构建
run: dotnet build --configuration Release --no-restore
- name: 打包并进行API验证
run: dotnet pack --configuration Release --no-build
# EnablePackageValidation在打包期间运行,若检测到破坏性变更则构建失败undefinedStandalone ApiCompat Tool for Assembly Comparison
用于程序集比较的独立ApiCompat工具
When you need to compare assemblies without packing (e.g., comparing a feature branch build against the main branch build), use the standalone ApiCompat tool:
yaml
undefined当你需要在不打包的情况下比较程序集(例如,比较特性分支构建与主分支构建),可使用独立的ApiCompat工具:
yaml
undefinedGitHub Actions -- compare assemblies directly
GitHub Actions -- 直接比较程序集
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.dllundefinedname: 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: 安装ApiCompat工具
run: dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
- name: 构建当前分支
run: dotnet build src/MyLib/MyLib.csproj -c Release -o artifacts/current
- name: 构建基线(主分支)
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: 比较API
run: |
apicompat --left-assembly artifacts/baseline/MyLib.dll \
--right-assembly artifacts/current/MyLib.dllundefinedPR Labeling for API Changes
为API变更添加PR标签
Combine ApiCompat with PR labeling to surface API changes to reviewers:
yaml
- 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 }}将ApiCompat与PR标签结合,向审核者展示API变更:
yaml
- name: 检查API变更
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: 为PR添加API变更标签
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 }}Handling Intentional Breaking Changes
处理有意的破坏性变更
When a breaking change is intentional (new major version), generate a suppression file:
bash
dotnet pack /p:GenerateCompatibilitySuppressionFile=trueThis creates in the project directory. Reference it explicitly if stored elsewhere:
CompatibilitySuppressions.xmlxml
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>Note: is an ItemGroup item, not a PropertyGroup property. Using PropertyGroup syntax silently does nothing.
ApiCompatSuppressionFileThe suppression file documents the specific breaking changes that are accepted:
xml
<?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>Commit suppression files to source control. Reviewers can inspect the file to verify that breaking changes are documented and intentional.
当破坏性变更是有意的(主版本升级),生成抑制文件:
bash
dotnet pack /p:GenerateCompatibilitySuppressionFile=true这会在项目目录中创建。如果文件存储在其他位置,需显式引用:
CompatibilitySuppressions.xmlxml
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>注意:是ItemGroup项,而非PropertyGroup属性。使用PropertyGroup语法会被静默忽略,抑制将不起作用。
ApiCompatSuppressionFile抑制文件记录了被接受的具体破坏性变更:
xml
<?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>将抑制文件提交到源代码管理。审核者可查看该文件,验证破坏性变更是否已记录且是有意的。
Enforcing PublicApiAnalyzers Files in CI
在CI中强制执行PublicApiAnalyzers文件
Combine PublicApiAnalyzers warnings-as-errors with a CI step that verifies tracking files are not stale:
yaml
- 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将PublicApiAnalyzers警告视为错误,并添加CI步骤验证跟踪文件未过时:
yaml
- name: 启用API跟踪强制的构建
run: dotnet build -c Release /p:TreatWarningsAsErrors=true /warnaserror:RS0016,RS0017,RS0036,RS0037
- name: 验证PublicAPI文件已提交
run: |
if git diff --name-only | grep -q 'PublicAPI'; then
echo "::error::PublicAPI跟踪文件存在未提交变更"
git diff -- '**/PublicAPI.*.txt'
exit 1
fiMulti-Library Monorepo Enforcement
多库单体仓库强制执行
For repositories with multiple libraries, apply API validation at the solution level:
xml
<!-- Directory.Build.props -- applied to all library projects -->
<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>This ensures every packable project in the repository has both PublicApiAnalyzers and package validation enabled without duplicating configuration.
对于包含多个库的仓库,在解决方案级别应用API验证:
xml
<!-- Directory.Build.props -- 应用于所有库项目 -->
<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 Gotchas
常见陷阱
- Do not forget to create both and
PublicAPI.Shipped.txt-- PublicApiAnalyzers requires both files to exist, even if empty. Missing files cause RS0037 warnings on every public member.PublicAPI.Unshipped.txt - Do not omit the header from PublicAPI tracking files -- without it (RS0036), the analyzer ignores nullable annotation differences, missing real API surface changes in nullable-enabled libraries.
#nullable enable - Do not put in a PropertyGroup -- it is an ItemGroup item (
ApiCompatSuppressionFile). PropertyGroup syntax is silently ignored, and suppression will not work.<ApiCompatSuppressionFile Include="..." /> - Do not move entries from to
PublicAPI.Unshipped.txtmid-development -- move entries only at release time. Premature shipping makes it impossible to cleanly revert unreleased API additions.PublicAPI.Shipped.txt - Do not use the Verify API surface snapshot as the sole validation mechanism -- it runs at test time, after compilation. Use PublicApiAnalyzers for immediate build-time feedback and ApiCompat for baseline comparison; add Verify snapshots as an additional safety net.
- Do not hardcode TFM-specific paths in CI ApiCompat workflows -- use MSBuild output path variables or parameterize the TFM to avoid breakage when TFMs are added or changed.
- Do not suppress RS0016 globally with -- this silently disables all public API tracking. Instead, add the missing API entries to the tracking files. If an API is intentionally internal but must be
<NoWarn>(e.g., forpublicalternatives), useInternalsVisibleToand add it to the tracking files.[EditorBrowsable(EditorBrowsableState.Never)] - Do not generate the suppression file with and forget to review it -- the file may suppress more changes than intended. Always review the generated XML before committing.
GenerateCompatibilitySuppressionFile=true
- 不要忘记创建和
PublicAPI.Shipped.txt两个文件——PublicApiAnalyzers要求两个文件都存在,即使为空。缺少文件会导致每个公共成员都触发RS0037警告。PublicAPI.Unshipped.txt - 不要省略PublicAPI跟踪文件中的头部——没有该头部(RS0036),分析器会忽略可空注解差异,遗漏启用可空的库中真实的API表面变更。
#nullable enable - 不要将放在PropertyGroup中——它是ItemGroup项(
ApiCompatSuppressionFile)。PropertyGroup语法会被静默忽略,抑制将不起作用。<ApiCompatSuppressionFile Include="..." /> - 不要在开发过程中将条目从移动到
PublicAPI.Unshipped.txt——仅在发布时移动条目。过早标记为已发布会导致无法干净地回滚未发布的API添加。PublicAPI.Shipped.txt - 不要仅使用Verify API表面快照作为唯一验证机制——它在测试时运行,即编译后。使用PublicApiAnalyzers获取即时的构建时反馈,使用ApiCompat进行基线比较;添加Verify快照作为额外的安全保障。
- 不要在CI ApiCompat工作流中硬编码TFM特定路径——使用MSBuild输出路径变量或参数化TFM,避免添加或更改TFM时出现中断。
- 不要使用全局抑制RS0016——这会静默禁用所有公共API跟踪。相反,将缺失的API条目添加到跟踪文件中。如果某个API必须是
<NoWarn>但有意作为内部使用(例如,替代public),使用InternalsVisibleTo并将其添加到跟踪文件中。[EditorBrowsable(EditorBrowsableState.Never)] - 不要使用生成抑制文件后忘记审核——该文件可能抑制了超出预期的变更。提交前务必查看生成的XML。
GenerateCompatibilitySuppressionFile=true
Prerequisites
前置条件
- .NET 8.0+ SDK
- NuGet package (for RS0016/RS0017 diagnostics)
Microsoft.CodeAnalysis.PublicApiAnalyzers - MSBuild property (for baseline API comparison during
EnablePackageValidation)dotnet pack - (optional, for standalone assembly comparison outside of
Microsoft.DotNet.ApiCompat.Tool)dotnet pack - Verify test library and test framework integration package (for API surface snapshot testing) -- see [skill:dotnet-snapshot-testing] for setup
- Understanding of binary vs source compatibility rules -- see [skill:dotnet-library-api-compat]
- .NET 8.0+ SDK
- NuGet包(用于RS0016/RS0017诊断)
Microsoft.CodeAnalysis.PublicApiAnalyzers - MSBuild属性(用于
EnablePackageValidation期间的基线API比较)dotnet pack - (可选,用于
Microsoft.DotNet.ApiCompat.Tool之外的独立程序集比较)dotnet pack - Verify测试库和测试框架集成包(用于API表面快照测试)——设置详见[skill:dotnet-snapshot-testing]
- 了解二进制与源代码兼容性规则——详见[skill:dotnet-library-api-compat]