dotnet-library-api-compat
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-library-api-compat
.NET类库API兼容性
Binary and source compatibility rules for .NET library authors. Covers which API changes break consumers at the binary level (assembly loading, JIT resolution) versus at the source level (compilation), how to use type forwarders for assembly reorganization without breaking consumers, and how versioning decisions map to SemVer major/minor/patch increments.
Version assumptions: .NET 8.0+ baseline. Compatibility rules apply to all .NET versions but examples target modern SDK-style projects.
面向.NET类库开发者的二进制与源代码兼容性规则。涵盖哪些API变更会在二进制层面(程序集加载、JIT解析)或源代码层面(编译阶段)对使用者造成破坏,如何使用类型转发器在重组程序集时不影响现有使用者,以及版本决策如何对应到SemVer的主版本/次版本/修订版本增量。
版本前提: 以.NET 8.0+为基准。兼容性规则适用于所有.NET版本,但示例针对现代SDK风格项目。
Scope
适用范围
- Binary compatibility rules (safe vs breaking changes, runtime failures)
- Source compatibility rules (overload resolution, extension method conflicts)
- Type forwarders for assembly reorganization
- SemVer impact mapping (change category to major/minor/patch)
- Deprecation lifecycle with [Obsolete]
- EnablePackageValidation and ApiCompat verification
- 二进制兼容性规则(安全变更 vs 破坏性变更、运行时故障)
- 源代码兼容性规则(重载解析、扩展方法冲突)
- 用于程序集重组的类型转发器
- SemVer影响映射(变更类别对应主/次/修订版本)
- 结合[Obsolete]特性的弃用生命周期
- EnablePackageValidation与ApiCompat验证
Out of scope
不适用范围
- HTTP API versioning -- see [skill:dotnet-api-versioning]
- NuGet package metadata, signing, and publish workflows -- see [skill:dotnet-nuget-authoring]
- Multi-TFM packaging mechanics (polyfill strategy, conditional compilation) -- see [skill:dotnet-multi-targeting]
- PublicApiAnalyzers and API surface validation tooling -- see [skill:dotnet-api-surface-validation]
- Roslyn analyzer configuration -- see [skill:dotnet-roslyn-analyzers]
Cross-references: [skill:dotnet-api-versioning] for HTTP API versioning, [skill:dotnet-nuget-authoring] for NuGet packaging and SemVer rules, [skill:dotnet-multi-targeting] for multi-TFM packaging and ApiCompat tooling.
- HTTP API版本控制 —— 参考[skill:dotnet-api-versioning]
- NuGet包元数据、签名与发布流程 —— 参考[skill:dotnet-nuget-authoring]
- 多TFM打包机制(填充策略、条件编译)—— 参考[skill:dotnet-multi-targeting]
- PublicApiAnalyzers与API表面验证工具 —— 参考[skill:dotnet-api-surface-validation]
- Roslyn分析器配置 —— 参考[skill:dotnet-roslyn-analyzers]
交叉引用:HTTP API版本控制请见[skill:dotnet-api-versioning],NuGet打包与SemVer规则请见[skill:dotnet-nuget-authoring],多TFM打包与ApiCompat工具请见[skill:dotnet-multi-targeting]。
Binary Compatibility
二进制兼容性
Binary compatibility means existing compiled assemblies continue to work at runtime without recompilation. A binary-breaking change causes , , , or at runtime.
TypeLoadExceptionMissingMethodExceptionMissingFieldExceptionTypeInitializationException二进制兼容性指已编译的程序集无需重新编译即可在运行时正常工作。二进制破坏性变更会导致运行时抛出、、或。
TypeLoadExceptionMissingMethodExceptionMissingFieldExceptionTypeInitializationExceptionSafe Changes (Binary Compatible)
安全变更(二进制兼容)
| Change | Why Safe |
|---|---|
| Add new public type | Existing code never references it |
| Add new public method to non-sealed class | Existing call sites resolve to their original overload |
| Add new overload with different parameter count | Existing binaries bind to the original method token |
| Add optional parameter to existing method | Callers compiled against the old signature have default values embedded in their IL; the runtime resolves the same method token regardless of whether the optional parameter is supplied |
Widen access modifier ( | Existing references remain valid at higher visibility |
| Add non-abstract interface member with default implementation | Existing implementors inherit the default; no |
Remove | Removes a restriction; existing code never subclassed it |
Add new | Existing binaries that switch on the enum simply fall through to |
| 变更内容 | 安全原因 |
|---|---|
| 添加新的公共类型 | 现有代码从未引用过该类型 |
| 向非密封类添加新的公共方法 | 现有调用站点仍解析为原重载方法 |
| 添加参数数量不同的新重载 | 现有二进制文件绑定到原方法的令牌 |
| 为现有方法添加可选参数 | 针对旧签名编译的调用方会在其IL中嵌入默认值;无论是否提供可选参数,运行时都会解析到同一个方法令牌 |
放宽访问修饰符(从 | 现有引用在更高可见性下依然有效 |
| 向接口添加带默认实现的非抽象成员 | 现有实现者会继承该默认实现;不会抛出 |
移除类的 | 移除了限制;现有代码从未继承该类 |
添加新的 | 针对枚举使用switch的现有二进制文件会直接执行default分支 |
Breaking Changes (Binary Incompatible)
破坏性变更(二进制不兼容)
| Change | Runtime Failure | Example |
|---|---|---|
| Remove public type | | Delete |
| Remove public method | | Remove |
| Change method return type | | |
| Change method parameter types | | |
| Change field type | | |
| Reorder struct fields | Memory layout change | Breaks interop and |
| Add abstract member to public class | | Existing subclasses lack the implementation |
| Add interface member without default implementation | | Existing implementors lack the member |
Change | | Overriders compiled expecting virtual dispatch |
| Seal a previously unsealed class | | Existing subclasses cannot load |
| Change namespace of public type | | Unless a type forwarder is added (see below) |
Remove | | Consumers compiled with |
| 变更内容 | 运行时故障 | 示例 |
|---|---|---|
| 删除公共类型 | | 删除 |
| 删除公共方法 | | 移除 |
| 修改方法返回类型 | | 从 |
| 修改方法参数类型 | | 从 |
| 修改字段类型 | | 从 |
| 重新排序结构体字段 | 内存布局变更 | 破坏互操作与 |
| 向公共类添加抽象成员 | | 现有子类缺少该实现 |
| 向接口添加无默认实现的成员 | | 现有实现者缺少该成员 |
将 | 重写方会抛出 | 重写方法的代码编译时预期虚调度 |
为原本非密封的类添加 | | 现有子类无法加载 |
| 修改公共类型的命名空间 | | 除非添加类型转发器(见下文) |
移除方法的 | | 使用 |
Default Interface Members
默认接口成员(DIM)
Default interface members (DIM) added in C# 8 allow adding members to interfaces without breaking existing implementors -- but only at the binary level:
csharp
public interface IWidget
{
string Name { get; }
// Binary-safe: existing implementors inherit this default
string DisplayName => Name.ToUpperInvariant();
}However, if a consumer explicitly casts to the interface and the runtime cannot find the default implementation (older runtime), this fails. All runtimes in the .NET 8.0+ baseline support DIMs.
C# 8中引入的默认接口成员允许向接口添加成员而不破坏现有实现者——但仅在二进制层面:
csharp
public interface IWidget
{
string Name { get; }
// 二进制安全:现有实现者继承此默认实现
string DisplayName => Name.ToUpperInvariant();
}不过,如果使用者显式转换为接口类型,且运行时不支持默认实现(旧版本运行时),则会失败。.NET 8.0+基准版本的所有运行时均支持DIM。
Source Compatibility
源代码兼容性
Source compatibility means existing consumer code continues to compile without changes. A source-breaking change causes compiler errors or changes behavior silently (which is worse).
源代码兼容性指现有使用者的代码无需修改即可正常编译。源代码破坏性变更会导致编译错误或静默改变行为(后者更严重)。
Common Source-Breaking Changes
常见源代码破坏性变更
| Change | Compiler Impact | Example |
|---|---|---|
| Add overload causing ambiguity | CS0121 (ambiguous call) | Add |
| Add extension method conflicting with instance method | New extension hides or conflicts | Adding |
| Change optional parameter default value | Silent behavior change | |
| Add member to interface (even with DIM) | CS0535 if consumer explicitly implements all members | Consumer using explicit interface implementation must add the new member |
| Remove default value from parameter (make required) | CS7036 (required argument missing) | Callers relying on default value must now pass it explicitly |
| Add required namespace import | CS0246 if consumer does not import | New public types in consumer's namespace collide |
| Change parameter name | Breaks callers using named arguments | |
Change | Breaks | Fundamental semantic change |
| Add new namespace that collides with existing type names | CS0104 (ambiguous reference) | Adding |
| 变更内容 | 编译影响 | 示例 |
|---|---|---|
| 添加导致歧义的重载 | CS0121(调用不明确) | 当 |
| 添加与实例方法冲突的扩展方法 | 新扩展方法隐藏或冲突 | 在使用者导入的命名空间中添加 |
| 修改可选参数的默认值 | 静默行为变更 | 从 |
| 向接口添加成员(即使带DIM) | 如果使用者显式实现所有成员则会触发CS0535 | 使用显式接口实现的使用者必须添加新成员 |
| 移除参数的默认值(设为必填) | CS7036(缺少必需的参数) | 依赖默认值的调用方现在必须显式传入参数 |
| 添加必需的命名空间导入 | 如果使用者未导入则触发CS0246 | 使用者命名空间中的新公共类型发生冲突 |
| 修改参数名称 | 破坏使用命名参数的调用方 | |
将 | 破坏 | 根本性语义变更 |
| 添加与现有类型名称冲突的新命名空间 | CS0104(引用不明确) | 添加 |
Overload Resolution Pitfalls
重载解析陷阱
Adding overloads is the most common source of source-breaking changes in libraries. The C# compiler picks the "best" overload at compile time, and a new overload can change which method wins:
csharp
// V1 -- only overload
public void Send(object message) { }
// V2 -- new overload; ALL callers passing string now bind here
public void Send(string message) { }This is source-breaking (callers silently rebind) but binary-compatible (old compiled code still calls the overload token).
objectMitigation: When adding overloads to public APIs, prefer parameter types that do not create implicit conversion paths from existing parameter types. Use on compatibility shims that must remain for binary compatibility but should not appear in IntelliSense.
[EditorBrowsable(EditorBrowsableState.Never)]添加重载是类库中最常见的源代码破坏性变更来源。C#编译器在编译时选择“最佳”重载,新重载可能会改变最终选中的方法:
csharp
// V1 —— 仅有的重载
public void Send(object message) { }
// V2 —— 新增重载;所有传入string的调用方现在都会绑定到这里
public void Send(string message) { }这是源代码破坏性(调用方静默重新绑定)但二进制兼容(旧编译代码仍调用重载的令牌)。
object缓解措施: 向公共API添加重载时,优先选择不会与现有参数类型产生隐式转换路径的参数类型。对于为了二进制兼容性必须保留但不应在智能提示中显示的兼容垫片,使用。
[EditorBrowsable(EditorBrowsableState.Never)]Extension Method Conflicts
扩展方法冲突
Extension methods resolve at compile time based on imported namespaces. Adding a new extension method can shadow an existing instance method or conflict with extensions from other libraries:
csharp
// Library V1 ships in namespace MyLib.Extensions
public static class StringExtensions
{
public static string Truncate(this string s, int maxLength) =>
s.Length <= maxLength ? s : s[..maxLength];
}
// Library V2 adds to SAME namespace -- safe
// Library V2 adds to DIFFERENT namespace -- may conflict
// if consumer imports both namespacesMitigation: Keep extension methods in the same namespace across versions. Document any namespace additions in release notes.
扩展方法在编译时根据导入的命名空间解析。添加新的扩展方法可能会遮蔽现有实例方法,或与其他类库的扩展方法冲突:
csharp
// 类库V1发布在MyLib.Extensions命名空间
public static class StringExtensions
{
public static string Truncate(this string s, int maxLength) =>
s.Length <= maxLength ? s : s[..maxLength];
}
// 类库V2添加到同一命名空间 —— 安全
// 类库V2添加到不同命名空间 —— 可能冲突
// 如果使用者同时导入两个命名空间缓解措施: 跨版本保持扩展方法在同一命名空间中。在发行说明中记录所有命名空间的新增内容。
Type Forwarders
类型转发器
Type forwarders allow moving a public type from one assembly to another without breaking existing compiled references. The original assembly contains a forwarding entry that redirects the runtime type resolver to the new location.
类型转发器允许将公共类型从一个程序集移动到另一个程序集,而不破坏现有编译引用。原程序集包含一个转发条目,将运行时类型解析器重定向到新位置。
When to Use Type Forwarders
使用场景
- Splitting a large assembly into smaller, focused assemblies
- Merging assemblies for packaging simplification
- Reorganizing namespaces across assembly boundaries
- Moving types to a shared assembly consumed by multiple packages
- 将大型程序集拆分为更小、聚焦的程序集
- 为了简化打包合并程序集
- 跨程序集重组命名空间
- 将类型移动到多个包共享的共享程序集中
Adding Type Forwarders
添加类型转发器
In the original assembly (the one types are moving FROM), add forwarding attributes after moving the types to the new assembly:
csharp
// In the ORIGINAL assembly's AssemblyInfo.cs or a dedicated TypeForwarders.cs
// This tells the runtime: "Widget now lives in MyLib.Core"
using System.Runtime.CompilerServices;
[assembly: TypeForwardedTo(typeof(MyLib.Core.Widget))]
[assembly: TypeForwardedTo(typeof(MyLib.Core.IWidgetFactory))]
[assembly: TypeForwardedTo(typeof(MyLib.Core.WidgetOptions))]The original assembly must reference the destination assembly so that resolves correctly.
typeof()在原程序集(类型从中移出的程序集)中,将类型移动到新程序集后添加转发属性:
csharp
// 在原程序集的AssemblyInfo.cs或专用的TypeForwarders.cs中
// 这会告诉运行时:“Widget现在位于MyLib.Core中”
using System.Runtime.CompilerServices;
[assembly: TypeForwardedTo(typeof(MyLib.Core.Widget))]
[assembly: TypeForwardedTo(typeof(MyLib.Core.IWidgetFactory))]
[assembly: TypeForwardedTo(typeof(MyLib.Core.WidgetOptions))]原程序集必须引用目标程序集,以便能正确解析。
typeof()Receiving Type Forwarders
接收类型转发器
The destination assembly (the one types are moving TO) contains the actual type definitions. No special attributes are needed on the destination side. The attribute is optional metadata that records where the type originally lived -- useful for serialization compatibility:
[TypeForwardedFrom]csharp
// In the DESTINATION assembly -- optional but recommended for
// types that participate in serialization
using System.Runtime.CompilerServices;
namespace MyLib.Core;
[TypeForwardedFrom("MyLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
public class Widget
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}[TypeForwardedFrom]BinaryFormatterDataContractSerializerTypeLoadException目标程序集(类型移入的程序集)包含实际的类型定义。目标端无需特殊属性。属性是可选元数据,用于记录类型的原始位置——对序列化兼容性很有用:
[TypeForwardedFrom]csharp
// 在目标程序集中 —— 可选但推荐用于
// 参与序列化的类型
using System.Runtime.CompilerServices;
namespace MyLib.Core;
[TypeForwardedFrom("MyLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
public class Widget
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}对于通过、或任何编码程序集限定类型名称的序列化器进行反序列化的类型,至关重要。没有它,旧版本写入的数据将无法反序列化,会抛出。
BinaryFormatterDataContractSerializer[TypeForwardedFrom]TypeLoadExceptionType Forwarder Chain
类型转发链
Type forwarders can chain: Assembly A forwards to Assembly B, which forwards to Assembly C. The runtime follows the chain. However, keep chains short (ideally one hop) to minimize assembly loading overhead.
类型转发可以链式进行:程序集A转发到程序集B,程序集B转发到程序集C。运行时会跟随该链。不过,应尽量缩短链长(理想情况下仅一跳)以最小化程序集加载开销。
Multi-TFM Type Forwarder Pattern
多TFM类型转发模式
When restructuring assemblies in a multi-TFM library, the forwarding assembly must target all TFMs that consumers might use. A common pattern:
xml
<!-- Original assembly (MyLib.csproj) -- now just a forwarding shim -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../MyLib.Core/MyLib.Core.csproj" />
</ItemGroup>
</Project>See [skill:dotnet-multi-targeting] for multi-TFM packaging mechanics and [skill:dotnet-nuget-authoring] for NuGet packaging of forwarding shims.
在多TFM类库中重组程序集时,转发程序集必须针对使用者可能使用的所有TFM。常见模式如下:
xml
<!-- 原程序集(MyLib.csproj)—— 现在只是一个转发垫片 -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../MyLib.Core/MyLib.Core.csproj" />
</ItemGroup>
</Project>多TFM打包机制请参考[skill:dotnet-multi-targeting],转发垫片的NuGet打包请参考[skill:dotnet-nuget-authoring]。
SemVer Impact Summary
SemVer影响总结
Map API changes to Semantic Versioning increments. For full SemVer rules and NuGet versioning strategies, see [skill:dotnet-nuget-authoring].
| Change Category | SemVer | Reason |
|---|---|---|
| Remove public type or member | Major | Binary-breaking |
| Change method signature (return type, parameters) | Major | Binary-breaking |
| Add abstract member to public class | Major | Binary-breaking for subclasses |
| Add interface member without DIM | Major | Binary-breaking for implementors |
Add | Major | Binary-breaking for subclasses |
| Change struct field layout | Major | Binary-breaking for interop consumers |
| Change namespace without type forwarder | Major | Binary-breaking |
Mark member | Minor | Binary-compatible; signals deprecation |
| Add new public type | Minor | Additive, no breaking impact |
| Add overload (may be source-breaking) | Minor | Binary-compatible; source impact is accepted at minor |
| Add optional parameter | Minor | Binary-compatible; recompilation picks up new default |
| Add DIM to interface | Minor | Binary-compatible; additive |
| Change namespace WITH type forwarder | Minor | Binary-compatible via forwarding |
| Widen access modifier | Minor | Binary-compatible; additive |
| Bug fix with no API change | Patch | No public API impact |
| Documentation or metadata-only change | Patch | No public API impact |
| Performance improvement with same API | Patch | No public API impact |
将API变更映射到语义化版本增量。完整的SemVer规则与NuGet版本策略请参考[skill:dotnet-nuget-authoring]。
| 变更类别 | SemVer版本 | 原因 |
|---|---|---|
| 删除公共类型或成员 | 主版本 | 二进制破坏性 |
| 修改方法签名(返回类型、参数) | 主版本 | 二进制破坏性 |
| 向公共类添加抽象成员 | 主版本 | 对子类是二进制破坏性 |
| 向接口添加无DIM的成员 | 主版本 | 对实现者是二进制破坏性 |
为原本非密封的类添加 | 主版本 | 对子类是二进制破坏性 |
| 修改结构体字段布局 | 主版本 | 对互操作使用者是二进制破坏性 |
| 未使用类型转发器修改命名空间 | 主版本 | 二进制破坏性 |
标记成员为 | 次版本 | 二进制兼容;表示弃用 |
| 添加新的公共类型 | 次版本 | 增量变更,无破坏性影响 |
| 添加重载(可能是源代码破坏性) | 次版本 | 二进制兼容;源代码影响在次版本中可接受 |
| 添加可选参数 | 次版本 | 二进制兼容;重新编译会使用新默认值 |
| 向接口添加DIM | 次版本 | 二进制兼容;增量变更 |
| 使用类型转发器修改命名空间 | 次版本 | 通过转发实现二进制兼容 |
| 放宽访问修饰符 | 次版本 | 二进制兼容;增量变更 |
| 无API变更的Bug修复 | 修订版本 | 无公共API影响 |
| 仅文档或元数据变更 | 修订版本 | 无公共API影响 |
| 无API变更的性能优化 | 修订版本 | 无公共API影响 |
Deprecation Lifecycle with [Obsolete]
[Obsolete]结合[Obsolete]
的弃用生命周期
[Obsolete]The standard workflow for removing public API members across major versions:
| Release | Action | Effect |
|---|---|---|
| v2.1 (Minor) | Add | Compiler warning CS0618; existing code compiles and runs |
| v2.3 (Minor) | Change to | Compiler error CS0619; existing binaries still run (binary-compatible) |
| v3.0 (Major) | Remove the member entirely | Binary-breaking; consumers must migrate |
csharp
// v2.1 -- warn consumers
[Obsolete("Use CalculateAsync() instead. This method will be removed in v3.0.")]
public int Calculate() => CalculateAsync().GetAwaiter().GetResult();
// v2.3 -- block new compilation against this member
[Obsolete("Use CalculateAsync() instead. This method will be removed in v3.0.", error: true)]
public int Calculate() => CalculateAsync().GetAwaiter().GetResult();
// v3.0 -- remove the member (Major version bump)Always include the replacement API and the planned removal version in the obsolete message so both humans and agents can migrate proactively.
跨主版本移除公共API成员的标准流程:
| 版本 | 操作 | 效果 |
|---|---|---|
| v2.1(次版本) | 添加 | 编译器警告CS0618;现有代码可正常编译和运行 |
| v2.3(次版本) | 改为 | 编译器错误CS0619;现有二进制文件仍可运行(二进制兼容) |
| v3.0(主版本) | 完全移除该成员 | 二进制破坏性;使用者必须迁移 |
csharp
// v2.1 —— 警告使用者
[Obsolete("请改用CalculateAsync()。该方法将在v3.0中移除。")]
public int Calculate() => CalculateAsync().GetAwaiter().GetResult();
// v2.3 —— 阻止针对该成员的新编译
[Obsolete("请改用CalculateAsync()。该方法将在v3.0中移除。", error: true)]
public int Calculate() => CalculateAsync().GetAwaiter().GetResult();
// v3.0 —— 移除该成员(主版本升级)在弃用消息中始终包含替代API和计划移除的版本,以便人工和Agent都能主动迁移。
Multi-TFM Binary Compatibility
多TFM二进制兼容性
Adding or removing target frameworks affects binary compatibility for consumers:
- Adding a new TFM (e.g., adding to an existing
net9.0package): Minor version bump. Existing consumers onnet8.0are unaffected; new consumers onnet8.0gain optimized code paths.net9.0 - Removing a TFM (e.g., dropping ): Major version bump. Consumers targeting the removed TFM can no longer resolve a compatible assembly.
netstandard2.0 - Changing the lowest supported TFM (e.g., to
net6.0): Major version bump. Consumers on the dropped TFM lose compatibility.net8.0
See [skill:dotnet-multi-targeting] for practical guidance on managing TFM additions and removals.
添加或移除目标框架会影响使用者的二进制兼容性:
- 添加新TFM(例如,在现有包中添加
net8.0):次版本升级。net9.0上的现有使用者不受影响;net8.0上的新使用者获得优化代码路径。net9.0 - 移除TFM(例如,放弃):主版本升级。针对已移除TFM的使用者无法再解析到兼容程序集。
netstandard2.0 - 修改最低支持TFM(例如,从改为
net6.0):主版本升级。已放弃TFM上的使用者失去兼容性。net8.0
管理TFM添加和移除的实用指南请参考[skill:dotnet-multi-targeting]。
Compatibility Verification
兼容性验证
Use in your to automatically compare the current build against the previously shipped package and detect binary/source-breaking changes:
EnablePackageValidation.csprojxml
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<!-- Compare against the last shipped version -->
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>Build output flags breaking changes:
error CP0002: Member 'MyLib.Widget.Calculate()' was removed
error CP0006: Cannot change return type of 'MyLib.Widget.GetName()'To suppress known intentional breaks, generate a suppression file:
bash
dotnet pack /p:GenerateCompatibilitySuppressionFile=trueThis produces a file that can be checked in. If unspecified, the SDK reads from the project directory automatically. To specify explicit suppression files:
CompatibilitySuppressions.xmlCompatibilitySuppressions.xmlxml
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>Note: is an ItemGroup item, not a PropertyGroup property. Multiple suppression files can be included.
ApiCompatSuppressionFileFor deeper API surface tracking with PublicApiAnalyzers and CI enforcement workflows, see [skill:dotnet-api-surface-validation].
在中使用,自动将当前构建与之前发布的包进行比较,检测二进制/源代码破坏性变更:
.csprojEnablePackageValidationxml
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<!-- 与上一个发布版本比较 -->
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>构建输出会标记破坏性变更:
error CP0002: 成员'MyLib.Widget.Calculate()'已被移除
error CP0006: 无法修改'MyLib.Widget.GetName()'的返回类型要抑制已知的有意破坏性变更,生成抑制文件:
bash
dotnet pack /p:GenerateCompatibilitySuppressionFile=true这会生成一个文件,可提交到版本控制系统。如果未指定,SDK会自动从项目目录读取。要指定显式的抑制文件:
CompatibilitySuppressions.xmlCompatibilitySuppressions.xmlxml
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>注意:是ItemGroup项,而非PropertyGroup属性。可以包含多个抑制文件。
ApiCompatSuppressionFile使用PublicApiAnalyzers和CI强制执行工作流进行更深入的API表面跟踪,请参考[skill:dotnet-api-surface-validation]。
Agent Gotchas
Agent注意事项
- Do not assume adding an overload is always safe -- it is binary-compatible but can be source-breaking due to overload resolution changes. Always check for implicit conversion paths between existing and new parameter types.
- Do not remove public members without a major version bump -- even members must be preserved until the next major version to maintain binary compatibility.
[Obsolete] - Do not forget type forwarders when moving types between assemblies -- without , consumers get
[TypeForwardedTo]at runtime. Always add forwarders in the original assembly.TypeLoadException - Do not change parameter default values in patch releases -- this silently changes behavior for recompiled consumers while old binaries retain the old default, creating version-dependent behavior divergence.
optional - Do not confuse binary compatibility with source compatibility -- a change can be binary-safe but source-breaking (new overload) or source-safe but binary-breaking (changing return type from to
int). Test both.long - Do not skip on serializable types -- serializers that encode assembly-qualified type names (DataContractSerializer, legacy BinaryFormatter) will fail to deserialize data written by older versions.
[TypeForwardedFrom] - Do not put in a PropertyGroup -- it is an ItemGroup item (
ApiCompatSuppressionFile), not a property. Using PropertyGroup syntax silently does nothing.<ApiCompatSuppressionFile Include="..." /> - Do not remove a TFM from a library package without a major version bump -- consumers on the removed TFM lose compatibility with no fallback.
- 不要假设添加重载总是安全的——它是二进制兼容的,但可能因重载解析变更而成为源代码破坏性变更。始终检查现有参数类型与新参数类型之间的隐式转换路径。
- 不要在未升级主版本的情况下删除公共成员——即使是成员也必须保留到下一个主版本,以维持二进制兼容性。
[Obsolete] - 在程序集间移动类型时不要忘记类型转发器——如果没有,使用者会在运行时抛出
[TypeForwardedTo]。务必在原程序集中添加转发器。TypeLoadException - 不要在修订版本中修改可选参数的默认值——这会为重新编译的使用者静默改变行为,而旧二进制文件仍保留旧默认值,导致版本依赖的行为差异。
- 不要混淆二进制兼容性与源代码兼容性——变更可能是二进制安全但源代码破坏性(新增重载),或源代码安全但二进制破坏性(将返回类型从改为
int)。两者都要测试。long - 不要在可序列化类型上省略——编码程序集限定类型名称的序列化器(DataContractSerializer、旧版BinaryFormatter)将无法反序列化旧版本写入的数据。
[TypeForwardedFrom] - 不要将放在PropertyGroup中——它是ItemGroup项(
ApiCompatSuppressionFile),而非属性。使用PropertyGroup语法会静默失效。<ApiCompatSuppressionFile Include="..." /> - 不要在未升级主版本的情况下从类库包中移除TFM——已移除TFM上的使用者会失去兼容性且无回退方案。
Prerequisites
前置条件
- .NET 8.0+ SDK
- MSBuild property for automated compatibility checking
EnablePackageValidation - Understanding of SemVer 2.0 conventions (see [skill:dotnet-nuget-authoring])
- Familiarity with assembly loading and binding (strong naming concepts)
- .NET 8.0+ SDK
- 用于自动兼容性检查的MSBuild属性
EnablePackageValidation - 了解SemVer 2.0约定(参考[skill:dotnet-nuget-authoring])
- 熟悉程序集加载与绑定(强命名概念)