dotnet-api-surface-validation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-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
Microsoft.CodeAnalysis.Analyzers
or standalone
Microsoft.CodeAnalysis.PublicApiAnalyzers
). ApiCompat tooling included in .NET 8+ SDK.
Out of scope: Binary vs source compatibility rules, type forwarders, SemVer impact -- see [skill:dotnet-library-api-compat]. NuGet packaging,
EnablePackageValidation
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].
Cross-references: [skill:dotnet-library-api-compat] for binary/source compatibility rules, [skill:dotnet-nuget-authoring] for
EnablePackageValidation
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.

用于验证和跟踪.NET库公共API表面的工具与工作流。涵盖三种互补方法:PublicApiAnalyzers(通过Roslyn诊断跟踪已发布/未发布API的文本文件方式)、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

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
.csproj
):
MyLib/
  MyLib.csproj
  PublicAPI.Shipped.txt    # APIs shipped in released versions
  PublicAPI.Unshipped.txt  # APIs added since last release
Both files must exist, even if empty. Each must contain a header comment:
#nullable enable
The
#nullable enable
header tells the analyzer to track nullable annotations in API signatures. Without it, nullable context differences are ignored.
安装分析器包:
xml
<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.*" PrivateAssets="all" />
</ItemGroup>
在项目根目录(与
.csproj
同级)创建两个跟踪文件:
MyLib/
  MyLib.csproj
  PublicAPI.Shipped.txt    # 已发布版本中的API
  PublicAPI.Unshipped.txt  # 上次发布后新增的API
两个文件必须存在,即使为空。每个文件都必须包含头部注释:
#nullable enable
#nullable enable
头部告知分析器跟踪API签名中的可空注解。如果没有该头部,可空上下文的差异将被忽略。

Diagnostic Rules

诊断规则

RuleSeverityMeaning
RS0016WarningPublic API member not declared in API tracking files
RS0017WarningPublic API member removed but still in tracking files
RS0024WarningPublic API member has wrong nullable annotation
RS0025WarningPublic API symbol marked shipped but has changed signature
RS0026WarningNew public API added without
PublicAPI.Unshipped.txt
entry
RS0036WarningAPI file missing
#nullable enable
header
RS0037WarningPublic API declared but does not exist in source
RS0016 is the most common diagnostic. When you add a new
public
or
protected
member, RS0016 fires until you add the member's signature to
PublicAPI.Unshipped.txt
. Use the code fix (lightbulb) in the IDE to automatically add the entry.
RS0017 fires when you remove or rename a
public
member but the old signature still exists in the tracking files. Remove the stale line from the appropriate file.
规则严重级别含义
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中的代码修复(小灯泡图标)自动添加条目。
RS0017会在你移除或重命名
public
成员,但旧签名仍存在于跟踪文件中时触发。从相应文件中删除过时的行即可。

File 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 -> void
Key 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
    .get
    and
    .set
    entries
  • 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):
  1. Add new public API member to source code
  2. RS0016 fires -- member not tracked
  3. Use code fix or manually add to
    PublicAPI.Unshipped.txt
  4. RS0016 clears
At release time:
  1. Move all entries from
    PublicAPI.Unshipped.txt
    to
    PublicAPI.Shipped.txt
  2. Clear
    PublicAPI.Unshipped.txt
    back to just the
    #nullable enable
    header
  3. Commit both files as part of the release PR
  4. Tag the release
When removing a previously shipped API (major version):
  1. Remove the member from source code
  2. Remove the entry from
    PublicAPI.Shipped.txt
  3. RS0017 clears (if it fired)
  4. Document the removal in release notes
When removing an unshipped API (before release):
  1. Remove the member from source code
  2. Remove the entry from
    PublicAPI.Unshipped.txt
  3. No SemVer impact -- the API was never released
跨发布周期的工作流:
开发期间(发布间隔期):
  1. 在源代码中添加新的公共API成员
  2. RS0016触发——成员未被跟踪
  3. 使用代码修复或手动添加到
    PublicAPI.Unshipped.txt
  4. RS0016警告消除
发布时:
  1. PublicAPI.Unshipped.txt
    中的所有条目移动到
    PublicAPI.Shipped.txt
  2. PublicAPI.Unshipped.txt
    重置为仅保留
    #nullable enable
    头部
  3. 将两个文件作为发布PR的一部分提交
  4. 标记发布版本
移除已发布API时(主版本升级):
  1. 从源代码中移除成员
  2. PublicAPI.Shipped.txt
    中删除条目
  3. RS0017警告消除(如果已触发)
  4. 在发布说明中记录移除操作
移除未发布API时(发布前):
  1. 从源代码中移除成员
  2. PublicAPI.Unshipped.txt
    中删除条目
  3. 无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 APIs
The 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
.csproj
:
xml
<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跟踪文件,在
.csproj
中添加:
xml
<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
.verified.txt
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.
csharp
[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表面与已批准的快照进行比较。公共成员的任何添加、删除或修改都会导致测试失败,并显示清晰的差异。

Reviewing API Surface Changes

审核API表面变更

When the snapshot test fails:
  1. Verify generates a
    .received.txt
    file showing the new API surface
  2. Diff the
    .received.txt
    against
    .verified.txt
    to review changes
  3. If the changes are intentional, accept the new snapshot with
    verify accept
  4. 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.
当快照测试失败时:
  1. Verify会生成一个显示新API表面的
    .received.txt
    文件
  2. 对比
    .received.txt
    .verified.txt
    以查看变更
  3. 如果变更是有意的,使用
    verify accept
    接受新快照
  4. 如果变更是意外的,回滚代码变更
这创建了一个代码审核检查点,每个公共API变更都必须由审核拉取请求中快照差异的人员明确批准。

Combining with PublicApiAnalyzers

与PublicApiAnalyzers结合使用

The two approaches serve different purposes:
ConcernPublicApiAnalyzersVerify Snapshot
Detection timingBuild time (in-IDE)Test time (post-compile)
GranularityPer-member signaturesAssembly-wide surface
Nullable annotationsTracked via
#nullable enable
Requires explicit reflection
Approval workflowEdit text files (shipped/unshipped)Accept snapshot diffs
Multi-TFMPer-TFM filesPer-TFM test targets
CI gatingWarnings-as-errorsTest 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.

两种方法各有侧重:
关注点PublicApiAnalyzersVerify快照
检测时机构建时(IDE内)测试时(编译后)
粒度按成员签名程序集级表面
可空注解通过
#nullable enable
跟踪
需要显式反射处理
批准流程编辑文本文件(已发布/未发布)接受快照差异
多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
EnablePackageValidation
basics and suppression file mechanics, see [skill:dotnet-nuget-authoring] and [skill:dotnet-multi-targeting].
ApiCompat比较两个程序集(或基线NuGet包与当前构建)并报告API差异。集成到CI后,它会通过API表面变更管控拉取请求——任何破坏性变更都会产生构建错误,作者必须明确确认。
EnablePackageValidation
基础用法和抑制文件机制详见[skill:dotnet-nuget-authoring]和[skill:dotnet-multi-targeting]。

Package Validation in CI

CI中的包验证

The simplest enforcement uses
EnablePackageValidation
during
dotnet pack
:
xml
<PropertyGroup>
  <EnablePackageValidation>true</EnablePackageValidation>
  <PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>
In a CI pipeline,
dotnet pack
runs package validation automatically:
yaml
undefined
最简单的强制执行方式是在
dotnet pack
期间使用
EnablePackageValidation
xml
<PropertyGroup>
  <EnablePackageValidation>true</EnablePackageValidation>
  <PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>
在CI流水线中,
dotnet pack
会自动运行包验证:
yaml
undefined

GitHub 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 detected
undefined
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: 还原依赖
    run: dotnet restore

  - name: 构建
    run: dotnet build --configuration Release --no-restore

  - name: 打包并进行API验证
    run: dotnet pack --configuration Release --no-build
    # EnablePackageValidation在打包期间运行,若检测到破坏性变更则构建失败
undefined

Standalone 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
undefined

GitHub 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.dll
undefined
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: 安装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.dll
undefined

PR 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=true
This creates
CompatibilitySuppressions.xml
in the project directory. Reference it explicitly if stored elsewhere:
xml
<ItemGroup>
  <ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
Note:
ApiCompatSuppressionFile
is an ItemGroup item, not a PropertyGroup property. Using PropertyGroup syntax silently does nothing.
The 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.xml
。如果文件存储在其他位置,需显式引用:
xml
<ItemGroup>
  <ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
注意:
ApiCompatSuppressionFile
ItemGroup项,而非PropertyGroup属性。使用PropertyGroup语法会被静默忽略,抑制将不起作用。
抑制文件记录了被接受的具体破坏性变更:
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
          fi

Multi-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

常见陷阱

  1. Do not forget to create both
    PublicAPI.Shipped.txt
    and
    PublicAPI.Unshipped.txt
    -- PublicApiAnalyzers requires both files to exist, even if empty. Missing files cause RS0037 warnings on every public member.
  2. Do not omit the
    #nullable enable
    header from PublicAPI tracking files
    -- without it (RS0036), the analyzer ignores nullable annotation differences, missing real API surface changes in nullable-enabled libraries.
  3. Do not put
    ApiCompatSuppressionFile
    in a PropertyGroup
    -- it is an ItemGroup item (
    <ApiCompatSuppressionFile Include="..." />
    ). PropertyGroup syntax is silently ignored, and suppression will not work.
  4. Do not move entries from
    PublicAPI.Unshipped.txt
    to
    PublicAPI.Shipped.txt
    mid-development
    -- move entries only at release time. Premature shipping makes it impossible to cleanly revert unreleased API additions.
  5. 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.
  6. 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.
  7. Do not suppress RS0016 globally with
    <NoWarn>
    -- 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
    public
    (e.g., for
    InternalsVisibleTo
    alternatives), use
    [EditorBrowsable(EditorBrowsableState.Never)]
    and add it to the tracking files.
  8. Do not generate the suppression file with
    GenerateCompatibilitySuppressionFile=true
    and forget to review it
    -- the file may suppress more changes than intended. Always review the generated XML before committing.

  1. 不要忘记创建
    PublicAPI.Shipped.txt
    PublicAPI.Unshipped.txt
    两个文件
    ——PublicApiAnalyzers要求两个文件都存在,即使为空。缺少文件会导致每个公共成员都触发RS0037警告。
  2. 不要省略PublicAPI跟踪文件中的
    #nullable enable
    头部
    ——没有该头部(RS0036),分析器会忽略可空注解差异,遗漏启用可空的库中真实的API表面变更。
  3. 不要将
    ApiCompatSuppressionFile
    放在PropertyGroup中
    ——它是ItemGroup项(
    <ApiCompatSuppressionFile Include="..." />
    )。PropertyGroup语法会被静默忽略,抑制将不起作用。
  4. 不要在开发过程中将条目从
    PublicAPI.Unshipped.txt
    移动到
    PublicAPI.Shipped.txt
    ——仅在发布时移动条目。过早标记为已发布会导致无法干净地回滚未发布的API添加。
  5. 不要仅使用Verify API表面快照作为唯一验证机制——它在测试时运行,即编译后。使用PublicApiAnalyzers获取即时的构建时反馈,使用ApiCompat进行基线比较;添加Verify快照作为额外的安全保障。
  6. 不要在CI ApiCompat工作流中硬编码TFM特定路径——使用MSBuild输出路径变量或参数化TFM,避免添加或更改TFM时出现中断。
  7. 不要使用
    <NoWarn>
    全局抑制RS0016
    ——这会静默禁用所有公共API跟踪。相反,将缺失的API条目添加到跟踪文件中。如果某个API必须是
    public
    但有意作为内部使用(例如,替代
    InternalsVisibleTo
    ),使用
    [EditorBrowsable(EditorBrowsableState.Never)]
    并将其添加到跟踪文件中。
  8. 不要使用
    GenerateCompatibilitySuppressionFile=true
    生成抑制文件后忘记审核
    ——该文件可能抑制了超出预期的变更。提交前务必查看生成的XML。

Prerequisites

前置条件

  • .NET 8.0+ SDK
  • Microsoft.CodeAnalysis.PublicApiAnalyzers
    NuGet package (for RS0016/RS0017 diagnostics)
  • EnablePackageValidation
    MSBuild property (for baseline API comparison during
    dotnet pack
    )
  • Microsoft.DotNet.ApiCompat.Tool
    (optional, for standalone assembly comparison outside of
    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
  • Microsoft.CodeAnalysis.PublicApiAnalyzers
    NuGet包(用于RS0016/RS0017诊断)
  • EnablePackageValidation
    MSBuild属性(用于
    dotnet pack
    期间的基线API比较)
  • Microsoft.DotNet.ApiCompat.Tool
    (可选,用于
    dotnet pack
    之外的独立程序集比较)
  • Verify测试库和测试框架集成包(用于API表面快照测试)——设置详见[skill:dotnet-snapshot-testing]
  • 了解二进制与源代码兼容性规则——详见[skill:dotnet-library-api-compat]

References

参考资料