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

Onion Architecture

Overview

Onion Architecture là một architectural pattern tập trung vào việc tạo layered architecture với dependencies chỉ hướng vào trong. Nó tương tự Clean Architecture nhưng có cách tổ chức khác, nhấn mạnh vào việc tách biệt core domain khỏi infrastructure.

Layer Structure

┌─────────────────────────────────────────────┐
│                Presentation                │  ← Outer Layer
│          (Controllers, APIs, UI)            │
├─────────────────────────────────────────────┤
│              Application                   │
│         (Use Cases, Services)              │
├─────────────────────────────────────────────┤
│                 Domain                      │  ← Core (innermost)
│        (Entities, Value Objects)            │
├─────────────────────────────────────────────┤
│              Infrastructure                 │  ← Outer Layer
│    (Repositories, External Services)       │
└─────────────────────────────────────────────┘

Core Principles

1. Dependency Inward

  • Tất cả dependencies chỉ đi từ outer layers vào inner layers
  • Inner layers không biết gì về outer layers
  • Domain layer là hoàn toàn standalone

2. Coupling

  • Loose coupling giữa các layers
  • Sử dụng interfaces để abstract dependencies

3. Testability

  • Core domain có thể test không cần infrastructure
  • Application services dễ dàng mock dependencies

Layer Details

1. Domain Layer (Core)

// Domain/Entities/Customer.cs
public class Customer
{
    public Guid Id { get; private set; }
    public string Email { get; private set; }
    public string Name { get; private set; }
    
    private Customer() { } // For ORM
    
    public static Customer Create(string email, string name)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException("Email is required");
            
        return new Customer
        {
            Id = Guid.NewGuid(),
            Email = email,
            Name = name
        };
    }
    
    public void UpdateName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name is required");
        Name = name;
    }
}

// Domain/ValueObjects/Email.cs
public class Email
{
    public string Value { get; }
    
    public Email(string value)
    {
        if (!IsValid(value))
            throw new InvalidEmailException(value);
        Value = value;
    }
    
    private bool IsValid(string email) 
        => email.Contains("@") && email.Contains(".");
}

// Domain/Services/OrderDomainService.cs
public class OrderDomainService
{
    public void ApplyDiscount(Order order, Discount discount)
    {
        if (order.Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot apply discount to placed order");
            
        order.ApplyDiscount(discount);
    }
}

2. Application Layer

// Application/Interfaces/ICustomerRepository.cs
public interface ICustomerRepository
{
    Task<Customer> GetByIdAsync(Guid id);
    Task AddAsync(Customer customer);
    Task UpdateAsync(Customer customer);
}

// Application/Services/CustomerService.cs
public class CustomerService
{
    private readonly ICustomerRepository _customerRepository;
    
    public CustomerService(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
    
    public async Task<CustomerDto> CreateCustomerAsync(CreateCustomerCommand command)
    {
        var customer = Customer.Create(command.Email, command.Name);
        
        await _customerRepository.AddAsync(customer);
        
        return new CustomerDto
        {
            Id = customer.Id,
            Email = customer.Email,
            Name = customer.Name
        };
    }
}

// Application/DTOs/CustomerDto.cs
public class CustomerDto
{
    public Guid Id { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
}

3. Infrastructure Layer

// Infrastructure/Persistence/EfCustomerRepository.cs
public class EfCustomerRepository : ICustomerRepository
{
    private readonly DbContext _context;
    
    public EfCustomerRepository(DbContext context)
    {
        _context = context;
    }
    
    public async Task<Customer> GetByIdAsync(Guid id)
    {
        return await _context.Customers.FindAsync(id);
    }
    
    public async Task AddAsync(Customer customer)
    {
        await _context.Customers.AddAsync(customer);
        await _context.SaveChangesAsync();
    }
    
    public async Task UpdateAsync(Customer customer)
    {
        _context.Customers.Update(customer);
        await _context.SaveChangesAsync();
    }
}

// Infrastructure/Mapping/CustomerMapping.cs
public class CustomerMapping : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Email).IsRequired().HasMaxLength(256);
        builder.Property(c => c.Name).IsRequired().HasMaxLength(128);
    }
}

4. Presentation Layer

// Presentation/Controllers/CustomersController.cs
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
    private readonly CustomerService _customerService;
    
    public CustomersController(CustomerService customerService)
    {
        _customerService = customerService;
    }
    
    [HttpPost]
    public async Task<ActionResult<CustomerDto>> Create(
        [FromBody] CreateCustomerCommand command)
    {
        var result = await _customerService.CreateCustomerAsync(command);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<CustomerDto>> GetById(Guid id)
    {
        var customer = await _customerService.GetCustomerAsync(id);
        if (customer == null)
            return NotFound();
        return Ok(customer);
    }
}

Dependency Injection Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register Infrastructure
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// Register Application Services
builder.Services.AddScoped<ICustomerRepository, EfCustomerRepository>();
builder.Services.AddScoped<CustomerService>();

// Register Controllers
builder.Services.AddControllers();

var app = builder.Build();
app.Run();

Benefits

BenefitDescription
Strong Domain FocusBusiness logic is central
TestabilityEasy to test each layer
FlexibilityEasy to change infrastructure
MaintainabilityClear structure
Framework AgnosticDomain not tied to frameworks

Comparison with Other Patterns

AspectOnionCleanHexagonal
Layers4 layers4+ layersPorts/Adapters
FocusInward dependenciesLayer separationPort/Adapter
DomainEntities, ServicesDomain + Use CasesDomain only
InfrastructureExplicit layerExternalAdapters

When to Use

  • Medium to large applications
  • Domain-driven design projects
  • Teams familiar with layered architecture
  • Projects needing clear boundaries

Key Differences from Clean Architecture

  1. Simplified: Fewer explicit layers
  2. Domain-Centric: Stronger focus on domain
  3. Flexible: More freedom in implementation
  4. Integration: Can integrate with various patterns

References