首页
/ ASP.NET Core API Baselines:PublicAPI 分析器与 PublicAPI.Shipped/Unshipped.txt 工作流详解

ASP.NET Core API Baselines:PublicAPI 分析器与 PublicAPI.Shipped/Unshipped.txt 工作流详解

2026-09-05 17:54:45作者:郦嵘贵Just

在大型 .NET 框架仓库中,每一次公共 API 的新增、修改或删除都可能悄悄破坏下游用户的二进制兼容性。ASP.NET Core 仓库通过 Roslyn 的 Public API Analyzers 配合 PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt 两个基线文件,把"API 变更"从隐性风险变成了 PR 中一目了然、必须显式记录的显式条目。读完本文,你将能够理解这两个基线文件各自的职责、如何在项目中正确启用或禁用该检查、如何为新项目初始化基线文件,并掌握新增、移除、更新 API 时逐条维护基线的完整操作步骤。

基线机制概述

本文内容基于仓库官方文档 API Baselines。该文档说明:API 基线文件本身由 Microsoft.CodeAnalysis.PublicApiAnalyzers 这一 Roslyn 分析器包实现,分析器的完整文档以 .NET 官方的 roslyn-analyzers 仓库中 PublicApiAnalyzers.Help.mdMicrosoft.CodeAnalysis.PublicApiAnalyzers.md 为准(见 docs/APIBaselines.md 中的外部引用)。仓库内维护基线文件的惯例如下:

  • PublicAPI.Shipped.txt:记录上一个主版本中已经发布(shipped)的全部公共 API。该文件只允许由构建团队在主版本发布之后通过脚本修改,其他任何情况下都不应手动修改
  • PublicAPI.Unshipped.txt:记录自上一个主版本以来新增、修改、删除的 API 条目。日常开发中所有 API 变更都落在这个文件里。

这种"已发布/未发布"分文件的设计,使得 PR diff 中出现的每一条基线变更都代表一次真实的 API 表面变动,审阅者可以逐条审查兼容性影响。

仓库中的启用机制:默认开启 + 属性开关

从源码结构看,仓库并不是靠每个项目逐个声明来开启基线检查的。在 eng/targets/CSharp.Common.targets 中可以看到默认行为:

<!-- Ensure API changes show up clearly in PRs. -->
<AddPublicApiAnalyzers Condition=" '$(AddPublicApiAnalyzers)' == '' AND
    '$(IsImplementationProject)' == 'true' AND
    ! $(RepoRelativeProjectDir.Contains('Tools')) ">true</AddPublicApiAnalyzers>
<AddPublicApiAnalyzers Condition=" '$(AddPublicApiAnalyzers)' == '' ">false</AddPublicApiAnalyzers>

也就是说:只要项目被标记为实现项目(IsImplementationProject == true,通常即 src 目录下的实现工程)且不在 Tools 目录下,基线检查就默认启用——这正是 docs/APIBaselines.md 中"API baseline are enabled by default"这句话的工程来源。分析器引用也在这里注入:

<ItemGroup Condition=" '$(DotNetBuildSourceOnly)' != 'true' AND $(AddPublicApiAnalyzers) ">
  <Reference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" ExcludeAssets="Compile" PrivateAssets="All" />
</ItemGroup>

所引入的分析器版本由 eng/Versions.props 中的 MicrosoftCodeAnalysisPublicApiAnalyzersVersion(当前仓库为 3.3.3)统一管理。

文档给出的禁用方式与上述属性一一对应:如果新项目是非发布(non-shipping)或纯测试项目,在项目文件中加入:

<AddPublicApiAnalyzers>false</AddPublicApiAnalyzers>

即可关闭检查。另外还有一个值得注意的保护逻辑:若某个项目目录下存在 PublicAPI.Shipped.txtAddPublicApiAnalyzers 又被关闭,构建会输出 "Public API baseline files ignored." 的警告(同样是 eng/targets/CSharp.Common.targets_CheckIgnoredPublicApiFiles 目标),提示你基线文件被静默忽略了。

为新项目添加基线文件

创建新的 src 实现项目后,必须手动添加基线文件。仓库提供了空模板 eng/PublicAPI.empty.txt,其内容只有一行 #nullable enable(开启基线文件的可空性感知,使后续条目可以携带 !? 等可空性标注)。按 docs/APIBaselines.md 的步骤操作(文档中使用的是 Windows PowerShell 命令):

cp .\eng\PublicAPI.empty.txt {new folder}\PublicAPI.Shipped.txt
cp .\eng\PublicAPI.empty.txt {new folder}\PublicAPI.Unshipped.txt

在 Linux/macOS 环境下等价写法为:

cp eng/PublicAPI.empty.txt {new folder}/PublicAPI.Shipped.txt
cp eng/PublicAPI.empty.txt {new folder}/PublicAPI.Unshipped.txt

创建完成后,即可按后文"添加和更新 API 的步骤"把新 API 补进 PublicAPI.Unshipped.txt

一个容易被忽略的细节:在 eng/targets/CSharp.Common.targets 中,AddPublicApiAnalyzers 只对实现项目生效,而 _IsSrcProject 的定义还纳入了分析器项目与规范测试项目,同时会把这些项目默认置为 Nullable enable——基线条目中常见的 string!Task<T!>! 写法正是由此而来。

基线条目的实际形态:以 OpenApi 为例

docs/APIBaselines.md 中的示例比较抽象,对照仓库真实文件会更直观。例如 src/OpenApi/src/PublicAPI.Shipped.txt 中已发布的 API 条目形如:

#nullable enable
Microsoft.AspNetCore.OpenApi.IOpenApiDocumentProvider
Microsoft.AspNetCore.OpenApi.IOpenApiDocumentProvider.GetOpenApiDocumentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.OpenApi.OpenApiDocument!>!
static Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! documentName, System.Action<Microsoft.AspNetCore.OpenApi.OpenApiOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

src/OpenApi/src/PublicAPI.Unshipped.txt 则展示了"未发布变更"的真实样例——主版本开发期间新加入的 API 就停在这里:

#nullable enable
Microsoft.AspNetCore.OpenApi.IAdditionalOpenApiDocumentNameResolver
Microsoft.AspNetCore.OpenApi.IAdditionalOpenApiDocumentNameResolver.ResolveDocumentNames() -> System.Collections.Generic.IEnumerable<string!>!
static Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApiCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

此外,对多目标框架(multi-targeting)项目,分析器支持按 TFM 分目录存放基线。例如 src/DataProtection/DataProtection/src/PublicAPI/net11.0/PublicAPI.Shipped.txt 与同目录下的 netstandard2.0net462 子目录各自维护一套基线,说明当不同目标框架的 API 表面不同时(如平台条件特性),可以按 TFM 独立管理。src/StaticAssets/src/PublicAPI.Shipped.txt 等单目标项目则直接使用项目根目录下的两个文件。

PublicAPI.Unshipped.txt 的三种变更写法

docs/APIBaselines.md,任何 API 变更都要在 PublicAPI.Unshipped.txt新增条目(注意:不是修改旧条目,因为旧条目可能已在 Shipped 文件中或属于本版本内更早的 Unshipped 条目)。

新增 API(New APIs)

为每个新 API 增加一条目。文档示例:

#nullable enable
Microsoft.AspNetCore.Builder.NewApplicationBuilder.New() -> Microsoft.AspNetCore.Builder.IApplicationBuilder!

移除 API(Removed APIs)

被移除的 API 不能直接删掉旧条目,而要新增一条以 *REMOVED* 为前缀的条目来"抵消"原基线:

#nullable enable
*REMOVED*Microsoft.Builder.OldApplicationBuilder.New() -> Microsoft.AspNetCore.Builder.IApplicationBuilder!

更新 API(Updated APIs)

API 签名变化(包括从源码结构看属于同一类型的"变为可空感知"变化)需要两条新条目:一条 *REMOVED* 删除旧签名,一条写入新签名。文档示例展示了一个接口属性从非空变为可空的情形:

#nullable enable
*REMOVED*Microsoft.AspNetCore.DataProtection.Infrastructure.IApplicationDiscriminator.Discriminator.get -> string!
Microsoft.AspNetCore.DataProtection.Infrastructure.IApplicationDiscriminator.Discriminator.get -> string?

添加和更新 API 的完整操作步骤

docs/APIBaselines.md 给出的标准流程是"让分析器报错、用快速修复补条目",而非手写文本:

  1. 如需要,把新项目加入 AspNetCore.sln 及相关的 *.slnf 解决方案筛选文件(仓库根目录为 AspNetCore.slnx,各模块目录如 src/OpenApi/OpenApi.slnf 提供过滤入口)。
  2. 运行所在模块目录的 startvs.cmd,例如 src/OpenApi/startvs.cmd(其内容即调用仓库根 startvs.cmd 并加载本模块的 OpenApi.slnf)。
  3. 按 F6(或你习惯的构建方式)编译。
  4. 点击出现的 RS0016 等 API 基线诊断错误。
  5. 在编辑器中对下划线的符号右键,或点击其左侧的"快速修复"图标(快捷键 Ctrl+. 亦可)。
  6. 选择 "Add Blah to public API" / "Fix all occurrences in … Solution"。
  7. 点击 Apply。
  8. 再次 F6,确认修复器没有漏项,或是否暴露出其他 RS00xx 错误(这并不罕见)。
  9. 如有其他问题,按需要修复或抑制;文档特别要求:抑制必须使用特性(attributes)而非全局 NoWarn#pragma,以便在代码现场看清抑制理由,例如针对常见且无法修复的错误:
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
// 或
[SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")]

从源码结构看,这一流程与仓库配置自洽:分析器以 Analyzer 身份参与编译,而 eng/targets/CSharp.Common.targets 中还专门设有 _RemovePublicApiAnalyzer / _RestorePublicApiAnalyzer 目标,在 Razor 编译前后临时移除/恢复该分析器,避免 Razor 源生成物干扰 API 基线的比对。

主版本发布后的基线更新

docs/APIBaselines.md,主版本发布后的基线"转正"工作由构建团队使用 .NET 仓库(dotnet/roslyn)scripts/PublicApi 目录下的脚本(或其 Arcade 后续方案)执行,核心动作是把 PublicAPI.Unshipped.txt 的内容**搬移(move)**进 PublicAPI.Shipped.txt。普通贡献者不需要也不应该手工做这一步——这正是"Shipped 文件除主版本发布后由构建团队修改外永不可改"规则的意义所在:保证 Shipped 文件忠实反映已发布版本,任何手工改动都可能掩盖真实发生的 API 断裂。

小结

登录后查看全文
热门项目推荐
相关项目推荐