JB
_
·4 min de leitura

Clean Architecture no .NET: Separando Responsabilidades com Elegância

Aprenda a estruturar projetos .NET com Clean Architecture, entendendo camadas, dependências e como aplicar SOLID na prática para criar código manutenível e testável.

.NETC#ArquiteturaClean Architecture

O que é Clean Architecture?

Proposta por Robert C. Martin (Uncle Bob), Clean Architecture organiza o código em camadas concêntricas onde as dependências sempre apontam para o centro — o domínio. O resultado é um sistema onde regras de negócio são completamente independentes de frameworks, bancos de dados e interfaces externas.

┌─────────────────────────────────────┐
│           Presentation              │  ← Controllers, APIs, UI
│  ┌─────────────────────────────┐    │
│  │       Application           │    │  ← Use Cases, DTOs
│  │  ┌───────────────────────┐  │    │
│  │  │       Domain          │  │    │  ← Entities, Value Objects
│  │  │  (sem dependências)   │  │    │
│  │  └───────────────────────┘  │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘
        Infrastructure (implementações)

Estrutura de Pastas

src/
├── Domain/
│   ├── Entities/
│   │   └── User.cs
│   ├── ValueObjects/
│   │   └── Email.cs
│   ├── Interfaces/
│   │   └── IUserRepository.cs
│   └── Exceptions/
│       └── DomainException.cs
│
├── Application/
│   ├── Users/
│   │   ├── Commands/
│   │   │   └── CreateUser/
│   │   │       ├── CreateUserCommand.cs
│   │   │       └── CreateUserCommandHandler.cs
│   │   └── Queries/
│   │       └── GetUser/
│   │           ├── GetUserQuery.cs
│   │           └── GetUserQueryHandler.cs
│   └── Common/
│       └── Interfaces/
│           └── IApplicationDbContext.cs
│
├── Infrastructure/
│   ├── Persistence/
│   │   ├── ApplicationDbContext.cs
│   │   └── Repositories/
│   │       └── UserRepository.cs
│   └── Services/
│       └── EmailService.cs
│
└── Presentation/
    └── Controllers/
        └── UsersController.cs

A Camada de Domínio

O coração do sistema. Zero dependências externas.

// Domain/ValueObjects/Email.cs
public record Email
{
    public string Value { get; }
 
    private Email(string value) => Value = value;
 
    public static Email Create(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new DomainException("Email não pode ser vazio.");
 
        if (!value.Contains('@'))
            throw new DomainException("Email inválido.");
 
        return new Email(value.ToLowerInvariant());
    }
 
    public static implicit operator string(Email email) => email.Value;
}
 
// Domain/Entities/User.cs
public class User : BaseEntity
{
    public string Name { get; private set; }
    public Email Email { get; private set; }
    public DateTime CreatedAt { get; private set; }
 
    private User() { } // EF Core
 
    public static User Create(string name, string email)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new DomainException("Nome é obrigatório.");
 
        return new User
        {
            Id = Guid.NewGuid(),
            Name = name,
            Email = Email.Create(email),
            CreatedAt = DateTime.UtcNow
        };
    }
 
    public void UpdateName(string newName)
    {
        if (string.IsNullOrWhiteSpace(newName))
            throw new DomainException("Nome não pode ser vazio.");
 
        Name = newName;
    }
}

A Camada de Aplicação (Use Cases)

Orquestra o domínio. Depende apenas do Domain — nunca de Infrastructure.

// Application/Users/Commands/CreateUser/CreateUserCommand.cs
public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
 
// Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
    private readonly IUserRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
 
    public CreateUserCommandHandler(
        IUserRepository repository,
        IUnitOfWork unitOfWork)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
    }
 
    public async Task<Guid> Handle(
        CreateUserCommand request,
        CancellationToken cancellationToken)
    {
        var existingUser = await _repository.GetByEmailAsync(request.Email);
        if (existingUser is not null)
            throw new ConflictException("Email já cadastrado.");
 
        var user = User.Create(request.Name, request.Email);
        await _repository.AddAsync(user);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
 
        return user.Id;
    }
}

A Interface do Repositório (Domain/Interfaces)

// Domain/Interfaces/IUserRepository.cs
public interface IUserRepository
{
    Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
    Task<IEnumerable<User>> GetAllAsync(CancellationToken ct = default);
    Task AddAsync(User user, CancellationToken ct = default);
    void Update(User user);
    void Delete(User user);
}

A Implementação em Infrastructure

// Infrastructure/Persistence/Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
    private readonly ApplicationDbContext _context;
 
    public UserRepository(ApplicationDbContext context)
        => _context = context;
 
    public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default)
        => await _context.Users.FindAsync([id], ct);
 
    public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
        => await _context.Users
            .FirstOrDefaultAsync(u => u.Email.Value == email.ToLower(), ct);
 
    public async Task AddAsync(User user, CancellationToken ct = default)
        => await _context.Users.AddAsync(user, ct);
 
    public void Update(User user)
        => _context.Users.Update(user);
 
    public void Delete(User user)
        => _context.Users.Remove(user);
 
    public async Task<IEnumerable<User>> GetAllAsync(CancellationToken ct = default)
        => await _context.Users.AsNoTracking().ToListAsync(ct);
}

O Controller (Presentation)

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly ISender _sender;
 
    public UsersController(ISender sender) => _sender = sender;
 
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateUserRequest request)
    {
        var command = new CreateUserCommand(request.Name, request.Email);
        var userId = await _sender.Send(command);
        return CreatedAtAction(nameof(GetById), new { id = userId }, new { id = userId });
    }
}

Registrando as Dependências

// Program.cs
builder.Services
    .AddDomain()
    .AddApplication()
    .AddInfrastructure(builder.Configuration);
 
// Infrastructure/DependencyInjection.cs
public static IServiceCollection AddInfrastructure(
    this IServiceCollection services,
    IConfiguration config)
{
    services.AddDbContext<ApplicationDbContext>(opts =>
        opts.UseSqlServer(config.GetConnectionString("Default")));
 
    services.AddScoped<IUserRepository, UserRepository>();
    services.AddScoped<IUnitOfWork, UnitOfWork>();
 
    return services;
}

Por que Vale a Pena?

  • Testabilidade: use cases testados com repositórios mock, sem banco real
  • Manutenibilidade: trocar EF Core por Dapper? Só muda Infrastructure
  • Clareza: ao abrir Application/Users/Commands, você entende o negócio imediatamente
  • Onboarding: novos devs entendem onde cada coisa deve ficar

Clean Architecture exige uma curva de aprendizado, mas em projetos que crescem, o investimento se paga rápido.