LabResults

EmpresaSistema de Laboratorio Clinico
Año2025
TipoAPI REST + gRPC · Arquitectura Hexagonal · DDD
RolArquitectura · Backend · Testing
Volver
01

Los laboratorios clinicos manejan el ciclo de vida de muestras (sangre, orina, etc.) con procesos manuales o sistemas monoliticos acoplados a una base de datos especifica. Se necesitaba un sistema que modelara el dominio real del laboratorio — recepcion de muestra, carga de resultado con rangos de referencia, validacion medica, notificacion al paciente y generacion de PDF — con una arquitectura que permitiera cambiar la infraestructura (base de datos, servicio de email, cache) sin tocar la logica de negocio.

02

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]
03
.NET 8
ASP.NET Core Minimal APIs
gRPC
MediatR 12
FluentValidation 12
Entity Framework Core 8
PostgreSQL 16
Redis 7
StackExchange.Redis
Swagger / Swashbuckle
xUnit
FluentAssertions
NSubstitute
Docker Compose
GitHub Actions
04
HexagonalPorts & Adapters pura
DDDAggregate Root + Value Objects + Domain Events
CQRS5 Commands + 4 Queries via MediatR
10Endpoints REST + adaptador gRPC
16 testsxUnit en < 100ms sin infra externa
6 proyectosDomain, Application, Infra, API, gRPC, Tests
05
Siguiente proyectoCourtBooking