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.
┌─────────────────────────────────────────────┐
│ Presentation │ ← Outer Layer
│ (Controllers, APIs, UI) │
├─────────────────────────────────────────────┤
│ Application │
│ (Use Cases, Services) │
├─────────────────────────────────────────────┤
│ Domain │ ← Core (innermost)
│ (Entities, Value Objects) │
├─────────────────────────────────────────────┤
│ Infrastructure │ ← Outer Layer
│ (Repositories, External Services) │
└─────────────────────────────────────────────┘
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
Loose coupling giữa các layers
Sử dụng interfaces để abstract dependencies
Core domain có thể test không cần infrastructure
Application services dễ dàng mock dependencies
// 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);
}
}
// 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; }
}
// 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);
}
}
// 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);
}
}
// 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();
Benefit Description
Strong Domain Focus Business logic is central
Testability Easy to test each layer
Flexibility Easy to change infrastructure
Maintainability Clear structure
Framework Agnostic Domain not tied to frameworks
Aspect Onion Clean Hexagonal
Layers 4 layers 4+ layers Ports/Adapters
Focus Inward dependencies Layer separation Port/Adapter
Domain Entities, Services Domain + Use Cases Domain only
Infrastructure Explicit layer External Adapters
Medium to large applications
Domain-driven design projects
Teams familiar with layered architecture
Projects needing clear boundaries
Simplified : Fewer explicit layers
Domain-Centric : Stronger focus on domain
Flexible : More freedom in implementation
Integration : Can integrate with various patterns