- Published on
ASP.NET Core Web API(三):Vertical Slice Architecture 與 AI 開發
Table of Contents
如果你問一個在企業用傳統 N-Layer 分層架構(Controller → Service → Repository → Database)幹了三年的工程師,最痛的地方是什麼,答案通常是:
「改一個功能,要改五個檔案。而且我永遠不確定改了 Service 會不會影響到另一個完全無關的功能。」
這個痛點催生了 Vertical Slice Architecture(垂直切片架構)。
分層架構的根本問題
傳統分層架構的問題不是「分層」這件事本身,而是它按技術職責分組,而不是按功能分組。
傳統分層(按技術分):
├── Controllers/
│ ├── TodosController.cs ← 所有 Todo 相關路由
│ └── UsersController.cs
├── Services/
│ ├── TodoService.cs ← 所有 Todo 相關邏輯
│ └── UserService.cs
├── Repositories/
│ ├── TodoRepository.cs ← 所有 Todo 相關 DB 操作
│ └── UserRepository.cs
└── Models/
├── Todo.cs
└── User.cs
你想新增「建立 Todo」這個功能,需要動 Controllers/、Services/、Repositories/、Models/ 四個不同目錄。中間任何層都可能有意外的跨功能耦合(例如 TodoService 裡偷偷呼叫了 UserRepository)。
Vertical Slice:按功能切,而不是按技術切
圖說:分層架構是橫向切(按技術層),VSA 是縱向切(按功能)。每個 Slice 是完全自給自足的單元。
VSA 的核心思路:每個功能(Feature)是一個完整的縱向切片,從 HTTP Endpoint 一路切到資料庫操作,全部放在同一個 Feature Folder 裡。
VSA 結構(按功能分):
├── Features/
│ ├── Todos/
│ │ ├── CreateTodo/
│ │ │ ├── CreateTodoCommand.cs ← Command + Handler + Response 全都在這
│ │ │ └── CreateTodoEndpoint.cs ← 路由定義
│ │ ├── GetTodos/
│ │ │ ├── GetTodosQuery.cs
│ │ │ └── GetTodosEndpoint.cs
│ │ └── GetTodoById/
│ │ ├── GetTodoByIdQuery.cs
│ │ └── GetTodoByIdEndpoint.cs
│ └── Users/
│ └── CreateUser/
│ ├── CreateUserCommand.cs
│ └── CreateUserEndpoint.cs
└── Program.cs
改「建立 Todo」功能?你只需要打開 Features/Todos/CreateTodo/ 這一個資料夾。
MediatR + CQRS:VSA 的骨架
VSA 通常搭配 CQRS(Command Query Responsibility Segregation) 模式和 MediatR 函式庫實作。
- Command:修改狀態的操作(Create、Update、Delete)
- Query:讀取狀態的操作(Get、List)
- MediatR:中間人,把 Command/Query 送去對應的 Handler 執行
// Features/Todos/CreateTodo/CreateTodoCommand.cs
public record CreateTodoCommand(string Title) : IRequest<TodoResponse>;
public class CreateTodoHandler : IRequestHandler<CreateTodoCommand, TodoResponse>
{
private readonly AppDbContext _db;
public CreateTodoHandler(AppDbContext db)
{
_db = db;
}
public async Task<TodoResponse> Handle(
CreateTodoCommand request,
CancellationToken cancellationToken)
{
var todo = new Todo { Title = request.Title };
_db.Todos.Add(todo);
await _db.SaveChangesAsync(cancellationToken);
return new TodoResponse(todo.Id, todo.Title, todo.IsCompleted);
}
}
public record TodoResponse(int Id, string Title, bool IsCompleted);
這個 Handler 完全自給自足:它知道要操作什麼資料、要回傳什麼格式。任何人看到這個檔案,就知道「建立 Todo」的完整邏輯是什麼。
// Features/Todos/CreateTodo/CreateTodoEndpoint.cs
public static class CreateTodoEndpoint
{
public static IEndpointRouteBuilder MapCreateTodo(
this IEndpointRouteBuilder routes)
{
routes.MapPost("/api/todos", async (
CreateTodoCommand command,
IMediator mediator) =>
{
var result = await mediator.Send(command);
return Results.Created($"/api/todos/{result.Id}", result);
})
.WithName("CreateTodo")
.WithSummary("建立一個 Todo 項目");
return routes;
}
}
Endpoint 只負責一件事:接收 HTTP Request,把 Command 交給 MediatR,回傳結果。
Pipeline Behaviors:橫切關注點的優雅解法
CQRS 的另一個好處:跨功能的橫切關注點(Logging、Validation、Transaction)可以用 MediatR Pipeline Behavior 統一處理,不需要在每個 Handler 裡重複寫。
// 驗證行為:所有 Command 執行前自動驗證
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(e => e != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}
搭配 FluentValidation:
// Features/Todos/CreateTodo/CreateTodoValidator.cs
public class CreateTodoValidator : AbstractValidator<CreateTodoCommand>
{
public CreateTodoValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title 不可為空")
.MaximumLength(200).WithMessage("Title 最多 200 字");
}
}
每個 Feature Slice 自己管理自己的驗證規則,但驗證的執行機制是共用的。 這就是 VSA 的精髓:Feature 之間 Low Coupling、Feature 內部 High Cohesion。
為什麼 AI Coding Agent 特別適合 VSA?
這是這篇最有趣的部分。
你有沒有用過 Claude Code 或 GitHub Copilot 幫你改程式?你大概遇過這個問題:你說「幫我新增一個建立訂單的功能」,AI 改了 OrderService.cs,但同時偷偷又動了 CustomerService.cs,因為兩個 Service 之間有隱藏的耦合。
傳統分層架構的問題在於:功能的邊界是模糊的。AI Agent 沒辦法知道「建立訂單」的完整邏輯在哪裡結束,因為它分散在三個不同的層裡面。
圖說:VSA 讓每個功能的邊界非常清晰,AI Agent 可以精準地操作單一 Slice,不會意外影響其他功能。
VSA 解決了這個問題,原因有三:
1. 功能邊界清晰 = AI 的 Context Window 聚焦
當你叫 AI 「幫我修改 CreateTodo 功能」,在 VSA 裡它只需要讀 Features/Todos/CreateTodo/ 這個資料夾。所有需要的 Context(Command、Handler、Validator、Endpoint)都在這裡,一個 folder 就夠了。
在傳統分層架構,AI 可能需要同時讀 Controllers/TodosController.cs、Services/TodoService.cs、Repositories/TodoRepository.cs,而且還不確定哪些部分跟這個功能有關。
2. 一致的程式碼命名與結構
VSA 強制你遵循 FeatureName + Command/Query/Handler/Endpoint 的命名慣例。AI Agent 在看過幾個 Slice 之後,能夠準確預測下一個 Slice 長什麼樣子。
這就像是在 AGENTS.md 裡告訴 AI「每個 Feature 的結構長這樣」,AI 就能照著模板生成新功能,而不是亂猜。
3. 低耦合讓 AI 的修改不會「連帶傷亡」
因為每個 Slice 的 Handler 只操作自己需要的資料庫表,AI 在修改 CreateOrder Slice 時,不可能意外修改到 GetCustomer Slice 的邏輯,因為它們根本不模一個 class。
實戰經驗:叫 Claude Code 新增一個功能
在 VSA 專案裡叫 AI:「幫我新增一個 MarkTodoComplete 功能」,AI 會:
在
Features/Todos/建立MarkTodoComplete/資料夾根據
CreateTodo的模板生成 Command + Handler + Endpoint + Validator- 完全不動其他 Slice
這種規律性的程式結構,讓 AI 的成功率大幅提升。
完整的 Program.cs 設定
var builder = WebApplication.CreateBuilder(args);
// Services
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite("Data Source=todos.db"));
// MediatR:自動掃描所有 Handler
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
// FluentValidation:自動掃描所有 Validator
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// ValidationBehavior Pipeline
builder.Services.AddTransient(
typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 掛載所有 Feature Endpoints
app.MapTodoEndpoints();
app.Run();
// TodoEndpoints.cs(統整所有 Todo 相關 Endpoint)
public static class TodoEndpoints
{
public static IEndpointRouteBuilder MapTodoEndpoints(
this IEndpointRouteBuilder routes)
{
routes.MapCreateTodo();
routes.MapGetTodos();
routes.MapGetTodoById();
routes.MapMarkTodoComplete();
return routes;
}
}
VSA 適合哪些場景?
VSA 不是銀彈,它也有代價:學習曲線、需要引入 MediatR 套件、命名慣例需要全團隊共識。
| 場景 | 建議架構 |
|---|---|
| 小型 API,< 10 個端點 | Minimal API 就夠了 |
| 中型,功能持續增加 | Controller + Service 分層,或開始考慮 VSA |
| 大型,多人協作,功能複雜 | VSA + MediatR + FluentValidation |
| AI Agent 大量參與開發 | 強烈推薦 VSA,AI 效率提升顯著 |
| Microservice 單一職責 | Minimal API,單個 Service 本身就是「一個 Slice」 |
總結:三篇的脈絡
這個系列從傳統 Controller 出發,到 Minimal API 的輕量設計,最後到 Vertical Slice Architecture。
三種方式都在現實專案中大量使用,它們不是互斥的,而是在不同規模、不同需求下的工具:
- Controller-based:成熟、穩定、適合大型 MVC 系統
- Minimal API:輕量、AOT 友好、適合 Microservice
- Vertical Slice:功能聚焦、低耦合、AI 時代最好的後端架構模式
在 AI coding tool(Claude Code、GitHub Copilot)越來越普及的今天,設計一個「讓 AI 容易理解和操作」的架構,本身就是工程師的核心職責之一。