api-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePublic API Design and Compatibility
公共API设计与兼容性
When to Use This Skill
何时使用此技能
Use this skill when:
- Designing public APIs for NuGet packages or libraries
- Making changes to existing public APIs
- Planning wire format changes for distributed systems
- Implementing versioning strategies
- Reviewing pull requests for breaking changes
在以下场景使用此技能:
- 为NuGet包或类库设计公共API
- 对现有公共API进行修改
- 规划分布式系统的有线格式变更
- 实现版本控制策略
- 审查包含破坏性变更的拉取请求
The Three Types of Compatibility
三种兼容性类型
| Type | Definition | Scope |
|---|---|---|
| API/Source | Code compiles against newer version | Public method signatures, types |
| Binary | Compiled code runs against newer version | Assembly layout, method tokens |
| Wire | Serialized data readable by other versions | Network protocols, persistence formats |
Breaking any of these creates upgrade friction for users.
| 类型 | 定义 | 范围 |
|---|---|---|
| API/源代码 | 代码可针对新版本编译 | 公共方法签名、类型 |
| 二进制 | 已编译代码可在新版本上运行 | 程序集布局、方法令牌 |
| 有线 | 序列化数据可被其他版本读取 | 网络协议、持久化格式 |
破坏其中任何一种都会给用户带来升级阻碍。
Extend-Only Design
仅扩展设计
The foundation of stable APIs: never remove or modify, only extend.
稳定API的基础:绝不删除或修改,仅进行扩展。
Three Pillars
三大支柱
- Previous functionality is immutable - Once released, behavior and signatures are locked
- New functionality through new constructs - Add overloads, new types, opt-in features
- Removal only after deprecation period - Years, not releases
- 原有功能不可变 - 一旦发布,行为和签名即被锁定
- 通过新结构添加新功能 - 添加重载、新类型、可选加入的功能
- 仅在弃用期后移除 - 以年为单位,而非以版本发布周期
Benefits
优势
- Old code continues working in new versions
- New and old pathways coexist
- Upgrades are non-breaking by default
- Users upgrade on their schedule
Resources:
API Change Guidelines
API变更指南
Safe Changes (Any Release)
安全变更(任何版本均可)
csharp
// ADD new overloads with default parameters
public void Process(Order order, CancellationToken ct = default);
// ADD new optional parameters to existing methods
public void Send(Message msg, Priority priority = Priority.Normal);
// ADD new types, interfaces, enums
public interface IOrderValidator { }
public enum OrderStatus { Pending, Complete, Cancelled }
// ADD new members to existing types
public class Order
{
public DateTimeOffset? ShippedAt { get; init; } // NEW
}csharp
// ADD new overloads with default parameters
public void Process(Order order, CancellationToken ct = default);
// ADD new optional parameters to existing methods
public void Send(Message msg, Priority priority = Priority.Normal);
// ADD new types, interfaces, enums
public interface IOrderValidator { }
public enum OrderStatus { Pending, Complete, Cancelled }
// ADD new members to existing types
public class Order
{
public DateTimeOffset? ShippedAt { get; init; } // NEW
}Unsafe Changes (Never or Major Version Only)
不安全变更(禁止或仅在主版本中进行)
csharp
// REMOVE or RENAME public members
public void ProcessOrder(Order order); // Was: Process()
// CHANGE parameter types or order
public void Process(int orderId); // Was: Process(Order order)
// CHANGE return types
public Order? GetOrder(string id); // Was: public Order GetOrder()
// CHANGE access modifiers
internal class OrderProcessor { } // Was: public
// ADD required parameters without defaults
public void Process(Order order, ILogger logger); // Breaks callers!csharp
// REMOVE or RENAME public members
public void ProcessOrder(Order order); // Was: Process()
// CHANGE parameter types or order
public void Process(int orderId); // Was: Process(Order order)
// CHANGE return types
public Order? GetOrder(string id); // Was: public Order GetOrder()
// CHANGE access modifiers
internal class OrderProcessor { } // Was: public
// ADD required parameters without defaults
public void Process(Order order, ILogger logger); // Breaks callers!Deprecation Pattern
弃用模式
csharp
// Step 1: Mark as obsolete with version (any release)
[Obsolete("Obsolete since v1.5.0. Use ProcessAsync instead.")]
public void Process(Order order) { }
// Step 2: Add new recommended API (same release)
public Task ProcessAsync(Order order, CancellationToken ct = default);
// Step 3: Remove in next major version (v2.0+)
// Only after users have had time to migratecsharp
// Step 1: Mark as obsolete with version (any release)
[Obsolete("Obsolete since v1.5.0. Use ProcessAsync instead.")]
public void Process(Order order) { }
// Step 2: Add new recommended API (same release)
public Task ProcessAsync(Order order, CancellationToken ct = default);
// Step 3: Remove in next major version (v2.0+)
// Only after users have had time to migrateAPI Approval Testing
API批准测试
Prevent accidental breaking changes with automated API surface testing.
通过自动化API表面测试防止意外的破坏性变更。
Using ApiApprover + Verify
使用ApiApprover + Verify
bash
dotnet add package PublicApiGenerator
dotnet add package Verify.Xunitcsharp
[Fact]
public Task ApprovePublicApi()
{
var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
return Verify(api);
}Creates :
ApprovePublicApi.verified.txtcsharp
namespace MyLibrary
{
public class OrderProcessor
{
public OrderProcessor() { }
public void Process(Order order) { }
public Task ProcessAsync(Order order, CancellationToken ct = default) { }
}
}Any API change fails the test - reviewer must explicitly approve changes.
bash
dotnet add package PublicApiGenerator
dotnet add package Verify.Xunitcsharp
[Fact]
public Task ApprovePublicApi()
{
var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
return Verify(api);
}生成:
ApprovePublicApi.verified.txtcsharp
namespace MyLibrary
{
public class OrderProcessor
{
public OrderProcessor() { }
public void Process(Order order) { }
public Task ProcessAsync(Order order, CancellationToken ct = default) { }
}
}任何API变更都会导致测试失败 - 审核者必须明确批准变更。
PR Review Process
拉取请求审查流程
- PR includes changes to files
*.verified.txt - Reviewers see exact API surface changes in diff
- Breaking changes are immediately visible
- Conscious decision required to approve
- 拉取请求包含对文件的变更
*.verified.txt - 审核者可在差异中查看确切的API表面变更
- 破坏性变更会立即显现
- 批准前需做出明确决策
Wire Compatibility
有线兼容性
For distributed systems, serialized data must be readable across versions.
对于分布式系统,序列化数据必须能跨版本读取。
Requirements
要求
| Direction | Requirement |
|---|---|
| Backward | Old writers → New readers (current version reads old data) |
| Forward | New writers → Old readers (old version reads new data) |
Both are required for zero-downtime rolling upgrades.
| 方向 | 要求 |
|---|---|
| 向后兼容 | 旧写入端 → 新读取端(当前版本可读取旧数据) |
| 向前兼容 | 新写入端 → 旧读取端(旧版本可读取新数据) |
两者都是零停机滚动升级的必要条件。
Safely Evolving Wire Formats
安全演进有线格式
Phase 1: Add read-side support (opt-in)
csharp
// New message type - readers deployed first
public sealed record HeartbeatV2(
Address From,
long SequenceNr,
long CreationTimeMs); // NEW field
// Deserializer handles both old and new
public object Deserialize(byte[] data, string manifest) => manifest switch
{
"Heartbeat" => DeserializeHeartbeatV1(data), // Old format
"HeartbeatV2" => DeserializeHeartbeatV2(data), // New format
_ => throw new NotSupportedException()
};Phase 2: Enable write-side (opt-out, next minor version)
csharp
// Config to enable new format (off by default initially)
akka.cluster.use-heartbeat-v2 = onPhase 3: Make default (future version)
After install base has absorbed read-side code.
阶段1:添加读取端支持(可选加入)
csharp
// New message type - readers deployed first
public sealed record HeartbeatV2(
Address From,
long SequenceNr,
long CreationTimeMs); // NEW field
// Deserializer handles both old and new
public object Deserialize(byte[] data, string manifest) => manifest switch
{
"Heartbeat" => DeserializeHeartbeatV1(data), // Old format
"HeartbeatV2" => DeserializeHeartbeatV2(data), // New format
_ => throw new NotSupportedException()
};阶段2:启用写入端(可选退出,下一个次要版本)
csharp
// Config to enable new format (off by default initially)
akka.cluster.use-heartbeat-v2 = on阶段3:设为默认(未来版本)
在安装基础已适配读取端代码之后。
Schema-Based Serialization
基于Schema的序列化
Prefer schema-based formats over reflection-based:
| Format | Type | Wire Compatibility |
|---|---|---|
| Protocol Buffers | Schema-based | Excellent - explicit field numbers |
| MessagePack | Schema-based | Good - with contracts |
| System.Text.Json | Schema-based (with source gen) | Good - explicit properties |
| Newtonsoft.Json | Reflection-based | Poor - type names in payload |
| BinaryFormatter | Reflection-based | Terrible - never use |
See skill for details.
dotnet/serialization优先选择基于Schema的格式而非基于反射的格式:
| 格式 | 类型 | 有线兼容性 |
|---|---|---|
| Protocol Buffers | 基于Schema | 极佳 - 明确的字段编号 |
| MessagePack | 基于Schema | 良好 - 配合契约 |
| System.Text.Json | 基于Schema(配合源代码生成) | 良好 - 明确的属性 |
| Newtonsoft.Json | 基于反射 | 较差 - 负载中包含类型名称 |
| BinaryFormatter | 基于反射 | 极差 - 绝不使用 |
详情请查看技能。
dotnet/serializationEncapsulation Patterns
封装模式
Internal APIs
内部API
Mark non-public APIs explicitly:
csharp
// Attribute for documentation
[InternalApi]
public class ActorSystemImpl { }
// Namespace convention
namespace MyLibrary.Internal
{
public class InternalHelper { } // Public for extensibility, not for users
}Document clearly:
Types innamespaces or marked with.Internalmay change between any releases without notice.[InternalApi]
显式标记非公共API:
csharp
// Attribute for documentation
[InternalApi]
public class ActorSystemImpl { }
// Namespace convention
namespace MyLibrary.Internal
{
public class InternalHelper { } // Public for extensibility, not for users
}明确文档说明:
位于命名空间或标记有.Internal的类型可能在任意版本间变更,恕不另行通知。[InternalApi]
Sealing Classes
密封类
csharp
// DO: Seal classes not designed for inheritance
public sealed class OrderProcessor { }
// DON'T: Leave unsealed by accident
public class OrderProcessor { } // Users might inherit, blocking changescsharp
// DO: Seal classes not designed for inheritance
public sealed class OrderProcessor { }
// DON'T: Leave unsealed by accident
public class OrderProcessor { } // Users might inherit, blocking changesInterface Segregation
接口隔离
csharp
// DO: Small, focused interfaces
public interface IOrderReader
{
Order? GetById(OrderId id);
}
public interface IOrderWriter
{
Task SaveAsync(Order order);
}
// DON'T: Monolithic interfaces (can't add methods without breaking)
public interface IOrderRepository
{
Order? GetById(OrderId id);
Task SaveAsync(Order order);
// Adding new methods breaks all implementations!
}csharp
// DO: Small, focused interfaces
public interface IOrderReader
{
Order? GetById(OrderId id);
}
public interface IOrderWriter
{
Task SaveAsync(Order order);
}
// DON'T: Monolithic interfaces (can't add methods without breaking)
public interface IOrderRepository
{
Order? GetById(OrderId id);
Task SaveAsync(Order order);
// Adding new methods breaks all implementations!
}Versioning Strategy
版本控制策略
Semantic Versioning (Practical)
语义化版本控制(实践版)
| Version | Changes Allowed |
|---|---|
| Patch (1.0.x) | Bug fixes, security patches |
| Minor (1.x.0) | New features, deprecations, obsolete removal |
| Major (x.0.0) | Breaking changes, old API removal |
| 版本 | 允许的变更 |
|---|---|
| 补丁版本 (1.0.x) | Bug修复、安全补丁 |
| 次要版本 (1.x.0) | 新功能、弃用标记、移除已过时内容 |
| 主版本 (x.0.0) | 破坏性变更、旧API移除 |
Key Principles
核心原则
- No surprise breaks - Even major versions should be announced and planned
- Extensions anytime - New APIs can ship in any release
- Deprecate before remove - for at least one minor version
[Obsolete] - Communicate timelines - Users need to plan upgrades
- 无意外破坏 - 即使主版本变更也应提前公告并规划
- 随时扩展 - 新API可在任意版本中发布
- 先弃用再移除 - 标记至少保留一个次要版本
[Obsolete] - 沟通时间线 - 用户需要时间规划升级
Chesterton's Fence
切斯特顿栅栏原则
Before removing or changing something, understand why it exists.
Assume every public API is used by someone. If you want to change it:
- Socialize the proposal on GitHub
- Document migration path
- Provide deprecation period
- Ship in planned release
在移除或修改某事物之前,先理解它存在的原因。
假设每个公共API都有用户在使用。如果想要变更它:
- 在GitHub上公示提案
- 记录迁移路径
- 提供弃用周期
- 在计划好的版本中发布
Pull Request Checklist
拉取请求检查清单
When reviewing PRs that touch public APIs:
- No removed public members (use instead)
[Obsolete] - No changed signatures (add overloads instead)
- No new required parameters (use defaults)
- API approval test updated (changes reviewed)
.verified.txt - Wire format changes are opt-in (read-side first)
- Breaking changes documented (release notes, migration guide)
当审查涉及公共API的拉取请求时:
- 无公共成员被移除(改用标记)
[Obsolete] - 无签名变更(改用添加重载)
- 无新增必填参数(使用默认值)
- API批准测试已更新(已审查的变更)
.verified.txt - 有线格式变更为可选加入(先部署读取端)
- 破坏性变更已文档化(发布说明、迁移指南)
Anti-Patterns
反模式
Breaking Changes Disguised as Fixes
伪装成修复的破坏性变更
csharp
// "Bug fix" that breaks users
public async Task<Order> GetOrderAsync(OrderId id) // Was sync!
{
// "Fixed" to be async - but breaks all callers
}
// Correct: Add new method, deprecate old
[Obsolete("Use GetOrderAsync instead")]
public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;
public async Task<Order> GetOrderAsync(OrderId id) { }csharp
// "Bug fix" that breaks users
public async Task<Order> GetOrderAsync(OrderId id) // Was sync!
{
// "Fixed" to be async - but breaks all callers
}
// Correct: Add new method, deprecate old
[Obsolete("Use GetOrderAsync instead")]
public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;
public async Task<Order> GetOrderAsync(OrderId id) { }Silent Behavior Changes
静默行为变更
csharp
// Changing defaults breaks users who relied on old behavior
public void Configure(bool enableCaching = true) // Was: false!
// Correct: New parameter with new name
public void Configure(
bool enableCaching = false, // Original default preserved
bool enableNewCaching = true) // New behavior opt-incsharp
// Changing defaults breaks users who relied on old behavior
public void Configure(bool enableCaching = true) // Was: false!
// Correct: New parameter with new name
public void Configure(
bool enableCaching = false, // Original default preserved
bool enableNewCaching = true) // New behavior opt-inPolymorphic Serialization
多态序列化
csharp
// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }
// Renaming Order class = wire break!
// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }csharp
// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }
// Renaming Order class = wire break!
// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }