Disene y desarrolle un sistema completo con Arquitectura Hexagonal pura (Ports & Adapters), DDD y CQRS en .NET 8. El dominio es el centro: el aggregate Sample gestiona todo el ciclo de vida con Value Objects que encapsulan invariantes, Domain Events que reaccionan a transiciones de estado, y puertos que abstraen la infraestructura. La API expone 10 endpoints REST via Minimal APIs + un adaptador gRPC para tecnicos. 16 tests unitarios cubren reglas de dominio y handlers de aplicacion, todos corren en menos de 100ms sin infraestructura externa.
Arquitectura Hexagonal pura (Ports & Adapters): el dominio no depende de nada externo — ni de Entity Framework, ni de ASP.NET, ni de Redis. Se puede cambiar PostgreSQL por MongoDB sin tocar logica de negocio
6 proyectos en la solucion: LabResults.Domain (aggregate, Value Objects, puertos), LabResults.Application (commands, queries, handlers MediatR), LabResults.Infrastructure (EF Core, Redis, Email, PDF), LabResults.API (10 endpoints Minimal APIs), LabResults.GrpcService (adaptador gRPC), LabResults.Tests (16 tests xUnit)
CQRS con MediatR 12: Commands para escrituras (SubmitSampleCommand, AddResultCommand, ValidateResultCommand, RejectSampleCommand, NotifyPatientCommand) y Queries para lecturas (GetSampleQuery, GetSampleByCodeQuery, GetPatientSamplesQuery, GetPendingValidationQuery)
ValidationBehavior como Pipeline Behavior de MediatR: intercepta cada command antes de ejecutarlo y corre las validaciones de FluentValidation automaticamente. Si falla, el handler nunca se ejecuta
Aggregate Root Sample que gestiona el ciclo de vida completo: Recibida → [CargarResultado] → Completada → [Validar] → Validada → [Notificar] → Notificada. Tambien: Completada → [Rechazar] → Rechazada
Factory Method Sample.Create() que fuerza invariantes en construccion: genera SampleCode automatico, setea status inicial a Received, resultStatus a Pending y dispara SampleReceivedEvent
Transiciones de estado protegidas: no se puede validar una muestra que no esta Completada, no se puede cargar resultado en una muestra ya Validada, no se puede notificar sin validacion previa
Value Object SampleCode con formato LAB-YYYY-XXXXXX: SampleCode.Generate() produce codigos validos automaticamente. SampleCode.Create("invalido") lanza excepcion de dominio si no cumple el patron
Value Object ResultValue que calcula el estado automaticamente comparando con rangos de referencia: numeric=5.5 con rango [3.9, 6.1] → Normal. numeric=7.0 con max=6.1 → High. Encapsula la logica de clasificacion clinica
Value Objects PatientId (rechaza GUID vacio) y Email (valida formato) — inmutables, comparacion por valor, encapsulan validaciones del dominio
4 Domain Events que reaccionan a cada transicion: SampleReceivedEvent (al crear), ResultCompletedEvent (al cargar resultado), ResultValidatedEvent (al validar medico), PatientNotifiedEvent (al notificar paciente)
10 endpoints REST con Minimal APIs + Swagger/Swashbuckle: POST crear muestra, POST cargar resultado, POST validar, POST rechazar, POST notificar, GET por id, GET por codigo, GET historial paciente, GET pendientes de validacion, GET descargar PDF
Cada endpoint mapeado a un actor del dominio: Tecnico (crear, cargar resultado), Medico (validar, pendientes), Supervisor (rechazar), Sistema (notificar), Paciente/Medico (consultar, PDF)
Adaptador gRPC (LabResults.GrpcService) como punto de entrada alternativo para tecnicos de laboratorio — misma logica de dominio, diferente protocolo de transporte
PostgreSQL 16 + Entity Framework Core 8 + Npgsql como adaptador de persistencia via ISampleRepository. Migraciones automaticas con dotnet ef
Redis 7 + StackExchange.Redis como adaptador de cache via ICachePort — cache de consultas frecuentes sin acoplar el dominio a Redis
Stub de notificacion por email via INotificationPort y stub de generacion de PDF via IPdfPort — los puertos estan definidos, la implementacion se puede cambiar sin tocar nada del dominio
Repository Pattern: ISampleRepository definido en el dominio, implementado en infraestructura. El dominio nunca importa Entity Framework
16 tests unitarios con xUnit + FluentAssertions + NSubstitute: 11 de dominio (aggregate, Value Objects, Domain Events) y 5 de handlers de aplicacion
Tests de dominio en memoria pura — sin base de datos, sin servicios externos. Tests de aplicacion con mocks via NSubstitute. Todo el suite corre en menos de 100ms
Cobertura de reglas de negocio: creacion de muestra genera codigo, transiciones de estado correctas, ResultValue clasifica Normal/High/Low, PatientId rechaza GUID vacio, Domain Events se disparan
Docker Compose para levantar PostgreSQL + Redis con un comando. CI con GitHub Actions
FLUJO — Ciclo de vida de una muestra: [Tecnico] POST /samples {patientId, testType} → Sample.Create() genera SampleCode LAB-2025-XXXXXX → status=Received, resultStatus=Pending → dispara SampleReceivedEvent → response 201 con sampleId y codigo
FLUJO — Carga de resultado: [Tecnico] POST /samples/{id}/result {value, unit, refMin, refMax} → Sample.AddResult() valida status==Received → crea ResultValue que clasifica Normal/High/Low automaticamente → status=Completed → dispara ResultCompletedEvent
FLUJO — Validacion medica: [Medico] POST /samples/{id}/validate {validatedBy} → Sample.Validate() verifica status==Completed → status=Validated → dispara ResultValidatedEvent → la muestra queda lista para notificar
FLUJO — Rechazo: [Supervisor] POST /samples/{id}/reject {reason} → Sample.Reject() verifica status==Completed → status=Rejected con motivo → la muestra sale del flujo normal
FLUJO — Notificacion: [Sistema] POST /samples/{id}/notify → Sample.Notify() verifica status==Validated → INotificationPort.SendAsync() envia email al paciente → status=Notified → dispara PatientNotifiedEvent → fin del ciclo
FLUJO — Request HTTP completo: HTTP Request → Minimal API endpoint → MediatR.Send(Command) → ValidationBehavior ejecuta FluentValidation → si pasa, CommandHandler carga aggregate del repositorio → llama metodo del aggregate → aggregate emite Domain Event → handler persiste via ISampleRepository → handler publica eventos → response HTTP
FLUJO — Capas hexagonales: [Adapter entrada: API/gRPC] → [Puerto entrada: IMediator] → [Application: Handler] → [Domain: Aggregate + Value Objects] → [Puerto salida: ISampleRepository, ICachePort] → [Adapter salida: EF Core/Redis]