以下內容基於原文精煉重組,聚焦於「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(根因分析)
- 直接原因:
- REST Controller 可任意新增公開 method,缺少合約約束。
- 僅靠 code review 易漏掉細節,無系統化檢查。
- 因缺少自動化檢查,規格異動無法即時被攔截。
- 深層原因:
- 架構層面:缺少統一的 API contract 物件作為單一事實來源。
- 技術層面:沒有 compile-time 機制,需在 runtime 才能辨識實際執行之 Action。
- 流程層面:缺少將 contract 變更與版控、權限與審核綁定的流程。
Solution Design(解決方案設計)
-
解決策略:以「IApiContract 標記介面 + 專屬 contract interface + ActionFilter 檢查」組成第二道防線;每次執行 Action 前,檢查該 Action 是否存在於對應 contract interface,否則擋下執行。
- 實施步驟:
- 定義標記與契約
- 實作細節:定義 IApiContract 與 IBirdsApiContract;Controller 必須實作契約。
- 所需資源:C#, ASP.NET WebAPI
- 預估時間:0.5 天
- 建置檢查 Filter
- 實作細節:以反射比對 Action 名稱是否出現在契約 interface;不符則拋 NotSupportedException。
- 所需資源:ActionFilterAttribute
- 預估時間:0.5 天
- 套用到 Controller 並驗證
- 實作細節:以屬性方式套用 Filter;撰寫簡單整合測試。
- 所需資源:xUnit/NUnit
- 預估時間:0.5 天
- 定義標記與契約
- 關鍵程式碼/設定: ```csharp // 標記介面 interface IApiContract { }
// 契約介面
interface IBirdsApiContract : IApiContract
{
void Head();
IEnumerable
// 運行前檢查 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
- 直接原因:
- 未定義版本策略,造成升級不可預期。
- 同時維持多版本導致維護負擔大。
- 單一最新版若不相容會傷害整體信任。
- 深層原因:
- 架構層面:缺少向前相容設計規則。
- 技術層面:無版本檢查機制(參見 Case #6/7/8/9)。
- 流程層面:廢止程序與公告不足。
Solution Design
-
解決策略:採用 Compatible Versioning(只保留最新版),嚴格遵守「只增不減」的契約演進與版本語意;搭配 SDK/Server 雙向版本檢查,保證舊 SDK 可繼續存活於同 Major 之下。
- 實施步驟:
- 制定規則
- 實作細節:契約只增不減;非相容變更升 Major;相容擴充升 Minor;[Obsolete] 只在下個 Major 移除
- 所需資源:團隊規範文件
- 預估時間:0.5 天
- 建立檢查
- 實作細節:SDK init 檢查 + Server Filter 檢查
- 所需資源:參見 Case #6-#9
- 預估時間:2 天
- 廢止流程
- 實作細節:公告 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
- 直接原因:
- 無統一版號語意。
- 無自動化比對規則。
- 深層原因:
- 架構層面:缺少語意層政策。
- 技術層面:版本比對邏輯未標準化。
- 流程層面:缺乏審核。
Solution Design
-
解決策略:採用 MSDN System.Version 語意;Major=破壞性;Minor=相容擴充;Build/Revision=重編譯/修補;SDK 檢查 Major 相等 + Server.Minor >= SDK.Minor。
- 實施步驟:
- 版本語意文件化(0.5 天)
- SDK 比對邏輯標準化(0.5 天)
- 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 與合規。
- 實施步驟:
- 設定 CODEOWNERS(0.5 天)
- CI 驗證:契約介面有 diff 則 AssemblyVersion 必變(0.5 天)
- 記錄 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 於初始化時呼叫,記錄與判斷。
- 實施步驟:
- 契約新增 Options(0.5 天)
- Server 實作 Options(0.5 天)
- 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
- 實測數據:
- 改善前:版本未知,第一個呼叫才失敗。
- 改善後:初始化即得知版本,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 於每次請求前檢查。
- 實施步驟:
- 設定預設 Header(0.5 天)
- 驗證 Header 正確傳送(0.5 天)
- 搭配 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 比對,不相容即拋例外。
- 實施步驟:
- 建置 Filter(0.5 天)
- 套用至 Controller(0.5 天)
- 增加日誌輸出與追蹤(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 為準。
- 實施步驟:
- 改 AssemblyInfo.cs(0.5 天)
- Build Server 注入(0.5 天)
- 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 -> 契約方法集合」,第一次建表,後續直接查表;契約不需過期(除非部署新版本)。
- 實施步驟:
- 建置快取結構與初始化(0.5 天)
- Filter 使用快取(0.5 天)
- 併發安全評估(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 才實際移除。
- 實施步驟:
- 加上 [Obsolete] 與說明(0.5 天)
- 公告 EOL 時程(0.5 天)
- 監控使用量以調整節奏(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,轉換為人性化提示。
- 實施步驟:
- 啟用/維持 JSON 錯誤輸出(0.5 天)
- SDK 捕捉並解析(0.5 天)
- 封裝成 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 向前相容」從理念、契約、版本到治理與效能的全鏈路做法,並以原文程式碼與觀察結果為據,可直接用於教學、演練與評估。