## [.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 進階用法