Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kiến trúc Ứng dụng

Clean Architecture

Layer Structure

┌─────────────────────────────────────────────────────────────────┐
│                      PRESENTATION LAYER                         │
│   (Controllers, Views, DTOs, ViewModels)                       │
├─────────────────────────────────────────────────────────────────┤
│                      APPLICATION LAYER                          │
│   (Use Cases, Commands, Queries, Services, DTOs)                │
├─────────────────────────────────────────────────────────────────┤
│                        DOMAIN LAYER                             │
│   (Entities, Value Objects, Domain Services, Interfaces)       │
├─────────────────────────────────────────────────────────────────┤
│                     INFRASTRUCTURE LAYER                        │
│   (DbContext, Repositories, External Services, Logging)         │
└─────────────────────────────────────────────────────────────────┘

Project Structure

src/
├── Domain/
│   ├── Entities/
│   │   └── Product.cs
│   ├── ValueObjects/
│   │   └── Money.cs
│   ├── Interfaces/
│   │   ├── IProductRepository.cs
│   │   └── IEmailService.cs
│   └── Services/
│       └── DomainProductService.cs
│
├── Application/
│   ├── Commands/
│   │   ├── CreateProductCommand.cs
│   │   └── UpdateProductCommand.cs
│   ├── Queries/
│   │   └── GetProductsQuery.cs
│   ├── DTOs/
│   │   └── ProductDto.cs
│   └── Services/
│       └── ApplicationProductService.cs
│
├── Infrastructure/
│   ├── Data/
│   │   ├── AppDbContext.cs
│   │   └── Repositories/
│   │       └── ProductRepository.cs
│   └── Services/
│       └── EmailService.cs
│
└── Presentation/
    └── Controllers/
        └── ProductsController.cs

DDD (Domain-Driven Design)

Domain Entities

public class Order : Entity
{
    public Guid Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public Money TotalAmount { get; private set; }
    
    private Order() { } // For EF Core
    
    public static Order Create(CustomerId customerId)
    {
        return new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = customerId,
            Status = OrderStatus.Draft,
            Items = new List<OrderItem>(),
            TotalAmount = Money.Zero
        };
    }
    
    public void AddItem(Product product, int quantity)
    {
        // Domain logic
        if (Status != OrderStatus.Draft)
            throw new DomainException("Cannot add items to confirmed order");
            
        Items.Add(OrderItem.Create(product, quantity));
        RecalculateTotal();
    }
}

Value Objects

public record Money(decimal Amount, string Currency)
{
    public static Money Zero => new(0, "USD");
    
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new DomainException("Cannot add different currencies");
        return new Money(a.Amount + b.Amount, a.Currency);
    }
}

public record Address(
    string Street,
    string City,
    string State,
    string ZipCode,
    string Country)
{
    public string FullAddress => $"{Street}, {City}, {State} {ZipCode}, {Country}";
}

Aggregates

public class OrderAggregate
{
    private readonly List<OrderItem> _items = new();
    
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    
    public void AddItem(Product product, int quantity)
    {
        // Aggregate boundary - maintain invariants
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
            
        var existing = _items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existing != null)
            existing.IncreaseQuantity(quantity);
        else
            _items.Add(OrderItem.Create(product, quantity));
    }
}

CQRS (Command Query Responsibility Segregation)

Command Handler

public class CreateProductCommand : IRequest<ProductDto>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

public class CreateProductCommandHandler 
    : IRequestHandler<CreateProductCommand, ProductDto>
{
    private readonly AppDbContext _context;
    
    public CreateProductCommandHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<ProductDto> Handle(
        CreateProductCommand request, 
        CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            CategoryId = request.CategoryId
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync(cancellationToken);
        
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price
        };
    }
}

Query Handler

public class GetProductsQuery : IRequest<List<ProductDto>>
{
    public int? CategoryId { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}

public class GetProductsQueryHandler 
    : IRequestHandler<GetProductsQuery, List<ProductDto>>
{
    private readonly AppDbContext _context;
    
    public GetProductsQueryHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<List<ProductDto>> Handle(
        GetProductsQuery request, 
        CancellationToken cancellationToken)
    {
        var query = _context.Products
            .AsNoTracking();
            
        if (request.CategoryId.HasValue)
            query = query.Where(p => p.CategoryId == request.CategoryId);
            
        return await query
            .Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price
            })
            .Skip((request.Page - 1) * request.PageSize)
            .Take(request.PageSize)
            .ToListAsync(cancellationToken);
    }
}

MediatR Integration

// Program.cs
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));

// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public ProductsController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
    {
        var result = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
    
    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] GetProductsQuery query)
    {
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}

Separate Read/Write Models

// Write Model (Command)
public class CreateOrderCommand
{
    public List<CreateOrderItemCommand> Items { get; set; }
    public Guid CustomerId { get; set; }
}

// Read Model (Query)
public class OrderSummaryDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public int ItemCount { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; }
}