Vấn đề thực tế: Cơn ác mộng “Nhảy File” khi maintain dự án
Chắc hẳn anh em dev .NET nào cũng từng trải qua cảm giác này: Sếp bảo thêm một field “Số điện thoại” vào tính năng đăng ký người dùng. Bạn bắt đầu hành trình “du lịch” qua một loạt các folder: Mở Web.API để sửa Controller, nhảy sang Application để sửa DTO và Service, lặn xuống Domain để thêm field vào Entity, rồi cuối cùng là chui vào Infrastructure để cập nhật DbContext và Repository.
Chỉ một thay đổi nhỏ mà chúng ta phải mở đến 5-6 file ở 5-6 project khác nhau. Đây chính là hệ quả của việc áp dụng Clean Architecture hoặc N-Tier Architecture một cách máy móc. Khi dự án phình to lên với hàng trăm tính năng, việc quản lý code theo “lớp” (layers) như vậy khiến chúng ta mất quá nhiều thời gian để định vị code, chưa kể việc thay đổi ở lớp này rất dễ làm hỏng lớp kia do sự phụ thuộc chằng chịt.
Phân tích nguyên nhân: Tại sao Clean Architecture đôi khi lại gây khó dễ?
Clean Architecture sinh ra để giải quyết vấn đề tách biệt mối quan tâm (Separation of Concerns). Ý tưởng rất tốt: Code nghiệp vụ không nên phụ thuộc vào Database hay UI. Tuy nhiên, sai lầm phổ biến là chúng ta tách biệt theo Technical Concerns (tách theo tầng kỹ thuật) thay vì Business Concerns (tách theo nghiệp vụ).
Khi tổ chức theo tầng, chúng ta đang nhóm những thứ giống nhau về mặt kỹ thuật lại với nhau (ví dụ: tất cả Controller vào một chỗ, tất cả Repository vào một chỗ). Nhưng thực tế, khi code một tính năng, chúng ta cần sự phối hợp của cả Controller, Service và Repository đó. Việc xé lẻ một tính năng ra nhiều tầng khiến tính đóng gói (encapsulation) bị phá vỡ. Mỗi khi cần sửa tính năng A, bạn lại phải lục tung cả bản đồ dự án lên.
Các cách giải quyết thường gặp
Để xử lý sự cồng kềnh này, giới developer thường đưa ra vài lựa chọn:
- Giữ nguyên Clean Architecture nhưng chia nhỏ Project: Chia thành nhiều cụm project nhỏ hơn. Cách này vẫn khiến bạn phải nhảy file liên tục, chỉ là trong phạm vi hẹp hơn một chút.
- Chuyển sang Microservices: Tách hẳn mỗi tính năng thành một service riêng. Cách này giải quyết triệt để sự phụ thuộc nhưng lại làm tăng chi phí vận hành, hạ tầng và độ phức tạp khi giao tiếp giữa các service.
- Sử dụng Vertical Slice Architecture (VSA): Đây là giải pháp trung dung và cực kỳ hiệu quả cho các dự án Monolith (đơn khối).
Cách tốt nhất: Áp dụng Vertical Slice Architecture
Thay vì cắt bánh theo chiều ngang (layers), chúng ta cắt bánh theo chiều dọc (slices). Mỗi “miếng bánh” là một tính năng hoàn chỉnh. Tất cả những gì tính năng đó cần — từ API Endpoint, Logic xử lý, Validation đến Data Access — đều nằm chung một chỗ.
1. Cấu trúc thư mục của Vertical Slice
Trong một dự án ASP.NET Core áp dụng VSA, cấu trúc thư mục sẽ trông như thế này:
Features/
├── Products/
│ ├── CreateProduct/
│ │ ├── CreateProductCommand.cs
│ │ ├── CreateProductHandler.cs
│ │ ├── CreateProductValidator.cs
│ │ └── CreateProductEndpoint.cs
│ ├── GetProductDetail/
│ └── DeleteProduct/
├── Orders/
└── Users/
Mỗi folder như CreateProduct chứa toàn bộ logic của tính năng đó. Nếu sau này sếp bảo xóa tính năng tạo sản phẩm, bạn chỉ cần xóa đúng cái folder đó là xong, không lo để lại “rác” ở các tầng khác.
2. Triển khai thực tế với MediatR
Để hiện thực hóa VSA một cách mượt mà, mình thường dùng thư viện MediatR. Nó đóng vai trò như một sứ giả giúp tách biệt phần nhận request và phần xử lý logic.
Đầu tiên, cài đặt package cần thiết:
dotnet add package MediatR
Giả sử mình làm tính năng tạo sản phẩm mới. Thay vì viết Service dài dằng dặc, mình tạo một file CreateProduct.cs duy nhất chứa tất cả:
// Features/Products/CreateProduct.cs
public class CreateProduct
{
// 1. Request - Dữ liệu đầu vào
public record Command(string Name, decimal Price) : IRequest<int>;
// 2. Validator (Dùng FluentValidation chẳng hạn)
public class Validator : AbstractValidator<Command>
{
public Validator() => RuleFor(x => x.Name).NotEmpty();
}
// 3. Handler - Nơi chứa logic nghiệp vụ
public class Handler : IRequestHandler<Command, int>
{
private readonly AppDbContext _context;
public Handler(AppDbContext context) => _context = context;
public async Task<int> Handle(Command request, CancellationToken ct)
{
var product = new Product { Name = request.Name, Price = request.Price };
_context.Products.Add(product);
await _context.SaveChangesAsync(ct);
return product.Id;
}
}
}
Tại Controller hoặc Minimal API, bạn chỉ việc gọi đơn giản thế này:
app.MapPost("/products", async (IMediator mediator, CreateProduct.Command command) =>
{
var id = await mediator.Send(command);
return Results.Created($"/products/{id}", id);
});
3. Kinh nghiệm cá nhân khi làm Vertical Slice
Khi mới chuyển sang VSA, mình cũng hơi bỡ ngỡ vì thấy một file sao mà dài thế (vừa có Command, vừa có Handler). Nhưng tin mình đi, khi dự án vào giai đoạn bảo trì, bạn sẽ thấy nó cực kỳ sướng. Cần sửa gì cứ mở đúng file đó ra là có hết.
Một mẹo nhỏ là khi làm việc với các request/response JSON phức tạp trong MediatR, mình hay dùng toolcraft.app/vi/tools/developer/json-formatter để format lại dữ liệu cho dễ nhìn, đỡ phải cài mấy cái extension nặng nề trong VS Code hay Visual Studio. Nó giúp mình kiểm tra nhanh cấu trúc data trước khi map vào Command.
4. Tại sao VSA lại tăng khả năng bảo trì?
- Ít sự phụ thuộc (Low Coupling): Thay đổi ở tính năng “Tạo sản phẩm” không bao giờ làm hỏng tính năng “Xóa sản phẩm”.
- Tính đóng gói cao (High Cohesion): Mọi thứ liên quan đến một nghiệp vụ nằm sát cạnh nhau.
- Dễ dàng mở rộng: Muốn thêm tính năng mới? Cứ tạo folder mới. Không cần sửa code cũ, giảm thiểu rủi ro regression bug.
- Phù hợp với Agile: Team có thể chia nhau làm theo tính năng (slices) mà ít khi bị xung đột code (conflict) trên Git hơn so với việc mọi người cùng sửa chung một file Service to đùng.
Kết luận
Vertical Slice Architecture không phải là “viên đạn bạc”, nhưng nó là một cách tiếp cận cực kỳ thực dụng cho các dự án ASP.NET Core hiện đại. Nó giúp chúng ta tập trung vào cái khách hàng cần — đó là Tính năng — thay vì quá sa đà vào việc phân chia các tầng kỹ thuật phức tạp.
Nếu bạn đang cảm thấy mệt mỏi với việc nhảy qua nhảy lại giữa hàng chục project trong một Solution, hãy thử áp dụng Vertical Slice cho tính năng tiếp theo xem sao. Chúc anh em code vui vẻ và ít bug!
