iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
佛心分享-IT 人自學之術

學過一點 React,卻被撈進 C#:新手用 AI 硬啃 MVC 企業專案的 3 個月實錄系列 第 21

Day 21|Controller 為什麼不能什麼都自己做?Service 負責什麼?

  • 分享至 

  • xImage
  •  

前面幾篇一路從:

Route
↓
Controller
↓
View
↓
Model / ViewModel

慢慢把 MVC 接起來。

看到這裡,我原本會覺得:

既然 Controller 已經收到請求了,
那我就在 Controller 裡:

查資料
判斷條件
轉換欄位
存資料庫
最後 return View()

不就好了?

技術上當然不是完全不能這樣寫。

但企業專案如果每一個 Controller 都什麼事情自己做,很快就會變成:

Controller 超長
邏輯全部混在一起
同一套規則到處重複
很難測試
很難維護
改一個規則不知道會影響哪裡

所以實際專案裡很常會再多一層:

Service

這篇就來整理:

Controller 到底該做什麼?
Service 又負責什麼?
為什麼不要把所有邏輯都塞在 Controller?

先看一個「什麼都自己做」的 Controller

假設現在要做:

新增使用者

如果所有事情都寫在 Controller,可能會變成:

[HttpPost]
public IActionResult Create(CreateUserViewModel model)
{
    if (string.IsNullOrWhiteSpace(model.Name))
    {
        ModelState.AddModelError("Name", "姓名不能空白");

        return View(model);
    }

    if (model.RoleId == 1)
    {
        model.RoleName = "管理員";
    }
    else
    {
        model.RoleName = "一般使用者";
    }

    User entity = new User
    {
        Name = model.Name,
        Email = model.Email,
        RoleId = model.RoleId,
        CreateTime = DateTime.Now
    };

    _dbContext.Users.Add(entity);

    _dbContext.SaveChanges();

    return RedirectToAction("Index");
}

乍看之下好像也沒什麼問題。

但這個 Controller 同時在做:

接收 HTTP Request
驗證資料
判斷角色
轉換 ViewModel
建立 Entity
存資料庫
決定最後要去哪裡

也就是:

什麼都做

如果功能再複雜一點,可能還會加入:

檢查目前登入者權限
查詢其他資料表
判斷資料狀態
組合名稱
寫入操作紀錄
寄通知

最後一個 Action 可能就變成上百行。

這種 Controller 常會被叫做:

Fat Controller
胖 Controller

Controller 比較適合做什麼?

我後來會把 Controller 先理解成:

接住 Request,決定要呼叫誰處理,最後決定怎麼 Response。

例如:

接收網址參數
接收表單資料
接收 Request Model
基本 ModelState 判斷
呼叫 Service
return View()
RedirectToAction()
return Ok()
return BadRequest()

它比較像整個流程的:

入口
+
協調者

而不是所有事情的實作者。

可以先記:

Controller
比較關心 HTTP 流程

例如:

[HttpPost]
public IActionResult Create(CreateUserViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    _userService.Create(model);

    return RedirectToAction("Index");
}

這樣 Controller 一眼就能看懂:

資料不合法
↓
回原頁面

資料合法
↓
交給 Service

成功
↓
回列表頁

真正「怎麼建立使用者」則交給 Service。

Service 是什麼?

Service 可以先理解成:

負責系統的業務邏輯和流程規則。

例如:

角色怎麼判斷
狀態怎麼轉換
資料能不能修改
資料要怎麼組合
哪些欄位要轉換
要查哪些資料
完成後還要做哪些事情

這些都比較像 Service 的工作。

例如:

public class UserService
{
    public void Create(CreateUserViewModel model)
    {
        string roleName;

        if (model.RoleId == 1)
        {
            roleName = "管理員";
        }
        else
        {
            roleName = "一般使用者";
        }

        User entity = new User
        {
            Name = model.Name,
            Email = model.Email,
            RoleId = model.RoleId,
            CreateTime = DateTime.Now
        };

        _userRepository.Create(entity);
    }
}

Controller 就只剩:

[HttpPost]
public IActionResult Create(CreateUserViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    _userService.Create(model);

    return RedirectToAction("Index");
}

這時分工就清楚很多。

Controller 和 Service 可以怎麼分?

可以先用這張表記。

Controller Service
接收 Request 處理業務邏輯
接收網址參數 判斷規則
接收表單資料 欄位轉換
ModelState 判斷 狀態處理
呼叫 Service 組合資料
決定 View / Redirect / JSON 呼叫資料存取
處理 HTTP Response 執行功能流程

我自己會記成:

Controller
「這個 Request 要怎麼處理?」

Service
「這個功能實際要怎麼做?」

什麼叫做「業務邏輯」?

這個詞我一開始也覺得很抽象。

其實可以直接看需求。

例如需求說:

只有管理員可以修改資料

這就是規則。

狀態為完成後不能再次編輯

也是規則。

員工編號要先轉成內部主鍵才能查資料

也是規則。

新增資料時要自動填入建立者和建立時間

也是規則。

這些不是單純:

收到 HTTP Request

也不是:

SELECT * FROM ...

而是:

這個系統「應該怎麼運作」

這些就很適合放在 Service。

資料庫操作也全部放 Service 嗎?

不一定。

這就要看專案架構。

有些比較簡單的專案可能是:

Controller
↓
Service
↓
DbContext
↓
Database

Service 直接用:

_dbContext.Users

查資料。

但比較分層的專案可能是:

Controller
↓
Service
↓
Repository
↓
DbContext
↓
Database

或:

Controller
↓
Service
↓
DBService
↓
SQL
↓
Database

也就是:

Service
負責「要查什麼、要做什麼」

資料存取層
負責「怎麼跟資料庫拿」

例如 Service:

public UserDetailViewModel GetDetail(int id)
{
    User user = _userRepository.GetById(id);

    UserDetailViewModel model = new UserDetailViewModel
    {
        Name = user.Name,
        Email = user.Email
    };

    return model;
}

Repository:

public User GetById(int id)
{
    return _dbContext.Users
        .FirstOrDefault(x => x.Id == id);
}

這樣:

Service
不用知道太多資料庫操作細節

Repository
不用知道畫面怎麼顯示

為什麼要拆這麼多層?

一開始真的會覺得:

本來 Controller 一支檔案就寫完了

現在變成:

Controller
Service
Repository

不是更麻煩嗎?

小功能確實可能看起來比較多。

但專案一大,這種分工會開始有價值。

第一個好處:Controller 比較看得懂

假設 Controller 是:

public IActionResult Detail(int id)
{
    UserDetailViewModel model = _userService.GetDetail(id);

    return View(model);
}

我一眼就知道:

收到 id
↓
取得詳細資料
↓
傳給 View

不用先讀 80 行 SQL、權限判斷、欄位轉換。

第二個好處:同一套規則不用寫很多次

假設:

使用者姓名顯示規則

不只一個 Controller 需要。

如果全部寫 Controller:

UserController 寫一次
ReportController 再寫一次
AdminController 再寫一次

以後規則改了,三個地方都要改。

如果集中在:

UserService

其他地方就能共用同一套邏輯。

Controller A
↓
UserService

Controller B
↓
UserService

Controller C
↓
UserService

第三個好處:比較容易找到問題在哪一層

例如畫面上的角色名稱錯了。

如果所有東西都塞在 Controller,我只能從一大坨程式裡慢慢找。

但有分層後,可以先問:

Controller 傳錯資料?
Service 轉換錯?
Repository 查錯資料?
Database 原始資料就錯?

問題比較容易被拆開。

第四個好處:View 不需要知道資料庫細節

前一篇有講:

Entity
比較靠近資料庫

ViewModel
比較靠近畫面

Service 就常站在中間做這件事:

Entity
↓
整理、轉換、判斷
↓
ViewModel

例如資料庫:

RoleId = 1

畫面不要顯示:

1

而是:

管理員

Service 可以負責轉換:

UserDetailViewModel model = new UserDetailViewModel
{
    Name = entity.Name,
    RoleName = GetRoleName(entity.RoleId)
};

View 就只需要:

@Model.RoleName

不需要自己判斷:

@if (Model.RoleId == 1)
{
    ...
}

那所有 if 都應該丟到 Service 嗎?

也不是。

不是看到:

if

就一定叫業務邏輯。

例如 Controller:

if (!ModelState.IsValid)
{
    return View(model);
}

這比較是在處理:

HTTP 表單驗證流程

放 Controller 很合理。

但例如:

if (user.Status == "Completed")
{
    throw new Exception("完成後不可修改");
}

這就比較像:

系統規則

通常更適合 Service。

所以不能只看語法。

要問的是:

這個判斷是在處理 HTTP?

還是在處理系統規則?

Controller 可以做資料轉換嗎?

簡單的小轉換技術上當然可以。

但如果開始出現:

員工編號轉內部 ID
狀態代碼轉名稱
權限判斷
建立者欄位填值
更新者欄位填值
多張資料組合

全部塞 Controller,就很容易變胖。

比較理想的方向通常是:

Controller
↓
把輸入交出去

Service
↓
處理規則和資料轉換

尤其同一套轉換如果很多功能都會用,更不適合到處複製。

一個查詢功能可能怎麼跑?

假設現在是:

查看使用者詳細資料

Controller:

public IActionResult Detail(int id)
{
    UserDetailViewModel model = _userService.GetDetail(id);

    return View(model);
}

Service:

public UserDetailViewModel GetDetail(int id)
{
    User user = _userRepository.GetById(id);

    UserDetailViewModel model = new UserDetailViewModel
    {
        Name = user.Name,
        Email = user.Email
    };

    return model;
}

Repository:

public User GetById(int id)
{
    return _dbContext.Users
        .FirstOrDefault(x => x.Id == id);
}

最後 View:

@model UserDetailViewModel

<h1>@Model.Name</h1>

<p>@Model.Email</p>

完整流程就是:

瀏覽器
↓
Controller
↓
Service
↓
Repository
↓
Database
↓
Repository 回傳 Entity
↓
Service 轉成 ViewModel
↓
Controller
↓
View

為什麼有些 Service 前面還有 Interface?

實際專案可能看到:

IUserService.cs
UserService.cs

Controller 宣告:

private readonly IUserService _userService;

而真正實作是在:

public class UserService : IUserService
{
}

這裡可以先理解成:

IUserService
定義「這個 Service 能做哪些事情」

UserService
真正實作「這些事情怎麼做」

例如 Interface:

public interface IUserService
{
    UserDetailViewModel GetDetail(int id);
}

Service:

public class UserService : IUserService
{
    public UserDetailViewModel GetDetail(int id)
    {
        // 真正處理邏輯
    }
}

現在先不用深入 Interface 和 Dependency Injection。

只要知道:

F12 按下去
有時候會先跳到 Interface

真正程式可能還在另一支 Service 裡

這件事下一篇追程式時會很重要。

實際看到 Controller,我現在會先看什麼?

如果我打開一個 Controller,看到:

public IActionResult Detail(int id)
{
    var model = _userService.GetDetail(id);

    return View(model);
}

以前可能會覺得:

就這樣?

真正的程式去哪了?

現在我會先拆:

id
↓
從 Request 進來的資料

_userService.GetDetail(id)
↓
真正功能往 Service 走

model
↓
Service 回傳的資料

return View(model)
↓
把資料交給 View

所以我不會期待 Controller 一定有很多程式。

有時候:

Controller 越乾淨
反而代表真正邏輯被放到其他負責的層

如果 Controller 很胖,我會怎麼看?

假設看到:

public IActionResult Update(UserViewModel model)
{
    // 30 行資料查詢
    // 20 行權限判斷
    // 10 行欄位轉換
    // 20 行資料庫更新
    // 10 行通知
}

我不會直接說:

這一定是錯的

因為還是要看專案規模和團隊架構。

但我會開始注意:

這些邏輯是不是很多地方都會用?
是不是屬於系統規則?
是不是讓 Controller 很難閱讀?
是不是和 HTTP 沒有直接關係?

如果答案都是:

那通常就很適合往 Service 拆。

實際追企業專案,我會怎麼往下找?

例如 Controller:

var model = _userService.GetDetail(id);

我想知道資料到底怎麼來。

現在可以:

把游標放在 GetDetail
↓
按 F12

有可能先跳到:

IUserService

看到:

UserDetailViewModel GetDetail(int id);

這裡只有宣告,沒有真正內容。

接著要再找到:

UserService

裡真正的:

public UserDetailViewModel GetDetail(int id)
{
    ...
}

然後裡面可能又看到:

_userRepository.GetById(id);

再繼續往下追。

也就是:

Controller
↓
Service Interface
↓
Service
↓
Repository / DBService
↓
Database

這就是下一篇的主題。

這篇先記住什麼?

Controller 不是不能寫邏輯。

真正的重點是:

不要讓 Controller 同時負責太多不同責任

可以先這樣分:

Controller
↓
接收 Request
取得參數
基本 HTTP 流程判斷
呼叫 Service
決定 View / Redirect / JSON

Service
↓
處理業務規則
權限與狀態判斷
資料組合
欄位轉換
協調資料存取

Repository / DBService / DbContext
↓
實際和資料庫溝通

所以一個比較清楚的流程可能是:

Request
↓
Controller
↓
Service
↓
資料存取層
↓
Database

查詢結果再回來:

Database
↓
Entity / DTO
↓
Service
↓
ViewModel
↓
Controller
↓
View

這樣前一篇的:

Entity
ViewModel
DTO

也開始真的有位置了。

以前看到 Controller 裡只有:

_userService.GetDetail(id);

可能會覺得:

真正的程式是不是不見了?

現在會知道:

不是不見了

只是被分到 Service 裡去了

下一篇就不只講概念,直接開始實際追:

Controller 裡只看到 _userService.GetDetail(id),我要怎麼用 F12 一路追到 Service、資料存取,最後找到資料到底是從哪裡查出來的?


上一篇
Day 20|`@model`、ViewModel、Entity 和 DTO 到底是什麼?
下一篇
Day 22|用 F12 從 Controller 追到 Service,資料怎麼一路查到資料庫?
系列文
學過一點 React,卻被撈進 C#:新手用 AI 硬啃 MVC 企業專案的 3 個月實錄22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言