# Clean Architecture với .NET: Hướng Dẫn Thực Tiễn > Làm chủ Clean Architecture trong .NET với C#. Tìm hiểu các nguyên tắc SOLID, phân tách lớp và mẫu triển khai cho ứng dụng dễ bảo trì. - Published: 2026-02-13 - Updated: 2026-04-28 - Author: SharpSkill - Tags: dotnet, clean architecture, csharp, aspnet core, design patterns - Reading time: 16 min --- Clean Architecture, được Robert C. Martin (Uncle Bob) phổ biến, tổ chức mã nguồn theo cách đặt logic nghiệp vụ ở trung tâm ứng dụng, độc lập với framework và chi tiết triển khai. Cách tiếp cận kiến trúc này đảm bảo khả năng kiểm thử, bảo trì và mở rộng cho các ứng dụng .NET. Hướng dẫn này trình bày một triển khai thực tiễn với ASP.NET Core. > **Vì sao chọn Clean Architecture?** > > Ứng dụng pha trộn logic nghiệp vụ với mã hạ tầng nhanh chóng trở nên khó bảo trì. Clean Architecture áp đặt sự phân tách nghiêm ngặt, cho phép thay đổi chi tiết kỹ thuật mà không ảnh hưởng đến lõi nghiệp vụ. ## Các nguyên tắc cốt lõi của Clean Architecture Clean Architecture dựa trên đảo ngược phụ thuộc: các lớp bên trong không biết gì về các lớp bên ngoài. Miền nghiệp vụ vẫn được cô lập và có thể tiến hóa độc lập với các lựa chọn kỹ thuật như framework web hay cơ sở dữ liệu. ``` ┌─────────────────────────────────────────────────────────┐ │ Presentation │ │ (Controllers, Razor Pages, Blazor, API Endpoints) │ ├─────────────────────────────────────────────────────────┤ │ Infrastructure │ │ (EF Core, External APIs, File System, Email) │ ├─────────────────────────────────────────────────────────┤ │ Application │ │ (Use Cases, Commands, Queries, DTOs) │ ├─────────────────────────────────────────────────────────┤ │ Domain │ │ (Entities, Value Objects, Domain Services) │ └─────────────────────────────────────────────────────────┘ ``` Các phụ thuộc luôn hướng vào trong: Presentation → Infrastructure → Application → Domain. Domain không tham chiếu bất kỳ dự án nào khác. ## Cấu trúc dự án .NET cho Clean Architecture Cách tổ chức dự án phản ánh các lớp khác nhau. Mỗi lớp tương ứng với một dự án riêng biệt trong solution của Visual Studio, đảm bảo phân tách trách nhiệm về mặt vật lý. ```bash # terminal # Tạo cấu trúc solution dotnet new sln -n CleanArchitecture # Tạo các dự án cho từng lớp dotnet new classlib -n CleanArchitecture.Domain -o src/CleanArchitecture.Domain dotnet new classlib -n CleanArchitecture.Application -o src/CleanArchitecture.Application dotnet new classlib -n CleanArchitecture.Infrastructure -o src/CleanArchitecture.Infrastructure dotnet new webapi -n CleanArchitecture.Api -o src/CleanArchitecture.Api # Thêm các dự án vào solution dotnet sln add src/CleanArchitecture.Domain dotnet sln add src/CleanArchitecture.Application dotnet sln add src/CleanArchitecture.Infrastructure dotnet sln add src/CleanArchitecture.Api # Cấu hình tham chiếu giữa các dự án cd src/CleanArchitecture.Application dotnet add reference ../CleanArchitecture.Domain cd ../CleanArchitecture.Infrastructure dotnet add reference ../CleanArchitecture.Application cd ../CleanArchitecture.Api dotnet add reference ../CleanArchitecture.Infrastructure ``` Cấu trúc này đảm bảo Domain luôn độc lập và các phụ thuộc tuân thủ Dependency Rule của Clean Architecture. ## Lớp Domain: lõi nghiệp vụ Lớp Domain chứa các entity nghiệp vụ, Value Object và interface của repository. Trong lớp này không cho phép bất kỳ phụ thuộc bên ngoài nào. ```csharp // Domain/Entities/Order.cs namespace CleanArchitecture.Domain.Entities; // Entity với danh tính và vòng đời nghiệp vụ public class Order { // Định danh duy nhất của đơn hàng public Guid Id { get; private set; } // Tham chiếu khách hàng (Value Object cho email) public string CustomerEmail { get; private set; } // Tập hợp các mục (quan hệ một-nhiều) private readonly List _items = new(); public IReadOnlyCollection Items => _items.AsReadOnly(); // Trạng thái đơn hàng (enum nghiệp vụ) public OrderStatus Status { get; private set; } // Tổng số tiền được tính toán public decimal TotalAmount => _items.Sum(i => i.TotalPrice); // Mốc thời gian theo dõi public DateTime CreatedAt { get; private set; } public DateTime? ShippedAt { get; private set; } // Constructor riêng buộc phải dùng factory method private Order() { } // Factory method tạo đơn hàng hợp lệ public static Order Create(string customerEmail) { // Kiểm tra quy tắc nghiệp vụ khi tạo if (string.IsNullOrWhiteSpace(customerEmail)) throw new DomainException("Email khách hàng là bắt buộc."); if (!IsValidEmail(customerEmail)) throw new DomainException("Định dạng email không hợp lệ."); return new Order { Id = Guid.NewGuid(), CustomerEmail = customerEmail, Status = OrderStatus.Pending, CreatedAt = DateTime.UtcNow }; } // Phương thức nghiệp vụ: thêm một mục public void AddItem(Product product, int quantity) { // Quy tắc nghiệp vụ: không được sửa đơn hàng đã giao if (Status == OrderStatus.Shipped) throw new DomainException("Không thể chỉnh sửa đơn hàng đã giao."); if (quantity <= 0) throw new DomainException("Số lượng phải dương."); // Kiểm tra xem sản phẩm đã tồn tại chưa var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id); if (existingItem != null) { existingItem.IncreaseQuantity(quantity); } else { _items.Add(OrderItem.Create(this, product, quantity)); } } // Phương thức nghiệp vụ: xác nhận đơn hàng public void Confirm() { if (Status != OrderStatus.Pending) throw new DomainException("Chỉ những đơn hàng đang chờ mới được xác nhận."); if (!_items.Any()) throw new DomainException("Đơn hàng phải có ít nhất một mục."); Status = OrderStatus.Confirmed; } // Phương thức nghiệp vụ: giao đơn hàng public void Ship() { if (Status != OrderStatus.Confirmed) throw new DomainException("Đơn hàng phải được xác nhận trước khi giao."); Status = OrderStatus.Shipped; ShippedAt = DateTime.UtcNow; } private static bool IsValidEmail(string email) => email.Contains('@') && email.Contains('.'); } ``` Entity Order đóng gói các quy tắc nghiệp vụ và bảo vệ trạng thái nội bộ. Mọi thay đổi đều phải đi qua phương thức nghiệp vụ kiểm tra bất biến. ```csharp // Domain/Entities/OrderItem.cs namespace CleanArchitecture.Domain.Entities; // Entity con với danh tính riêng public class OrderItem { public Guid Id { get; private set; } public Guid OrderId { get; private set; } public Guid ProductId { get; private set; } public string ProductName { get; private set; } public decimal UnitPrice { get; private set; } public int Quantity { get; private set; } // Tính tổng giá của dòng public decimal TotalPrice => UnitPrice * Quantity; private OrderItem() { } // Factory method có kiểm tra public static OrderItem Create(Order order, Product product, int quantity) { return new OrderItem { Id = Guid.NewGuid(), OrderId = order.Id, ProductId = product.Id, ProductName = product.Name, UnitPrice = product.Price, Quantity = quantity }; } // Phương thức tăng số lượng public void IncreaseQuantity(int additionalQuantity) { if (additionalQuantity <= 0) throw new DomainException("Số lượng bổ sung phải dương."); Quantity += additionalQuantity; } } ``` ```csharp // Domain/Enums/OrderStatus.cs namespace CleanArchitecture.Domain.Enums; // Liệt kê các trạng thái đơn hàng public enum OrderStatus { Pending = 0, // Chờ xác nhận Confirmed = 1, // Đã xác nhận, sẵn sàng giao Shipped = 2, // Đã giao Delivered = 3, // Đã nhận Cancelled = 4 // Đã hủy } ``` > **Value Objects** > > Các Value Object như Money, Address hay Email đóng gói các khái niệm nghiệp vụ không có danh tính riêng. Tính bằng nhau dựa trên giá trị, không phải tham chiếu. Cách tiếp cận này tăng cường tính biểu đạt của miền. ## Interface Repository trong Domain Interface của các repository được định nghĩa trong Domain, còn phần triển khai nằm ở Infrastructure. Mẫu này tôn trọng nguyên tắc đảo ngược phụ thuộc. ```csharp // Domain/Interfaces/IOrderRepository.cs namespace CleanArchitecture.Domain.Interfaces; // Interface của repository Order public interface IOrderRepository { // Lấy theo định danh Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); // Lấy kèm các mục Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default); // Lấy theo email khách hàng Task> GetByCustomerEmailAsync( string email, CancellationToken cancellationToken = default); // Thêm đơn hàng mới Task AddAsync(Order order, CancellationToken cancellationToken = default); // Cập nhật đơn hàng hiện có Task UpdateAsync(Order order, CancellationToken cancellationToken = default); // Xóa đơn hàng Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); } ``` ```csharp // Domain/Interfaces/IUnitOfWork.cs namespace CleanArchitecture.Domain.Interfaces; // Mẫu Unit of Work cho quản lý giao dịch public interface IUnitOfWork : IDisposable { // Các repository truy cập qua UoW IOrderRepository Orders { get; } IProductRepository Products { get; } // Lưu nguyên tử mọi thay đổi Task SaveChangesAsync(CancellationToken cancellationToken = default); // Quản lý giao dịch tường minh Task BeginTransactionAsync(CancellationToken cancellationToken = default); Task CommitTransactionAsync(CancellationToken cancellationToken = default); Task RollbackTransactionAsync(CancellationToken cancellationToken = default); } ``` Mẫu Unit of Work điều phối các thao tác trên nhiều repository trong cùng một giao dịch. ## Lớp Application: điều phối Use Case Lớp Application chứa logic ứng dụng (use case), DTO và interface của các dịch vụ bên ngoài. Lớp này điều phối tương tác giữa miền nghiệp vụ và thế giới bên ngoài. ```csharp // Application/Orders/Commands/CreateOrderCommand.cs using MediatR; namespace CleanArchitecture.Application.Orders.Commands; // Command thể hiện ý định tạo đơn hàng public record CreateOrderCommand( string CustomerEmail, List Items ) : IRequest; // DTO cho các mục đơn hàng public record OrderItemDto( Guid ProductId, int Quantity ); ``` ```csharp // Application/Orders/Commands/CreateOrderCommandHandler.cs using MediatR; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Application.Orders.Commands; // Handler triển khai use case public class CreateOrderCommandHandler : IRequestHandler { private readonly IUnitOfWork _unitOfWork; public CreateOrderCommandHandler(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public async Task Handle( CreateOrderCommand request, CancellationToken cancellationToken) { // Tạo entity Order qua factory method var order = Order.Create(request.CustomerEmail); // Thêm các mục vào đơn hàng foreach (var item in request.Items) { // Lấy sản phẩm từ repository var product = await _unitOfWork.Products .GetByIdAsync(item.ProductId, cancellationToken); if (product == null) throw new NotFoundException($"Sản phẩm {item.ProductId} không được tìm thấy."); // Sử dụng phương thức nghiệp vụ của entity order.AddItem(product, item.Quantity); } // Lưu trữ qua repository await _unitOfWork.Orders.AddAsync(order, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); return order.Id; } } ``` MediatR triển khai mẫu Mediator để tách handler khỏi controller. Mỗi command chỉ có một handler chịu trách nhiệm xử lý. ```csharp // Application/Orders/Queries/GetOrderByIdQuery.cs using MediatR; namespace CleanArchitecture.Application.Orders.Queries; // Query để lấy đơn hàng theo ID public record GetOrderByIdQuery(Guid OrderId) : IRequest; // DTO phản hồi cho đơn hàng public record OrderDto( Guid Id, string CustomerEmail, string Status, decimal TotalAmount, DateTime CreatedAt, DateTime? ShippedAt, List Items ); // DTO cho các mục trong phản hồi public record OrderItemResponseDto( Guid Id, string ProductName, decimal UnitPrice, int Quantity, decimal TotalPrice ); ``` ```csharp // Application/Orders/Queries/GetOrderByIdQueryHandler.cs using MediatR; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Application.Orders.Queries; public class GetOrderByIdQueryHandler : IRequestHandler { private readonly IOrderRepository _orderRepository; public GetOrderByIdQueryHandler(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public async Task Handle( GetOrderByIdQuery request, CancellationToken cancellationToken) { // Lấy kèm các mục var order = await _orderRepository .GetByIdWithItemsAsync(request.OrderId, cancellationToken); if (order == null) return null; // Ánh xạ sang DTO (chiếu thủ công) return new OrderDto( Id: order.Id, CustomerEmail: order.CustomerEmail, Status: order.Status.ToString(), TotalAmount: order.TotalAmount, CreatedAt: order.CreatedAt, ShippedAt: order.ShippedAt, Items: order.Items.Select(i => new OrderItemResponseDto( Id: i.Id, ProductName: i.ProductName, UnitPrice: i.UnitPrice, Quantity: i.Quantity, TotalPrice: i.TotalPrice )).ToList() ); } } ``` Việc tách Commands/Queries tuân theo mẫu CQRS (Command Query Responsibility Segregation), tối ưu hóa đọc và ghi một cách độc lập. ## Kiểm tra dữ liệu với FluentValidation Việc kiểm tra command diễn ra ở lớp Application, trước khi handler được thực thi. ```csharp // Application/Orders/Validators/CreateOrderCommandValidator.cs using FluentValidation; using CleanArchitecture.Application.Orders.Commands; namespace CleanArchitecture.Application.Orders.Validators; public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator() { // Email bắt buộc và hợp lệ RuleFor(x => x.CustomerEmail) .NotEmpty().WithMessage("Email khách hàng là bắt buộc.") .EmailAddress().WithMessage("Định dạng email không hợp lệ."); // Bắt buộc ít nhất một mục RuleFor(x => x.Items) .NotEmpty().WithMessage("Đơn hàng phải có ít nhất một mục."); // Kiểm tra từng mục RuleForEach(x => x.Items).ChildRules(item => { item.RuleFor(i => i.ProductId) .NotEmpty().WithMessage("Định danh sản phẩm là bắt buộc."); item.RuleFor(i => i.Quantity) .GreaterThan(0).WithMessage("Số lượng phải dương.") .LessThanOrEqualTo(100).WithMessage("Số lượng tối đa: 100."); }); } } ``` ```csharp // Application/Common/Behaviors/ValidationBehavior.cs using FluentValidation; using MediatR; namespace CleanArchitecture.Application.Common.Behaviors; // Pipeline behavior cho việc kiểm tra tự động public class ValidationBehavior : IPipelineBehavior where TRequest : notnull { private readonly IEnumerable> _validators; public ValidationBehavior(IEnumerable> validators) { _validators = validators; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { // Nếu không có validator, tiếp tục if (!_validators.Any()) return await next(); // Chạy tất cả validator var context = new ValidationContext(request); var validationResults = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, cancellationToken))); // Tổng hợp lỗi var failures = validationResults .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); // Ném ngoại lệ nếu có lỗi if (failures.Any()) throw new ValidationException(failures); return await next(); } } ``` ValidationBehavior chạy tự động trước mỗi handler, đảm bảo chỉ command hợp lệ mới đi tới logic nghiệp vụ. ## Lớp Infrastructure: triển khai kỹ thuật Lớp Infrastructure cung cấp triển khai cụ thể cho các interface được định nghĩa trong Domain và Application. ```csharp // Infrastructure/Persistence/AppDbContext.cs using Microsoft.EntityFrameworkCore; using CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Infrastructure.Persistence; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } public DbSet Orders => Set(); public DbSet OrderItems => Set(); public DbSet Products => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // Áp dụng cấu hình từ assembly modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); } } ``` ```csharp // Infrastructure/Persistence/Configurations/OrderConfiguration.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Infrastructure.Persistence.Configurations; public class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { // Bảng và khóa chính builder.ToTable("Orders"); builder.HasKey(o => o.Id); // Thuộc tính builder.Property(o => o.CustomerEmail) .IsRequired() .HasMaxLength(256); builder.Property(o => o.Status) .IsRequired() .HasConversion(); // Lưu dưới dạng chuỗi dễ đọc // Quan hệ với OrderItems builder.HasMany(o => o.Items) .WithOne() .HasForeignKey(i => i.OrderId) .OnDelete(DeleteBehavior.Cascade); // Chỉ mục cho tìm kiếm theo email builder.HasIndex(o => o.CustomerEmail); // Truy cập trường riêng _items builder.Navigation(o => o.Items) .UsePropertyAccessMode(PropertyAccessMode.Field); } } ``` ```csharp // Infrastructure/Repositories/OrderRepository.cs using Microsoft.EntityFrameworkCore; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Infrastructure.Persistence; namespace CleanArchitecture.Infrastructure.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 .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task GetByIdWithItemsAsync( Guid id, CancellationToken cancellationToken = default) { // Tải eager các mục return await _context.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task> GetByCustomerEmailAsync( string email, CancellationToken cancellationToken = default) { return await _context.Orders .Include(o => o.Items) .Where(o => o.CustomerEmail == email) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); } public async Task AddAsync( Order order, CancellationToken cancellationToken = default) { await _context.Orders.AddAsync(order, cancellationToken); } public Task UpdateAsync( Order order, CancellationToken cancellationToken = default) { // EF Core tự theo dõi thay đổi _context.Orders.Update(order); return Task.CompletedTask; } public async Task DeleteAsync( Guid id, CancellationToken cancellationToken = default) { await _context.Orders .Where(o => o.Id == id) .ExecuteDeleteAsync(cancellationToken); } } ``` > **Cẩn thận với Leaky Abstractions** > > Repository không nên phơi bày IQueryable trực tiếp vì sẽ tạo ra phụ thuộc vào EF Core ở các lớp trên. Hãy ưu tiên các phương thức cụ thể với tham số rõ ràng. ## Triển khai Unit of Work ```csharp // Infrastructure/Persistence/UnitOfWork.cs using Microsoft.EntityFrameworkCore.Storage; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Infrastructure.Repositories; namespace CleanArchitecture.Infrastructure.Persistence; public class UnitOfWork : IUnitOfWork { private readonly AppDbContext _context; private IDbContextTransaction? _transaction; // Tải lười repository private IOrderRepository? _orderRepository; private IProductRepository? _productRepository; public UnitOfWork(AppDbContext context) { _context = context; } // Tạo theo yêu cầu (lazy initialization) public IOrderRepository Orders => _orderRepository ??= new OrderRepository(_context); public IProductRepository Products => _productRepository ??= new ProductRepository(_context); public async Task SaveChangesAsync( CancellationToken cancellationToken = default) { return await _context.SaveChangesAsync(cancellationToken); } public async Task BeginTransactionAsync( CancellationToken cancellationToken = default) { _transaction = await _context.Database .BeginTransactionAsync(cancellationToken); } public async Task CommitTransactionAsync( CancellationToken cancellationToken = default) { if (_transaction == null) throw new InvalidOperationException("Không có giao dịch đang hoạt động."); await _transaction.CommitAsync(cancellationToken); await _transaction.DisposeAsync(); _transaction = null; } public async Task RollbackTransactionAsync( CancellationToken cancellationToken = default) { if (_transaction == null) throw new InvalidOperationException("Không có giao dịch đang hoạt động."); await _transaction.RollbackAsync(cancellationToken); await _transaction.DisposeAsync(); _transaction = null; } public void Dispose() { _transaction?.Dispose(); _context.Dispose(); } } ``` ## Cấu hình Dependency Injection Việc đăng ký dịch vụ diễn ra ở mỗi lớp thông qua các extension method, sau đó được điều phối trong Program.cs. ```csharp // Infrastructure/DependencyInjection.cs using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Infrastructure.Persistence; using CleanArchitecture.Infrastructure.Repositories; namespace CleanArchitecture.Infrastructure; public static class DependencyInjection { public static IServiceCollection AddInfrastructure( this IServiceCollection services, IConfiguration configuration) { // Cấu hình Entity Framework services.AddDbContext(options => options.UseSqlServer( configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName))); // Đăng ký Unit of Work (Scoped) services.AddScoped(); // Repository riêng lẻ khi cần services.AddScoped(); services.AddScoped(); return services; } } ``` ```csharp // Application/DependencyInjection.cs using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; using CleanArchitecture.Application.Common.Behaviors; namespace CleanArchitecture.Application; public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { var assembly = typeof(DependencyInjection).Assembly; // Đăng ký MediatR cùng các handler services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly)); // Đăng ký các validator FluentValidation services.AddValidatorsFromAssembly(assembly); // Pipeline behavior (thứ tự thực thi quan trọng) services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); return services; } } ``` ```csharp // Api/Program.cs using CleanArchitecture.Application; using CleanArchitecture.Infrastructure; var builder = WebApplication.CreateBuilder(args); // Đăng ký các lớp builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); // Cấu hình API builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); ``` ## Lớp Presentation: controller API Controller là các adapter đơn giản giao việc cho MediatR. ```csharp // Api/Controllers/OrdersController.cs using MediatR; using Microsoft.AspNetCore.Mvc; using CleanArchitecture.Application.Orders.Commands; using CleanArchitecture.Application.Orders.Queries; namespace CleanArchitecture.Api.Controllers; [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IMediator _mediator; public OrdersController(IMediator mediator) { _mediator = mediator; } [HttpPost] [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Create( [FromBody] CreateOrderCommand command, CancellationToken cancellationToken) { // Giao toàn bộ cho MediatR var orderId = await _mediator.Send(command, cancellationToken); return CreatedAtAction( nameof(GetById), new { id = orderId }, orderId); } [HttpGet("{id:guid}")] [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetById( Guid id, CancellationToken cancellationToken) { var order = await _mediator.Send( new GetOrderByIdQuery(id), cancellationToken); if (order == null) return NotFound(); return Ok(order); } [HttpPost("{id:guid}/confirm")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Confirm( Guid id, CancellationToken cancellationToken) { await _mediator.Send(new ConfirmOrderCommand(id), cancellationToken); return NoContent(); } [HttpPost("{id:guid}/ship")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Ship( Guid id, CancellationToken cancellationToken) { await _mediator.Send(new ShipOrderCommand(id), cancellationToken); return NoContent(); } } ``` Controller luôn mỏng và không chứa logic nghiệp vụ, chỉ thực hiện ánh xạ HTTP. ## Unit test cho lớp Application Clean Architecture giúp việc viết unit test dễ dàng nhờ cô lập phụ thuộc. ```csharp // Tests/Application/CreateOrderCommandHandlerTests.cs using Moq; using Xunit; using CleanArchitecture.Application.Orders.Commands; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Tests.Application; public class CreateOrderCommandHandlerTests { private readonly Mock _unitOfWorkMock; private readonly CreateOrderCommandHandler _handler; public CreateOrderCommandHandlerTests() { _unitOfWorkMock = new Mock(); _handler = new CreateOrderCommandHandler(_unitOfWorkMock.Object); } [Fact] public async Task Handle_ValidCommand_CreatesOrderAndReturnsId() { // Arrange var product = new Product { Id = Guid.NewGuid(), Name = "Test", Price = 100 }; _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync( It.IsAny(), It.IsAny())) .ReturnsAsync(product); _unitOfWorkMock.Setup(x => x.Orders.AddAsync( It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); _unitOfWorkMock.Setup(x => x.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); var command = new CreateOrderCommand( CustomerEmail: "test@example.com", Items: new List { new(ProductId: product.Id, Quantity: 2) }); // Act var result = await _handler.Handle(command, CancellationToken.None); // Assert Assert.NotEqual(Guid.Empty, result); _unitOfWorkMock.Verify(x => x.Orders.AddAsync( It.Is(o => o.CustomerEmail == "test@example.com"), It.IsAny()), Times.Once); _unitOfWorkMock.Verify(x => x.SaveChangesAsync( It.IsAny()), Times.Once); } [Fact] public async Task Handle_ProductNotFound_ThrowsNotFoundException() { // Arrange _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync( It.IsAny(), It.IsAny())) .ReturnsAsync((Product?)null); var command = new CreateOrderCommand( CustomerEmail: "test@example.com", Items: new List { new(ProductId: Guid.NewGuid(), Quantity: 1) }); // Act & Assert await Assert.ThrowsAsync( () => _handler.Handle(command, CancellationToken.None)); } } ``` Mock từ Moq cho phép cô lập handler khỏi các phụ thuộc, chỉ kiểm thử logic điều phối. ## Kết luận Clean Architecture với .NET cung cấp một cấu trúc vững chắc cho ứng dụng doanh nghiệp. Việc phân tách trách nhiệm chặt chẽ giữa các lớp Domain, Application, Infrastructure và Presentation đảm bảo mã nguồn dễ bảo trì và kiểm thử trong dài hạn. ### Checklist Clean Architecture .NET - ✅ Domain được cô lập, không phụ thuộc bên ngoài - ✅ Entity đóng gói logic nghiệp vụ - ✅ Interface repository đặt trong Domain - ✅ Use Case thông qua Commands/Queries (CQRS) - ✅ Kiểm tra dữ liệu với FluentValidation và Pipeline Behaviors - ✅ Infrastructure triển khai các interface của Domain - ✅ Controller mỏng giao việc cho MediatR - ✅ Unit test được cô lập với mock Khoản đầu tư ban đầu cho kiến trúc này nhanh chóng đem lại giá trị trong các dự án cỡ trung và lớn, nơi những thay đổi yêu cầu kỹ thuật (di chuyển cơ sở dữ liệu, đổi framework) chỉ ảnh hưởng tới các lớp ngoài và giữ nguyên lõi nghiệp vụ. --- 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-practical-guide