关于 api-design
面向 .NET 库开发者的公共 API 设计与兼容性技能,涵盖源代码、二进制和网络格式兼容。提供 API 变更审查、版本策略及自动化表面测试的指导。
主要功能
- 01源代码、二进制与网络兼容原则
- 02只扩展 API 的设计策略
- 03弃用与语义化版本指南
- 04使用 Verify 审批 API 表面
- 05演进序列化格式的方法
适用场景
- 01规划 NuGet 包的公共 API
- 02评估现有 API 变更是否破坏兼容
- 03为分布式系统安排网络格式升级
- 04在 PR 中审核 API 变更
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill api-design安装后在 Claude Code 中输入/api-design确认已加载。
详细介绍
公共 API 设计与兼容性
何时使用此技能
在以下情况下使用此技能:
- 为 NuGet 包或库设计公共 API
- 修改现有公共 API
- 规划分布式系统的线路格式变更
- 实施版本控制策略
- 审查可能引入破坏性变更的拉取请求
三种兼容性类型
| 类型 | 定义 | 范围 |
|---|---|---|
| API/源代码 | 代码可针对较新版本编译 | 公共方法签名、类型 |
| 二进制 | 已编译代码可在较新版本上运行 | 程序集布局、方法标记 |
| 线路 | 其他版本可读取序列化数据 | 网络协议、持久化格式 |
破坏其中任何一种兼容性,都会增加用户升级的阻力。
仅扩展设计
稳定 API 的基础:只扩展,绝不删除或修改。
三大支柱
- 先前功能不可变 - 发布后,行为和签名即固定
- 通过新构造添加新功能 - 添加重载、新类型、可选择启用的功能
- 仅在弃用期结束后移除 - 以年为单位,而非以发布次数为单位
优点
- 旧代码在新版本中继续可用
- 新旧路径并存
- 默认情况下,升级不会造成破坏
- 用户可按自己的计划升级
资源:
API 变更指南
安全变更(任何版本均可)
// SAFE: Add NEW overload methods that delegate to existing methods
// Existing method - do not modify its signature
public void Process(Order order) { ... }
// New overload - safe to add
public void Process(Order order, CancellationToken ct)
{
// implementation that handles cancellation
}
// SAFE: Add NEW overloads for additional functionality
// Existing method - do not modify
public void Send(Message msg) { ... }
// New overload - safe to add
public void Send(Message msg, Priority priority)
{
// implementation that handles priority
}
// 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
}
不安全变更(绝不允许,或仅限主版本)
// 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 optional parameters to EXISTING methods (binary incompatible!)
// The compiled IL method signature changes - callers compiled against
// the old signature will get MissingMethodException at runtime.
// Optional parameter defaults are baked into the CALLER's assembly at compile time.
public void Process(Order order, CancellationToken ct = default); // Breaks binary compat!
public void Send(Message msg, Priority priority = Priority.Normal); // Breaks binary compat!
// Correct approach: add a NEW overload method instead (see Safe Changes above)
// ADD required parameters without defaults
public void Process(Order order, ILogger logger); // Breaks callers!
弃用模式
// 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 审批测试
通过自动化 API 表面测试,防止意外的破坏性变更。
使用 ApiApprover + Verify
dotnet add package PublicApiGenerator
dotnet add package Verify.Xunit
[Fact]
public Task ApprovePublicApi()
{
var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
return Verify(api);
}
生成 ApprovePublicApi.verified.txt:
namespace MyLibrary
{
public class OrderProcessor
{
public OrderProcessor() { }
public void Process(Order order) { }
public Task ProcessAsync(Order order, CancellationToken ct = default) { }
}
}
任何 API 变更都会导致测试失败 - 审查者必须明确批准变更。
PR 审查流程
- PR 包含对
*.verified.txt文件的变更 - 审查者可在差异中看到精确的 API 表面变更
- 破坏性变更会立即显现
- 必须经过审慎决策才能批准
线路兼容性
对于分布式系统,序列化数据必须能跨版本读取。
要求
| 方向 | 要求 |
|---|---|
| 向后兼容 | 旧写入方 → 新读取方(当前版本读取旧数据) |
| 向前兼容 | 新写入方 → 旧读取方(旧版本读取新数据) |
要实现零停机滚动升级,两者缺一不可。
安全演进线路格式
阶段 1:添加读取端支持(选择启用)
// 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:启用写入端(选择退出,下一个次版本)
// Config to enable new format (off by default initially)
akka.cluster.use-heartbeat-v2 = on
阶段 3:设为默认(未来版本)
待已安装版本群体都具备读取端代码后。
基于架构的序列化
优先选择基于架构的格式,而非基于反射的格式:
| 格式 | 类型 | 线路兼容性 |
|---|---|---|
| Protocol Buffers | 基于架构 | 极佳 - 字段编号明确 |
| MessagePack | 基于架构 | 良好 - 需使用契约 |
| System.Text.Json | 基于架构(使用源生成器) | 良好 - 属性明确 |
| Newtonsoft.Json | 基于反射 | 较差 - 负载中包含类型名称 |
| BinaryFormatter | 基于反射 | 极差 - 绝不要使用 |
详情请参阅 dotnet/serialization 技能。
封装模式
内部 API
明确标记非公共 API:
// Attribute for documentation
[InternalApi]
public class ActorSystemImpl { }
// Namespace convention
namespace MyLibrary.Internal
{
public class InternalHelper { } // Public for extensibility, not for users
}
清晰地说明:
.Internal命名空间中的类型或标记为[InternalApi]的类型,可能会在任何版本中随时变更,恕不另行通知。
密封类
// 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
接口隔离
// 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!
}
版本控制策略
语义化版本控制(实用版)
| 版本 | 允许的变更 |
|---|---|
| 补丁(1.0.x) | Bug 修复、安全补丁 |
| 次版本(1.x.0) | 新功能、弃用、移除已过时内容 |
| 主版本(x.0.0) | 破坏性变更、移除旧 API |
核心原则
- 不会造成意外破坏 — 即使是主要版本,也应提前公告并规划
- 随时扩展 — 可在任何版本中发布新 API
- 先弃用,再移除 — 至少在一个次要版本中使用
[Obsolete] - 明确沟通时间表 — 用户需要规划升级
切斯特顿围栏
在移除或更改某项内容之前,先弄清楚它存在的原因。
假设每个公共 API 都有人在使用。如果你想更改它:
- 在 GitHub 上公开讨论提案
- 编写迁移路径文档
- 提供弃用期
- 在计划的版本中发布
拉取请求检查清单
审核涉及公共 API 的 PR 时:
- 不移除公共成员(改用
[Obsolete]) - 不更改签名(改为新增重载)
- 不新增必需参数(使用默认值)
- 更新 API 审批测试(审核
.verified.txt的更改) - 线格式更改设为可选(先支持读取)
- 记录破坏性更改(发行说明、迁移指南)
反模式
伪装成修复的破坏性更改
// "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) { }
静默更改行为
// 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
多态序列化
// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }
// Renaming Order class = wire break!
// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }
资源
评论
支持 Markdown