Lỗi sai kinh điển khi nhầm lẫn giữa entity, dto và view model, request model... trong dotnet

Công nghệ - 10/11/2025 15:00:24

Anh em đang dùng 1 class User (Entity) cho TẤT CẢ các tác vụ?
- Nhận data từ client? (RequestModel)
- Trả data về API? (ResponseModel)
- Trực tiếp binding ra View? (ViewModel)

Dùng chung Entity cho DTO, RequestModel, ViewModel... Tiện lợi hay thảm họa?

Trong suốt đời coder của mình, tôi nhận thấy một sai lầm "kinh điển" mà rất nhiều lập trình viên, từ junior đến cả senior, vẫn thường mắc phải: đánh đồng Entity, DTO, ViewModel và các RequestModel.... và sẽ đặc biệt gặp khó khăn khi tối ưu hệ thống.

Anh em có thể nghĩ: "Cũng chỉ là class để chứa data thôi mà, dùng chung cho tiện? Đỡ viết nhiều code". Không. Đây không chỉ là vấn đề về "tên gọi" (naming convention). Đây là vấnd đề về kiến trúc, bảo mật và hiệu năng.

Hôm nay chúng ta cùng nhau mổ xẻ một phần vấn đề này nhé.


Tại sao phải phân biệt rạch ròi?

Câu trả lời nằm gọn trong một nguyên tắc: Separation of Concerns (SoC) - Tách biệt các mối quan tâm. Dữ liệu phải được "cô lập" và "biến đổi" (transform) khi đi qua các ranh giới (boundaries) của hệ thống.

Cụ thể Mỗi lớp (layer) trong ứng dụng của bạn có một nhiệm vụ riêng:

  • View (UI): Hiển thị dữ liệu.
  • Controller: Điều phối request.
  • Service/Business Logic: Xử lý nghiệp vụ.
  • Data Access (Repository): Giao tiếp với Database (nơi chứa các Entity).

 

Mỗi "Model" bạn tạo ra là "người vận chuyển" dữ liệu giữa các lớp này. Bạn cứ tưởng tượng bạn đặt hàng và chuyển một kiện hoa quả từ Long An đến Pháp, nếu bạn dùng chung một kiểu vận chuyển, một người vận chuyển thì chi phí sẽ tăng đáng kể. Nhà cung cấp vận chuyển (shipping provider) sẽ gom nhiều kiện hàng, phân loại hàng và lựa chọn phương tiện vận chuyển phù hợp để giảm chi phí. Hàng hoa quả mà để chung với hàng đông lạnh và vận chuyển bằng ô tô thì thảm họa sẽ xảy ra.

Phân biệt một số model phổ biến

1. Entity (Domain Model) "Nguồn sống" của Hệ Thống

Để mô hình hóa logic nghiệp vụ (Business Logic)trạng thái (State) của miền (Domain).

Nhiệm vụ: Đây không phải là một class "câm" chỉ có get/set. Một Entity đúng nghĩa (theo triết lý Domain-Driven Design - DDD) phải chứa cả dữ liệu VÀ các hành vi (methods) xử lý trên dữ liệu đó.

2. DTO (Data Transfer Object) "Người đưa thư"

Vận chuyển dữ liệu "an toàn" qua các ranh giới (boundaries), đặc biệt là giữa các lớp (Service Layer ↔ Controller) hoặc giữa các hệ thống (Microservices).

Nhiệm vụ: flatten các Entity phức tạp, che giấu các trường nhạy cảm, và tối ưu payload.

Đặc điểm: dumb, chỉ có get/set, không hành vi, không logic.

Nơi ở: Thường nằm ở Application Layer (Service Layer)

Ví dụ: User Entity có 50 trường (cả HashedPassword), nhưng UserDto chỉ có 5 trường (Id, FullName, Email) để trả về cho API.

3. ViewModel (VM)

Phục vụ DUY NHẤT cho một View (trang web, màn hình app) cụ thể.

Nhiệm vụ: "Biên kịch" lại dữ liệu (thường là từ DTO) thành thứ mà View có thể hiển thị ngay lập tức.

Đặc điểm: Chứa các thuộc tính đã được định dạng (format), các logic chỉ dành cho UI (ví dụ: bool ShowAdminButton), và các dữ liệu hỗ trợ UI (List<SelectListItem>).

Nơi ở: Thường nằm ở Presentation Layer (Controller/UI).

Lưu ý: Trong API-first (nhằm hỗ trợ cho React, Vue, Angular hay Mobile app), ViewModel gần như biến mất phía server. Vai trò của nó được "chuyển" cho DTO (hoặc ResponseModel) đảm nhận, và logic "ViewModel" sẽ nằm ở client (ví dụ: trong React State).

4. RequestModel (InputModel) - "Tờ Khai" Đầu Vào

Mục đích: "Nắm bắt" (capture) và Validate dữ liệu đầu vào từ một HTTP Request.

Nhiệm vụ: Đại diện cho ý định (intent) của người dùng. "Tôi muốn tạo sản phẩm với các thông tin này...".

Đặc điểm: Chứa đầy đủ Data Annotations ([Required], [StringLength], [Range]). Cấu trúc phải khớp với JSON/Form mà client gửi lên.

5. ResponseModel

Định nghĩa "Hợp đồng" công khai của API, quy định dữ liệu mà API hứa sẽ trả về cho client.

Nhiệm vụ: Đại diện cho cấu trúc dữ liệu chính xáccuối cùng mà client (app mobile, web React/Vue...) nhận được từ một endpoint cụ thể.

Đặc điểm: Chỉ chứa các trường dữ liệu "công khai", được gọt giũa để phù hợp yêu cầu của từng endpoint và phải rất ổn định, ít thay đổi. Nếu thay đổi, thường phải qua versioning.


Sai lầm phổ biến thường gặp

  • Dùng Entity để làm mọi thứ, ViewModel, RequestModel (nhận input) và dùng luôn làm dto: việc này sẽ dẫn đến cả lỗi bảo mật, và hiệu năng, lẫn lỗi kiến trúc mà sẽ dẫn đến khó phát triển, debug sau này.
  • "Tận dụng" DTO làm ResponseModel: Với một dự án cá nhân hoặc "chạy cho nhanh", việc dùng DTO làm ResponseModel là chấp nhận được. Nhưng một hệ thống enterprise, việc đảm bảo ổn định bảo mật thì cần phải rạch ròi.
  • RequestModel = ResponseModel: Model nhận dữ liệu vào và trả ở đầu ra, 2 nhiệm vụ đã khác nhau rồi thì không thể biến thành 1, phải không :) Đây chính là "bộ mặt" chuyên nghiệp của API. Việc gõ thêm vài class và vài dòng Mapper sẽ cứu bạn khỏi vô số đêm mất ngủ khi hệ thống phình to.

 


Lời kết

Code thôi mà phải sinh ra lắm nguyên tắc để hành nhau làm gì nhỉ :D Nhưng việc phân tách rõ ràng các loại Model này không phải là "vẽ vời" cho phức tạp. Đó là yêu cầu bắt buộcđể xây dựng một hệ thống enterprise đó.

Một hệ thống "sạch" là một hệ thống mà mỗi đối tượng đều có một và chỉ một lý do để thay đổi (Single Responsibility Principle).

  • Entity: Thay đổi khi nghiệp vụ thay đổi.
  • DTO: Thay đổi khi hợp đồng qua các lớp thay đổi.
  • ViewModel: Thay đổi khi Giao diện (UI) thay đổi.
  • RequestModel: Thay đổi khi yêu cầu đầu vào thay đổi.
  • ResponseModel: Thay đổi khi yêu cầu đầu ra thay đổi.

 

Việc ngại gõ thêm vài class Model và viết Mapper hôm nay sẽ cứu bạn khỏi việc thức đêm debug cả tuần trong tương lai =))))

Happy coding (Y)

/Son Do - I share real-world lessons, team building & developer growth.

#dotnet #aspnetcore #softwarearchitecture #cleancode#systemdesign #optimization #dto #viewmodel #entity #developer #techlead

Công nghệ

Xem tất cả