現代のWebアプリケーションにおけるAIとモバイルネイティブ機能の統合: .NETの進化
ミラン・ヨヴァノビッチは最近、APIのバージョニングを早期に行うことに対する強い反対意見を発表しました。 核心となる点:多くのチームはv2をあまりに早い段階で使用しようとします。それは、契約進化戦略が欠けているためです。 バージョニングは互換性のためのツールであり、設計戦略ではありません。
この議論はIron Softwareのエンジニアリングチームに共鳴します。 私たちは.NETライブラリを出荷しているため、私たちの製品のパブリックな表面はAPIです。 すべてのメソッドシグネチャ、すべてのプロパティ、すべてのデフォルトの動作が、何千もの顧客のコードベース内に座っている契約です。 メジャーバージョンのバンプはリリースではありません。 それは、すべての下流の人々にとって移行プロジェクトです。
以下は、ライブラリアー作者の視点から見るミランの作品についての開発者の見解であり、同じ互換性のルールが、REST APIを出荷する場合でも、NuGetパッケージを出荷する場合でも適用される方法です。
要約
- バージョニングは設計戦略ではありません。 共存が失敗したときの非常口です。
- 破壊的な変更は、URLやスキーマだけでなく、動作にも隠れています。
- 4つの互換性のルール:削除しない、処理を変更しない、検証を厳しくしない、追加をオプションに保つ。
- 新しいバージョンよりも新しい操作を行う方がほとんど常に安価です。
- 真の非推奨化には、ランタイムシグナルとテレメトリが必要であり、単なるドキュメント更新ではありません。
HTTPルールはライブラリAPIにも適用されます
ミランは議論を/ordersのためのREST APIを中心に展開しますが、APIがNuGetパッケージで提供される公開C#クラスである場合も同じ規則が適用されます。 マッピングは直接的です:
| REST APIの変更 | NuGetライブラリの等価 |
|---|---|
| JSONフィールドの名前変更 | 公開プロパティの名前変更 |
| エンドポイントの削除 | 公開メソッドの削除 |
| リクエスト検証の強化 | 非NULLパラメータの追加 |
| 操作動作の変更 | メソッドが内部で行うことの変更 |
| 必須フィールドの追加 | 必須のコンストラクターパラメータの追加 |
人気のある.NETライブラリのメジャーバージョンをプルして、リネームされたAPIを修正するのに半日を費やした経験があれば、それはおそらく加えた形で処理できたv2決定の受け手となったことがあります。
実際に消費者を壊すもの
ミランのリストは正確です:
- フィールドの削除または名前の変更
- 既存データの意味の変更
- リクエストの検証の強化
- ページネーションやエラーフォーマットの変更
- enumのような値が永遠に閉じていると仮定すること
2番目の項目は、最も頻繁にチームを驚かせるものです。既存のデータの形状を変えずにその意味を変えることです。 JSONは同じように見えます。 C#のシグネチャは同じように見えます。 すべてコンパイルされます。 実行時に何もスローされません。しかし、フィールドは今や異なる意味を持ち、古いセマンティクスに依存していたすべての消費者は静かに間違っています。
ミランの例:
// Before
{ "total": 100 }
// After
{ "total": { "amount": 100, "currency": "USD" } }同じフィールド名。 同じエンドポイント。 totalを数値として解析していたすべてのクライアントが今は壊れています。
ライブラリの同等物とは、メソッドが何を返すか、もしくは入力をどう解釈するかを変更することです。 以前は上書きしていたSave()メソッドが、現在では追加を行います。 デフォルトがTrimパラメータ。 無効な入力でスローされる代わりに、静かにデフォルト値を返すようになるメソッドです。
4つの互換性ルール
ミランはルールを次のように要約します: 何も取り除かない、処理ルールを変えない、オプションのものを必須にしない、追加するものはすべてオプションでなければならない。 公共APIを担当するチームの前にはこの4原則を保つ価値があります:
- 既存のフィールドと挙動を維持する。
- オプションのリクエストデータを必須データにしない。
- 既存の操作が行うことを変更しない。
- 新しいものはデフォルトで付加的かつオプションにする。
これらはライブラリ設計に直接反映されます。 "何も取り除かない"というのは、公開メンバーを削除しないことを意味します。 "処理ルールを変えない"は、既存のメソッドは出荷時にしていたように振る舞うべきということです。 "オプションを必須にしない"は、既存のメソッドに必須パラメータを追加しないことを意味します。 代わりにオーバーロードを提供します。 "付加的かつオプション"というのは、新しい機能性は新しいメソッドまたは賢明なデフォルトを持つオプションのパラメータに属するということです。
実際にはどう機能するか
これらのルールを明示する最も明確な方法は、本物のAPI決定を使うことです。したがって、こちらが私たちの1例です。
IronPDF は、HTMLからPDFへの変換のためによりリッチなレンダリングオプションセットをサポートする必要がありました。カスタムペーパーサイズ、カスタムマージン、CSSメディアエミュレーション、ヘッダーとフッターテンプレートなどがあります。 直接的なアプローチは、既存のレンダリングメソッドを変更し、新しいオプションを受け入れるようにすることだったでしょう。 その決定は、APIのシンプルな形式を使用しているすべての顧客を壊してしまうでしょう。
文脈として、ライブラリは、標準 for .NETパッケージチャネルを通じてインストールされます:
# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdf# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdfIronPdf NuGetパッケージは、1,800万を超えるダウンロードを蓄積しており、APIの安定性が重要である理由の一部です。すべての破壊的な変更が、その多くの統合で波及します。
代わりに出荷したアプローチ:
// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");' The original, three-year-old API. Still works. Still unchanged.
Dim renderer As New ChromePdfRenderer()
Dim pdf As PdfDocument = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")
' New rendering options live on an options object, not in the method signature.
renderer = New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {.HtmlFragment = "..."}
pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")決定に関する3つの観察点:
- 元の
RenderHtmlAsPdf(string html)シグネチャは変更されていません。 アップグレードした顧客はコードを1行も変更する必要がありませんでした。 - 新しい機能は、消費者が選択して使うオプションオブジェクトに存在します。 メソッドには新しい必須のパラメータはありません。
RenderingOptions上のデフォルトは、以前のAPIと同等の出力を生成します。 何も構成しない人にとっては挙動は変わりません。
それはミランのリストからのルール1、2、4が一度に適用されたものです。 製品は進化しました。 契約は進化しませんでした。
RenderHtmlAsPdfV2(string html, RenderingOptions options)を出荷する誘惑は本物でした。 APIリファレンスページではさらに整然として見えましたでしょう。 それはすべての顧客に移行をコストとして課したでしょう。 私たちは別の選択をしました。
寛容な読み手
ミランの"追加し、置き換えない"という議論のもう片方は、消費者もまた責任を持っているということです。 よく設計されたクライアントは、理解できないフィールドを無視するべきです。
.NETでは、System.Text.Jsonはデフォルトで不明なプロパティを無視します。これが正しいデフォルトです。 通常リスクは2つの場所に現れます:
- 予期しないフィールドを拒絶する厳密なスキーマを持った生成されたSDK
- 正確なJSONの等価性を確認する契約テスト
どちらも"未知のフィールドを無視する"という保証をトリップワイヤーに変えます。サーバーが新しいオプションのプロパティを追加した瞬間にCIが壊れるのであれば、後方互換性を持っているわけではありません。 互換性ポリシーとしての回帰検出器を持っています。
挙動は契約の一部
ミランのDELETE /orders/{id}に関するセクションでの、ソフト削除からハード削除への静かな移行は、この問題について我々が見た最も明確な説明です。
URLは同じです。 リクエストボディは同じです。 レスポンスの形も同じです。 サーバーでの操作が異なります。
これはスキーマ差分で捉えられない最も危険なカテゴリーの破壊的変更です。 OpenAPI仕様は同一です。 生成されたクライアントはコンパイルされます。 統合テストは合格します。 そして、削除されたオーダーは復元可能であると考えてツールを作ったすべての消費者が、本番で静かにデータを破壊します。
ライブラリの同等品はシグニチャを変更せずにメソッドが何を行うかを変更することです。 私たちが明確に避けた例:
- 同期フラッシュから非同期fire-and-forgetに静かに変わった
Save()メソッド - 生の結果を返して始め、結果を後処理し始めるOCRメソッド
- 読み取れない入力でスローし、空の文字列を返し始めるバーコードリーダー
これらの各々は、改善として装った契約の破たんです。 正しい応答は、ミランのものと同じです: 新しいメソッドやオプションを追加し、旧動作は変更せず、テレメトリが安全であると示したときにのみ古いパスを廃止します。
検証の引き締め
このカテゴリは最終的にすべてのチームに影響を与えます。 同じ間違いの2つのバリエーションがあります:
- 既存のオプションフィールドを必須にする
- 新しいフィールドを追加し、初日から必須としてマークする
どちらも古いクライアントを壊します。 エンドポイントパスは移動しませんが、以前は成功していたリクエストが今や実行時に失敗します。
ライブラリのバージョンとしてのこの間違いは、必須のコンストラクターパラメータを追加するか、既存のオプションパラメータを必須にすることです。 既存のすべての呼び出し元がコンパイル時間で壊れるので、実行時の失敗よりも好ましいですが、それでもすべての消費者に移行コストを課します。
より安全なパス:
- 移行期間中に欠落した値を受け入れ、可能な場合はデフォルトを推測します。
- よりリッチな入力形状を必要とする新しいオーバーロードまたはビルダーを追加します。
- より厳格なワークフローのために新しい操作またはコンストラクターを導入します。
基礎となるルールは一貫しています: 契約に追加されたものはすべてオプションでなければならず、以前はオプションであったものは引き続きオプションである必要があります。 本当に厳格な要件が必要なら、それは既存のものを締め付けるのではなく、新しい操作に属します。
新しい操作は新しいバージョンよりもほとんどの時間で安価です
これは最も内面化する価値がある原則です。
既存のエンドポイントがきれいにサポートする以上に進化したユースケースがある場合、一般的な反応はフラグでエンドポイントを過負荷にすることです:
POST /orders?validateOnly=true&includeTaxEstimate=true&reserveInventory=trueまたは、より混乱を招くものとして、変更をバージョニングの問題と宣言して/v2/ordersに取り組みを開始する。 どちらも通常間違っています。 よりクリーンなアプローチは、既存のものの隣に新しい操作を追加することです:
POST /orders
POST /orders/quote
POST /checkout-sessions各操作にはクリーンな契約、明確な許可、独立した検証、独自の進化パスがあります。 元のエンドポイントはシンプルなまま。 残りのAPIはメジャーバージョンアップに引きずり込まれません。
ライブラリコンテキストでは、同等のものは、複数のオプションパラメータが不可読になるまで既存のものにオーバーロードするのではなく、新しいメソッドを追加することです。 ExtractText()は単純なテキスト抽出器として残ります。 ExtractTextWithLayout()は、より豊富なバリエーションになります。 ExtractStructuredDocument()は最も豊富になります。明確な契約を持つ3つのメソッドは、8つのオプションパラメータを持つ1つのメソッドより好ましいです。
慎重に廃止する
これは、API変更管理の半分であり、ほとんどのチームが省略します。そして、それが戦略が働くかを決定します。
本当の廃止は、チェンジログのメモではありません。 それは4つのステップが伴います:
- フィールドまたはエンドポイントに対してOpenAPI説明で非推奨としてマークする(.NETの世界では
[Obsolete]属性と共に)。 - ランタイムで廃止を知らせ、ライブトラフィックがそれを表面化するようにします。
- 実際の移行ガイドへのリンクを提供します。
- テレメトリを使用して使用を測定し、削除が安全であるかを判断します。
HTTP APIの場合、ランタイムシグネリングは簡単です:
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/migrations/orders-total>; rel="deprecation".NETライブラリでは同等のものは、[Obsolete("新しいメソッドを使用します。 これはv2026.xで削除されます", DiagnosticId = "IRON001")] attribute paired with a UrlFormat が移行ページを指し示しています。 コンパイラ警告は各消費者のビルド出力に現れ、診断IDは意図的な抑制を許可し、リンクは消費者に文書化された移行パスを提供します。
テレメトリステップは非交渉です。 どの顧客がまだ廃止方法に依存しているかを知らない場合、削除は推測になります。 結果は、アクティブな統合を破壊する早期削除か、廃止の目的を損なう無期限の運用コストになります。
バージョニングが正しい選択のとき
ミランは反バージョニングではありませんし、私たちもそうではありません。 バージョニングが適切なとき:
- 古いと新しいセマンティクスが本当に共存できない
- リソースモデルが根本的に変わった
- 互換性ルールが、誰もが理解できない契約を強要する
ポイントは、完全にバージョニングを避けることではありません。 ポイントは、共存が失敗したためにそれに手を伸ばすことであり、それが机の上で最初のアイデアだったからではありません。
バージョニングが必要なときは、実際の廃止プロセスと対を成す必要があります。 難しい作業はv2を出荷することではありません。 難しい作業は消費者をv1から移行させることです。
決定ルール
ミランのフレームワークが適用するのに適切なものです:
- 置換する代わりに追加できますか?
- 移行期間中に古い契約と新しい契約が共存できますか?
- 古いものを変化させる代わりに新しい操作を導入できますか?
- ドキュメント、ヘッダー、テレメトリと共に古い形を廃止できますか?
すべてに対して"はい"の答えが得られるなら、新しいバージョンはおそらく不要です。 そうでない場合、そして2つの世界が本当に共存できない場合は、意図的にバージョンを付けます。
契約を進化するようにデザインします。 消費者を今日のコードではなく長寿命の統合として扱います。 互換性が本当に尽きたケースのためにバージョニングを予約します。
完全な記事、より長い手続き型の例を含め、ミランのオリジナルの投稿を読んでください。
.NETライブラリを依存するために選択するときに尋ねる価値のある質問は、ミランの投稿が構築されているものです。3年後、このライブラリは私が統合に使用したAPIのように見えますか?
それは私たちが各リリースで答えようと努力する質問です。 2020年の簡単な呼び出しはまだ機能します。 新しい機能はその横に置かれ、オプションかつ付加的です。メジャーバージョン移行を強制されません。
もしそのライブラリ設計アプローチが必要に一致するなら、30日間の無料トライアルを開始し、自分でAPIリファレンスをレビューしてください。 5分のクイックスタートはインストール、ライセンス認証、最初のレンダリングされたPDFを説明します。 パッケージ自体は任意 for .NETプロジェクトから1コマンドで利用できます:
NuGetが好まれるパスではない環境では、直接ダウンロードでDLLとWindowsインストーラを提供します。
