Gestire la Persistenza dei Dati
La persistenza dei dati in Lino viene generata attorno ai confini della Clean Architecture: il modello di dominio resta indipendente da Entity Framework Core, mentre le responsabilità di database rimangono in Infrastructure.Persistence. Questo mantiene le entità focalizzate sulle regole di business e colloca mapping, configurazione del provider, migrations, repositories, transazioni e inizializzazione nel layer di infrastruttura.
Quando crei un servizio con lino service new, Lino registra decisioni importanti di persistenza, come architettura del servizio e provider dati. Le opzioni attuali di provider sono SqlServer e PostgreSql. Nei servizi tradizionali, la persistenza viene generata direttamente sotto il servizio. Nei servizi modulari, ogni modulo riceve il proprio progetto Infrastructure.Persistence e il proprio ApplicationDbContext, mentre il database resta condiviso dal servizio.
Nei servizi tradizionali o modulari, il database è unico per servizio. Nei servizi modulari, ogni modulo viene mappato in uno schema distinto dentro lo stesso database. Questo permette a ogni bounded context di mantenere un namespace logico separato, con isolamento funzionale, versionamento più chiaro e migrations più organizzate.
Questa pagina spiega come Lino organizza configurazioni di Entity Framework Core, registrazione di DbContext, repositories, IUnitOfWork, transazioni, flusso di Transactional Outbox e ciclo di migrations esposto dalla CLI.
Importante: il codice di persistenza generato è scaffolding orientato alla produzione. Rivedi sempre mapping, constraints, indici, comportamento di eliminazione, transazioni e script di migration prima di applicare modifiche a database condivisi o di produzione.
Entity Type Configurations
Lino segue il principio Persistence-Ignorant: le entità di dominio non conoscono i dettagli dell'infrastruttura. Tutto il mapping ORM rimane in classi che implementano IEntityTypeConfiguration<TEntity>, situate in Infrastructure.Persistence/Configurations. In questo modo le entità esprimono il comportamento di business, mentre l'infrastruttura definisce come verranno archiviate.
I file di configurazione vengono generati dalle decisioni prese nella CLI: primary keys, strongly typed IDs, campi obbligatori, dimensioni delle stringhe, Value Objects, relazioni, enum, campi di audit, tenant fields ed entità figlie. Il risultato generato deve essere trattato come un solido punto di partenza, non come qualcosa che non debba mai essere rivisto.
Cosa definiscono di solito le configurazioni
- Tabella e schema: nome della tabella e schema del servizio o modulo corrente.
- Primary keys: identificatori di entità, conversioni di strongly typed IDs e comportamento di generazione della chiave.
- Proprietà: campi obbligatori, lunghezza massima, tipi di colonna, persistenza degli enum, owned structures di Value Objects e colonne di metadata dei file.
- Relazioni: one-to-one, one-to-many, many-to-many, child collections, foreign keys e comportamento di eliminazione.
- Indici e constraints: unicità, performance di lookup, unicità tenant-aware e consistenza a livello di database.
Convenzioni globali
Le convenzioni ripetute possono essere centralizzate in ModelConfiguration o helper equivalenti, come precisione decimale, collation, conversione di DateTime, naming convention, filtri globali, proprietà auditabili e regole comuni per campi multi-tenant.
Come vengono applicate le configurazioni
L'ApplicationDbContext generato applica le configurazioni in OnModelCreating. Per impostazione predefinita, Lino usa Source Generators per registrare le configurazioni con migliori prestazioni e senza usare Reflection. Quando si usa lo scanning per assembly, la configurazione può apparire così:
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
Quando la messaggistica è abilitata, Lino applica anche la configurazione di OutboxMessage, permettendo agli eventi di integrazione di essere persistiti nella stessa transazione del database prima che un worker li pubblichi in modo asincrono.
Checklist di revisione
- Conferma che nomi di tabelle, schemas e convenzioni rispettino il confine del modulo.
- Rivedi lunghezze delle stringhe, precisione decimale, colonne nullable e persistenza degli enums prima di generare migrations.
- Rivedi relazioni e delete behavior con attenzione, soprattutto per aggregate roots ed entità figlie.
- Aggiungi o adegua gli indici per query usate da grids, filtri, integrazioni e background jobs.
- Esegui la build prima di creare una migration per garantire che EF Core veda un modello consistente.
DbContexts
L'ApplicationDbContext generato è il contesto EF Core per il confine di persistenza di un servizio o modulo. Espone proprietà DbSet<TEntity> per le entità mappate e implementa IApplicationDbContext, permettendo a query handlers e servizi applicativi di dipendere da un'astrazione applicativa invece che dalla classe concreta dell'infrastruttura.
Servizi tradizionali e servizi modulari
- Servizio tradizionale: la persistenza è generata in
src/Services/<ServiceName>/Infrastructure.Persistence, e l'API del servizio è usata come startup project per le migrations. - Servizio modulare: ogni modulo ha il proprio progetto
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistencee il proprioApplicationDbContext. L'host del servizio è usato come startup project per le migrations.
Nei servizi modulari, questa separazione rende ogni bounded context esplicito nel codice. I moduli possono evolvere il proprio modello di persistenza in modo indipendente, anche quando vengono eseguiti nello stesso database del servizio se questa è l'architettura scelta.
Registrazione e configurazione del provider
Lino genera la registrazione della persistenza tramite un'estensione di IHostApplicationBuilder. Il codice generato registra Unit of Work, repositories, domain services, una factory pooled di ApplicationDbContext, l'ApplicationDbContext scoped e l'astrazione IApplicationDbContext.
La configurazione specifica del provider viene generata in base al database selezionato per il servizio:
UseSqlServer(...)per servizi SQL Server.UseNpgsql(...).UseSnakeCaseNamingConvention()per servizi PostgreSQL.MigrationsHistoryTable(Constants.Database.MigrationsHistoryTable, Constants.Database.Schema)per archiviare lo storico delle migrations nella tabella e nello schema previsti.- Constraint validators specifici del provider vengono registrati per convertire violazioni del database in errori applicativi consistenti.
Contexts tenant-aware
Quando un modulo contiene entità tenant-aware, il contesto generato può includere stato del tenant e filtri globali di query. In questo scenario, la factory scoped crea un contesto configurato per lo scope corrente, evitando che gli handlers ripetano manualmente filtri di tenant in ogni punto di lettura e scrittura.
Repositories
Lino genera interfacce di repository nel layer di dominio e implementazioni concrete in Infrastructure.Persistence/Repositories. Questo preserva la direzione delle dipendenze: il dominio definisce il contratto di persistenza di cui ha bisogno, e l'infrastruttura fornisce l'implementazione con Entity Framework Core.
Repositories concreti possono apparire anche in <ModuleName>/Infrastructure.Persistence.Repositories e implementare interfacce di <ModuleName>/Domain.Repositories, in base alla struttura del servizio. Il punto importante è che applicazione e dominio non dipendano direttamente dai dettagli concreti di EF Core.
I repositories sono usati principalmente da command handlers e flussi orientati al dominio che devono persistere aggregates. I Query Handlers possono usare direttamente IApplicationDbContext quando hanno bisogno di proiezioni di lettura ottimizzate, filtri, paginazione e risultati in formato DTO.
Responsabilità di repository
- Caricare aggregate roots e dati figli necessari per command handlers.
- Aggiungere, aggiornare e rimuovere aggregates secondo il modello di persistenza.
- Incapsulare query di persistenza che fanno parte del comportamento dell'aggregate o dell'orchestrazione di scrittura.
- Mantenere operazioni specifiche di EF Core fuori dal progetto di dominio.
- Incapsulare query complesse quando appartengono al flusso di scrittura, inclusi LINQ,
FromSqle proiezioni ausiliarie.
Indicazioni pratiche
- Usa repositories per use cases di scrittura che devono preservare la consistenza dell'aggregate.
- Usa proiezioni di query per letture invece di caricare aggregates completi solo per comporre DTO.
- Mantieni espliciti i metodi di repository; evita metodi generici che espongono operazioni arbitrarie di persistenza all'applicazione.
- Rivedi includes e tracking quando un handler modifica aggregate, child collection o relazione many-to-many.
- Esponi solo i metodi necessari al dominio e all'applicazione, mantenendo il repository aggregate-root-centric.
Unit of Work
IUnitOfWork è il confine transazionale generato da Lino. Coordina la persistenza con EF Core, il controllo delle transazioni, la pubblicazione di domain events e la persistenza nell'Outbox per integration events. Gli handlers usano questa astrazione per confermare esplicitamente le modifiche, invece di chiamare DbContext.SaveChangesAsync direttamente in ogni punto dell'applicazione.
In scenari semplici, lo stesso DbContext è spesso sufficiente come unità di lavoro. Lino genera comunque un'implementazione dedicata di UnitOfWork per offrire controllo consistente su transazioni, eventi e integrazione con Outbox.
Operazioni generate
SaveChangesAsync(cancellationToken): salva le modifiche e pubblica domain events quando configurato.SaveChangesAsync(publishDomainEvents, cancellationToken): permette di salvare con o senza pubblicazione di domain events.SaveChangesInTransactionAsync(cancellationToken): apre una transazione, salva le modifiche e conferma.BeginTransactionAsync,CommitAsynceRollbackAsync: permettono controllo esplicito della transazione.CommitOrRollbackAsync: conferma o annulla la transazione in base a unResult.
Domain events e Outbox
Quando esistono domain events, Lino richiede una transazione aperta prima di pubblicarli. Questo è intenzionale. Se un domain event genera integration events, questi eventi vengono registrati nell'Outbox e persistiti nella stessa transazione. Poi, un worker può processare i record dell'Outbox e pubblicare messaggi tramite l'infrastruttura di messaggistica configurata.
Questo flusso protegge la consistenza: quando un command crea entità che sollevano eventi e il progetto usa Outbox, l'handler deve salvare tramite una transazione, per esempio con SaveChangesInTransactionAsync, oppure aprire e confermare la transazione esplicitamente. Senza questa transazione, l'infrastruttura deve fallire invece di permettere un flusso di eventi non affidabile.
Quando usare ogni stile di save
- Usa
SaveChangesAsyncper persistenza diretta senza requisito di consistenza tra eventi e Outbox. - Usa
SaveChangesInTransactionAsyncquando domain events e persistenza nell'Outbox devono far parte dello stesso commit. - Usa
BeginTransactionAsync,CommitAsynceRollbackAsyncquando l'handler ha più passaggi e deve decidere il risultato finale in base a unResult.
Regola pratica: se un'operazione di scrittura pubblica domain events che possono produrre integration events, mantieni modifiche del database e record Outbox nella stessa transazione.
Managing Migrations
Lino incapsula il ciclo di migrations di Entity Framework Core con comandi CLI che conoscono servizio, modulo, architettura, provider, DbContext, startup project, file di versione e percorso di output degli scripts. Questo riduce errori nei comandi manuali e mantiene la metadata della migration tracciata dal modello del progetto Lino.
Le migrations registrano l'evoluzione del database a partire da modifiche a entità, Value Objects, relazioni, indici e configurazioni di Entity Framework. Prima di creare una migration, compila la soluzione o almeno il servizio interessato affinché EF Core carichi il modello corrente.
Creare una migration
lino database migrations add --service <ServiceName> --module <ModuleName>
Il comando richiede servizio, modulo, versione corrente del servizio in src/Services/<ServiceName>/version.txt e descrizione della migration. Lino crea un nome usando versione e sequenza, poi esegue dotnet ef migrations add per il progetto di persistenza e lo startup project corretti.
Lino genera anche uno script SQL della migration usando dotnet ef migrations script. Lo script viene scritto sotto il progetto di persistenza interessato in una cartella versionata, seguendo il pattern:
Infrastructure.Persistence/Scripts/<Version>/<Sequence>_<Description>.sql Infrastructure.Persistence/Scripts/v1.2.3/001_AddCustomerIsActive.sql
Nota: oltre ai file .cs generati da Entity Framework, Lino genera lo script .sql corrispondente con istruzioni DDL. Questo facilita la revisione da parte di team di infrastruttura, DBAs e processi di deploy controllato.
Applicare, elencare, revertire e rimuovere
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 per modifiche pendenti nel modello.list: elenca le migrations note per il contesto selezionato.apply: eseguedotnet ef database updateper applicare migrations nel database configurato.revert: torna indietro dall'ultimo percorso di migration riuscito quando il flusso lo permette.remove: rimuove l'ultimo record di migration creata e i file EF generati quando applicabile.
Il gruppo canonico è database migrations. Aliases come db e migration possono esistere per produttività, ma la documentazione deve preferire la forma completa.
Cosa sceglie Lino per te
- Nei servizi tradizionali, il progetto EF è
src/Services/<ServiceName>/Infrastructure.Persistencee lo startup project èsrc/Services/<ServiceName>/Api. - Nei servizi modulari, il progetto EF è
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistencee lo startup project èsrc/Services/<ServiceName>/Host. - Il contesto è l'
ApplicationDbContextgenerato per il servizio o modulo selezionato. - La tabella dello storico delle migrations di EF è configurata con
Constants.Database.MigrationsHistoryTableeConstants.Database.Schema.
Workflow consigliato
- Modella o modifica entità, Value Objects, relazioni, indici o configurazioni di persistenza.
- Esegui la build e correggi errori di compilazione prima di generare la migration.
- Esegui
lino database migrations addper il servizio e modulo corretti. - Rivedi la migration C# e lo script SQL generati prima di applicarli.
- Applica la migration in ambiente locale o di sviluppo e verifica lo schema del database.
- Committa file di migration e scripts SQL insieme alla modifica di dominio o applicazione che ha richiesto il cambiamento.
Checklist: rivedi il diff della migration, applicala su un database locale, esegui la build e testa il flusso interessato prima di pubblicare.
