mcpField
A
api-design

api-design

github.com/aaronontheweb開発1.2k673 回インストール3 日前に更新
リポジトリ

api-design について

.NET ライブラリ開発者向けに、公開 API の設計とソース、バイナリ、ワイヤー互換性を扱うスキルです。変更レビュー、バージョン管理、公開 API の自動検査について指針を示します。

主な機能

  1. 01ソース・バイナリ・ワイヤー互換性の原則
  2. 02API を拡張のみで設計する方針
  3. 03非推奨化とセマンティックバージョニングの指針
  4. 04Verify による API サーフェス承認
  5. 05シリアライズ形式を進化させる方法

活用シーン

  1. 01NuGet パッケージの公開 API の計画
  2. 02API 変更の互換性リスク評価
  3. 03分散システムのワイヤー形式更新の計画
  4. 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 つの柱
  1. 以前の機能は不変です - リリース後は、動作とシグネチャが固定されます
  2. 新しい構成要素で機能を追加します - オーバーロードや新しい型、オプトイン機能を追加します
  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 レビュープロセス
  1. PR に *.verified.txt ファイルの変更を含めます
  2. レビュアーは差分で API サーフェスの変更内容を正確に確認できます
  3. 破壊的変更がすぐに分かります
  4. 承認には意識的な判断が必要です

ワイヤ互換性

分散システムでは、シリアル化されたデータをバージョン間で読み取れる必要があります。

要件
方向要件
後方互換性古い書き込み側 → 新しい読み取り側(現行バージョンで古いデータを読み取る)
前方互換性新しい書き込み側 → 古い読み取り側(古いバージョンで新しいデータを読み取る)

ダウンタイムなしでローリングアップグレードを行うには、両方が必要です。

ワイヤ形式を安全に進化させる

フェーズ 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 の削除
主要な原則
  1. 予期しない破壊的変更を避ける - メジャーバージョンであっても、事前に告知し、計画を立てる必要があります
  2. いつでも拡張可能 - 新しい API はどのリリースでも提供できます
  3. 削除前に非推奨にする - 少なくとも 1 つのマイナーバージョンで [Obsolete] を使用します
  4. スケジュールを伝える - ユーザーがアップグレードを計画できるようにします
チェスタトンの柵

何かを削除したり変更したりする前に、それが存在する理由を理解してください。

すべての公開 API は誰かに使われていると考えてください。変更する場合は、次の手順に従います。

  1. GitHub で提案を共有する
  2. 移行手順を文書化する
  3. 非推奨期間を設ける
  4. 計画したリリースで提供する

プルリクエストのチェックリスト

公開 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 }

リソース

コメント