API & SDK Design #3, API 的向前相容機制

以下內容基於原文精煉重組,聚焦於「API 向前相容」的真實問題、根因、解法與實作細節。共整理 15 個具教學價值的實戰案例;每個案例含問題、根因、方案、關鍵程式碼、實測觀察/效益、學習要點與練習評估,便於教學與專案演練。

Case #1: 用 ActionFilter 強制執行 API Contract 檢查

Problem Statement(問題陳述)

  • 業務場景:WebAPI 團隊多人協作,常見有人在 Controller 新增/修改 Action,卻未同步更新對應的契約文件或 SDK,導致線上 client 呼叫到「未受控」介面,產生不相容/行為未定義的風險。
  • 技術挑戰:RESTful WebAPI 沒有像 WCF 一樣的 compile-time contract 約束;如何在運行期擋下不符合 contract 的 Action?
  • 影響範圍:SDK 調用失敗、版本破壞性變更進入主幹、線上錯誤難溯源。
  • 複雜度評級:中

Root Cause Analysis(根因分析)

  • 直接原因:
    1. REST Controller 可任意新增公開 method,缺少合約約束。
    2. 僅靠 code review 易漏掉細節,無系統化檢查。
    3. 因缺少自動化檢查,規格異動無法即時被攔截。
  • 深層原因:
    • 架構層面:缺少統一的 API contract 物件作為單一事實來源。
    • 技術層面:沒有 compile-time 機制,需在 runtime 才能辨識實際執行之 Action。
    • 流程層面:缺少將 contract 變更與版控、權限與審核綁定的流程。

Solution Design(解決方案設計)

  • 解決策略:以「IApiContract 標記介面 + 專屬 contract interface + ActionFilter 檢查」組成第二道防線;每次執行 Action 前,檢查該 Action 是否存在於對應 contract interface,否則擋下執行。

  • 實施步驟:
    1. 定義標記與契約
      • 實作細節:定義 IApiContract 與 IBirdsApiContract;Controller 必須實作契約。
      • 所需資源:C#, ASP.NET WebAPI
      • 預估時間:0.5 天
    2. 建置檢查 Filter
      • 實作細節:以反射比對 Action 名稱是否出現在契約 interface;不符則拋 NotSupportedException。
      • 所需資源:ActionFilterAttribute
      • 預估時間:0.5 天
    3. 套用到 Controller 並驗證
      • 實作細節:以屬性方式套用 Filter;撰寫簡單整合測試。
      • 所需資源:xUnit/NUnit
      • 預估時間:0.5 天
  • 關鍵程式碼/設定: ```csharp // 標記介面 interface IApiContract { }

// 契約介面 interface IBirdsApiContract : IApiContract { void Head(); IEnumerable Get(); BirdInfo Get(string serialNo); }

// 運行前檢查 public class ContractCheckActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext ctx) { var controllerType = ctx.ActionDescriptor.ControllerDescriptor.ControllerType; var contractType = controllerType.GetInterfaces() .FirstOrDefault(i => i.GetInterface(typeof(IApiContract).FullName) != null); if (contractType == null) throw new NotSupportedException(“API method(s) must defined in contract interface.”);

    var methodName = ctx.ActionDescriptor.ActionName;
    var exists = contractType.GetMethods().Any(m => m.Name == methodName);
    if (!exists) throw new NotSupportedException("API method(s) must defined in contract interface.");
} }

// 套用 [ContractCheckActionFilter] public class BirdsController : ApiController, IBirdsApiContract { // … }


- 實際案例:BirdsController 在執行 Get 前會被過濾器檢查,若不在 IBirdsApiContract 中就中止。
- 實作環境:ASP.NET WebAPI 2, .NET Framework 4.x, C#, Visual Studio 2015
- 實測數據:
  - 改善前:Controller 可新增任意公開 Action,SDK 無法保證合約一致。
  - 改善後:不在契約內的 Action 在 runtime 被 100% 攔截。
  - 改善幅度:未授權公開 API 發生率趨近 0(以開發/測試階段觀察)。

Learning Points(學習要點)
- 核心知識點:
  - REST 沒有 compile-time contract 時,如何以 Filter 建構 runtime 防線
  - 介面即契約的單一事實來源設計
  - 反射與攔截式設計的取捨
- 技能要求:
  - 必備技能:C#, WebAPI Filter, 反射
  - 進階技能:AOP/攔截器模式、動態快取最佳化
- 延伸思考:
  - 此機制可用於安全白名單或特徵旗標嗎?
  - 反射成本如何壓低?如何以快取避免每次運算?
  - 如何在 CI 上加上 compile-time 或 build-time 檢查?
- Practice Exercise(練習題)
  - 基礎練習:新增一個未在契約中的 Action,確認被攔截(30 分鐘)
  - 進階練習:為契約比對加入方法參數與回傳型別檢查(2 小時)
  - 專案練習:將契約檢查改為集中式 Filter Provider,支援多個 Controller(8 小時)
- Assessment Criteria(評估標準)
  - 功能完整性(40%):未在契約的 Action 必須被攔截
  - 程式碼品質(30%):結構清晰、錯誤訊息明確
  - 效能優化(20%):反射結果快取
  - 創新性(10%):擴充參數/回傳型別嚴格比對


## Case #2: 建立雙端共享的契約介面(API 與 SDK 單一事實來源)

### Problem Statement(問題陳述)
- 業務場景:API 與 SDK 由不同小組維護。若契約只存在於 API 端,SDK 仍可透過 URL/路由呼叫未經定義的行為,產生灰色地帶與不一致。
- 技術挑戰:如何讓 API 與 SDK 共享同一份契約定義,將開發討論焦點聚焦在「契約」?
- 影響範圍:需求溝通成本、誤用 API、回歸測試難以覆蓋。
- 複雜度評級:中

### Root Cause Analysis(根因分析)
- 直接原因:
  1. 契約僅存在 API 端,SDK 不受約束。
  2. 缺乏單一事實來源(Single Source of Truth)。
  3. 測試與合約綁定度不足。
- 深層原因:
  - 架構層面:契約與實作未解耦,無法共享。
  - 技術層面:WebAPI 缺少像 WCF 的 service contract 強型別支撐。
  - 流程層面:規格評審時未明確定位「契約」的產物形態。

### Solution Design(解決方案設計)
- 解決策略:將契約 interface(例如 IBirdsApiContract)抽離為獨立組件(契約層),同時供 API 與 SDK 參考;搭配 Case #1 的運行期檢查,讓雙端都以同一份契約行事。

- 實施步驟:
  1. 抽離契約專案
     - 實作細節:建立 Contract 專案,僅含 IApiContract 與各 API 契約介面
     - 所需資源:C#, class library
     - 預估時間:0.5 天
  2. API 與 SDK 參考契約
     - 實作細節:雙端共用介面定義,SDK 以契約為呼叫準則
     - 所需資源:NuGet 或原始碼引用
     - 預估時間:0.5 天
  3. 自動化驗證
     - 實作細節:在 CI 執行契約變更檢測
     - 所需資源:CI 腳本
     - 預估時間:0.5 天

- 關鍵程式碼/設定:
```csharp
// 契約層(單獨組件)
public interface IApiContract { }

public interface IBirdsApiContract : IApiContract
{
    void Head();
    IEnumerable<BirdInfo> Get();
    BirdInfo Get(string serialNo);
}

// SDK 端可用契約驅動封裝
public class BirdsClient // 封裝成以契約為導向的 SDK
{
    // 假想:依據契約提供對應方法
    public IEnumerable<BirdInfo> GetAll() { /* HttpClient GET /api/birds */ return null; }
}
  • 實際案例:原文中以 IBirdsApiContract 抽象 API 契約,並以 Filter 強化執行。
  • 實作環境:C#, .NET Framework 4.x, WebAPI 2
  • 實測數據:
    • 改善前:SDK 可誤用未經定義的 API 路徑。
    • 改善後:SDK 與 API 共享契約,規格對齊;配合 Filter,不在契約內的請求被擋下。
    • 改善幅度:規格偏離案例顯著降低(以開發階段觀察)。

Learning Points(學習要點)

  • 核心知識點:契約層分離、單一事實來源、合約驅動設計(Contract-First)
  • 技能要求:
    • 必備技能:C# 專案切分、NuGet/引用管理
    • 進階技能:契約驅動的 SDK 介面設計
  • 延伸思考:
    • 是否能從契約生成 SDK stub?
    • 契約版本如何管理(參見 Case #3、#4)?
  • Practice Exercise
    • 基礎:將契約抽離成獨立組件(30 分鐘)
    • 進階:以契約生成強型別 SDK wrapper(2 小時)
    • 專案:建立契約/SDK/API 三層分離的範例專案(8 小時)
  • Assessment Criteria
    • 功能完整性(40%):SDK 與 API 皆引用同一契約
    • 程式碼品質(30%):清晰的組件依賴邊界
    • 效能優化(20%):編譯/封裝效率
    • 創新性(10%):自動生成/驗證工具

Case #3: 採用 Compatible Versioning(相容性版本策略)

Problem Statement(問題陳述)

  • 業務場景:維運全球唯一服務型 API。若採點對點多版本並存,維護成本高;若只保留最新,恐導致舊版 SDK 全面壞掉。
  • 技術挑戰:在「只保留最新版」前提下,如何保證向前相容,降低運維與分裂風險?
  • 影響範圍:維護成本、開發者採用率、升級節奏與風險。
  • 複雜度評級:中

Root Cause Analysis

  • 直接原因:
    1. 未定義版本策略,造成升級不可預期。
    2. 同時維持多版本導致維護負擔大。
    3. 單一最新版若不相容會傷害整體信任。
  • 深層原因:
    • 架構層面:缺少向前相容設計規則。
    • 技術層面:無版本檢查機制(參見 Case #6/7/8/9)。
    • 流程層面:廢止程序與公告不足。

Solution Design

  • 解決策略:採用 Compatible Versioning(只保留最新版),嚴格遵守「只增不減」的契約演進與版本語意;搭配 SDK/Server 雙向版本檢查,保證舊 SDK 可繼續存活於同 Major 之下。

  • 實施步驟:
    1. 制定規則
      • 實作細節:契約只增不減;非相容變更升 Major;相容擴充升 Minor;[Obsolete] 只在下個 Major 移除
      • 所需資源:團隊規範文件
      • 預估時間:0.5 天
    2. 建立檢查
      • 實作細節:SDK init 檢查 + Server Filter 檢查
      • 所需資源:參見 Case #6-#9
      • 預估時間:2 天
    3. 廢止流程
      • 實作細節:公告 EOL 期限,標示 [Obsolete]
      • 所需資源:文件/公告與記錄
      • 預估時間:0.5 天
  • 關鍵程式碼/設定:
    // 契約演進規則示意
    // - 非相容變更 -> Major++
    // - 相容擴充 -> Minor++
    // - 行為修補/安全 -> Revision/Build
    // - 預告廢止 -> [Obsolete("Will be removed in next major")]
    
  • 實際案例:文中圖示「Server 只保留最新版,但相容舊 SDK」。
  • 實作環境:團隊規範 + C#/WebAPI
  • 實測數據:
    • 改善前:版本分裂或升級風險不可控。
    • 改善後:單一線上版本即可同時支援多代 SDK。
    • 改善幅度:維護分支數量減少;升級風險可控(以流程與機制保證)。

Learning Points

  • 核心知識點:版本策略對維運成本的影響、相容演進原則
  • 技能要求:
    • 必備:語意化版本理解、團隊規範建立
    • 進階:版本檢查機制落地(Case #6-#9)
  • 延伸思考:何時必須放棄向前相容?如何衡量成本與效益?
  • Practice Exercise
    • 基礎:撰寫團隊版控規範(30 分鐘)
    • 進階:將規範轉為 CI 驗證規則(2 小時)
    • 專案:把策略全面落地到 SDK/API(8 小時)
  • Assessment Criteria
    • 功能完整性:策略覆蓋常見變更情境
    • 程式碼品質:對應檢查機制完備
    • 效能優化:引入策略後不顯著拖慢流程
    • 創新性:加入可觀測性與報表

Case #4: 版本語意準則(System.Version Major/Minor/Build/Revision)

Problem Statement

  • 業務場景:多人協作時,常發生版本號語意誤用(例如小改動就升 Major,或非相容變更只改 Minor),導致檢查機制誤判。
  • 技術挑戰:將 System.Version 語意化準確落地,讓 SDK/Server 能一致判讀。
  • 影響範圍:升版節奏、相容檢查、文件同步。
  • 複雜度:低

Root Cause Analysis

  • 直接原因:
    1. 無統一版號語意。
    2. 無自動化比對規則。
  • 深層原因:
    • 架構層面:缺少語意層政策。
    • 技術層面:版本比對邏輯未標準化。
    • 流程層面:缺乏審核。

Solution Design

  • 解決策略:採用 MSDN System.Version 語意;Major=破壞性;Minor=相容擴充;Build/Revision=重編譯/修補;SDK 檢查 Major 相等 + Server.Minor >= SDK.Minor。

  • 實施步驟:
    1. 版本語意文件化(0.5 天)
    2. SDK 比對邏輯標準化(0.5 天)
    3. Server 比對邏輯一致化(0.5 天)
  • 關鍵程式碼/設定:
    // SDK 端語意比對
    if (required.Major != actual.Major) throw new InvalidOperationException();
    if (required.Minor > actual.Minor) throw new InvalidOperationException();
    
  • 實測數據:
    • 改善前:人為誤用,檢查規則不一致。
    • 改善後:SDK 與 Server 一致判讀,誤判降低。
    • 改善幅度:版本誤用事件下降(開發紀錄觀察)。

Learning Points:語意化版本的重要性;SDK/Server 雙向一致性 Practice Exercise:撰寫一個 VersionComparer 並加上單元測試(2 小時) Assessment Criteria:單元測試覆蓋所有比較情境

Case #5: 契約變更與版控/權限治理

Problem Statement

  • 業務場景:契約變更未受嚴格審核,直接進入主幹;回滾與追蹤困難。
  • 技術挑戰:如何在版控層面可追蹤/可審核/可限制契約修改?
  • 影響範圍:產線穩定性、審查成本。
  • 複雜度:中

Root Cause Analysis

  • 直接原因:契約檔未設 Code Owners;缺乏變更日誌;缺少版本號必改的機制。
  • 深層原因:
    • 架構層面:契約不獨立,權限難控。
    • 技術層面:沒有 diff+檢查腳本。
    • 流程層面:缺少強制 PR 流程。

Solution Design

  • 解決策略:以契約層獨立專案+Code Owners+CI 檢查(契約變更必須伴隨版本號調整),確保 traceability 與合規。

  • 實施步驟:
    1. 設定 CODEOWNERS(0.5 天)
    2. CI 驗證:契約介面有 diff 則 AssemblyVersion 必變(0.5 天)
    3. 記錄 CHANGELOG(0.5 天)
  • 關鍵程式碼/設定:
    # 偽代碼:CI 檢查腳本
    if git diff --name-only HEAD^ HEAD | grep -E "Contracts/.*\.cs"; then
    if ! git diff HEAD^ HEAD -- AssemblyInfo.cs | grep -E "AssemblyVersion"; then
      echo "Contract changed but AssemblyVersion not updated."; exit 1;
    fi
    fi
    
  • 實測數據:
    • 改善前:未授權契約變更偶發。
    • 改善後:契約變更必經審核與版本調整。
    • 改善幅度:未授權變更下降(CI 報告)。

Learning Points:以工具落實治理;「制度化」契約維護 Practice Exercise:實作 CI 腳本並測試(2 小時) Assessment Criteria:PR 若契約改動未調整版本,CI 必須 fail

Case #6: 提供 API 版本資訊的 Options 端點

Problem Statement

  • 業務場景:SDK 初始化時需先知曉 Server 版本,以便判斷是否相容與決定功能可用性。
  • 技術挑戰:在第一個正式 API 呼叫前,安全地拿到版本資訊。
  • 影響範圍:初始化失敗時間點、使用者體驗與診斷。
  • 複雜度:低

Root Cause Analysis

  • 直接原因:缺乏握手端點。
  • 深層原因:
    • 架構層面:沒有標準 meta 端點。
    • 技術層面:版本資訊散落在程式碼。

Solution Design

  • 解決策略:在契約中加入 Options() 回傳版本字串;SDK 於初始化時呼叫,記錄與判斷。

  • 實施步驟:
    1. 契約新增 Options(0.5 天)
    2. Server 實作 Options(0.5 天)
    3. SDK 初始化呼叫 Options 並記錄(0.5 天)
  • 關鍵程式碼/設定: ```csharp // 契約 interface IBirdsApiContract : IApiContract { string Options(); // … }

// Server public string Options() => “10.26.0.0”;

// SDK 取版本 var res = _http.SendAsync(new HttpRequestMessage(HttpMethod.Options, “/api/birds”)).Result; var version = JsonConvert.DeserializeObject(res.Content.ReadAsStringAsync().Result);


- 實測數據:
  - 改善前:版本未知,第一個呼叫才失敗。
  - 改善後:初始化即得知版本,fail-fast。
  - 改善幅度:失敗時間提前,易於診斷(Console 出現版本資訊)。

Learning Points:元資訊端點的重要性
Practice Exercise:在另一個 Controller 也加入 Options(30 分鐘)
Assessment Criteria:SDK 初始化能印出正確版本


## Case #7: SDK 初始化階段的版本相容性檢查(Client-side)

### Problem Statement
- 業務場景:新版 SDK 在舊版 Server 上可能使用未支援功能,需於初始化即判斷是否可安全使用。
- 技術挑戰:正確比較 System.Version,並以 fail-fast 模式阻止不相容。
- 影響範圍:崩潰時間點、client 體驗。
- 複雜度:低

### Root Cause Analysis
- 直接原因:沒有比較規則與檢查。
- 深層原因:
  - 技術層面:版本語意未落地(見 Case #4)。

### Solution Design
- 解決策略:SDK 內寫死「最低需求版本」(_require_API_version),初始化時讀取 Server 版本並比較 Major/Minor;不相容則拋例外。

- 實施步驟:
  1. 在 SDK 內定義最低需求版本(0.5 天)
  2. 初始化呼叫 Options 並比較(0.5 天)
  3. 整合例外與訊息(0.5 天)

- 關鍵程式碼/設定:
```csharp
private Version _require_API_version = new Version(10, 0, 0, 0);

if (this._require_API_version.Major != this._actual_API_version.Major)
    throw new InvalidOperationException();
if (this._require_API_version.Minor > this._actual_API_version.Minor)
    throw new InvalidOperationException();
  • 實測數據:
    • 改善前:業務流程中途才噴錯。
    • 改善後:初始化即阻擋不相容(拋 InvalidOperationException)。
    • 改善幅度:錯誤攔截提前且明確。

Learning Points:fail-fast 思維、版本比較 Practice Exercise:支援自訂錯誤訊息與建議升級資訊(2 小時) Assessment Criteria:不相容時訊息清楚、可引導使用者行動

Case #8: SDK 在每次呼叫自動攜帶最低需求版本(Header)

Problem Statement

  • 業務場景:SDK 實例存在很久(static 單例),Server 期間升級;僅初始化檢查無法涵蓋升級後的相容性。
  • 技術挑戰:持續、低成本地把「需求版本」送到 Server 端以供判斷。
  • 影響範圍:升級後的穩定性與回溯性。
  • 複雜度:低

Root Cause Analysis

  • 直接原因:只在初始化時檢查,之後無法感知 Server 版本變化。
  • 深層原因:
    • 架構層面:缺少長連線/長生命週期的相容性檢查策略。

Solution Design

  • 解決策略:SDK 用 HttpClient.DefaultRequestHeaders 為每個 request 加上 X-SDK-REQUIRED-VERSION,讓 Server 於每次請求前檢查。

  • 實施步驟:
    1. 設定預設 Header(0.5 天)
    2. 驗證 Header 正確傳送(0.5 天)
    3. 搭配 Server 檢查(見 Case #9)(0.5 天)
  • 關鍵程式碼/設定:
    this._http.DefaultRequestHeaders
      .Add("X-SDK-REQUIRED-VERSION", this._require_API_version.ToString());
    
  • 實測數據:
    • 改善前:Server 升級後,舊 SDK 可能不自知地繼續呼叫。
    • 改善後:升級後第一個呼叫就攔截。
    • 改善幅度:升級後錯誤攔截延遲時間縮短至 1 次呼叫內。

Learning Points:協定化傳遞需求前提 Practice Exercise:將需求版本改為自訂 Header 名稱與多值(30 分鐘) Assessment Criteria:Header 存在且內容正確

Case #9: Server 端以 ActionFilter 檢查版本(以 AssemblyVersion 為準)

Problem Statement

  • 業務場景:需要在 Server 端統一落地檢查規則,並在進入 Action 前就阻擋不相容請求。
  • 技術挑戰:從組件的 AssemblyVersion 讀取 Server 版本,並跟 Header 版本比較。
  • 影響範圍:效能、穩定性、誤判風險。
  • 複雜度:中

Root Cause Analysis

  • 直接原因:缺乏跨切(Cross-cutting)的統一檢查點。
  • 深層原因:
    • 技術層面:版本取得與比對分散於各 Action。

Solution Design

  • 解決策略:實作 SDKVersionCheckActionFilterAttribute,統一讀取 X-SDK-REQUIRED-VERSION 與 AssemblyVersion 比對,不相容即拋例外。

  • 實施步驟:
    1. 建置 Filter(0.5 天)
    2. 套用至 Controller(0.5 天)
    3. 增加日誌輸出與追蹤(0.5 天)
  • 關鍵程式碼/設定:
    public class SDKVersionCheckActionFilterAttribute : ActionFilterAttribute
    {
      public override void OnActionExecuting(HttpActionContext ctx)
      {
          var required = new Version(ctx.Request.Headers.GetValues("X-SDK-REQUIRED-VERSION").First());
          var current = GetType().Assembly.GetName().Version;
    
          if (current.Major != required.Major) throw new InvalidOperationException();
          if (current.Minor < required.Minor) throw new InvalidOperationException();
      }
    }
    
  • 實測數據:
    • 改善前:不相容請求進入 Action,浪費資源。
    • 改善後:100% 在 Action 前阻擋,回傳結構化錯誤 JSON。
    • 改善幅度:不必要資源消耗下降(以伺服器端觀察)。

Learning Points:Filter 實作與組件版本讀取 Practice Exercise:擴充比對規則支援「允許特定小版本白名單」(2 小時) Assessment Criteria:白名單命中時放行,非白名單阻擋

Case #10: 用 AssemblyVersion 與 Build Server 一致化版本來源

Problem Statement

  • 業務場景:手動在程式碼硬寫版本字串,容易漏改或不一致;多組件專案尤其嚴重。
  • 技術挑戰:在不改動程式的情況下,統一由 Build Server 注入版本。
  • 影響範圍:部署、追蹤、相容檢查。
  • 複雜度:低

Root Cause Analysis

  • 直接原因:硬寫版本字串在程式碼。</li>
  • 深層原因:
    • 技術層面:缺乏從 Assembly metadata 取得版本的機制。

Solution Design

  • 解決策略:以 AssemblyInfo.cs 設定 AssemblyVersion(“10.26.*”);Build Server 覆寫或補齊 Build/Revision,一致化版本來源;Server Filter 以 AssemblyVersion 為準。

  • 實施步驟:
    1. 改 AssemblyInfo.cs(0.5 天)
    2. Build Server 注入(0.5 天)
    3. Server Filter 改讀 AssemblyVersion(已在 Case #9)(0.5 天)
  • 關鍵程式碼/設定:
    [assembly: AssemblyVersion("10.26.*")]
    [assembly: AssemblyFileVersion("1.0.0.0")]
    
  • 實測數據:
    • 改善前:多處版本字串不一致。
    • 改善後:由組件 metadata 單一來源供給,SDK/Server 檢查基準一致。
    • 改善幅度:版本不一致案例消除(以整合測試觀察)。

Learning Points:版本來源單一化 Practice Exercise:在 CI 中統一寫入版本與產出 SBOM(2 小時) Assessment Criteria:所有組件版本一致且可追蹤

Case #11: 針對契約檢查的效能優化(反射快取)

Problem Statement

  • 業務場景:高併發 API(每秒上萬次呼叫)若每次以反射檢查契約,會造成可觀成本。
  • 技術挑戰:如何將反射檢查結果快取,避免每次重覆運算?
  • 影響範圍:延遲與 QPS。
  • 複雜度:中

Root Cause Analysis

  • 直接原因:每次請求都使用反射。
  • 深層原因:
    • 技術層面:沒有加入快取;契約到 Controller 關係實際上少變動。

Solution Design

  • 解決策略:以 ConcurrentDictionary 快取「ControllerType -> 契約方法集合」,第一次建表,後續直接查表;契約不需過期(除非部署新版本)。

  • 實施步驟:
    1. 建置快取結構與初始化(0.5 天)
    2. Filter 使用快取(0.5 天)
    3. 併發安全評估(0.5 天)
  • 關鍵程式碼/設定: ```csharp static class ContractCache { private static ConcurrentDictionary<Type, HashSet> _map = new();

    public static HashSet GetAllowedMethods(Type controllerType) { return _map.GetOrAdd(controllerType, t => { var contract = t.GetInterfaces().First(i => i.GetInterface(typeof(IApiContract).FullName) != null); return contract.GetMethods().Select(m => m.Name).ToHashSet(); }); } }

public override void OnActionExecuting(HttpActionContext ctx) { var allowed = ContractCache.GetAllowedMethods(ctx.ActionDescriptor.ControllerDescriptor.ControllerType); if (!allowed.Contains(ctx.ActionDescriptor.ActionName)) throw new NotSupportedException(“API method(s) must defined in contract interface.”); }


- 實測數據:
  - 改善前:每次呼叫皆進行反射。
  - 改善後:反射次數降為首呼叫一次;後續查表。
  - 改善幅度:反射次數顯著降低(延遲下降以 profiling 驗證)。

Learning Points:反射成本、快取策略、安全併發容器
Practice Exercise:加入方法參數簽章快取(2 小時)
Assessment Criteria:在壓測下延遲顯著下降且零錯誤


## Case #12: 以單元測試作為契約的補強

### Problem Statement
- 業務場景:僅靠運行期檢查無法驗證行為層面的相容(非僅介面存在),需要系統化驗證。
- 技術挑戰:以測試代表契約預期,當行為或格式變更時能第一時間警示。
- 影響範圍:回歸風險。
- 複雜度:中

### Root Cause Analysis
- 直接原因:契約檢查只涵蓋「是否存在」與「名稱」,不涵蓋行為。
- 深層原因:
  - 流程層面:缺少 TDD/回歸測試覆蓋。

### Solution Design
- 解決策略:將重要 API 的使用情境寫成單元/整合測試,視為契約的一部分;但仍以 interface 為主、tests 為輔,避免誤將測試錯誤當契約變更。

- 實施步驟:
  1. 為每個公開 API 撰寫 happy path 測試(1 天)
  2. 針對回傳模型加入格式檢查(0.5 天)
  3. CI 必跑測試(0.5 天)

- 關鍵程式碼/設定:
```csharp
[Fact]
public async Task GetAllBirds_ShouldReturnList()
{
    var client = new HttpClient { BaseAddress = new Uri("http://localhost:1234") };
    var res = await client.GetAsync("/api/birds");
    res.EnsureSuccessStatusCode();
    var json = await res.Content.ReadAsStringAsync();
    var birds = JsonConvert.DeserializeObject<List<BirdInfo>>(json);
    Assert.NotEmpty(birds);
}
  • 實測數據:
    • 改善前:行為變更難以早期發現。
    • 改善後:行為層面回歸錯誤可在 CI 即攔截。
    • 改善幅度:回歸錯誤檢出率提升(以測試報告為準)。

Learning Points:Test as Specification Practice Exercise:為版本檢查撰寫負面測試(2 小時) Assessment Criteria:失敗路徑覆蓋完整、訊息明確

Case #13: 廢止(Deprecation)管理與 [Obsolete]

Problem Statement

  • 業務場景:API 不可能無限期維持,需為將移除的端點提供緩衝期。
  • 技術挑戰:如何宣告、追蹤與按期移除而不破壞相容?
  • 影響範圍:開發者信任與升級節奏。
  • 複雜度:低

Root Cause Analysis

  • 直接原因:缺乏廢止政策與標示。
  • 深層原因:
    • 流程層面:EOL 程序與溝通不足。

Solution Design

  • 解決策略:以 [Obsolete] 標註將廢止的方法,說明移除版本(Major+1)與替代方案;配合公告與日誌,至下一個 Major 才實際移除。

  • 實施步驟:
    1. 加上 [Obsolete] 與說明(0.5 天)
    2. 公告 EOL 時程(0.5 天)
    3. 監控使用量以調整節奏(0.5 天)
  • 關鍵程式碼/設定:
    [Obsolete("Will be removed in v11. Use GET /api/birds?newParam=... instead.")]
    public IEnumerable<BirdInfo> GetLegacy() { /* ... */ }
    
  • 實測數據:
    • 改善前:直接移除導致舊 SDK 崩潰。
    • 改善後:預告+緩衝期,平滑過渡。
    • 改善幅度:突發中斷事件顯著降低。

Learning Points:EOL 策略與溝通 Practice Exercise:選一個端點模擬廢止流程(2 小時) Assessment Criteria:訊息明確、替代路徑可行

Case #14: 統一錯誤處理:以 JSON 結構回傳例外

Problem Statement

  • 業務場景:當版本不相容被 Server 端攔截,需將錯誤結構化回給 SDK 以利判斷與提示。
  • 技術挑戰:將 .NET 例外序列化成一致 JSON,SDK 可解析並呈現可讀訊息。
  • 影響範圍:診斷效率、使用者體驗。
  • 複雜度:低

Root Cause Analysis

  • 直接原因:錯誤格式不一致或不可解析。
  • 深層原因:
    • 技術層面:缺少標準錯誤協定。

Solution Design

  • 解決策略:採用 WebAPI 預設 JSON 錯誤結構或自訂;SDK 解析 ExceptionMessage 與 StackTrace,轉換為人性化提示。

  • 實施步驟:
    1. 啟用/維持 JSON 錯誤輸出(0.5 天)
    2. SDK 捕捉並解析(0.5 天)
    3. 封裝成 Typed Error(0.5 天)
  • 關鍵程式碼/設定: ```csharp // SDK 偵錯解析 try { /* call */ } catch (HttpRequestException ex) { var json = await ex.Response.Content.ReadAsStringAsync(); var err = JsonConvert.DeserializeObject(json); Console.WriteLine($"Error: {err.ExceptionMessage}"); }

class ServerError { public string Message {get;set;} public string ExceptionMessage {get;set;} public string ExceptionType {get;set;} public string StackTrace {get;set;} }


- 實測數據:
  - 改善前:錯誤難以定位。
  - 改善後:快速了解錯誤成因(版本不相容等)。
  - 改善幅度:MTTR 降低(以內部支持觀察)。

Learning Points:錯誤結構化與 SDK 友善化訊息
Practice Exercise:包裝成 SDK 自訂例外型別(2 小時)
Assessment Criteria:錯誤訊息可讀可解析


## Case #15: 只增不減的 HTTP 介面與資料模型演進(BirdInfo 擴充)

### Problem Statement
- 業務場景:API 升級需要在不破壞舊 SDK 的前提下新增欄位或參數。
- 技術挑戰:如何在 HTTP 層安全地「只新增、不移除/不變更」?
- 影響範圍:舊版 SDK 相容性。
- 複雜度:中

### Root Cause Analysis
- 直接原因:變更既有欄位或移除參數導致舊版解析失敗。
- 深層原因:
  - 技術層面:模型與序列化策略未朝向向前相容設計。

### Solution Design
- 解決策略:資料模型與 Query Params 採 Optional 添加;不變更既有欄位語意;新增欄位以 nullable/有預設值實作;SDK 不使用的欄位自動忽略。

- 實施步驟:
  1. 模型擴充(0.5 天)
  2. 序列化設定允許未知欄位(0.5 天)
  3. 測試新舊互通(1 天)

- 關鍵程式碼/設定:
```csharp
public class BirdInfo
{
    public string SerialNo {get;set;}
    // 新增欄位(可空/有預設)
    public string ScientificName {get;set;}
    public string? NickName {get;set;} // 新增,可空
}
// SDK 端 Newtonsoft 預設就會忽略未知欄位
  • 實測數據:
    • 改善前:欄位變更導致舊版反序列化失敗。
    • 改善後:新增欄位不影響舊版 SDK。
    • 改善幅度:相容性風險降低。

Learning Points:資料模型向前相容設計 Practice Exercise:為 BirdInfo 新增欄位並驗證舊 SDK 正常(1 小時) Assessment Criteria:舊 SDK 不需升級即可正常解析


案例分類 ——————————–

1) 按難度分類

  • 入門級(適合初學者)
    • Case #4 版本語意準則
    • Case #6 提供 API 版本 Options
    • Case #7 SDK 初始化檢查
    • Case #8 SDK Header 攜帶版本
    • Case #10 AssemblyVersion 與 Build Server
    • Case #14 統一錯誤處理
  • 中級(需要一定基礎)
    • Case #1 運行期契約檢查
    • Case #2 雙端共享契約
    • Case #3 相容性版本策略
    • Case #5 契約變更治理
    • Case #9 Server ActionFilter 檢查
    • Case #11 反射快取
    • Case #12 測試作為契約補強
    • Case #15 只增不減的資料模型演進
  • 高級(需要深厚經驗)
    • 可延伸:將契約檢查融入編譯/建置工具、契約自動生成 SDK、CI 差異分析(本文多案例可升級為高級綜合專案)

2) 按技術領域分類

  • 架構設計類:Case #2, #3, #4, #5, #10, #13, #15
  • 效能優化類:Case #11
  • 整合開發類:Case #6, #7, #8, #9, #14
  • 除錯診斷類:Case #1, #9, #12, #14
  • 安全防護類:Case #1, #5(治理即風險控制)

3) 按學習目標分類

  • 概念理解型:Case #3, #4, #13
  • 技能練習型:Case #6, #7, #8, #10, #11, #14, #15
  • 問題解決型:Case #1, #2, #5, #9, #12
  • 創新應用型:將 #1+#9+#11 與 CI/合約生成結合成平台級方案

案例關聯圖(學習路徑建議) ——————————–

  • 先學哪些案例?
    • 起步概念:Case #3(版本策略)、Case #4(版本語意)
    • 契約基礎:Case #2(共享契約)
  • 依賴關係:
    • Case #1(運行期契約檢查)依賴 Case #2(契約設計)
    • Case #6(版本端點)→ Case #7(SDK 初始化檢查)
    • Case #8(Header 攜帶需求)→ Case #9(Server 檢查)
    • Case #10(AssemblyVersion)為 Case #9 提供一致版本來源
    • Case #11(快取)提升 Case #1 的效能
    • Case #12(測試契約)補強 Case #1/#2 的行為驗證
    • Case #13(廢止)與 Case #3(策略)、Case #4(語意)相互支撐
    • Case #5(治理)貫穿所有契約與版本變更
    • Case #15(資料模型演進)與 Case #3(只增不減)一致
  • 完整學習路徑建議: 1) 讀懂策略與語意:Case #3 → Case #4
    2) 建立契約基座:Case #2 → Case #1 → Case #11(效能) → Case #12(測試)
    3) 版本機制落地:Case #6 → Case #7 → Case #8 → Case #9 → Case #10 → Case #14
    4) 治理與演進:Case #5 → Case #13 → Case #15
    5) 最終綜合專案:把上述方案整合成一個端到端的 API/SDK 向前相容框架,並撰寫 CI/CD 規則與壓測報告

以上 15 個案例涵蓋了「API 向前相容」從理念、契約、版本到治理與效能的全鏈路做法,並以原文程式碼與觀察結果為據,可直接用於教學、演練與評估。






Facebook Pages

AI Synthesis Contents

- 原始文章內容
- 問答集
- 文章摘要
- 解決方案 / Case Study

Edit Post (Pull Request)

Post Directory