Triển khai Repository Pattern & Unit of Work: Đừng để Code ASP.NET Core thành ‘nồi lẩu thập cẩm’

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Tại sao bạn nên “chia tay” cách viết code cũ?

Ngày đầu chạm ngõ ASP.NET Core, mình thường tống toàn bộ logic xử lý database vào thẳng Controller. Lúc đó thấy nhanh lắm, chỉ cần _context.Users.Add(user); await _context.SaveChangesAsync(); là xong. Nhưng khi dự án bắt đầu phình to với hàng chục bảng, cơn ác mộng mới thực sự xuất hiện.

Thử tưởng tượng bạn có 50 Controller và cần đổi từ SQL Server sang MongoDB. Việc lục lọi từng file để sửa logic truy vấn sẽ ngốn của bạn cả tuần trời. Chưa kể, việc viết Unit Test cho Controller lúc này gần như bất khả thi vì nó dính chặt (tight coupling) lấy Entity Framework. Repository Pattern và Unit of Work ra đời chính là để giải quyết đống lộn xộn này.

Hãy coi Repository như một “kho chứa” dữ liệu thông minh. Thay vì yêu cầu trực tiếp từ Database, bạn chỉ cần hỏi Repository. Trong khi đó, Unit of Work đóng vai trò quản lý giao dịch. Nó đảm bảo nếu bạn thực hiện chuỗi 5 thao tác lưu dữ liệu, thì hoặc là tất cả thành công, hoặc là không có gì thay đổi cả. Điều này cực kỳ quan trọng để giữ toàn vẹn dữ liệu (ACID).

Cài đặt môi trường và Project mẫu

Trước tiên, bạn cần chuẩn bị một project ASP.NET Core Web API. Mình mặc định bạn đã quen với việc tạo project và cấu hình Entity Framework Core (EF Core). Hãy cài nhanh các package cần thiết qua NuGet:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

Chúng ta sẽ làm việc trên một thực thể Product đơn giản như sau:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Cấu hình chi tiết: Xây dựng bộ khung chuyên nghiệp

1. Thiết lập Generic Repository

Thay vì viết riêng Repository cho từng bảng (Product, Category…), mình ưu tiên dùng IGenericRepository. Cách tiếp cận này giúp giảm tới 60-70% lượng code lặp lại (boilerplate code) cho các thao tác CRUD cơ bản.

public interface IGenericRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
}

Dưới đây là phần triển khai thực tế (Implementation) để bạn tham khảo:

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    protected readonly MyDbContext _context;
    public GenericRepository(MyDbContext context)
    {
        _context = context;
    }

    public async Task<T> GetByIdAsync(int id) => await _context.Set<T>().FindAsync(id);
    public async Task<IEnumerable<T>> GetAllAsync() => await _context.Set<T>().ToListAsync();
    public async Task AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
    public void Update(T entity) => _context.Set<T>().Update(entity);
    public void Delete(T entity) => _context.Set<T>().Remove(entity);
}

2. Triển khai Unit of Work

Đây là “nhạc trưởng” điều phối các Repository. Giả sử bạn đang xử lý một đơn hàng: bạn cần trừ tồn kho ở Repository Product và tạo hóa đơn ở Repository Order. Unit of Work sẽ giúp bạn lưu cả hai trong cùng một Transaction duy nhất.

public interface IUnitOfWork : IDisposable
{
    IGenericRepository<Product> Products { get; }
    Task<int> CompleteAsync();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly MyDbContext _context;
    public IGenericRepository<Product> Products { get; private set; }

    public UnitOfWork(MyDbContext context)
    {
        _context = context;
        Products = new GenericRepository<Product>(_context);
    }

    public async Task<int> CompleteAsync() => await _context.SaveChangesAsync();
    public void Dispose() => _context.Dispose();
}

3. Đăng ký Dependency Injection (DI)

Mở file Program.cs và thêm hai dòng sau. Bước này giúp hệ thống tự động “bơm” các Service vào Controller khi cần:

builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Kiểm tra và Tối ưu Workflow

Giờ đây, Controller của bạn sẽ cực kỳ tinh gọn. Nó không còn quan tâm DbContext hoạt động ra sao, chỉ tập trung vào logic nghiệp vụ.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IUnitOfWork _unitOfWork;
    public ProductsController(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork;

    [HttpGet]
    public async Task<IActionResult> GetAll() 
    {
        var products = await _unitOfWork.Products.GetAllAsync();
        return Ok(products);
    }
}

Trong quá trình phát triển API, việc debug JSON trả về là việc làm thường xuyên. Thay vì cài đặt extension nặng nề, mình hay dùng toolcraft.app (như công cụ JSON Formatter). Nó giúp mình format nhanh dữ liệu, kiểm tra cấu trúc lồng nhau cực kỳ hiệu quả ngay trên trình duyệt.

Tại sao cách này là “cứu cánh” cho Unit Test?

Khi viết test cho ProductsController, bạn không cần cài đặt Database thật hay dùng In-Memory DB. Bạn chỉ cần Mock interface IUnitOfWork bằng thư viện Moq. Bạn có thể giả lập dữ liệu trả về từ GetAllAsync() chỉ trong vài dòng code. Điều này giúp tốc độ chạy test nhanh hơn gấp nhiều lần.

var mockUow = new Mock<IUnitOfWork>();
mockUow.Setup(u => u.Products.GetAllAsync()).ReturnsAsync(new List<Product>());
var controller = new ProductsController(mockUow.Object);

Một vài lưu ý từ thực tế

Đừng quá máy móc áp dụng Repository Pattern cho mọi dự án. Nếu bạn chỉ làm một App nhỏ với 1-2 bảng, việc tạo thêm interface chỉ làm code thêm cồng kềnh. Tuy nhiên, với dự án doanh nghiệp (Enterprise) hoặc khi cần viết Unit Test nghiêm túc, đây là lựa chọn hàng đầu.

Khi gặp các câu query phức tạp cần dùng Include hay ThenInclude, đừng cố nhồi nhét vào Generic Repository. Hãy tạo một Repository riêng (ví dụ ProductRepository) kế thừa từ Generic để xử lý các logic đặc thù đó.

Hy vọng bài viết này giúp bạn tự tin hơn trong việc tổ chức kiến trúc code ASP.NET Core chuyên nghiệp và dễ quản lý.

Share: