$mermaidjs
Clean Architecture Demo
Loading...
Searching...
No Matches
Introducción

A continuación describimos la arquitectura de esta demo de Clean Architecture utilizando modelos C4.

Recuerda que en Clean Architecture el código se organiza en círculos concéntricos, donde cada círculo representa diferentes áreas del software. Los elementos de código de un círculo no pueden tener referencias a elementos en un círculo exterior, o dicho de otra forma, las dependencias pueden ir sólo de afuera hacia adentro.

Clean
Architecture

Diagrama C4 de sistema de software

El primer diagrama C4 es el del sistema de software. Muestra los sistemas adyacentes que en este caso son el Usuario que gestiona las tareas y un Email Service que envía correos electrónicos para notificar cambios en las tareas.

Note
En esta demo no se utiliza un servidor de correo externo pero está diseñada de forma tal que podría agregarse uno.

Task Management System
Software

Diagrama C4 de contenedores

El segundo diagrama C4 es el de contenedores. Muestra la aplicación ASP .NET Core que expone una API REST TaskManagement.API y las librerías TaskManagement.Domain, TaskManagement.Application y TaskManagement.Infrastructure que implementan las capas del dominio, aplicación e infraestructura, respectivamente, de acuerdo a los círculos de Clean Architecture mostrados más arriba. También se muestra la base de datos utilizada por la capa de infraestructura para almacenamiento de datos.

Traduciendo la terminología C4 a .NET, los contenedores son los ensamblados que contienen el código de las librería -cada ensamblado se genera a partir de un proyecto .csproj con el mismo nombre; las propiedades de la librería se especifican en ese proyecto-.

Task Management
Containers

Puedes ver claramente en el diagrama que las dependencias van "de arriba hacia abajo", lo que equivale a "afuera hacia adentro" cuando las capas se muestran en círculos.

Diagrama C4 de componentes

El siguiente diagrama C4 es el de componentes. Muestra las funcionalidades de la aplicación encapsuladas detrás de interfaces -tipos- bien definidos.

Traduciendo la terminología de C4 a .NET, los componentes son las clases -las clases definen tipos-.

Task Management
Components

Puedes ver claramente como las clases en los contenedores mantienen las referencias del diagrama de contenedores, es decir, "de arriba hacia abajo".

Diagrama C4 de código

El último diagrama C4 es el de código. Muestra cómo los componentes son implementados en código.

En este caso usamos un diagrama de clases -parcial- mostrando algunas de las clases involucradas en la creación de una tarea.

Task Management
Components

Implementación de la arquitectura

La arquitectura en este demo está organizada en proyectos de C# independientes, donde cada proyecto corresponde a un círculo:

Note
En la documentación de Microsoft sobre Clean Architecture el círculo central es Application Core e incluye lo que en la documentación original de Clean Architecture son los círculos Entities y Use Cases. A pesar de ser una demo en .NET, usamos la terminología original.
  1. TaskManagement.Domain es una librería de C# en la que se define el dominio de la aplicación: entidades y objetos valor, eventos y excepciones. El domino utiliza eventos para informar cuando se crea, se completa, o se asigna una tarea, o cuando cambia su prioridad -ver por ejemplo la propiedad TaskItem.DomainEvents y el método TaskItem.Create en TaskItem-; por esto, esta aplicación también utiliza una arquitectura dirigida por eventos. La capa del dominio tiene la responsabilidad de generar eventos, pero es la capa de aplicación la que tienen la responsabilidad de procesarlos -ver por ejemplo CreateTaskCommand.Handle en CreateTaskCommand-. El proyecto TaskManagement.Domain no referencia ningún otro proyecto, es el centro de los círculos concéntricos.
  2. TaskManagement.Application es otra librería de C# en la que se definen las funcionalidades de la aplicación -comandos y consultas-, o dicho de otra forma, donde se implementa la lógica de los casos de uso. Esta aplicación usa el patrón CQRS donde los comandos están separados de las consultas -ver por ejemplo la clase CreateTaskCommand y la clase GetTaskByIdQuery-. La capa de aplicación define -y utiliza- abstracciones que emplean el patrón de inyección de dependencias -ver por ejemplo las interfaces IEmailService, ITaskRepository, ITaskReadRepository o IUnitOfWork-[^3]. Las clases concretas que implementan estas abstracciones están definidas en la capa de infraestructura -ver por ejemplo las clases TaskRepository y TaskReadRepository, respectivamente- y también son creadas en tiempo de ejecución en la capa de interfaz API -ver las llamadas a builder.services… .AddScoped<IUnitOfWork>(…), .AddScoped<ITaskReadRepository, TaskReadRepository>(), y .AddScoped<IDomainEventDispatcher, MediatRDomainEventDispatcher>() en Program-. Los eventos creados en la capa del dominio son procesados en la capa de aplicación usando una abstracción -ver la interfaz IDomainEventDispatcher-; y esa abstracción también está implementada en una clase definida en la capa de infraestructura -ver la clase MediatRDomainEventDispatcher-. El proyecto TaskManagement.Application referencia solamente el proyecto TaskManagement.Domain, la dependencia es de un círculo externo al centro de los círculos concéntricos.
  3. TaskManagement.Infrastructure es otra librería de C# en la que se definen cómo se implementa la infraestructura para las abstracciones definidas en la capa de aplicación de -repositorios, despacho de eventos y persistencia-. Las abstracciones ITaskRepository y ITaskReadRepository de la capa de aplicación se implementan con las clases TaskRepository y TaskReadRepository, respectivamente, de esta capa de infraestructura. También en este caso se usa injección de dependencias en la capa de interfaz API -ver por ejemplo en Program las llamadas a builder.services...AddScoped<ITaskRepository, TaskRepository>() y builder.services...AddScoped<ITaskReadRepository, TaskReadRepository>()-. Como ya fue mencionado antes, la abstracción IDomainEventDispatcher definida en la capa de aplicación se implementa con la clase MediatRDomainEventDispatcher de esta capa de infraestructura y la instancia se crea en tiempo de ejecución también con injección de dependencias en la capa de interfaz API en Program -ver builder.services...AddScoped<IDomainEventDispatcher, MediatRDomainEventDispatcher>()-. El proyecto TaskManagement.Infrastructure referencia solamente el proyecto TaskManagement.Application, la dependencia es de un círculo externo a un círculo interno. Esta capa de infraestructura tiene también la configuración de los frameworks de acceso a datos.
  4. TaskManagement.API es una aplicación web en .NET en la que se define la interfaz, en esta demo, una API REST. El proyecto referencia tanto al proyecto TaskManagement.Application como al proyecto TaskManagement.Infrastructure, ambos en círculos internos. Como toda aplicación web en .NET la carpeta contiene los controladores que implementan los endpoint de la API REST -ver por ejemplo TasksController-. El archivo TaskManagement.http tiene ejemplos para invocar la API REST.

[^3]: Algunos autores definen las abstracciones relacionadas con el dominio en la capa del dominio, aunque no sean utilizadas en esa capa. Siguiendo esos autores, interfaces como ITaskRepository se definirían en la capa de dominio. En esta demo, interfaces como esa son definidas en la capa de aplicación, porque es allí donde se usan.

Frameworks utilizados

La aplicación utiliza los siguientes frameworks.

FluentValidation

FluentValidation es una librería de .NET para constuir reglas de validación fuertemente tipadas.

MediatR

MediatR es una librería de .NET para mensajería en proceso -sin persistencia- sin dependencias. Admite solicitudes y respuestas, comandos, consultas, notificaciones y eventos, tanto síncronos como asíncronos, con despacho inteligente mediante tipos genéricos en C#.

Drapper

Dapper es una librería de mapeo objeto-relacional -ORM- de código abierto para aplicaciones .NET. Permite acceder de forma rápida y sencilla a los datos de las bases de datos sin necesidad de escribir código complejo.

Resumen de clases e interfaces definidos en cada capa

1. Capa del dominio

La capa del dominio está definida aquí.

Componente Archivos Propósito
Entidades Entities/TaskItem.cs, Entities/TaskStatus.cs, Entities/TaskPriority.cs Clases de modelo de negocio representando conceptos del dominio
Objetos valor ValueObjects/Email.cs, Shared/ValueObject.cs Objetos inmutables representando valores del dominio
Eventos del dominio Events/DomainEvent.cs Eventos representando ocurrencias importantes del dominio
Interfaces Ver nota [^3] Ver nota [^3]
Excepciones Exceptions/DomainException.cs Excepciones específicas del dominio
Tipos compartidos Shared/Result.cs, Shared/TaskErrors.cs Tipos comunes compartidos internamente en el dominio

Principio clave: La capa del dominio nunca depende de las capas de aplicación o infraestructura y contiene solo datos y reglas del negocio.

La entidad TaskItem es un aggregate root que define y gestiona sus propios datos y reglas del negocio. Está definida así:

public class TaskItem
{
// Método factory para creación segura de entidad
public static Result<TaskItem> Create(string title, string description, ...)
{
// La validación sucede en el dominio
var validation = Validate(title, description, dueDate);
if (validation.IsFailure)
return Result.Failure<TaskItem>(validation.Errors);
// La creación de la entidad genera un evento del dominio
var task = new TaskItem { ... };
task.AddDomainEvent(new TaskCreatedEvent(...));
return Result.Success(task);
}
// Métodos de negocio que refuerzan invariantes
public Result Complete()
{
if (Status == TaskStatus.Completed)
return Result.Failure(TaskErrors.AlreadyCompleted);
Status = TaskStatus.Completed;
AddDomainEvent(new TaskCompletedEvent(...));
return Result.Success();
}
}
TaskStatus
TaskStatus es un objeto valor del dominio que representa posibles estados de una tarea.
Definition TaskStatus.cs:25

La capa del dominio utiliza el patrón Result para el resultado de las operaciones. La clase Result representa tanto resultados exitosos -en cuyo caso incluye también el valor del resultado- como errores -en cuyo caso incluye la lista de errores-. Esto permite que un método pueda retornar tanto un resultado como un error.

public class Result
{
public bool IsSuccess { get; }
public IReadOnlyList<string> Errors { get; }
}
// Uso
var result = TaskItem.Create(…);
if (result.IsSuccess)
// Usar result.Value
else
// Manejar result.Errors

2. Capa de aplicación

La capa de aplicación está definida aquí.

Componente Archivos Propósito
Comandos Commands/CreateTaskCommand.cs, Commands/CompleteTaskCommand.cs Objetos para casos de uso que cambian estado (CQRS)
Consultas Queries/GetTaskByIdQuery.cs Objetos para casos de uso de lecturas de datos (CQRS)
DTO Queries/GetTaskByIdQuery.cs (TaskDto), Requests/CreateTaskRequest.cs Objetos de transferencia de datos para entrada/salida
Interfaces Interfaces/IUnitOfWork.cs, Interfaces/ITaskRepository.cs, Interfaces/ITaskReadRepository.cs, Interfaces/IEmailService.cs, Interfaces/IDomainEventDispatcher.cs Abstracciones para servicios de infraestructura y despacho de eventos
Comportamientos Behaviors/ValidationBehavior.cs Comportamientos de pipeline MediatR
Excepciones Exceptions/ValidationException.cs, Exceptions/NotFoundException.cs Excepciones específicas de la capa de aplicación
Compartido Shared/PagedResult.cs DTOs y tipos comunes

Principio clave: La capa de aplicación implementa casos de uso de negocio pero delega la lógica de negocio a la capa del dominio. Básicamente es un orquestador de objetos del dominio, sin lógica del negocio.

El siguiente diagrama muestra una versión simplificada de un comando de la capa de aplicación y su interacción con la capa del dominio.

Diagrama de
secuencia

La clase CreateTaskCommandHandler implementa un procesador de MediatR para el comando CreateTaskCommand.

public sealed class CreateTaskCommandHandler : IRequestHandler<CreateTaskCommand, Result<Guid>>
{
private readonly ITaskRepository _taskRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IDomainEventDispatcher _eventDispatcher;
public async Task<Result<Guid>> Handle(
CreateTaskCommand request,
CancellationToken cancellationToken)
{
// Delega a la capa del dominio la lógica de negocio
var createResult = TaskItem.Create(
request.Title,
request.Description,
request.Priority,
request.DueDate,
request.CreatedBy);
// Persiste la entidad del dominio
await _taskRepository.AddAsync(task, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// Despacha los eventos del dominio
await _eventDispatcher.DispatchAsync(task.DomainEvents, cancellationToken);
}
}

En ese método, las variables _taskRepository de tipo ITaskRepository -definido en la capa del dominio-, _unitOfWork de tipo IUnitOfWork e _eventDispatcher de tipo IDomainEventDispatcher -definidos en la propia capa de aplicación- son asignadas mediante injección de dependencias con clases implementadas en la capa de infraestructura.

3. Capa de infraestructura

La capa de infraestructura está definida aquí.

Componente Archivos Propósito
DbContext Persistence/TaskDbContext.cs Contexto de base de datos de Entity Framework
Repositorios Persistence/Repositories/TaskRepository.cs, TaskReadRepository.cs Implementaciones de acceso a datos con Entity Framework y Dapper respectivamente
Configuraciones Persistence/Configuration/TaskConfiguration.cs Configuraciones de Entity Framework
Despacho de eventos EventDispatching/MediatRDomainEventDispatcher.cs Implementación de publicación de eventos del dominio con MediatR

Principio clave: La capa de infraestructura implementa interfaces definidas en las capas del dominio y aplicación usando inyección de dependencias.

Esta capa implementa el patrón Repository. La interfaz ITaskRepository definida en la capa de aplicación es implementada por la clase TaskRepository definida en esta capa de infraestructura.

// Interfaz definida en capa de aplicación
public interface ITaskRepository
{
Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task AddAsync(TaskItem task, CancellationToken cancellationToken = default);
}
// Implementación en la capa de infraestructura
public sealed class TaskRepository : ITaskRepository
{
// Los detalles de infraestructura como el uso de Entity Framework quedan
// ocultos del dominio y de la aplicación
public async Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _dbContext.Tasks
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
}
}

4. Capa de interfaz

En esta demo la interfaz es una API web. La capa de interfaz está definida aquí.

Componente Archivos Propósito
Controladores Controllers/TasksController.cs Endpoints HTTP
Middleware Middleware/ExceptionHandlingMiddleware.cs Comportamientos de pipeline transversales
Extensiones Extensions/ClaimsPrincipalExtensions.cs Métodos de extensión auxiliares
Solicitudes Requests/CreateTaskRequest.cs DTOs de entrada para solicitudes HTTP
Inicio Program.cs Raíz de composición de inyección de dependencias

Principio clave: La capa de UI traduce HTTP a comandos o consultas de aplicación. No tiene lógica de negocio y delega todo a la capa de aplicación.

A continuación un fragmento de la clase TaskController con la implementación del POST de HTTP para crear un cliente -hay ejemplos para probar todos los endpoints aquí-.

[ApiController]
[Route("api/[controller]")]
public sealed class TasksController : ControllerBase
{
private readonly IMediator _mediator;
[HttpPost]
[Authorize]
public async Task<ActionResult<Guid>> Create(
[FromBody] CreateTaskRequest request,
CancellationToken cancellationToken)
{
// Traduce la solicitud HTTP a un comando de la aplicación
var command = new CreateTaskCommand(
request.Title,
request.Description,
request.Priority,
request.DueDate,
userId);
// Envía el comando a la capa de aplicación
var result = await _mediator.Send(command, cancellationToken);
// Traduce resultado a una respuesta HTTP
if (result.IsSuccess)
return CreatedAtAction(nameof(GetById), new { id = result.Value }, result.Value);
return BadRequest(CreateProblemDetails(result.Errors));
}
}

En el programa principal en Program se realiza la inyección de dependencias.

// Registra servicios para las abstracciones de la capa del dominio
builder.Services
.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<TaskDbContext>())
.AddScoped<ITaskRepository, TaskRepository>()
.AddScoped<IDomainEventDispatcher, MediatRDomainEventDispatcher>();
// Registra servicios para las abstracciones de la capa de aplicación
builder.Services
.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(...))
.AddValidatorsFromAssembly(...)
.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
// Registra servicios para las abstracciones de la capa de infraestructura
builder.Services
.AddDbContext<TaskDbContext>(options =>
options.UseSqlite(connectionString));
builder.Services. typeof(ValidationBehavior<,>)) .AddDbContext< TaskDbContext >(options
const string connectionString
Definition Program.cs:61

Cómo navegar el código

  1. Comienza con la capa del dominio TaskManagement.Domain
    • Lee TaskItem para entender el modelo del dominio
    • Lee Result para entender el manejo de errores
    • Lee DomainEvent para entender la arquitectura dirigida por eventos
  2. Muévete a la capa de aplicación TaskManagement.Application
  3. Explora la capa de infraestructura TaskManagement.Infrastructure
  4. Revisa la capa de interfaz API TaskManagement.API

Beneficios de Clean Architecture

Esta demo permite entender los siguientes beneficios de Clean Architecture:

  • Facilidad de testeo: La lógica del dominio sin dependencias externas facilita escribir casos de prueba.
  • Facilidad de mantenimiento: Separación clara de responsabilidades y aspectos.
  • Flexibilidad: Fácil de intercambiar implementaciones -base de datos, servicio de email, etc.-
  • Escalabilidad: Cada capa se puede optimizar de forma independiente.
  • Reutilización: La lógica del dominio se puede reutilizar en diferentes UIs -web, consola, API, etc.-
  • Independencia: La lógica de negocio está aislada de los frameworks.