在 dotnet/runtime 中为新增公共 API 更新参考程序集(Reference Assembly)的完整指南
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
本文以 updating-ref-source.md 为核心骨架,系统讲解在 .NET runtime 仓库中,当向实现程序集(implementation assembly)添加新的公共 API后,如何正确更新对应参考程序集(reference assembly,即ref目录下的 API 骨架源码)。文章覆盖大多数库的标准流程、System.Runtime的特殊处理、Full Facade 程序集与 .NETFramework Facade 程序集的专属命令,并结合仓库内的 ref 目录结构与源码佐证每一步的底层原理,帮助读者在完成 API Review 后,安全、无漂移地把新 API 同步进参考程序集并落地测试。
为什么需要更新参考程序集
在 .NET 的编译与运行时体系中,同一份公共 API 存在两套源码:一套位于src目录(实现程序集,包含真实的方法体与内部实现),另一套位于ref目录(参考程序集,只保留公共 API 的签名骨架,方法体为空或抛出异常)。以仓库中的 System.Collections 为例,其目录结构清晰地体现了这一分离:
src/libraries/System.Collections/ ├── ref/ # 参考程序集:System.Collections.cs、System.Collections.Forwards.cs ├── src/ # 实现程序集 └── tests/ # 测试参考程序集的作用是:对外发布一份稳定、不含实现细节的 API 契约,编译器在编译用户代码时针对这份契约解析签名,从而允许运行时自由调整内部实现而不破坏二进制兼容。因此,每当实现程序集新增公共类型或成员,都必须同步更新ref目录下的对应源文件,否则新 API 无法被外部消费,ApiCompat 校验也会失败。
需要注意的是:该更新流程的前提是 API 已经通过了 API Review,即新 API 的命名与签名已获得正式评审认可;本文不涉及 API 评审本身,只解决"评审通过之后如何落地"的问题。
大多数库的标准更新流程
对于src/libraries下绝大多数程序集,更新参考程序集遵循以下四步流程。
步骤 1:实现 API 并构建实现程序集
先在实现程序集中完成新 API 的代码编写,然后按照 构建单个库 的指引执行构建。
一个常见的坑是:新增公共类型时,构建可能直接报TypeMustExist错误。这是因为参考程序集还缺少该类型,而 ApiCompat(API 兼容性校验)在实现程序集中找不到对应契约时便拒绝继续。此时需要临时禁用 ApiCompat 的程序集校验来打破这个死锁:
dotnet build /p:ApiCompatValidateAssemblies=false通过该参数跳过校验后,即可进入下一步生成参考程序集源码。
步骤 2:用 GenAPI 工具重新生成参考程序集源码
从src目录(即src/libraries/<程序集>/src)执行以下命令,运行 GenAPI 工具:
dotnet msbuild /t:GenerateReferenceAssemblySourceGenAPI 会扫描实现程序集中的公共 API,并重新生成ref目录下的参考源码。需要特别强调两点:
- GenAPI 的输出往往存在大量噪音。仓库已知存在 GenAPI 生成大量不相关差异的问题(见 dotnet/runtime 仓库 issue #100843),因此生成后通常需要手工修正:筛掉无关改动,只保留与本次新 API 相关的差异(例如可以忽略某些被移除的 attribute)。
- 不建议完全手工编辑参考源文件。完全手写会导致参考程序集与 GenAPI 工具的生成结果产生漂移(drift),使后续更新变得难以 diff,并带来"参考程序集与运行时程序集不一致"的风险。正确姿势是以 GenAPI 输出为基础再做最小手工修正。
步骤 3:构建参考程序集
生成或修正完源码后,进入ref目录,构建参考程序集项目。以仓库中的 System.Collections/ref/System.Collections.csproj 为参考,参考程序集项目本质上是一个极简的类库项目:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>$(NetCoreAppCurrent)</TargetFramework> </PropertyGroup> <ItemGroup> <Compile Include="System.Collections.cs" /> <Compile Include="System.Collections.Forwards.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\System.Runtime\ref\System.Runtime.csproj" /> </ItemGroup> </Project>从中可以读出两个重要事实:参考程序集项目把System.Collections.cs与类型转发文件System.Collections.Forwards.cs一并编译;并且它通过ProjectReference依赖 System.Runtime 的参考程序集——这也解释了为什么System.Runtime在整个 ref 体系中处于最底层、更新时需特殊对待(见下文)。
步骤 4:添加并运行测试
新 API 不仅需要参考程序集与实现程序集,还需要配套的测试(位于程序集目录下的tests/文件夹)。添加测试后执行构建与测试,确认签名契约、实现与测试三者一致。
补充:已手工添加 API 后的重新生成
注意:如果你在生成参考源之前,已经手工把新 API 加进了参考源文件,那么在构建完实现程序集后重新执行GenerateReferenceAssemblySource,GenAPI 会将该 API全限定化(fully qualified)并归位到正确的排序位置。这种情况下命令从ref目录执行即可:
# 在 ref 目录下执行 dotnet msbuild /t:GenerateReferenceAssemblySource也就是说,GenerateReferenceAssemblySource这个 target 在src与ref两个目录下都可运行,只是适用场景不同:src下运行是"从实现生成参考源",ref下运行是"基于已有的参考源做规范整理"。
System.Runtime 的特殊流程
System.Runtime是整个 .NET 类库的地基参考程序集,且它直接依赖System.Private.CoreLib中的类型,因此不能直接套用通用流程,需要先构建底层再生成。
适用场景
这套流程同样适用于依赖 System.Private.CoreLib 变更的少数特殊程序集,例如System.Memory这类 partial facade(部分门面)程序集——它们只转发System.Private.CoreLib中已定义类型的部分成员。
操作步骤
- 从
System.Runtime/src目录执行以下命令(注意多了--no-incremental,确保不走增量缓存、完整重生成):
dotnet build --no-incremental /t:GenerateReferenceAssemblySource- 过滤无关变更:由于
System.Runtime参考源体量巨大,生成结果中会混入大量与本次改动无关的差异(例如某些 attribute 被移除)。这一步需要手工把无关差异全部剔除,只保留与你新增 API 相关的部分。文档明确指出:"Generally, this step is not required for other reference assemblies"——即对其他参考程序集而言通常不需要这么麻烦,唯独System.Runtime必须经历这一轮清理。
仓库中可以看到System.Runtime的 src 目录下存在 System.Runtime.Typeforwards.cs,说明该程序集的类型转发也是单独文件管理,与 System.Runtime/ref 下的参考源配合维护。
Full Facade 程序集的专属命令
仓库中有一类实现程序集本质上是另一程序集的full facade(完整门面),自身几乎不承载实现,却在参考程序集中定义类型。典型例子包括System.Runtime.Serialization.Json、System.Xml.XDocument等。
对于这类程序集,直接生成参考源会丢失类型转发关系,必须显式让 GenAPI 跟随类型转发。对应的命令为:
dotnet msbuild /t:GenerateReferenceAssemblySource /p:GenAPIFollowTypeForwards=true与标准命令相比,多出的/p:GenAPIFollowTypeForwards=true属性会指导 GenAPI 在遇到类型转发时沿着 TypeForwardedTo 继续展开,从而把被转发类型的契约也纳入参考源生成范围,保证门面程序集暴露出的 API 面完整。
.NETFramework Facade 程序集:手工补充类型转发
还有一类更特殊的程序集:它们在 .NETStandard 与 .NETCore 中定义类型,但在 .NETFramework 目标上只是facade(门面),需要把类型转发到 .NETFramework 中既有类型所在的位置。这种情况下,无法依赖 GenAPI 自动完成,必须手工为 .NETFramework 构建下的参考程序集添加类型转发。
具体要求如下:
- 对兼容的 .NETStandard 参考程序集中的每一个类型,只要该类型在 .NETFramework 中已存在,就必须为其添加
TypeForwardedTo; - 而对于那些在 .NETFramework 参考程序集中直接定义的类型,则应当被抽取到**共享源文件(shared source file)**中,供各目标框架复用,避免重复定义。
仓库中 System.Collections/ref/System.Collections.Forwards.cs 就是这种模式的具体呈现,其内容非常简短,本质上是带强约束注释的转发声明:
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // ------------------------------------------------------------------------------ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Collections.ObjectModel.ReadOnlySet<>))]注意文件头部的注释:"Changes to this file must follow the https://aka.ms/api-review process."——任何对类型转发文件的改动都必须走 API Review 流程,这与本文开头"先评审、后更新"的总体纪律完全一致。同时在 System.Collections/ref/System.Collections.csproj 中,System.Collections.Forwards.cs与主参考源System.Collections.cs一起被<Compile Include>纳入编译,印证了"转发文件 + 契约文件"并行维护的项目组织方式。
常见问题与最佳实践小结
| 场景 | 命令 / 操作 | 关键点 |
|---|---|---|
| 大多数库(标准流程) | 从src目录执行dotnet msbuild /t:GenerateReferenceAssemblySource | 新增公共类型时先以/p:ApiCompatValidateAssemblies=false打破TypeMustExist死锁 |
| 已在 ref 中手工添加过 API | 从ref目录执行同一 target | GenAPI 会做全限定化并重新排序 |
System.Runtime(及依赖 CoreLib 的 partial facade) | 从System.Runtime/src执行dotnet build --no-incremental /t:GenerateReferenceAssemblySource | 必须过滤无关差异,只保留本次改动 |
| Full Facade 程序集(如 System.Runtime.Serialization.Json、System.Xml.XDocument) | dotnet msbuild /t:GenerateReferenceAssemblySource /p:GenAPIFollowTypeForwards=true | 显式跟随类型转发,保证契约完整 |
| .NETFramework Facade | 手工为 .NETFramework 参考程序集添加TypeForwardedTo;框架内定义的类型抽入共享源文件 | 需走 API Review;改动遵守 ref 文件头部的评审注释约定 |
贯穿全文的三条纪律,值得在每一次 API 落地时自查:
- 先评审、再更新:新增公共 API 前必须先完成 API Review,参考程序集的任何变更都受此约束(参考 ref 源文件 中的文件头注释约定)。
- 以 GenAPI 输出为基底,而非全手工编写:手工硬编码会导致参考程序集与工具生成结果漂移,增加后续维护风险;生成后的人工修正应聚焦于过滤噪音、保留本次改动。
- 分层处理,特殊程序集走特殊流程:
System.Runtime需要--no-incremental全量重建并人工清理;Full Facade 需要GenAPIFollowTypeForwards=true;.NETFramework Facade 则需要手工维护类型转发。识别你正在处理的程序集属于哪一类,直接决定命令与工作量。
遵循上述流程,即可保证新增公共 API 在实现程序集、参考程序集与测试三端保持一致,让 API 契约稳定、可被编译器正确解析,也避免了参考程序集与运行时程序集之间的漂移风险。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考