Gestión de Persistencia de Datos
La persistencia de datos en Lino se genera alrededor de los límites de Clean Architecture: el modelo de dominio permanece independiente de Entity Framework Core, mientras las preocupaciones de base de datos quedan en Infrastructure.Persistence. Esto mantiene las entidades enfocadas en reglas de negocio y coloca mapeos, configuración de provider, migrations, repositories, transacciones e inicialización en la capa de infraestructura.
Al crear un servicio con lino service new, Lino registra decisiones importantes de persistencia, como la arquitectura del servicio y el provider de datos. Las opciones actuales de provider son SqlServer y PostgreSql. En servicios tradicionales, la persistencia se genera directamente debajo del servicio. En servicios modulares, cada módulo recibe su propio proyecto Infrastructure.Persistence y su propio ApplicationDbContext, mientras la base de datos permanece compartida por el servicio.
En servicios tradicionales o modulares, la base de datos es única por servicio. En servicios modulares, cada módulo se mapea en un schema distinto dentro de la misma base de datos. Esto permite que cada bounded context mantenga un namespace lógico separado, con aislamiento funcional, versionado más claro y migrations más organizadas.
Esta página explica cómo Lino organiza configuraciones de Entity Framework Core, registro de DbContext, repositories, IUnitOfWork, transacciones, flujo de Transactional Outbox y el ciclo de migrations expuesto por el CLI.
Importante: el código de persistencia generado es scaffolding orientado a producción. Revise siempre mapeos, constraints, índices, comportamiento de eliminación, transacciones y scripts de migration antes de aplicar cambios en bases de datos compartidas o de producción.
Entity Type Configurations
Lino sigue el principio Persistence-Ignorant: las entidades de dominio no conocen detalles de infraestructura. Todo el mapeo ORM queda en clases que implementan IEntityTypeConfiguration<TEntity>, ubicadas en Infrastructure.Persistence/Configurations. De esta forma, las entidades expresan comportamiento de negocio mientras la infraestructura define cómo se almacenan.
Los archivos de configuración se generan a partir de las decisiones tomadas en el CLI: primary keys, strongly typed IDs, campos obligatorios, tamaños de string, Value Objects, relaciones, enumeraciones, campos de auditoría, tenant fields y entidades hijas. El resultado generado debe tratarse como un punto de partida sólido, no como algo que nunca necesita revisión.
Qué suelen definir las configuraciones
- Tabla y schema: nombre de la tabla y schema del servicio o módulo actual.
- Primary keys: identificadores de entidad, conversiones de strongly typed IDs y comportamiento de generación de clave.
- Propiedades: campos obligatorios, tamaño máximo, tipos de columna, persistencia de enum, owned structures de Value Objects y columnas de metadata de archivos.
- Relaciones: one-to-one, one-to-many, many-to-many, child collections, foreign keys y comportamiento de eliminación.
- Índices y constraints: unicidad, performance de lookup, unicidad tenant-aware y consistencia a nivel de base de datos.
Convenciones globales
Las convenciones repetidas pueden centralizarse en ModelConfiguration o helpers equivalentes, como precisión decimal, collation, conversión de DateTime, naming convention, filtros globales, propiedades auditables y reglas comunes de campos multi-tenant.
Cómo se aplican las configuraciones
El ApplicationDbContext generado aplica las configuraciones en OnModelCreating. De forma predeterminada, Lino usa Source Generators para registrar configuraciones con mejor rendimiento y sin usar Reflection. Cuando se usa scanning por assembly, la configuración puede verse así:
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
Cuando la mensajería está habilitada, Lino también aplica la configuración de OutboxMessage, permitiendo que los eventos de integración se persistan en la misma transacción de la base de datos antes de que un worker los publique de forma asíncrona.
Checklist de revisión
- Confirme que los nombres de tablas, schemas y convenciones respeten el límite del módulo.
- Revise tamaños de string, precisión decimal, columnas nullable y persistencia de enums antes de generar migrations.
- Revise relaciones y delete behavior con cuidado, especialmente para aggregate roots y entidades hijas.
- Agregue o ajuste índices para queries usadas por grids, filtros, integraciones y jobs en background.
- Ejecute build antes de crear una migration para garantizar que EF Core vea un modelo consistente.
DbContexts
El ApplicationDbContext generado es el contexto de EF Core para el límite de persistencia de un servicio o módulo. Expone propiedades DbSet<TEntity> para las entidades mapeadas e implementa IApplicationDbContext, permitiendo que query handlers y servicios de aplicación dependan de una abstracción de aplicación en lugar de la clase concreta de infraestructura.
Servicios tradicionales y servicios modulares
- Servicio tradicional: la persistencia se genera en
src/Services/<ServiceName>/Infrastructure.Persistence, y la API del servicio se usa como startup project para migrations. - Servicio modular: cada módulo tiene su propio proyecto
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistencey su propioApplicationDbContext. El host del servicio se usa como startup project para migrations.
En servicios modulares, esta separación hace que cada bounded context sea explícito en el código. Los módulos pueden evolucionar su modelo de persistencia de forma independiente, incluso cuando se ejecutan en la misma base de datos del servicio si esa es la arquitectura elegida.
Registro y configuración de provider
Lino genera el registro de persistencia mediante una extensión de IHostApplicationBuilder. El código generado registra Unit of Work, repositories, domain services, una factory pooled de ApplicationDbContext, el ApplicationDbContext scoped y la abstracción IApplicationDbContext.
La configuración específica del provider se genera de acuerdo con la base de datos seleccionada para el servicio:
UseSqlServer(...)para servicios SQL Server.UseNpgsql(...).UseSnakeCaseNamingConvention()para servicios PostgreSQL.MigrationsHistoryTable(Constants.Database.MigrationsHistoryTable, Constants.Database.Schema)para almacenar el historial de migrations en la tabla y schema esperados.- Constraint validators específicos del provider se registran para convertir violaciones de base de datos en errores de aplicación consistentes.
Contexts tenant-aware
Cuando un módulo contiene entidades tenant-aware, el contexto generado puede incluir estado del tenant y filtros globales de query. En este escenario, la factory scoped crea un contexto configurado para el scope actual, evitando que los handlers repitan filtros de tenant manualmente en todos los puntos de lectura y escritura.
Repositorios
Lino genera interfaces de repository en la capa de dominio e implementaciones concretas en Infrastructure.Persistence/Repositories. Esto preserva la dirección de dependencia: el dominio define el contrato de persistencia que necesita, y la infraestructura proporciona la implementación con Entity Framework Core.
Los repositories concretos también pueden aparecer en <ModuleName>/Infrastructure.Persistence.Repositories e implementar interfaces de <ModuleName>/Domain.Repositories, según la estructura del servicio. El punto importante es que la aplicación y el dominio no dependen directamente de detalles concretos de EF Core.
Los repositories se usan principalmente por command handlers y flujos orientados al dominio que necesitan persistir aggregates. Query Handlers pueden usar IApplicationDbContext directamente cuando necesitan proyecciones optimizadas de lectura, filtros, paginación y resultados en formato DTO.
Responsabilidades de repository
- Cargar aggregate roots y datos hijos necesarios para command handlers.
- Agregar, actualizar y eliminar aggregates de acuerdo con el modelo de persistencia.
- Encapsular consultas de persistencia que forman parte del comportamiento del aggregate o de la orquestación de escritura.
- Mantener operaciones específicas de EF Core fuera del proyecto de dominio.
- Encapsular consultas complejas cuando pertenecen al flujo de escritura, incluyendo LINQ,
FromSqly proyecciones auxiliares.
Orientaciones prácticas
- Use repositories para use cases de escritura que necesitan preservar consistencia de aggregate.
- Use proyecciones de query para lectura en lugar de cargar aggregates completos solo para montar DTO.
- Mantenga métodos de repository explícitos; evite métodos genéricos que expongan operaciones arbitrarias de persistencia a la aplicación.
- Revise includes y tracking cuando un handler modifica aggregate, child collection o relación many-to-many.
- Exponga solo los métodos necesarios para el dominio y la aplicación, manteniendo el repository aggregate-root-centric.
Unit of Work
IUnitOfWork es el límite transaccional generado por Lino. Coordina persistencia con EF Core, control de transacción, publicación de eventos de dominio y persistencia en Outbox para eventos de integración. Los handlers usan esta abstracción para confirmar cambios explícitamente, en lugar de llamar DbContext.SaveChangesAsync directamente en todos los puntos de la aplicación.
En escenarios simples, el propio DbContext suele ser suficiente como unidad de trabajo. Aun así, Lino genera una implementación dedicada de UnitOfWork para ofrecer control consistente sobre transacciones, eventos e integración con Outbox.
Operaciones generadas
SaveChangesAsync(cancellationToken): guarda cambios y publica eventos de dominio cuando está configurado.SaveChangesAsync(publishDomainEvents, cancellationToken): permite guardar con o sin publicación de eventos de dominio.SaveChangesInTransactionAsync(cancellationToken): abre una transacción, guarda cambios y confirma.BeginTransactionAsync,CommitAsyncyRollbackAsync: permiten control explícito de transacción.CommitOrRollbackAsync: confirma o deshace la transacción con base en unResult.
Eventos de dominio y Outbox
Cuando existen eventos de dominio, Lino exige una transacción abierta antes de publicarlos. Esto es intencional. Si un evento de dominio genera eventos de integración, esos eventos se registran en Outbox y se persisten en la misma transacción. Después, un worker puede procesar los registros de Outbox y publicar mensajes mediante la infraestructura de mensajería configurada.
Este flujo protege la consistencia: cuando un command crea entidades que levantan eventos y el proyecto usa Outbox, el handler debe guardar mediante una transacción, por ejemplo con SaveChangesInTransactionAsync, o abrir y confirmar la transacción explícitamente. Sin esa transacción, la infraestructura debe fallar en lugar de permitir un flujo de eventos no confiable.
Cuándo usar cada estilo de save
- Use
SaveChangesAsyncpara persistencia directa sin requisito de consistencia entre eventos y Outbox. - Use
SaveChangesInTransactionAsynccuando eventos de dominio y persistencia en Outbox deben formar parte del mismo commit. - Use
BeginTransactionAsync,CommitAsyncyRollbackAsynccuando el handler tiene varias etapas y necesita decidir el resultado final con base en unResult.
Regla práctica: si una operación de escritura publica eventos de dominio que pueden producir eventos de integración, mantenga cambios de base de datos y registros de Outbox en la misma transacción.
Managing Migrations
Lino envuelve el ciclo de migrations de Entity Framework Core con comandos CLI que conocen servicio, módulo, arquitectura, provider, DbContext, startup project, archivo de versión y ubicación de salida de los scripts. Esto reduce errores de comandos manuales y mantiene la metadata de la migration rastreada por el modelo del proyecto Lino.
Migrations registran la evolución de la base de datos a partir de cambios en entidades, Value Objects, relaciones, índices y configuraciones de Entity Framework. Antes de crear una migration, compile la solución o al menos el servicio afectado para que EF Core cargue el modelo actual.
Crear una migration
lino database migrations add --service <ServiceName> --module <ModuleName>
El comando solicita servicio, módulo, versión actual del servicio en src/Services/<ServiceName>/version.txt y descripción de la migration. Lino crea un nombre usando versión y secuencia, y luego ejecuta dotnet ef migrations add para el proyecto de persistencia y startup project correctos.
Lino también genera un script SQL de la migration usando dotnet ef migrations script. El script se escribe debajo del proyecto de persistencia afectado en una carpeta versionada, siguiendo el patrón:
Infrastructure.Persistence/Scripts/<Version>/<Sequence>_<Description>.sql Infrastructure.Persistence/Scripts/v1.2.3/001_AddCustomerIsActive.sql
Nota: además de los archivos .cs generados por Entity Framework, Lino genera el script .sql correspondiente con instrucciones DDL. Esto facilita la revisión por equipos de infraestructura, DBAs y procesos de deploy controlado.
Aplicar, listar, revertir y eliminar
lino database migrations add --service <ServiceName> --module <ModuleName> lino database migrations list --service <ServiceName> --module <ModuleName> lino database migrations apply --service <ServiceName> --module <ModuleName> lino database migrations revert --service <ServiceName> --module <ModuleName> lino database migrations remove --service <ServiceName> --module <ModuleName>
add: crea una migration para cambios pendientes en el modelo.list: lista migrations conocidas para el contexto seleccionado.apply: ejecutadotnet ef database updatepara aplicar migrations en la base de datos configurada.revert: vuelve el último camino de migration exitoso cuando el flujo lo permite.remove: elimina el último registro de migration creada y los archivos EF generados cuando corresponde.
El grupo canónico es database migrations. Aliases como db y migration pueden existir para productividad, pero la documentación debe preferir la forma completa.
Qué elige Lino por usted
- En servicios tradicionales, el proyecto EF es
src/Services/<ServiceName>/Infrastructure.Persistencey el startup project essrc/Services/<ServiceName>/Api. - En servicios modulares, el proyecto EF es
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistencey el startup project essrc/Services/<ServiceName>/Host. - El contexto es el
ApplicationDbContextgenerado para el servicio o módulo seleccionado. - La tabla de historial de migrations de EF se configura con
Constants.Database.MigrationsHistoryTableyConstants.Database.Schema.
Workflow recomendado
- Modele o cambie entidades, Value Objects, relaciones, índices o configuraciones de persistencia.
- Ejecute build y corrija errores de compilación antes de generar la migration.
- Ejecute
lino database migrations addpara el servicio y módulo correctos. - Revise la migration C# y el script SQL generados antes de aplicar.
- Aplique la migration en ambiente local o de desarrollo y verifique el schema de la base de datos.
- Commitee los archivos de migration y scripts SQL junto con el cambio de dominio o aplicación que exigió la modificación.
Checklist: revise el diff de la migration, aplíquelo en una base local, ejecute build y pruebe el flujo afectado antes de publicar.
