api-design について
.NET ライブラリ開発者向けに、公開 API の設計とソース、バイナリ、ワイヤー互換性を扱うスキルです。変更レビュー、バージョン管理、公開 API の自動検査について指針を示します。
主な機能
- 01ソース・バイナリ・ワイヤー互換性の原則
- 02API を拡張のみで設計する方針
- 03非推奨化とセマンティックバージョニングの指針
- 04Verify による API サーフェス承認
- 05シリアライズ形式を進化させる方法
活用シーン
- 01NuGet パッケージの公開 API の計画
- 02API 変更の互換性リスク評価
- 03分散システムのワイヤー形式更新の計画
- 04プルリクエストでの API 変更レビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill api-designインストール後、Claude Code で/api-designを入力して読み込まれたか確認します。
詳細
パブリック API の設計と互換性
このスキルを使用する場面
次の場合にこのスキルを使用します:
- NuGet パッケージやライブラリのパブリック API を設計するとき
- 既存のパブリック API を変更するとき
- 分散システムのワイヤ形式の変更を計画するとき
- バージョニング戦略を実装するとき
- 破壊的変更についてプルリクエストをレビューするとき
3 種類の互換性
| 種類 | 定義 | 対象範囲 |
|---|---|---|
| API/ソース | 新しいバージョンに対してコードをコンパイルできる | パブリックメソッドのシグネチャ、型 |
| バイナリ | コンパイル済みコードが新しいバージョンで実行できる | アセンブリのレイアウト、メソッドトークン |
| ワイヤ | シリアル化されたデータを他のバージョンで読み取れる | ネットワークプロトコル、永続化形式 |
いずれかの互換性を損なうと、ユーザーにアップグレードの負担が生じます。
拡張のみの設計
安定した API の基盤は、削除や変更はせず、拡張のみを行うことです。
3 つの柱
- 以前の機能は不変です - リリース後は、動作とシグネチャが固定されます
- 新しい構成要素で機能を追加します - オーバーロードや新しい型、オプトイン機能を追加します
- 削除は非推奨期間の経過後に限ります - リリース単位ではなく、年単位です
メリット
- 古いコードは新しいバージョンでも引き続き動作します
- 新旧の経路が共存します
- デフォルトでは、アップグレードによって互換性が損なわれません
- ユーザーは自分の予定に合わせてアップグレードできます
参考資料:
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) | バグ修正、セキュリティパッチ |
| マイナー(1.x.0) | 新機能、非推奨化、廃止済み機能の削除 |
| メジャー(x.0.0) | 破壊的変更、古い API の削除 |
主要な原則
- 予期しない破壊的変更を避ける - メジャーバージョンであっても、事前に告知し、計画を立てる必要があります
- いつでも拡張可能 - 新しい API はどのリリースでも提供できます
- 削除前に非推奨にする - 少なくとも 1 つのマイナーバージョンで
[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 }