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ì.

Hướng dẫn Clean Architecture với .NET và C#

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.

text
┌─────────────────────────────────────────────────────────┐
│                     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.

Domain/Entities/Order.cscsharp
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<OrderItem> _items = new();
    public IReadOnlyCollection<OrderItem> 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.

Domain/Entities/OrderItem.cscsharp
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;
    }
}
Domain/Enums/OrderStatus.cscsharp
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.

Domain/Interfaces/IOrderRepository.cscsharp
namespace CleanArchitecture.Domain.Interfaces;

// Interface của repository Order
public interface IOrderRepository
{
    // Lấy theo định danh
    Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);

    // Lấy kèm các mục
    Task<Order?> GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default);

    // Lấy theo email khách hàng
    Task<IEnumerable<Order>> 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);
}
Domain/Interfaces/IUnitOfWork.cscsharp
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<int> 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.

Sẵn sàng chinh phục phỏng vấn .NET?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

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.

Application/Orders/Commands/CreateOrderCommand.cscsharp
using MediatR;

namespace CleanArchitecture.Application.Orders.Commands;

// Command thể hiện ý định tạo đơn hàng
public record CreateOrderCommand(
    string CustomerEmail,
    List<OrderItemDto> Items
) : IRequest<Guid>;

// DTO cho các mục đơn hàng
public record OrderItemDto(
    Guid ProductId,
    int Quantity
);
Application/Orders/Commands/CreateOrderCommandHandler.cscsharp
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<CreateOrderCommand, Guid>
{
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderCommandHandler(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<Guid> 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ý.

Application/Orders/Queries/GetOrderByIdQuery.cscsharp
using MediatR;

namespace CleanArchitecture.Application.Orders.Queries;

// Query để lấy đơn hàng theo ID
public record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;

// 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<OrderItemResponseDto> 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
);
Application/Orders/Queries/GetOrderByIdQueryHandler.cscsharp
using MediatR;
using CleanArchitecture.Domain.Interfaces;

namespace CleanArchitecture.Application.Orders.Queries;

public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
    private readonly IOrderRepository _orderRepository;

    public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task<OrderDto?> 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.

Application/Orders/Validators/CreateOrderCommandValidator.cscsharp
using FluentValidation;
using CleanArchitecture.Application.Orders.Commands;

namespace CleanArchitecture.Application.Orders.Validators;

public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    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.");
        });
    }
}
Application/Common/Behaviors/ValidationBehavior.cscsharp
using FluentValidation;
using MediatR;

namespace CleanArchitecture.Application.Common.Behaviors;

// Pipeline behavior cho việc kiểm tra tự động
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> 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<TRequest>(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.

Infrastructure/Persistence/AppDbContext.cscsharp
using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.Entities;

namespace CleanArchitecture.Infrastructure.Persistence;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();
    public DbSet<Product> Products => Set<Product>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Áp dụng cấu hình từ assembly
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}
Infrastructure/Persistence/Configurations/OrderConfiguration.cscsharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using CleanArchitecture.Domain.Entities;

namespace CleanArchitecture.Infrastructure.Persistence.Configurations;

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> 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<string>();  // 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);
    }
}
Infrastructure/Repositories/OrderRepository.cscsharp
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<Order?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task<Order?> 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<IEnumerable<Order>> 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

Infrastructure/Persistence/UnitOfWork.cscsharp
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<int> 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.

Infrastructure/DependencyInjection.cscsharp
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<AppDbContext>(options =>
            options.UseSqlServer(
                configuration.GetConnectionString("DefaultConnection"),
                b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));

        // Đăng ký Unit of Work (Scoped)
        services.AddScoped<IUnitOfWork, UnitOfWork>();

        // Repository riêng lẻ khi cần
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IProductRepository, ProductRepository>();

        return services;
    }
}
Application/DependencyInjection.cscsharp
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;
    }
}
Api/Program.cscsharp
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.

Api/Controllers/OrdersController.cscsharp
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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.

Tests/Application/CreateOrderCommandHandlerTests.cscsharp
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<IUnitOfWork> _unitOfWorkMock;
    private readonly CreateOrderCommandHandler _handler;

    public CreateOrderCommandHandlerTests()
    {
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _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<Guid>(),
            It.IsAny<CancellationToken>()))
            .ReturnsAsync(product);

        _unitOfWorkMock.Setup(x => x.Orders.AddAsync(
            It.IsAny<Order>(),
            It.IsAny<CancellationToken>()))
            .Returns(Task.CompletedTask);

        _unitOfWorkMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(1);

        var command = new CreateOrderCommand(
            CustomerEmail: "test@example.com",
            Items: new List<OrderItemDto>
            {
                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<Order>(o => o.CustomerEmail == "test@example.com"),
            It.IsAny<CancellationToken>()), Times.Once);
        _unitOfWorkMock.Verify(x => x.SaveChangesAsync(
            It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Handle_ProductNotFound_ThrowsNotFoundException()
    {
        // Arrange
        _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync(
            It.IsAny<Guid>(),
            It.IsAny<CancellationToken>()))
            .ReturnsAsync((Product?)null);

        var command = new CreateOrderCommand(
            CustomerEmail: "test@example.com",
            Items: new List<OrderItemDto>
            {
                new(ProductId: Guid.NewGuid(), Quantity: 1)
            });

        // Act & Assert
        await Assert.ThrowsAsync<NotFoundException>(
            () => _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

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

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ụ.

Thẻ

#dotnet
#clean architecture
#csharp
#aspnet core
#design patterns

Chia sẻ

Bài viết liên quan