---
# System prepended metadata

title: 從踩坑到重構 — 拆分 API 合約與 BO 參數
tags: [clean-architecture, architecture, Bee.NET, api-design, dotnet]

---

---
title: 從踩坑到重構 — 拆分 API 合約與 BO 參數
tags: [dotnet, architecture, api-design, Bee.NET, clean-architecture]

---

# 從踩坑到重構 — 拆分 API 合約與 BO 參數
> 分享我在開發 Bee.NET 框架的過程中，一路踩坑、反覆調整後摸索出的設計方式。如果你也有類似的困擾，或許可以參考看看。

![api-bo-contract](https://hackmd.io/_uploads/S1NXBMc3Wl.png)

# 1️⃣ 我踩過的坑

在開發 [Bee.NET](https://github.com/jeff377/bee-library) 框架的早期版本時，為了圖方便，我讓 API 端點的 Request / Response 物件和業務邏輯層（Business Object, BO）的參數物件共用同一個類別。一個 `LoginRequest` 從 API 層傳到 BO 層，一路暢通，開發起來很快，但完全沒顧及後續的擴展性。

但隨著框架演進，幾個讓我越來越頭痛的問題開始浮現：

- **序列化標記污染 BO 層**：`[MessagePackObject]`、`[Key(n)]` 這些只有 API 傳輸才需要的 Attribute，跟著混進了業務邏輯層，讓 BO 程式碼看起來莫名其妙
- **BO 專用屬性暴露給用戶端**：API 與 BO 共用同一個型別，導致用戶端也看得到只有 BO 才會用到的屬性，合約邊界不清晰
- **BO 無法自由擴充**：我想在 BO 間傳遞一個 `IsAutoLogin` 旗標，卻發現加上去就會暴露在 API 合約中
- **測試耦合**：測試 BO 邏輯時，還得處理序列化相關的相依性，明明跟測試目標無關

回頭看，這些問題的根源都一樣：**API 傳輸關注點與業務邏輯關注點全擠在同一個型別裡**。

---

# 2️⃣ 改善方式：合約介面作為唯一真實來源

經過幾次重構嘗試，我最終在 Bee.NET 中採用了**合約介面（Contract Interface）**的做法：定義一個介面作為屬性的唯一真實來源，API 型別和 BO 型別各自實作這個介面。

```
合約介面（ILoginRequest / ILoginResponse）   ← 定義共用屬性，唯一真實來源
     │
     ├── API 型別（LoginRequest / LoginResponse）  ← 含序列化標記，用於 API 傳輸
     │
     └── BO 型別（LoginArgs / LoginResult）        ← 純 POCO，用於業務邏輯
```

這個做法解決了前面提到的幾個痛點：

1. **用戶端只接觸 API 型別**，不會看到 BO 專用的屬性
2. **BO 層不依賴 API 組件**，可以獨立測試與演進
3. **BO 可在合約之外新增內部專用屬性**，不會污染 API 合約

---

# 3️⃣ 型別分層實作

### 第一層：合約介面

定義在 `Bee.Api.Contracts` 組件中，只包含唯讀屬性，不含任何序列化標記。這個專案的角色類似於 `Bee.Repository.Abstractions`，作為層與層之間的純介面合約：

```csharp
public interface ILoginRequest
{
    string UserId { get; }
    string Password { get; }
    string ClientPublicKey { get; }
}

public interface ILoginResponse
{
    Guid AccessToken { get; }
    DateTime ExpiredAt { get; }
    string ApiEncryptionKey { get; }
    string UserId { get; }
    string UserName { get; }
}
```

### 第二層：API 合約型別

定義在 `Bee.Api.Core` 組件中，繼承框架基底類別，實作合約介面，並標記 MessagePack 序列化屬性：

```csharp
[MessagePackObject]
[Serializable]
public class LoginRequest : ApiRequest, ILoginRequest
{
    [Key(100)] public string UserId { get; set; } = string.Empty;
    [Key(101)] public string Password { get; set; } = string.Empty;
    [Key(102)] public string ClientPublicKey { get; set; } = string.Empty;
}
```

> `[Key(n)]` 從 100 開始，避免與基底類別的保留欄位衝突。

### 第三層：BO 參數型別

定義在 `Bee.Business` 組件中，同樣實作合約介面，但是純 POCO — 沒有序列化標記，乾淨俐落：

```csharp
public class LoginArgs : BusinessArgs, ILoginRequest
{
    public string UserId { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public string ClientPublicKey { get; set; } = string.Empty;

    // BO 專用屬性 — 不在合約介面中，API 層看不到
    public bool IsAutoLogin { get; set; }
}
```

---

# 4️⃣ 三種使用情境

實務上不是每個 API 方法都需要建立完整三層型別。在 Bee.NET 的開發經驗中，我歸納出三種常見情境：

### 情境一：API 方法，BO 不需額外屬性（最常見）

API 與 BO 的參數完全相同，不需建立 Args / Result 型別。BO 方法直接使用合約介面：

```csharp
public ILoginResponse Login(ILoginRequest request)
{
    return new LoginResponse
    {
        AccessToken = Guid.NewGuid(),
        UserId = request.UserId
    };
}
```

### 情境二：API 方法，BO 需要額外屬性

BO 間互相呼叫時，需要傳遞 API 不可見的內部屬性。此時額外建立 Args 型別，BO 方法透過模式比對取得額外屬性：

```csharp
public ILoginResponse Login(ILoginRequest request)
{
    bool isAutoLogin = request is LoginArgs args && args.IsAutoLogin;
    // ...
}
```

外部 API 呼叫傳入 `LoginRequest`，BO 內部呼叫傳入 `LoginArgs`，同一個方法簽章兼容兩者。

### 情境三：BO 內部方法（不公開至 API）

僅 BO 內部使用的方法，不需合約介面也不需 API 型別，直接定義 BO 參數即可：

```csharp
public class RecalcArgs : BusinessArgs
{
    public string OrderId { get; set; }
    public bool ForceRecalc { get; set; }
}
```

---

# 5️⃣ 命名慣例與組件相依

## 命名慣例速查

| 用途 | 命名模式 | 範例 | 所在組件 |
|------|----------|------|----------|
| 合約介面（輸入） | `IXxxRequest` | `ILoginRequest` | Bee.Api.Contracts |
| 合約介面（輸出） | `IXxxResponse` | `ILoginResponse` | Bee.Api.Contracts |
| API 輸入 | `XxxRequest` | `LoginRequest` | Bee.Api.Core |
| API 輸出 | `XxxResponse` | `LoginResponse` | Bee.Api.Core |
| BO 輸入 | `XxxArgs` | `LoginArgs` | Bee.Business |
| BO 輸出 | `XxxResult` | `LoginResult` | Bee.Business |

## 組件相依方向

```
Bee.Api.Contracts       ← 合約介面（IXxxRequest / IXxxResponse）
    │
    ├── Bee.Api.Core    ← API 型別（含序列化）
    │       │
    │       └── Bee.Api.Client  ← 用戶端（只用 Request / Response）
    │
    └── Bee.Business    ← BO 型別（純 POCO）+ BO 介面
```

關鍵原則：`Bee.Api.Core` 與 `Bee.Business` **彼此不相依**，各自只依賴 `Bee.Api.Contracts`（純介面合約）。這確保了 API 傳輸層的變動（例如換掉序列化框架）不會影響 BO 層，反之亦然。

---

# 6️⃣ 實戰步驟：新增一個 API 方法

以新增 `GetOrder` 為例，完整步驟如下：

1. **定義合約介面**（`Bee.Api.Contracts`）— `IGetOrderRequest`、`IGetOrderResponse`
2. **建立 API 型別**（`Bee.Api.Core`）— `GetOrderRequest`、`GetOrderResponse`，標記 MessagePack
3. **實作 BO 方法** — 簽章使用 `IGetOrderRequest` / `IGetOrderResponse`
4. **註冊回應映射**（若 BO 回傳純 POCO）— `ApiContractRegistry.Register<IGetOrderResponse, GetOrderResponse>()`
5. **更新用戶端 Connector**（若需要）— 在 Connector 中新增方法

---

# ✅ 結語

回顧整個改善過程，核心思想其實很簡單：**讓每一層只關心自己該關心的事**。

API 型別負責序列化與傳輸格式，BO 型別負責承載業務邏輯所需的資料，合約介面則作為兩者之間的橋樑，確保屬性定義只寫一次。

確實多寫了一些型別，但對我來說換來的好處是值得的：

- BO 層可以獨立測試，不需處理序列化相依性
- API 合約變更不會意外影響業務邏輯
- 用戶端只需引用最小的組件集合
- 可以平行開發 API 層與 BO 層，互不阻塞

這個做法不一定適合所有專案，對於小型專案來說可能會覺得多此一舉。但如果你的 .NET 專案也開始感受到 API 型別與業務型別糾纏不清的痛苦，或許可以參考這個方向，依自己的需求取捨。

## 延伸閱讀

- **完整設計文件**
  [API/BO 合約設計原則 — Bee.NET](https://github.com/jeff377/bee-library/blob/main/docs/api-bo-contract-design.zh-TW.md)
- **[Bee.NET](https://github.com/jeff377/bee-library) 框架**
  [GitHub — Bee.NET Library](https://github.com/jeff377/bee-library)

---
📘 **HackMD 原文筆記：**  
👉 https://hackmd.io/@jeff377/api-bo-contract

**📢 歡迎轉載，請註明出處**
**📬 歡迎追蹤我的技術筆記與實戰經驗分享**
[Facebook](https://www.facebook.com/profile.php?id=61574839666569) ｜ [HackMD](https://hackmd.io/@jeff377) ｜ [GitHub](https://github.com/jeff377) ｜ [NuGet](https://www.nuget.org/profiles/jeff377)
