dotnet-library-api-compat

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-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
TypeLoadException
,
MissingMethodException
,
MissingFieldException
, or
TypeInitializationException
at runtime.
二进制兼容性指已编译的程序集无需重新编译即可在运行时正常工作。二进制破坏性变更会导致运行时抛出
TypeLoadException
MissingMethodException
MissingFieldException
TypeInitializationException

Safe Changes (Binary Compatible)

安全变更(二进制兼容)

ChangeWhy Safe
Add new public typeExisting code never references it
Add new public method to non-sealed classExisting call sites resolve to their original overload
Add new overload with different parameter countExisting binaries bind to the original method token
Add optional parameter to existing methodCallers 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 (
protected
to
public
)
Existing references remain valid at higher visibility
Add non-abstract interface member with default implementationExisting implementors inherit the default; no
TypeLoadException
Remove
sealed
from class
Removes a restriction; existing code never subclassed it
Add new
enum
member
Existing binaries that switch on the enum simply fall through to
default
变更内容安全原因
添加新的公共类型现有代码从未引用过该类型
向非密封类添加新的公共方法现有调用站点仍解析为原重载方法
添加参数数量不同的新重载现有二进制文件绑定到原方法的令牌
为现有方法添加可选参数针对旧签名编译的调用方会在其IL中嵌入默认值;无论是否提供可选参数,运行时都会解析到同一个方法令牌
放宽访问修饰符(从
protected
改为
public
现有引用在更高可见性下依然有效
向接口添加带默认实现的非抽象成员现有实现者会继承该默认实现;不会抛出
TypeLoadException
移除类的
sealed
修饰符
移除了限制;现有代码从未继承该类
添加新的
enum
成员
针对枚举使用switch的现有二进制文件会直接执行default分支

Breaking Changes (Binary Incompatible)

破坏性变更(二进制不兼容)

ChangeRuntime FailureExample
Remove public type
TypeLoadException
Delete
public class Widget
Remove public method
MissingMethodException
Remove
Widget.Calculate()
Change method return type
MissingMethodException
int Calculate()
to
long Calculate()
Change method parameter types
MissingMethodException
void Process(int id)
to
void Process(long id)
Change field type
MissingFieldException
public int Count
to
public long Count
Reorder struct fieldsMemory layout changeBreaks interop and
Unsafe.As<>
consumers
Add abstract member to public class
TypeLoadException
Existing subclasses lack the implementation
Add interface member without default implementation
TypeLoadException
Existing implementors lack the member
Change
virtual
method to
non-virtual
MissingMethodException
for overriders
Overriders compiled expecting virtual dispatch
Seal a previously unsealed class
TypeLoadException
Existing subclasses cannot load
Change namespace of public type
TypeLoadException
Unless a type forwarder is added (see below)
Remove
virtual
from a method
MissingMethodException
Consumers compiled with
callvirt
find no virtual slot
变更内容运行时故障示例
删除公共类型
TypeLoadException
删除
public class Widget
删除公共方法
MissingMethodException
移除
Widget.Calculate()
修改方法返回类型
MissingMethodException
int Calculate()
改为
long Calculate()
修改方法参数类型
MissingMethodException
void Process(int id)
改为
void Process(long id)
修改字段类型
MissingFieldException
public int Count
改为
public long Count
重新排序结构体字段内存布局变更破坏互操作与
Unsafe.As<>
使用者的代码
向公共类添加抽象成员
TypeLoadException
现有子类缺少该实现
向接口添加无默认实现的成员
TypeLoadException
现有实现者缺少该成员
virtual
方法改为非虚方法
重写方会抛出
MissingMethodException
重写方法的代码编译时预期虚调度
为原本非密封的类添加
sealed
修饰符
TypeLoadException
现有子类无法加载
修改公共类型的命名空间
TypeLoadException
除非添加类型转发器(见下文)
移除方法的
virtual
修饰符
MissingMethodException
使用
callvirt
编译的使用者找不到虚方法槽

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

常见源代码破坏性变更

ChangeCompiler ImpactExample
Add overload causing ambiguityCS0121 (ambiguous call)Add
Process(long id)
when
Process(int id)
exists; callers passing
int
literal now have two candidates
Add extension method conflicting with instance methodNew extension hides or conflictsAdding
Where()
extension in a namespace the consumer imports
Change optional parameter default valueSilent behavior change
void Log(string level = "info")
to
"debug"
-- recompiled callers get new default
Add member to interface (even with DIM)CS0535 if consumer explicitly implements all membersConsumer 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 importCS0246 if consumer does not importNew public types in consumer's namespace collide
Change parameter nameBreaks callers using named arguments
Process(id: 5)
fails if parameter renamed to
identifier
Change
class
to
struct
(or vice versa)
Breaks
new()
constraints,
is null
checks, boxing behavior
Fundamental semantic change
Add new namespace that collides with existing type namesCS0104 (ambiguous reference)Adding
MyLib.Tasks
namespace conflicts with
System.Threading.Tasks
变更内容编译影响示例
添加导致歧义的重载CS0121(调用不明确)
Process(int id)
存在时添加
Process(long id)
;传入int字面量的调用方现在有两个候选方法
添加与实例方法冲突的扩展方法新扩展方法隐藏或冲突在使用者导入的命名空间中添加
Where()
扩展方法
修改可选参数的默认值静默行为变更
void Log(string level = "info")
改为
"debug"
——重新编译的调用方会使用新默认值
向接口添加成员(即使带DIM)如果使用者显式实现所有成员则会触发CS0535使用显式接口实现的使用者必须添加新成员
移除参数的默认值(设为必填)CS7036(缺少必需的参数)依赖默认值的调用方现在必须显式传入参数
添加必需的命名空间导入如果使用者未导入则触发CS0246使用者命名空间中的新公共类型发生冲突
修改参数名称破坏使用命名参数的调用方
Process(id: 5)
在参数重命名为
identifier
后会失败
class
改为
struct
(反之亦然)
破坏
new()
约束、
is null
检查、装箱行为
根本性语义变更
添加与现有类型名称冲突的新命名空间CS0104(引用不明确)添加
MyLib.Tasks
命名空间与
System.Threading.Tasks
冲突

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
object
overload token).
Mitigation: When adding overloads to public APIs, prefer parameter types that do not create implicit conversion paths from existing parameter types. Use
[EditorBrowsable(EditorBrowsableState.Never)]
on compatibility shims that must remain for binary compatibility but should not appear in IntelliSense.
添加重载是类库中最常见的源代码破坏性变更来源。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 namespaces
Mitigation: 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
typeof()
resolves correctly.
原程序集(类型从中移出的程序集)中,将类型移动到新程序集后添加转发属性:
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
[TypeForwardedFrom]
attribute is optional metadata that records where the type originally lived -- useful for serialization compatibility:
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]
is critical for types deserialized by
BinaryFormatter
,
DataContractSerializer
, or any serializer that encodes assembly-qualified type names. Without it, deserialization of data written by older versions will fail with
TypeLoadException
.
目标程序集(类型移入的程序集)包含实际的类型定义。目标端无需特殊属性。
[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; }
}
对于通过
BinaryFormatter
DataContractSerializer
或任何编码程序集限定类型名称的序列化器进行反序列化的类型,
[TypeForwardedFrom]
至关重要。没有它,旧版本写入的数据将无法反序列化,会抛出
TypeLoadException

Type 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 CategorySemVerReason
Remove public type or memberMajorBinary-breaking
Change method signature (return type, parameters)MajorBinary-breaking
Add abstract member to public classMajorBinary-breaking for subclasses
Add interface member without DIMMajorBinary-breaking for implementors
Add
sealed
to a previously unsealed class
MajorBinary-breaking for subclasses
Change struct field layoutMajorBinary-breaking for interop consumers
Change namespace without type forwarderMajorBinary-breaking
Mark member
[Obsolete]
(warning or error)
MinorBinary-compatible; signals deprecation
Add new public typeMinorAdditive, no breaking impact
Add overload (may be source-breaking)MinorBinary-compatible; source impact is accepted at minor
Add optional parameterMinorBinary-compatible; recompilation picks up new default
Add DIM to interfaceMinorBinary-compatible; additive
Change namespace WITH type forwarderMinorBinary-compatible via forwarding
Widen access modifierMinorBinary-compatible; additive
Bug fix with no API changePatchNo public API impact
Documentation or metadata-only changePatchNo public API impact
Performance improvement with same APIPatchNo public API impact
将API变更映射到语义化版本增量。完整的SemVer规则与NuGet版本策略请参考[skill:dotnet-nuget-authoring]。
变更类别SemVer版本原因
删除公共类型或成员主版本二进制破坏性
修改方法签名(返回类型、参数)主版本二进制破坏性
向公共类添加抽象成员主版本对子类是二进制破坏性
向接口添加无DIM的成员主版本对实现者是二进制破坏性
为原本非密封的类添加
sealed
主版本对子类是二进制破坏性
修改结构体字段布局主版本对互操作使用者是二进制破坏性
未使用类型转发器修改命名空间主版本二进制破坏性
标记成员为
[Obsolete]
(警告或错误)
次版本二进制兼容;表示弃用
添加新的公共类型次版本增量变更,无破坏性影响
添加重载(可能是源代码破坏性)次版本二进制兼容;源代码影响在次版本中可接受
添加可选参数次版本二进制兼容;重新编译会使用新默认值
向接口添加DIM次版本二进制兼容;增量变更
使用类型转发器修改命名空间次版本通过转发实现二进制兼容
放宽访问修饰符次版本二进制兼容;增量变更
无API变更的Bug修复修订版本无公共API影响
仅文档或元数据变更修订版本无公共API影响
无API变更的性能优化修订版本无公共API影响

Deprecation Lifecycle with
[Obsolete]

结合
[Obsolete]
的弃用生命周期

The standard workflow for removing public API members across major versions:
ReleaseActionEffect
v2.1 (Minor)Add
[Obsolete("Use Widget.CalculateAsync() instead.")]
Compiler warning CS0618; existing code compiles and runs
v2.3 (Minor)Change to
[Obsolete("Use Widget.CalculateAsync() instead.", error: true)]
Compiler error CS0619; existing binaries still run (binary-compatible)
v3.0 (Major)Remove the member entirelyBinary-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(次版本)添加
[Obsolete("请改用Widget.CalculateAsync()。")]
编译器警告CS0618;现有代码可正常编译和运行
v2.3(次版本)改为
[Obsolete("请改用Widget.CalculateAsync()。", error: true)]
编译器错误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
    net9.0
    to an existing
    net8.0
    package): Minor version bump. Existing consumers on
    net8.0
    are unaffected; new consumers on
    net9.0
    gain optimized code paths.
  • Removing a TFM (e.g., dropping
    netstandard2.0
    ): Major version bump. Consumers targeting the removed TFM can no longer resolve a compatible assembly.
  • Changing the lowest supported TFM (e.g.,
    net6.0
    to
    net8.0
    ): Major version bump. Consumers on the dropped TFM lose compatibility.
See [skill:dotnet-multi-targeting] for practical guidance on managing TFM additions and removals.

添加或移除目标框架会影响使用者的二进制兼容性:
  • 添加新TFM(例如,在现有
    net8.0
    包中添加
    net9.0
    ):次版本升级。
    net8.0
    上的现有使用者不受影响;
    net9.0
    上的新使用者获得优化代码路径。
  • 移除TFM(例如,放弃
    netstandard2.0
    ):主版本升级。针对已移除TFM的使用者无法再解析到兼容程序集。
  • 修改最低支持TFM(例如,从
    net6.0
    改为
    net8.0
    ):主版本升级。已放弃TFM上的使用者失去兼容性。
管理TFM添加和移除的实用指南请参考[skill:dotnet-multi-targeting]。

Compatibility Verification

兼容性验证

Use
EnablePackageValidation
in your
.csproj
to automatically compare the current build against the previously shipped package and detect binary/source-breaking changes:
xml
<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=true
This produces a
CompatibilitySuppressions.xml
file that can be checked in. If unspecified, the SDK reads
CompatibilitySuppressions.xml
from the project directory automatically. To specify explicit suppression files:
xml
<ItemGroup>
  <ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
Note:
ApiCompatSuppressionFile
is an ItemGroup item, not a PropertyGroup property. Multiple suppression files can be included.
For deeper API surface tracking with PublicApiAnalyzers and CI enforcement workflows, see [skill:dotnet-api-surface-validation].

.csproj
中使用
EnablePackageValidation
,自动将当前构建与之前发布的包进行比较,检测二进制/源代码破坏性变更:
xml
<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
这会生成一个
CompatibilitySuppressions.xml
文件,可提交到版本控制系统。如果未指定,SDK会自动从项目目录读取
CompatibilitySuppressions.xml
。要指定显式的抑制文件:
xml
<ItemGroup>
  <ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
注意:
ApiCompatSuppressionFile
ItemGroup项,而非PropertyGroup属性。可以包含多个抑制文件。
使用PublicApiAnalyzers和CI强制执行工作流进行更深入的API表面跟踪,请参考[skill:dotnet-api-surface-validation]。

Agent Gotchas

Agent注意事项

  1. 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.
  2. Do not remove public members without a major version bump -- even
    [Obsolete]
    members must be preserved until the next major version to maintain binary compatibility.
  3. Do not forget type forwarders when moving types between assemblies -- without
    [TypeForwardedTo]
    , consumers get
    TypeLoadException
    at runtime. Always add forwarders in the original assembly.
  4. Do not change
    optional
    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.
  5. 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
    int
    to
    long
    ). Test both.
  6. Do not skip
    [TypeForwardedFrom]
    on serializable types
    -- serializers that encode assembly-qualified type names (DataContractSerializer, legacy BinaryFormatter) will fail to deserialize data written by older versions.
  7. Do not put
    ApiCompatSuppressionFile
    in a PropertyGroup
    -- it is an ItemGroup item (
    <ApiCompatSuppressionFile Include="..." />
    ), not a property. Using PropertyGroup syntax silently does nothing.
  8. Do not remove a TFM from a library package without a major version bump -- consumers on the removed TFM lose compatibility with no fallback.

  1. 不要假设添加重载总是安全的——它是二进制兼容的,但可能因重载解析变更而成为源代码破坏性变更。始终检查现有参数类型与新参数类型之间的隐式转换路径。
  2. 不要在未升级主版本的情况下删除公共成员——即使是
    [Obsolete]
    成员也必须保留到下一个主版本,以维持二进制兼容性。
  3. 在程序集间移动类型时不要忘记类型转发器——如果没有
    [TypeForwardedTo]
    ,使用者会在运行时抛出
    TypeLoadException
    。务必在原程序集中添加转发器。
  4. 不要在修订版本中修改可选参数的默认值——这会为重新编译的使用者静默改变行为,而旧二进制文件仍保留旧默认值,导致版本依赖的行为差异。
  5. 不要混淆二进制兼容性与源代码兼容性——变更可能是二进制安全但源代码破坏性(新增重载),或源代码安全但二进制破坏性(将返回类型从
    int
    改为
    long
    )。两者都要测试。
  6. 不要在可序列化类型上省略
    [TypeForwardedFrom]
    ——编码程序集限定类型名称的序列化器(DataContractSerializer、旧版BinaryFormatter)将无法反序列化旧版本写入的数据。
  7. 不要将
    ApiCompatSuppressionFile
    放在PropertyGroup中
    ——它是ItemGroup项(
    <ApiCompatSuppressionFile Include="..." />
    ),而非属性。使用PropertyGroup语法会静默失效。
  8. 不要在未升级主版本的情况下从类库包中移除TFM——已移除TFM上的使用者会失去兼容性且无回退方案。

Prerequisites

前置条件

  • .NET 8.0+ SDK
  • EnablePackageValidation
    MSBuild property for automated compatibility checking
  • Understanding of SemVer 2.0 conventions (see [skill:dotnet-nuget-authoring])
  • Familiarity with assembly loading and binding (strong naming concepts)

  • .NET 8.0+ SDK
  • 用于自动兼容性检查的
    EnablePackageValidation
    MSBuild属性
  • 了解SemVer 2.0约定(参考[skill:dotnet-nuget-authoring])
  • 熟悉程序集加载与绑定(强命名概念)

References

参考资料