api-design

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Public 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

三种兼容性类型

TypeDefinitionScope
API/SourceCode compiles against newer versionPublic method signatures, types
BinaryCompiled code runs against newer versionAssembly layout, method tokens
WireSerialized data readable by other versionsNetwork 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

三大支柱

  1. Previous functionality is immutable - Once released, behavior and signatures are locked
  2. New functionality through new constructs - Add overloads, new types, opt-in features
  3. Removal only after deprecation period - Years, not releases
  1. 原有功能不可变 - 一旦发布,行为和签名即被锁定
  2. 通过新结构添加新功能 - 添加重载、新类型、可选加入的功能
  3. 仅在弃用期后移除 - 以年为单位,而非以版本发布周期

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 migrate

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 migrate

API 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.Xunit
csharp
[Fact]
public Task ApprovePublicApi()
{
    var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
    return Verify(api);
}
Creates
ApprovePublicApi.verified.txt
:
csharp
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.Xunit
csharp
[Fact]
public Task ApprovePublicApi()
{
    var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
    return Verify(api);
}
生成
ApprovePublicApi.verified.txt
csharp
namespace MyLibrary
{
    public class OrderProcessor
    {
        public OrderProcessor() { }
        public void Process(Order order) { }
        public Task ProcessAsync(Order order, CancellationToken ct = default) { }
    }
}
任何API变更都会导致测试失败 - 审核者必须明确批准变更。

PR Review Process

拉取请求审查流程

  1. PR includes changes to
    *.verified.txt
    files
  2. Reviewers see exact API surface changes in diff
  3. Breaking changes are immediately visible
  4. Conscious decision required to approve

  1. 拉取请求包含对
    *.verified.txt
    文件的变更
  2. 审核者可在差异中查看确切的API表面变更
  3. 破坏性变更会立即显现
  4. 批准前需做出明确决策

Wire Compatibility

有线兼容性

For distributed systems, serialized data must be readable across versions.
对于分布式系统,序列化数据必须能跨版本读取。

Requirements

要求

DirectionRequirement
BackwardOld writers → New readers (current version reads old data)
ForwardNew 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 = on
Phase 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:
FormatTypeWire Compatibility
Protocol BuffersSchema-basedExcellent - explicit field numbers
MessagePackSchema-basedGood - with contracts
System.Text.JsonSchema-based (with source gen)Good - explicit properties
Newtonsoft.JsonReflection-basedPoor - type names in payload
BinaryFormatterReflection-basedTerrible - never use
See
dotnet/serialization
skill for details.

优先选择基于Schema的格式而非基于反射的格式:
格式类型有线兼容性
Protocol Buffers基于Schema极佳 - 明确的字段编号
MessagePack基于Schema良好 - 配合契约
System.Text.Json基于Schema(配合源代码生成)良好 - 明确的属性
Newtonsoft.Json基于反射较差 - 负载中包含类型名称
BinaryFormatter基于反射极差 - 绝不使用
详情请查看
dotnet/serialization
技能。

Encapsulation 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 in
.Internal
namespaces or marked with
[InternalApi]
may change between any releases without notice.
显式标记非公共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 changes
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 changes

Interface 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)

语义化版本控制(实践版)

VersionChanges 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

核心原则

  1. No surprise breaks - Even major versions should be announced and planned
  2. Extensions anytime - New APIs can ship in any release
  3. Deprecate before remove -
    [Obsolete]
    for at least one minor version
  4. Communicate timelines - Users need to plan upgrades
  1. 无意外破坏 - 即使主版本变更也应提前公告并规划
  2. 随时扩展 - 新API可在任意版本中发布
  3. 先弃用再移除 -
    [Obsolete]
    标记至少保留一个次要版本
  4. 沟通时间线 - 用户需要时间规划升级

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:
  1. Socialize the proposal on GitHub
  2. Document migration path
  3. Provide deprecation period
  4. Ship in planned release

在移除或修改某事物之前,先理解它存在的原因。
假设每个公共API都有用户在使用。如果想要变更它:
  1. 在GitHub上公示提案
  2. 记录迁移路径
  3. 提供弃用周期
  4. 在计划好的版本中发布

Pull Request Checklist

拉取请求检查清单

When reviewing PRs that touch public APIs:
  • No removed public members (use
    [Obsolete]
    instead)
  • No changed signatures (add overloads instead)
  • No new required parameters (use defaults)
  • API approval test updated (
    .verified.txt
    changes reviewed)
  • 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-in
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-in

Polymorphic 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 }

Resources

参考资源