FSTS 後端編碼規範
OMS 專案後端(.NET 8 / ASP.NET Core 8)的 Clean Architecture + CQRS + MediatR 編碼規範(v1.0,2026-02-26)。回到 FSTS 專案總覽、搭配前端:FSTS 前端編碼規範。
一句話
OMS 後端採 Clean Architecture 四層(Domain / Application / Infrastructure / Api)+ CQRS + MediatR Pipeline,依「外層可參考內層、內層不可參考外層」的相依方向編排,所有共用邏輯透過「決策樹」判定落點。1
架構總覽(四層)
| 層次 | 專案 | 相依方向 | 核心職責 |
|---|---|---|---|
| Domain | Oms.Domain | 無外部相依 | 實體、列舉、例外、事件 |
| Application | Oms.Application | 參考 Domain | CQRS、驗證、介面定義 |
| Infrastructure | Oms.Infrastructure | 參考 Application | EF Core、Repository、外部服務 |
| Api | Oms.Api | 參考全部 | Controller、Middleware、DI |
相依方向:Api → Infrastructure → Application → Domain。1
2. Domain Layer — Oms.Domain
最內層,完全不依賴任何外部套件,僅包含純粹的領域模型與不變量。2
主要目錄:2
Entities/— 資料實體(對應 DB 表)+ 商業不變量(如User.cs,Order.cs)Enums/— 領域列舉(如OrderStatus)Exceptions/— 領域特定例外Events/— 領域事件Services/— 純計算 Domain Service(static class,如PricingCalculator)
3. Application Layer — Oms.Application
核心商業邏輯層,採 CQRS + MediatR:每個 Use Case 對應一個 Command 或 Query,Handler 編排流程。3
3.1 目錄結構
Features/{Feature}/Commands/— 寫入操作(Command + Validator + Handler 合併於同一檔案)Features/{Feature}/Queries/— 讀取操作(Query + Validator + Handler 合併於同一檔案)Features/{Feature}/DTOs/— 資料傳輸物件Features/{Feature}/EventHandlers/— 域事件處理器Features/{Feature}/Specifications/— 查詢規格(封裝複雜查詢條件)Common/Interfaces/— Repository、外部服務的抽象介面Common/Attributes/— 自訂 Attribute(如敏感資料標記SensitiveDataAttribute)Common/Behaviors/— MediatR Pipeline 行為(橫切關注)Common/Models/— 共用回應模型(PagedResponse<T>,Result<T>)3
3.2 Handler 的角色 — 編排者模式
Handler 在 CQRS 架構中扮演「編排者」角色,不直接實作共用邏輯,而是透過依賴注入調用其他服務。職責流程:3
- 接收 Command/Query 請求
- 調用 Domain Services 執行純計算
- 調用 Repository 存取資料
- 調用 Application Interfaces 使用外部服務(如
IJwtTokenGenerator、IEmailService) - 組裝回應 DTO 並回傳
3.3 Domain Service vs Application Service
| 屬性 | Domain Service | Application Service(介面) |
|---|---|---|
| 位置 | Domain/Services/ | Application/Common/Interfaces/ 定義介面,Infrastructure/ 實作 |
| 是否需要介面 | 否(直接 static class) | 是(反轉相依) |
| 是否需要 DI | 否(直接呼叫 static) | 是(構造器注入) |
| 是否涉及 I/O | 否(純計算) | 是(DB / API / 檔案) |
| 可測試性 | 直接呼叫即可 | 透過 Mock 介面測試 |
判斷原則:如果邏輯不需要 await、不碰資料庫、不呼叫外部服務 → Domain Service;反之 → Application Interface。3
4. Infrastructure Layer — Oms.Infrastructure
負責所有外部資源存取與 Application 層介面的實作。4
Persistence/— EF CoreAppDbContextPersistence/Configurations/— Fluent API 實體設定(如UserConfiguration.cs)Persistence/Repositories/—IRepository<T>實作Persistence/Interceptors/— SaveChanges 攔截器(如AuditableEntityInterceptor)Services/— Application 介面的具體實作(如JwtTokenGenerator,BcryptPasswordHasher,MediatRDomainEventDispatcher)DependencyInjection/— Infrastructure DI 註冊(InfrastructureServiceRegistration.cs)4
5. Api Layer — Oms.Api
進入點層,Controller 僅負責轉發請求至 MediatR,不包含商業邏輯。5
Controllers/— HTTP 端點,僅做_mediator.Send()Middleware/— 全域例外處理(GlobalExceptionMiddleware)Filters/— Action FilterServices/— Api 層專用服務(如CurrentUserService從HttpContext取資料)5
三種 Service 的差異:5
| 層 | 目錄 | 性質 | 是否需要介面 | 範例 |
|---|---|---|---|---|
| Domain | Domain/Services/ | 純計算、static、零相依 | 不需要 | PricingCalculator |
| Application | Common/Interfaces/ | 介面定義(涉 I/O 的共用邏輯) | 是(實作在 Infra) | IStockService |
| Api | Api/Services/ | 框架膠水,橋接 ASP.NET 與 Application 介面 | 不需要(它本身就是實作) | CurrentUserService |
Controller 三件事原則
Controller 內不應出現 if/else 商業判斷,僅做三件事:5
- 組裝 Command / Query(加入 server-side 資訊,如 IP、UserAgent)
- 透過 MediatR 轉發(
_mediator.Send(command)) - 回傳 HTTP 狀態碼 + 結果
6. 共用邏輯決策樹
當一段商業邏輯被多個 Handler 使用時,依以下流程決定放置位置:6
| # | 判斷問題 | Yes | No |
|---|---|---|---|
| Q1 | 只被一個 Handler 使用? | 直接寫在該 Handler 內 | 繼續 Q2 |
| Q2 | 涉及 I/O(DB / API / 檔案 / 網路)? | 繼續 Q3 | Domain Service(純 static class) |
| Q3 | 是資料存取操作? | Repository 擴充方法 | 繼續 Q4 |
| Q4 | 涉及第三方服務(Email / SMS / 外部 API)? | Application Interface + Infrastructure 實作 | 繼續 Q5 |
| Q5 | 涉及多個 Repository 的組合操作? | Application Service(組合服務) | 重新檢視是否真的是共用邏輯 |
四種放置位置速查
- A. Domain Service:
Domain/Services/+public static class,無需 DI,如PricingCalculator.CalculateTotal(items, discount)。6 - B. Repository 擴充:介面在
Application/Common/Interfaces/IOrderRepository.cs,實作在Infrastructure/Persistence/Repositories/OrderRepository.cs(如GetByIdWithItemsAsync,GetByStatusAsync)。6 - C. Application Interface + Infra 實作:跨外部服務(Email、呼叫外部 API),介面在
Application/Common/Interfaces/IEmailService.cs,實作在Infrastructure/Services/SmtpEmailService.cs,DI 用services.AddScoped<IEmailService, SmtpEmailService>()。6 - D. Application Service(組合服務):橫跨多個 Repository 的編排(如
IStockService注入IProductRepository+IInventoryRepository),介面在 Application、實作在 Infrastructure。6
7. Pipeline Behaviors
MediatR Pipeline Behaviors 處理橫切關注點:7
| Behavior | 用途 |
|---|---|
ValidationBehavior | 自動執行 FluentValidation,失敗拋 ValidationException |
LoggingBehavior | 記錄請求名稱 + 執行時間,自動遮蔽 [SensitiveData] 標記欄位 |
TransactionBehavior(預留) | 自動包裝 UnitOfWork 交易(尚未實作) |
8. 請求參數驗證(FluentValidation)
8.1 驗證流程
搭配 MediatR Pipeline 實現自動驗證,不需在 Handler 或 Controller 手動呼叫:8
HTTP Request → Controller(不驗證,只轉發)
→ _mediator.Send(command)
→ ValidationBehavior → 通過 → Handler
→ 失敗 → ValidationException → GlobalExceptionMiddleware → HTTP 400 + ProblemDetails
8.2 三類驗證的職責分工
| 驗證類型 | 負責層 | 失敗 HTTP 狀態碼 | 範例 |
|---|---|---|---|
| 輸入格式 | Validator (Application) | 400 Bad Request | Email 格式不正確、密碼太短 |
| 業務規則 | Handler / Domain Entity | 422 Unprocessable | 帳號已存在、訂單狀態不可轉換 |
| 找不到資源 | Handler | 404 Not Found | 訂單不存在 |
關鍵:Validator 驗證「輸入格式」(email 格式、必填、長度限制);Handler / Domain 驗證「業務規則」(帳號是否存在、庫存是否足夠)。8
8.3 檔案放置
Validator 與對應的 Command/Query 放在同一個檔案中(CreateOrder.cs 含 Command + Validator + Handler);檔案過大才拆成 CreateOrder.Command.cs / .Validator.cs / .Handler.cs。9
8.4 驗證規則速查
| 類型 | 方法 | 適用場景 |
|---|---|---|
| 必填 | NotEmpty() | 字串、集合不可為空 |
| 字串長度 | MinimumLength(n) / MaximumLength(n) | 密碼下限、名稱上限 |
| 精確長度 | Length(min, max) | 手機號碼、統一編號 |
EmailAddress() | Email 欄位 | |
| 正則 | Matches("pattern") | 密碼複雜度、自定格式 |
| 數值 > N | GreaterThan(n) | ID、數量必須為正整數 |
| 數值 >= N | GreaterThanOrEqualTo(n) | 金額不可為負 |
| 數值範圍 | InclusiveBetween(min, max) | 分頁 PageSize 限制 |
| 欄位比對 | Equal(x => x.OtherField) | 確認密碼 |
| 自定義 | Must((cmd, val) => ...) | 任何自定邏輯 |
| 列舉值 | IsInEnum() | 確保值在 enum 範圍內 |
| 巢狀物件 | SetValidator(new ChildValidator()) | 驗證子物件 |
| 集合每項 | RuleForEach(x => x.Items).ChildRules(...) | 驗證集合內每個元素 |
8.5 驗證失敗回應格式(RFC 7807 ProblemDetails)
{
"status": 400,
"title": "Validation Error",
"detail": "One or more validation errors occurred.",
"errors": {
"CustomerId": ["CustomerId must be a positive integer."],
"CustomerName": ["CustomerName is required."],
"Items": ["Order must contain at least one item."]
},
"traceId": "00-abc123..."
}DI 註冊:services.AddValidatorsFromAssembly(assembly) 掃描所有 Validator,services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)) 註冊 Pipeline。10
9. 開發最佳實踐
9.1 C# 命名規範(.editorconfig 編譯時強制)
由 .editorconfig + EnforceCodeStyleInBuild + TreatWarningsAsErrors 在 dotnet build 時強制執行,違反即編譯失敗:11
| 對象 | 規則 | 範例 | 嚴重性 |
|---|---|---|---|
| 介面 | I 開頭 + PascalCase | IUserRepository | error |
| 泛型型別參數 | T 開頭 + PascalCase | TResponse | error |
| 型別(class, struct, enum) | PascalCase | OrderStatus, User | error |
| 公開成員(property, method, event) | PascalCase | TotalAmount, GetByIdAsync() | error |
| Private 欄位 | _camelCase | _userRepository | error |
| 參數 | camelCase | cancellationToken | error |
| 常數 | PascalCase(非 UPPER_SNAKE) | MaxRetryCount | warning→error |
| 靜態唯讀欄位 | PascalCase | DefaultTimeout | warning→error |
| Async 方法 | 以 Async 結尾 | SaveChangesAsync() | warning→error |
| 區域變數 | camelCase | var user = ... | warning→error |
設定檔:.editorconfig(命名規則)+ Directory.Build.props(EnforceCodeStyleInBuild, TreatWarningsAsErrors)。11
9.2 C# 程式碼風格
| 規則 | 設定值 | 說明 |
|---|---|---|
| Namespace 宣告 | file_scoped (error) | 用 namespace X; 而非 namespace X { } |
| using 位置 | outside_namespace (error) | using 放在 namespace 外 |
| using 排序 | System 優先 | using System.* 排最前 |
| var 使用 | 型別明顯時用 var | var user = new User() |
| 大括號 | Allman style(全部換行) | if (...)\n{ |
| Expression body | 單行時建議使用 | public int Id => _id; |
嚴重性等級:error(編譯失敗)> warning(搭配 TreatWarningsAsErrors 也失敗)> suggestion(僅 IDE 提示)> none(關閉)。12
常用指令:12
dotnet format --verify-no-changes— 檢查格式問題(不修改)dotnet format— 自動修正格式dotnet format style— 只修正命名規則dotnet format whitespace— 只修正空白/縮排
9.3 CQRS 架構命名慣例
| 類型 | 規則 | 範例 |
|---|---|---|
| Command | {Action}{Entity}Command | CreateOrderCommand |
| Query | Get{Entity}[By{Filter}]Query | GetOrderByIdQuery |
| Handler | {Command/Query}Handler | CreateOrderCommandHandler |
| Validator | {Command}Validator | CreateOrderCommandValidator |
| DTO | {Entity}Response | OrderResponse, AuthResponse |
| Interface | I{Service/Repository} | IOrderRepository, IJwtTokenGenerator |
9.4 跨層相依規則
- Domain 不參考任何其他專案(零依賴)
- Application 僅參考 Domain(定義介面,不實作 I/O)
- Infrastructure 參考 Application(實作介面)
- Api 參考全部(組裝 DI、只透過 MediatR 傳遞請求)
- Controller 不直接注入 Repository 或
DbContext13
9.5 新功能開發 Checklist(10 步)
- Domain — 新增/修改 Entity(如有需要)
- Application — 建立 Command/Query + Handler
- Application — 建立 Validator(FluentValidation)
- Application — 建立 DTO(回應格式)
- Infrastructure — 如需新 Repository 方法,擴充介面 + 實作
- Infrastructure — 如需新外部服務,建立 Interface + 實作
- Infrastructure — DI 註冊新服務
- Api — 新增 Controller endpoint(僅
MediatR.Send) - Database — 更新
schema.sql - 測試 — Unit Test Handler + Integration Test API14
11. 完整範例:查詢訂單列表
GET /api/v1/orders?status=Pending&page=1&pageSize=10 涵蓋:參數檢核(分頁、篩選條件驗證)、DB 查詢(分頁 + 條件篩選 + Include 關聯資料)。完整從建檔到完成流程詳見 raw §11。15
既有 Pipeline 在新功能中的複用:新增此 API 不需修改 DI 註冊(MediatR 自動掃描)、Pipeline(全域生效)、Middleware(全域生效)。15
12. 後端測試規範
12.1 測試金字塔
| 層級 | 位置 | 用途 |
|---|---|---|
| Unit | tests/Oms.UnitTests/ | Domain 邏輯、Handler、Validator |
| Integration | tests/Oms.IntegrationTests/ | Repository 查詢、EF Core 設定 |
| API | tests/Oms.Api.Tests/ | Controller 端到端 |
12.2 測試專案獨立
.NET 生態系慣例將測試放在獨立 csproj(透過 ProjectReference 引用 src/)。原因:測試專案不應打包進發布產物,xUnit 與 NSubstitute 只存在於測試依賴。16
12.3 何時必須寫測試(PR 合併前強制)
- Domain Entity 的任何變更(Order 狀態機、User 欄位等)
- 新增或修改 Command/Query Handler
- 新增或修改 FluentValidation Validator
- Bug 修復:先寫失敗的測試,再修正程式碼17
12.4 命名慣例
- 檔案:
{ClassName}Tests.cs - 方法:
{Method}_Should{ExpectedResult}_When{Condition} - DisplayName 用中文:
[Fact(DisplayName = "登入:密碼錯誤應拋出 DomainException")]17
12.5 覆蓋率目標
| 範圍 | 目標 |
|---|---|
| Domain Entities | 90%+ |
| Command/Query Handlers | 80%+ |
| FluentValidation Validators | 80%+ |
| Infrastructure Services | 70%+ |
12.6 測試工具
| 工具 | 版本 | 用途 |
|---|---|---|
| xUnit | 2.9.2 | 測試框架 |
| NSubstitute | 5.1.0 | Mocking 框架 |
| FluentAssertions | 6.12.1 | 斷言語法 |
| EF Core InMemory | 8.0.11 | 整合測試記憶體 DB |
| WebApplicationFactory | 8.0.11 | API 端到端測試 |
執行指令:18
dotnet test tests/Oms.UnitTests/— 全部單元測試dotnet test tests/Oms.UnitTests/ --filter "FullyQualifiedName~OrderTests"— 特定測試類別dotnet test --collect:"XPlat Code Coverage"— 含覆蓋率報告dotnet test— 全部測試
13. CI/CD 管線與 Git Hooks
13.1 GitHub Actions CI
觸發情境:Push 到 main 或 develop;對 main / develop 發起 PR。19
管線內容:19
- 後端:
dotnet restore→dotnet build→dotnet test(三個測試專案) - 前端:
npm ci→type-check→lint→test:coverage
設定檔:.github/workflows/ci.yml。
13.2 Git Hooks
啟用:git config core.hooksPath .githooks20
| Hook | 觸發時機 | 檢查內容 |
|---|---|---|
pre-commit | git commit | 後端編譯 + 前端 type-check + lint |
pre-push | git push | 後端單元測試 + 前端測試 |
13.3 PR Checklist
每次提交 PR 前確認:21
- 所有現有測試通過(
dotnet test+npm run test) - 新增/修改的商業邏輯有對應測試
- 測試涵蓋成功路徑和錯誤路徑
- 沒有被跳過的測試(除非有文件說明原因)
- Domain Entity 變更有對應 Domain 測試
- Handler 變更有對應 Handler 測試
- 前端新增的 utility / hook 有對應測試
相關頁面
- FSTS 前端編碼規範 — 對應的前端規範
- FSTS Vibe Coding 開發框架 — Multi-Agent Pipeline + Skills / Rules
- FSTS 系統架構 — 系統拓樸
- FSTS 專案總覽 — 專案入口
補充資訊
來源補充:OMS Coding Rule & CI/CD 簡報(2026-03,OMS Team)
OMS Team 於 2026-03 對後端規範做了一份簡報版摘要,強化了以下工程決策背景(與本頁主要規範一致,僅補充理由與社群參考):
- Clean Architecture 的選型理由:Robert C. Martin(Uncle Bob,SOLID / Agile Manifesto / 《Clean Code》作者)於 2012 提出;.NET 社群採用度高 — Jason Taylor 的
CleanArchitecture範本(★ 17,000+)、Ardalis 的Clean Architecture(★ 16,000+)、微軟eShopOnWeb官方推薦;OMS 專案以 Jason Taylor 範本為基礎搭建。22 - RFC 7807 ProblemDetails:錯誤格式選用 IETF 制定的 RFC 7807(2016,2022 更新為 RFC 9457);ASP.NET Core 自 .NET 7 起設為預設;Spring Boot / NestJS 皆有對應支援,選用此規範與業界主流一致;透過
GlobalExceptionHandler統一攔截例外轉成 ProblemDetails 格式回傳。23 - 「查詢訂單列表」四層協作範例:
OrdersController.GetOrders → _mediator.Send(query) → GetOrdersQueryHandler → IOrderRepository → Order Entity,示範「Controller 越薄越好 / Handler 只依賴介面 / Repository 以 EF Core 實作」的同心圓依賴方向。24 - Services/ 目錄在四層的角色差異(避免放錯):Domain → 純 static 計算;Application → 介面定義;Infrastructure → 介面實作;Api → HttpContext 膠水。25
上述簡報同時涵蓋 CI/CD 章節,獨立成頁:CD Pipeline。