# Clean Architecture .NET 2026: CQRS, MediatR và Câu Hỏi Phỏng Vấn Developer > Hướng dẫn toàn diện về Clean Architecture trong .NET với CQRS và MediatR 14. Tìm hiểu các mẫu kiến trúc phân lớp, pipeline behaviors và chuẩn bị phỏng vấn senior developer. - Published: 2026-09-15 - Updated: 2026-09-15 - Author: Anthony Fillion-Maillet - Tags: clean-architecture, cqrs, mediatr, dotnet, interview - Reading time: 8 min --- Clean Architecture trong .NET tách biệt logic nghiệp vụ khỏi các mối quan tâm về infrastructure, giúp ứng dụng dễ test, bảo trì và mở rộng hơn. Kết hợp với CQRS (Command Query Responsibility Segregation) và MediatR, mẫu kiến trúc này cung cấp phương pháp có cấu trúc để xây dựng các ứng dụng enterprise mà người phỏng vấn thường đánh giá. > **Điều Quan Trọng Trong Phỏng Vấn** > > Các câu hỏi về Clean Architecture xuất hiện trong 70% các buổi phỏng vấn senior .NET. Người phỏng vấn mong đợi ứng viên giải thích dependency rule: các dependency hướng vào trong, với domain layer không có dependency nào với framework bên ngoài. ## Các Layer trong Clean Architecture và Dependency Rule [Template Clean Architecture của Ardalis](https://github.com/ardalis/CleanArchitecture) cho ASP.NET Core 10 tổ chức code thành bốn layer đồng tâm. Mỗi layer có trách nhiệm cụ thể và quy tắc dependency nghiêm ngặt. ```csharp // Core/Domain Layer - Không có dependency bên ngoài // src/Core/Domain/Entities/Order.cs namespace Core.Domain.Entities; public class Order { public Guid Id { get; private set; } public string CustomerEmail { get; private set; } public List Lines { get; private set; } = new(); public OrderStatus Status { get; private set; } public DateTime CreatedAt { get; private set; } // Logic domain được đóng gói trong entity public void AddLine(Product product, int quantity) { if (Status != OrderStatus.Draft) throw new InvalidOperationException("Cannot modify a submitted order"); var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id); if (existingLine is not null) { existingLine.IncreaseQuantity(quantity); return; } Lines.Add(new OrderLine(product.Id, product.Price, quantity)); } public void Submit() { if (!Lines.Any()) throw new InvalidOperationException("Cannot submit an empty order"); Status = OrderStatus.Submitted; } } ``` Domain layer chứa entities, value objects và domain services. Layer này không biết gì về database, HTTP hay các framework bên ngoài. ## Mẫu CQRS: Tách Biệt Read và Write CQRS chia các thao tác thành Commands (thao tác write làm thay đổi state) và Queries (thao tác read trả về dữ liệu). Sự tách biệt này cho phép scale và tối ưu hóa độc lập cho từng đường dẫn. ```csharp // UseCases Layer - Commands và Queries // src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs namespace UseCases.Orders.Commands.CreateOrder; public record CreateOrderCommand( string CustomerEmail, List Lines ) : IRequest>; public record OrderLineDto(Guid ProductId, int Quantity); ``` Commands trả về dữ liệu tối thiểu, thường chỉ là chỉ báo thành công hoặc ID mới tạo. Queries trả về DTO được tối ưu hóa cho consumer. ```csharp // src/UseCases/Orders/Queries/GetOrderById/GetOrderByIdQuery.cs namespace UseCases.Orders.Queries.GetOrderById; public record GetOrderByIdQuery(Guid OrderId) : IRequest>; public record OrderDetailsDto( Guid Id, string CustomerEmail, string Status, decimal TotalAmount, List Lines ); ``` ## MediatR 14: Pipeline Behaviors và Triển Khai Handler [MediatR 14.2](https://www.nuget.org/packages/MediatR) cung cấp infrastructure messaging cho CQRS. Mỗi command hoặc query có đúng một handler, thực thi single responsibility. ```csharp // src/UseCases/Orders/Commands/CreateOrder/CreateOrderHandler.cs namespace UseCases.Orders.Commands.CreateOrder; public class CreateOrderHandler : IRequestHandler> { private readonly IOrderRepository _orderRepository; private readonly IProductRepository _productRepository; private readonly IUnitOfWork _unitOfWork; public CreateOrderHandler( IOrderRepository orderRepository, IProductRepository productRepository, IUnitOfWork unitOfWork) { _orderRepository = orderRepository; _productRepository = productRepository; _unitOfWork = unitOfWork; } public async Task> Handle( CreateOrderCommand request, CancellationToken cancellationToken) { // Load products để validate và lấy giá var productIds = request.Lines.Select(l => l.ProductId).ToList(); var products = await _productRepository .GetByIdsAsync(productIds, cancellationToken); if (products.Count != productIds.Count) return Result.NotFound("One or more products not found"); // Tạo order sử dụng logic domain var order = new Order(request.CustomerEmail); foreach (var line in request.Lines) { var product = products.First(p => p.Id == line.ProductId); order.AddLine(product, line.Quantity); } await _orderRepository.AddAsync(order, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); return Result.Success(order.Id); } } ``` ## Pipeline Behaviors cho Cross-Cutting Concerns Pipeline behaviors trong MediatR xử lý logging, validation, caching và quản lý transaction mà không làm ô nhiễm handlers. ```csharp // src/UseCases/Common/Behaviors/ValidationBehavior.cs namespace UseCases.Common.Behaviors; public class ValidationBehavior : IPipelineBehavior where TRequest : IRequest { private readonly IEnumerable> _validators; public ValidationBehavior(IEnumerable> validators) { _validators = validators; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { if (!_validators.Any()) return await next(); var context = new ValidationContext(request); var validationResults = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, cancellationToken))); var failures = validationResults .SelectMany(r => r.Errors) .Where(f => f is not null) .ToList(); if (failures.Any()) throw new ValidationException(failures); return await next(); } } ``` Đăng ký behaviors theo thứ tự thực thi. Validation thường chạy đầu tiên, tiếp theo là logging, sau đó là xử lý transaction. ```csharp // src/Web/Program.cs builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly); cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); cfg.AddOpenBehavior(typeof(TransactionBehavior<,>)); }); ``` ## Infrastructure Layer: Triển Khai Repository Infrastructure layer triển khai các interface được định nghĩa trong core layer. Repository Entity Framework Core chuyển đổi các thao tác domain thành các lệnh gọi database. ```csharp // src/Infrastructure/Data/Repositories/OrderRepository.cs namespace Infrastructure.Data.Repositories; public class OrderRepository : IOrderRepository { private readonly AppDbContext _context; public OrderRepository(AppDbContext context) { _context = context; } public async Task GetByIdAsync( Guid id, CancellationToken cancellationToken = default) { return await _context.Orders .Include(o => o.Lines) .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task AddAsync( Order order, CancellationToken cancellationToken = default) { await _context.Orders.AddAsync(order, cancellationToken); } public async Task> GetByCustomerEmailAsync( string email, CancellationToken cancellationToken = default) { return await _context.Orders .Where(o => o.CustomerEmail == email) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); } } ``` ## Câu Hỏi Phỏng Vấn: Senior Developer Cần Biết Gì Các buổi phỏng vấn kỹ thuật cho vị trí senior .NET thường bao gồm các câu hỏi về Clean Architecture. Dưới đây là các pattern mà người phỏng vấn đánh giá: **Q: Tại sao domain layer không có dependency với các framework bên ngoài?** Domain layer chứa các quy tắc nghiệp vụ cần giữ ổn định bất kể các thay đổi infrastructure. Nếu domain phụ thuộc vào Entity Framework, việc chuyển sang Dapper hoặc database khác đòi hỏi phải thay đổi logic nghiệp vụ. Dependency rule đảm bảo infrastructure là một chi tiết có thể hoán đổi mà không ảnh hưởng đến behavior cốt lõi. **Q: Khi nào CQRS là quá mức cần thiết?** CQRS thêm độ phức tạp mà các ứng dụng CRUD đơn giản không cần. Nếu model read và write gần như giống nhau, nếu không cần scale riêng biệt, và nếu team nhỏ, kiến trúc đơn giản hơn là phù hợp. CQRS tỏa sáng khi model read khác biệt đáng kể với model write, khi cần event sourcing, hoặc khi tải read và write cần scale độc lập. **Q: Làm thế nào để xử lý transaction xuyên suốt nhiều aggregates?** ```csharp // Transaction behavior bọc toàn bộ việc thực thi handler public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { private readonly IUnitOfWork _unitOfWork; public TransactionBehavior(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { // Commands sửa đổi nhiều aggregates sử dụng transaction tường minh if (request is not ICommand) return await next(); await using var transaction = await _unitOfWork .BeginTransactionAsync(cancellationToken); try { var response = await next(); await transaction.CommitAsync(cancellationToken); return response; } catch { await transaction.RollbackAsync(cancellationToken); throw; } } } ``` Đối với các thao tác xuyên suốt nhiều aggregates, sử dụng eventual consistency với domain events khi có thể. Transaction đồng bộ xuyên aggregates cho thấy vấn đề thiết kế tiềm ẩn: các aggregates có thể nên được gộp lại. ## Tối Ưu Hóa Read-Side với Model Query Riêng Biệt Queries bỏ qua hoàn toàn domain layer khi performance quan trọng. Truy cập database trực tiếp với [Dapper](https://github.com/DapperLib/Dapper) hoặc raw SQL tránh overhead của việc materialization toàn bộ entity graph. ```csharp // src/Infrastructure/Queries/GetOrderByIdQueryHandler.cs namespace Infrastructure.Queries; public class GetOrderByIdQueryHandler : IRequestHandler> { private readonly IDbConnection _connection; public GetOrderByIdQueryHandler(IDbConnection connection) { _connection = connection; } public async Task> Handle( GetOrderByIdQuery request, CancellationToken cancellationToken) { const string sql = """ SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt, SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount FROM Orders o LEFT JOIN OrderLines ol ON ol.OrderId = o.Id WHERE o.Id = @OrderId GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt """; var order = await _connection.QueryFirstOrDefaultAsync( new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken)); return order is null ? Result.NotFound($"Order {request.OrderId} not found") : Result.Success(order); } } ``` ## Chuẩn Bị Phỏng Vấn Clean Architecture: Những Điểm Chính - Dependency rule quy định rằng các dependency source code chỉ có thể hướng vào trong. Domain layer không biết gì về databases, frameworks hay cơ chế delivery. - CQRS tách commands (thay đổi state) khỏi queries (lấy dữ liệu). Commands đi qua domain model và validation. Queries có thể bỏ qua domain entities để tăng performance. - MediatR 14 định tuyến requests đến handlers. Pipeline behaviors xử lý cross-cutting concerns như validation, logging và transactions một cách có thể kết hợp. - Repository interfaces nằm trong core layer. Implementations nằm trong infrastructure. Sự đảo ngược này cho phép hoán đổi EF Core với Dapper mà không chạm vào logic nghiệp vụ. - Câu trả lời phỏng vấn nên bao gồm các trade-offs. Clean Architecture thêm độ phức tạp ban đầu nhưng mang lại kết quả trong các ứng dụng lớn, tồn tại lâu dài với nhiều developers. - Module [ASP.NET Core Clean Architecture](/technologies/dotnet/interview-questions/clean-architecture) trên SharpSkill bao gồm các pattern bổ sung như Specification, Result objects và Domain Events. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/dotnet/clean-architecture-dotnet-cqrs-mediatr-interview-questions-2026