aspnet-core-clean-architecture-capas

Clean Architecture: cómo organizar un proyecto en capas

  • 5 min

La Clean Architecture es una forma de separar las reglas de negocio, los casos de uso y la infraestructura haciendo que las dependencias apunten hacia el núcleo de la aplicación.

Cuando empiezas un proyecto pequeño, ponerlo todo en el controlador o en Program.cs es tentador. Es rápido, es fácil y funciona. A medida que el proyecto crece, esa “facilidad” puede acabar en lógica de negocio mezclada con SQL, validaciones duplicadas y pruebas difíciles de escribir.

Una forma de evitarlo es usar una arquitectura por capas. Clean Architecture, popularizada por Robert C. Martin (Uncle Bob), es una de las propuestas más conocidas.

Vamos a ver cómo dividir la solución en cuatro proyectos (.csproj) para conseguir un código más desacoplado y fácil de probar.

Estructura de carpetas en Visual Studio

Tu explorador de soluciones debería verse así:

📂 MiSolucion.sln / ├── 📂 src / │ ├── 📜 MiApp.Domain # (Class Library) │ ├── 📜 MiApp.Application # (Class Library) │ ├── 📜 MiApp.Infrastructure # (Class Library) │ └── 📜 MiApp.Api # (ASP.NET Core Web API) └── 📂 tests / └── 📜 MiApp.UnitTests

Las dependencias

Antes de ver las capas, hay que entender la regla que gobierna todo: La Regla de la Dependencia.

Las dependencias solo pueden apuntar hacia adentro.

Las capas interiores (el núcleo) no deben saber nada de las capas exteriores.

  • Tu Base de Datos (Externa) depende de tu Dominio (Interno), y no al revés.
  • Tu Dominio no sabe si estás usando SQL Server, Mongo o un Excel.
  • Tu Dominio no sabe si estás en una API Web, una App de Consola o una Azure Function.

Las cuatro capas de una solución .NET

En una solución estándar de Visual Studio, crearemos 4 proyectos (Class Libraries) separados:

Domain

Es el centro del universo. Aquí vive la lógica pura de tu negocio.

  • ¿Qué contiene?: Entidades (User, Product), Value Objects, Enums, Excepciones personalizadas y Lógica de Dominio pura.
  • Dependencias: NINGUNA. No depende de ningún otro proyecto. No tiene referencias a Entity Framework, ni a HTTP, ni a nada. Es C# puro.
// Proyecto: MiApp.Domain
public class Producto
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public decimal Precio { get; set; }

    // Lógica pura: Un producto no puede tener precio negativo
    public void ActualizarPrecio(decimal nuevoPrecio)
    {
        if (nuevoPrecio < 0) throw new DomainException("Precio inválido");
        Precio = nuevoPrecio;
    }
}
Copied!

Application

Rodea al Dominio. Aquí definimos qué puede hacer nuestra aplicación (Casos de Uso), pero no cómo se guardan los datos.

  • ¿Qué contiene?: Interfaces de Repositorios (IProductoRepository), DTOs, Mappers, Validaciones (FluentValidation) y Servicios de Aplicación (CQRS o Managers).
  • Dependencias: Solo del proyecto Domain.

La Inversión de Dependencia: Aquí definimos las Interfaces (IRepository), pero NO las implementamos. La capa de Aplicación dice: “Necesito alguien que sepa guardar un producto”, pero no le importa quién ni cómo lo haga.

// Proyecto: MiApp.Application
public interface IProductoRepository
{
    void Add(Producto producto); // Usa la entidad del Dominio
}

public class CrearProductoUseCase
{
    private readonly IProductoRepository _repo; // Depende de la abstracción
    // ...
}
Copied!

Infrastructure

En este punto nos manchamos las manos. Implementamos las interfaces que definió la capa de Application.

  • ¿Qué contiene?: Entity Framework (DbContext), Migraciones, clientes de Email (SendGrid), clientes de Archivos (Azure Blob Storage).
  • Dependencias: Application (para ver las interfaces) y Domain (para ver las entidades).
  • Paquetes NuGet: En este punto instalas Microsoft.EntityFrameworkCore.SqlServer.
// Proyecto: MiApp.Infrastructure
public class SqlProductoRepository : IProductoRepository
{
    private readonly ApplicationDbContext _db;

    public void Add(Producto producto)
    {
        _db.Productos.Add(producto); // Implementación real con EF Core
    }
}
Copied!

API / Presentation

Es el punto de entrada de los usuarios. En nuestro caso, es el proyecto Web API.

  • ¿Qué contiene?: Controladores, Program.cs, Configuración (appsettings.json), Middlewares y Filtros.
  • Dependencias: Application (para llamar a los casos de uso) e Infrastructure (para inyectar las dependencias en el contenedor DI).

¿Cómo se conecta todo?

Veamos el viaje de una petición para “Crear Usuario”:

API: El UsersController recibe un JSON. Lo convierte a un DTO (CrearUsuarioDto).

API -> Application: El controlador llama a un servicio/caso de uso en la capa de Aplicación (UserService.Crear(dto)).

Application -> Domain: El servicio convierte el DTO en una Entidad de Dominio (new Usuario(...)) y ejecuta validaciones de negocio.

Application -> Infrastructure: El servicio llama a _repository.Add(usuario). Como es una interfaz, no sabe que es SQL.

Infrastructure: En tiempo de ejecución, gracias a la Inyección de Dependencias, se ejecuta el código de EF Core que guarda en la BBDD.

  1. Independencia de infraestructura: Si cambias SQL Server por otra tecnología, la mayor parte del cambio queda en Infrastructure, aunque el modelo y las consultas también pueden necesitar ajustes.
  2. Testabilidad: Puedes probar la lógica de negocio (Domain) y los casos de uso (Application) sin necesidad de base de datos, simplemente “mockeando” las interfaces.
  3. Mantenibilidad: Sabes exactamente dónde buscar cada cosa.
  1. Sobre-ingeniería: Para un CRUD simple de 3 tablas o una prueba de concepto, esto es matar moscas a cañonazos.
  2. Boilerplate: Tienes que crear muchos archivos y mapeos (DTO -> Entidad -> DTO) solo para mover datos.