---
# System prepended metadata

title: '[.NET 筆記 014] Mapster: DTO 物件轉換'

---

## [.NET 筆記 014] Mapster: DTO 物件轉換
###### 📅 2026-04-30

---

### 📌 介紹-1. 什麼是 DTO？

DTO (Data Transfer Object) 白話來說就是「專門用來傳資料的容器」

對應的實際檔案：
- Models/Device.cs ➡️ Model (Data Model, 資料模型) | 對應資料庫的表格，是「存資料的容器」
- Dtos/DeviceDto.cs ➡️ DTO | 對應 API 回傳的格式，是「傳資料的容器」

目前範例專案裡有三種長相相似但用途不同的 class：

| 名稱 | 白話說明 | 範例 (目前範例專案)  |
|:-----|:---------|:----------------|
| Model (資料模型)  | 對應資料庫的表格，完整欄位都在這 | User、Device、Role |
| DTO (傳輸物件)  | 只放「這支 API 需要回傳的欄位」 | UserDto、DeviceDto |
| ViewModel | 只放「前端畫面需要顯示的欄位」 (Web 專案常見)  | 目前範例專案 **尚無** |

☠️ 問題：從 Model 轉到 DTO，目前是手動一個欄位一個欄位自己賦值：

```csharp
// 目前的寫法 (手動賦值) 
select new UserDto
{
    Id          = u.Id,
    Account     = u.Account,
    Name        = u.Name,
    Email       = u.Email,
    Pwd         = u.Pwd,
    IsActive    = u.IsActive,
    RoleId      = u.RoleId,
    CreatedDate = u.CreatedDate,
    UpdatedDate = u.UpdatedDate
}
```

欄位少還好，一旦欄位多或 DTO 很多個，這樣寫既累又容易漏這就是 Mapster 要解決的問題

---

### 📌 介紹-2. 什麼是 Mapster？

Mapster 是一個「自動幫你複製相同名稱欄位」的套件
只要兩個 class 的屬性名稱相同，Mapster 就能自動對應，不用手動一行一行寫

```csharp
// Mapster 的寫法 (一行搞定) 
var userDto = user.Adapt<UserDto>();
```

> 💡 `Adapt<T>()` 就是 Mapster 的核心方法，意思是「把我轉換成 T 這個型別」

---

### 📌 介紹-3. Mapster vs AutoMapper

有另一個類似的套件叫 AutoMapper，以下是兩者比較：

|  | Mapster | AutoMapper |
|:--|:--------|:-----------|
| 設定方式 | 預設零設定，名稱相同自動對應 | 需要先寫 Profile 設定檔 |
| 速度 | 較快 | 較慢 |
| 套件大小 | 較小 | 較大 |
| .NET 8 支援 | ✅ | ✅ |
| 適合場景 | 欄位名稱一致、快速開發 | 需要大量客製化對應規則 |

> 💡 目前範例專案 DTO 欄位名稱與 Model 大多一致，Mapster 零設定就能用，不需要 AutoMapper

---

### 📌 Step-0. 開啟專案

繼上一篇 [[.NET 筆記 013]](https://hackmd.io/@dada00321/SJVshzkRWl)
➡️ API_test_260428 (.NET 8.0)

---

### 📌 Step-1. 安裝 Mapster 套件

在終端機（檢視 > 終端）執行：
```
dotnet add package Mapster --version 7.4.0
```

安裝完後，確認 `.csproj` 出現以下內容：

```xml
<PackageReference Include="Mapster" Version="7.x.x" />
```

> 💡 Mapster 7.x 完整支援 .NET 8.0，不需要額外安裝相依套件

---

### 📌 Step-2. 改寫 UserService

找到 `Services/UserService.cs`，先在頂部加上 using：

```csharp
using Mapster;
```

#### UserService/GetAllAsync — 改寫前

```csharp
return await (from u in _context.Users
              join r in _context.Roles on u.RoleId equals r.Id
              select new UserDto
              {
                  Id = u.Id, Account = u.Account, Name = u.Name,
                  Email = u.Email, Pwd = u.Pwd, IsActive = u.IsActive,
                  RoleId = u.RoleId, CreatedDate = u.CreatedDate,
                  UpdatedDate = u.UpdatedDate
              }).ToListAsync();
```

#### UserService/GetAllAsync — 改寫後

```csharp
var users = await conn.QueryAsync<User>("SELECT * FROM Users");
return users.Select(u => u.Adapt<UserDto>()).ToList();
//                        ^^^^^^^^^^^^^^^^
//                        Mapster 自動把 User 轉成 UserDto
```

#### UserService/GetByIdAsync — 改寫後

```csharp
var user = await conn.QueryFirstOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Id = @Id", new { Id = userId });
return user?.Adapt<UserDto>();
//           ^^^^^^^^^^^^^^^^
//           user 是 null 就回 null，否則自動轉成 UserDto
```

---

### 📌 Step-3. 改寫 DeviceService

找到 `Services/DeviceService.cs`，同樣加上 `using Mapster`，然後改寫：

#### DeviceService/GetAllAsync — 改寫後

```csharp
var devices = await conn.QueryAsync<Device>("SELECT * FROM Devices");
return devices.Select(d => d.Adapt<DeviceDto>()).ToList();
```

#### DeviceService/GetByIdAsync — 改寫後
```csharp
var device = await conn.QueryFirstOrDefaultAsync<Device>(
    "SELECT * FROM Devices WHERE Id = @Id", new { Id = deviceId });
return device?.Adapt<DeviceDto>();
```

#### DeviceService/UpdateAsync — 改寫後

```csharp
// 更新完後，直接把 device 轉成 DeviceDto 回傳
return (await conn.QueryFirstOrDefaultAsync<Device>(
    "SELECT * FROM Devices WHERE Id = @Id", new { Id = deviceId }))
    ?.Adapt<DeviceDto>();
```

---

### 📌 Step-4. 改寫 RoleService

找到 `Services/RoleService.cs`，加上 `using Mapster`：

#### RoleService/GetByIdAsync — 改寫後

```csharp
var role = await conn.QueryFirstOrDefaultAsync<Role>(
    "SELECT * FROM Roles WHERE Id = @Id", new { Id = roleId });
if (role == null) return null;

var roleDto = role.Adapt<RoleDto>();  // Role → RoleDto 自動轉換
roleDto.Devices = await GetDevicesByRoleIdAsync(conn, roleId);  // 這個欄位手動補
return roleDto;
```

> 💡 `RoleDto` 有 `Devices` 這個欄位在 `Role` Model 裡沒有，所以這個欄位還是要手動補上，其他欄位交給 Mapster

---

### 📌 Step-5. 驗證結果

改寫完後執行：

```
dotnet build
```

確認沒有紅字後，跑起來用 Swagger 測試各支 API，回傳結果應與改寫前完全相同

---

### 📌 總結：改寫前後對比

|  | 改寫前 | 改寫後 (Mapster)  |
|:--|:------|:----------------|
| 欄位對應方式 | 手動一個一個賦值 | `Adapt<T>()` 一行搞定 |
| 新增欄位時 | DTO 和賦值兩個地方都要改 | 只需改 DTO class |
| 漏寫欄位風險 | 有 (不會編譯錯誤)  | 無 (自動對應)  |
| 適用條件 | 任何情況 | 欄位名稱需一致 |

> 💡 如果 Model 欄位名稱和 DTO 不同，可以用 `TypeAdapterConfig` 設定自訂對應規則，這屬於 Mapster 進階用法
