Strutturare il progetto

Lino struttura soluzioni .NET in modo che le decisioni architetturali siano esplicite fin dal primo commit. Un progetto generato nasce già con orchestrazione Aspire, blocchi condivisi, test, configurazione della qualità del codice e un posto chiaro per ogni servizio, modulo, contratto API, aspetto di persistenza e limite di integrazione.


Questa sezione spiega come lino project new, lino service new e lino module new modellano la soluzione. L'obiettivo non è solo creare cartelle, ma definire limiti di runtime, ownership del database, isolamento dei moduli e un percorso di evoluzione da un servizio semplice a un monolite modulare o a un sistema distribuito.

Creazione della fondazione della soluzione

Il comando lino project new crea la fondazione tecnica di una nuova soluzione .NET. Eseguilo in una directory vuota dopo aver installato e autenticato la CLI.

lino project new --name <ProjectName>

L'argomento <ProjectName> rappresenta il nome reale della soluzione. Questo nome entra a far parte di namespaces, assemblies, percorsi, configurazioni, artefatti e riferimenti tra componenti; scegli quindi un nome breve, stabile e rappresentativo.

La procedura guidata interattiva richiede decisioni che influenzano la struttura e il comportamento di runtime dell'intera soluzione:

  • Namespace del progetto: identità tecnica radice usata dai progetti generati.
  • Nome visualizzato: nome leggibile usato nei metadati generati e nei punti visibili all'utente.
  • Linguaggio e stack: attualmente C# con .NET 10 e Aspire.
  • Analizzatori di codice: abilitano pacchetti e regole condivise per mantenere coerenza e qualità fin dal bootstrap.
  • CQRS: prepara il layer applicativo a separare commands e queries, con orchestrazione tramite la libreria mediator selezionata.
  • Classi base nella soluzione: controlla se le astrazioni comuni saranno generate localmente dentro la soluzione.
  • Cache distribuita: definisce se Microsoft.Extensions.Caching.Hybrid userà solo la memoria locale dell'istanza o anche un layer distribuito con Redis configurato da Aspire.
  • Comunicazione asincrona: abilita RabbitMQ con MassTransit e i blocchi di messaggistica/outbox usati dagli eventi di integrazione.
  • Lingua dei dati: lingua usata per descrivere i metadati di dominio durante la generazione.
  • Culture supportate dall'applicazione: risorse di localizzazione generate per testi UI, validazioni, errori e risposte API.
  • Cultura predefinita: lingua principale usata quando l'applicazione ha bisogno di un fallback.

Dopo la conferma, Lino genera una soluzione organizzata per crescere. Un progetto minimo nasce con Aspire, layer condivisi e test dell'area Shared:

<ProjectName>/
├── <ProjectName>.slnx
├── Directory.Build.props
├── Directory.Packages.props
├── src/
│   ├── Aspire/
│   │   ├── AppHost/
│   │   │   └── <ProjectName>.AppHost.csproj
│   │   └── ServiceDefaults/
│   │       └── <ProjectName>.ServiceDefaults.csproj
│   └── Services/
│       └── Shared/
│           ├── Api/
│           │   └── <ProjectName>.Shared.Api.csproj
│           ├── Application/
│           │   └── <ProjectName>.Shared.Application.csproj
│           ├── Domain/
│           │   └── <ProjectName>.Shared.Domain.csproj
│           ├── Infrastructure/
│           │   └── <ProjectName>.Shared.Infrastructure.csproj
│           ├── Infrastructure.Persistence/
│           │   └── <ProjectName>.Shared.Infrastructure.Persistence.csproj
│           └── Integration.Events/          (quando esiste la messaggistica)
│               └── <ProjectName>.Shared.Integration.Events.csproj
└── tests/
    └── Services/
        └── Shared/
            └── UnitTests/
                ├── Domain/
                │   └── <ProjectName>.Shared.Domain.UnitTests.csproj
                └── Application/
                    └── <ProjectName>.Shared.Application.UnitTests.csproj

Il ruolo dei progetti Shared

Shared contiene codice di piattaforma condiviso dalla soluzione. Non è un servizio di business. Usa questo spazio per astrazioni cross-cutting, errori comuni, infrastruttura di localizzazione, contratti applicativi base, helper di persistenza, estensioni API, integrazione dell'host, osservabilità e utility tecniche riutilizzabili.

Evita di inserire regole di business in Shared. Se una regola appartiene a una capacità applicativa specifica, deve vivere nel servizio o nel modulo che possiede quella responsabilità. L'obiettivo di Shared è ridurre la ripetizione tecnica, non diventare una scorciatoia per accoppiare domini diversi.

Aspire e decisioni infrastrutturali

AppHost compone la soluzione e le sue risorse di runtime. Quando vengono aggiunti Redis, RabbitMQ, SQL Server, PostgreSQL, Redis Insight, servizi, WebApps o workers, Aspire diventa il punto di orchestrazione locale. Questo semplifica esecuzione, service discovery, log, metriche, trace e visualizzazione delle risorse durante lo sviluppo.

ServiceDefaults centralizza pattern di hosting come service discovery, health checks, resilienza, logging, metriche, tracing e integrazione OpenTelemetry. Invece di configurare tutto in modo isolato in ogni servizio, la soluzione parte da un punto di composizione comune.

Analizzatori di codice

Gli analizzatori statici di codice ispezionano il codice durante lo sviluppo e rendono visibili i problemi prima dell'esecuzione: incoerenze di stile, pattern fragili, opportunità di refactoring, possibili bug e avvisi di sicurezza.

Quando abiliti gli analizzatori in lino project new, la soluzione nasce con pacchetti come StyleCop.Analyzers, SonarAnalyzer.CSharp e Roslynator.Analyzers configurati centralmente. Questo evita che ogni progetto debba decidere da solo la propria baseline di qualità.

  • Miglioramento della qualità: mantiene il codice leggibile, coerente e allineato agli standard della soluzione.
  • Prevenzione degli errori: segnala i problemi presto, prima che arrivino ai test manuali o alla produzione.
  • Standardizzazione: riduce differenze di stile tra servizi, moduli e team.
  • Refactoring assistito: evidenzia possibili semplificazioni e opportunità di modernizzazione del codice C#.

Cache distribuita, cache ibrida e Redis

Lino prepara la soluzione a usare Microsoft.Extensions.Caching.Hybrid, la libreria Microsoft che centralizza le operazioni di cache tramite HybridCache. Questa astrazione permette a handlers, servizi applicativi e componenti infrastrutturali di memorizzare risultati di query, permessi, configurazioni o dati di supporto senza diffondere dettagli di implementazione della cache nel codice.

Quando non abiliti la cache distribuita durante la creazione del progetto, HybridCache resta disponibile, ma lavora solo con la memoria locale dell'istanza in esecuzione. Questa modalità è semplice e sufficiente per scenari locali, ambienti piccoli o applicazioni con una sola replica, ma ogni processo mantiene la propria cache e i dati non sono condivisi tra istanze.

Quando abiliti la cache distribuita, Lino aggiunge Redis alle risorse Aspire e configura l'infrastruttura per usare un layer di cache condiviso. Con Redis, più istanze dello stesso servizio possono consultare lo stesso layer, riducendo letture ripetute da database, API interne o integrazioni e preparando il progetto alla scala orizzontale.

  • Performance: riduce il tempo di risposta per letture ripetute e operazioni di supporto.
  • Scalabilità: permette a istanze diverse di condividere dati in cache.
  • Disponibilità: sposta parte della pressione di lettura dal database principale.
  • Costo operativo: riduce l'elaborazione ripetitiva in query e integrazioni ad alto volume.

Questa decisione viene presa in lino project new perché modifica la fondazione dell'ambiente: risorse di AppHost, secrets locali, pacchetti, configurazione infrastrutturale e topologia di esecuzione mostrata nella dashboard Aspire.

Comunicazione asincrona

La comunicazione asincrona permette a servizi, moduli e componenti di reagire a fatti del sistema senza bloccare il flusso principale dell'operazione. È particolarmente utile quando il produttore non deve dipendere dalla disponibilità immediata del consumatore o quando un'azione deve attivare effetti successivi, come notifiche, proiezioni, integrazioni esterne o sincronizzazione tra contesti.

Quando abiliti la comunicazione asincrona, Lino aggiunge RabbitMQ ad Aspire e configura MassTransit insieme ai blocchi di messaggistica e outbox usati dagli eventi di integrazione. Gli eventi pubblicati da servizi o moduli sono rappresentati da contratti espliciti in Integration.Events e possono essere elaborati con maggiore resilienza.

  • Performance: permette al caso d'uso principale di continuare senza attendere la conclusione di tutti i consumatori.
  • Scalabilità: distribuisce l'elaborazione tra consumatori e code, assorbendo i picchi con maggiore controllo.
  • Resilienza: consente il ritrattamento e riduce la perdita di messaggi quando combinata con outbox.
  • Disaccoppiamento: evita dipendenza diretta tra produttore e consumatore quando non è richiesta una risposta immediata.

Usa eventi di integrazione quando un errore del consumatore non deve annullare la transazione del produttore. Quando il consumatore deve rispondere immediatamente per completare il caso d'uso, preferisci un'integrazione sincrona esplicita e tratta disponibilità, timeout e fallback come parte del contratto.

Localizzazione e culture

Quando vengono selezionate le culture, Lino genera risorse affinché messaggi, validazioni, labels, errori e testi UI possano essere localizzati fin dall'inizio. Questo evita di trattare l'internazionalizzazione come una correzione tardiva, quando le stringhe sono già sparse tra endpoints, handlers e componenti.

Valida la fondazione e passa ai servizi

Dopo la generazione, ripristina le dipendenze, compila la soluzione ed esegui l'host Aspire:

dotnet restore <ProjectName>.slnx
dotnet build <ProjectName>.slnx
dotnet run --project src/Aspire/AppHost/<ProjectName>.AppHost.csproj

In questa fase, la soluzione è ancora una fondazione. Apri il progetto nell'editor, rivedi le risorse create da Aspire, conferma le decisioni su cache, messaggistica, localizzazione e qualità del codice, quindi passa ai servizi che rappresenteranno le capacità di business dell'applicazione.

Creazione e gestione dei servizi

Un servizio è un limite di runtime e ownership. In Lino, i servizi possono rappresentare API indipendenti in un sistema distribuito o aree di business più ampie dentro una soluzione che può evolvere gradualmente.

Dopo aver creato la fondazione del progetto, aggiungi servizi con:

lino service new

La procedura guidata del servizio richiede:

  • Namespace del servizio: nome tecnico usato in cartelle, progetti e namespaces.
  • Nome visualizzato: nome leggibile del servizio.
  • Tipo di servizio: scegli tra semplice e modulare.
  • Provider di database: scegli SQL Server o PostgreSQL per il database del servizio.
  • Stile architetturale: attualmente Clean Architecture per servizi semplici.
  • Strongly Typed IDs: nei servizi semplici, definisce se gli identificatori saranno generati come tipi dedicati.

Tipi di servizio

Servizio semplice: una struttura più diretta per una capacità di business con un limite chiaro. È una buona scelta per un'API focalizzata, un microservizio o un'area abbastanza piccola da evolvere come una sola unità.

Servizio modulare: una struttura indicata per sistemi più grandi, monoliti modulari o runtimes che devono ospitare diverse capacità indipendenti. I moduli migliorano organizzazione e scalabilità del modello, ma richiedono maggiore disciplina sulle dipendenze.

Indipendentemente dal tipo, ogni servizio possiede il proprio database. Nei servizi semplici, la decisione sugli Strongly Typed IDs viene presa a livello di servizio; nei servizi modulari, viene presa per modulo.

Servizio semplice

Un servizio semplice colloca i propri layer direttamente sotto src/Services/<ServiceName>. Usa questa struttura quando la capacità di business ha un limite chiaro e non ha bisogno di diversi moduli isolati dentro lo stesso runtime. Funziona bene per un'API focalizzata, un microservizio o un'area di dominio abbastanza piccola da evolvere come unità.

src/Services/<ServiceName>/
├── Domain/
│   └── <ProjectName>.<ServiceName>.Domain.csproj
├── Application/
│   └── <ProjectName>.<ServiceName>.Application.csproj
├── Infrastructure.Persistence/
│   └── <ProjectName>.<ServiceName>.Infrastructure.Persistence.csproj
├── Infrastructure/
│   └── <ProjectName>.<ServiceName>.Infrastructure.csproj
├── Api/
│   └── <ProjectName>.<ServiceName>.Api.csproj
├── Integration.Events/              (quando esiste la messaggistica)
│   └── <ProjectName>.<ServiceName>.Integration.Events.csproj
├── Api.Contracts/                   (quando esiste consumo HTTP tipizzato)
│   └── <ProjectName>.<ServiceName>.Api.Contracts.csproj
└── Api.Client/                      (quando esiste consumo HTTP tipizzato)
    └── <ProjectName>.<ServiceName>.Api.Client.csproj
tests/Services/<ServiceName>/
├── UnitTests/
│   ├── Domain/
│   │   └── <ProjectName>.<ServiceName>.Domain.UnitTests.csproj
│   └── Application/
│       └── <ProjectName>.<ServiceName>.Application.UnitTests.csproj
└── IntegrationTests/
    └── <ProjectName>.<ServiceName>.IntegrationTests.csproj

I layer hanno responsabilità distinte: Domain protegge regole di business e invarianti; Application orchestra i casi d'uso; Infrastructure.Persistence contiene Entity Framework Core, repository, Unit of Work e migrations; Infrastructure contiene integrazioni tecniche; e Api adatta HTTP ai casi d'uso.

I progetti Api.Contracts e Api.Client compaiono quando esiste consumo HTTP tipizzato, specialmente in soluzioni con Web App Blazor. Il primo concentra requests, responses, DTO, tipi pubblici e interfacce client condivise; il secondo fornisce l'implementazione HTTP di queste interfacce, usando HttpClient, affinché Blazor consumi le API generate senza duplicare contratti.

Il progetto Integration.Events compare quando la soluzione ha la messaggistica abilitata. Contiene eventi di integrazione pubblicati e consumati da altri moduli, servizi o sistemi, di solito insieme all'infrastruttura di messaggistica e outbox.

Servizio modulare

Un servizio modulare è indicato per monoliti modulari o per un runtime che ospita più bounded contexts. Il servizio in sé possiede host, infrastruttura comune e provider di database; le regole di business vivono dentro i moduli.

src/Services/<ServiceName>/
├── Host/
│   └── <ProjectName>.<ServiceName>.Host.csproj
├── Infrastructure/
│   └── <ProjectName>.<ServiceName>.Infrastructure.csproj
└── Modules/
tests/Services/<ServiceName>/
└── Modules/

Host compone moduli, configurazioni, endpoints e infrastruttura condivisa di quel servizio. Non deve contenere regole di business. Il progetto Infrastructure a livello di servizio fornisce supporto tecnico di composizione condiviso dai moduli.

Quando viene aggiunto un modulo, i progetti Domain, Application, Infrastructure.Persistence, Infrastructure e Api compaiono dentro Modules/<ModuleName>, preservando il limite interno. Api.Contracts, Api.Client e Integration.Events seguono le stesse condizioni: consumo HTTP tipizzato per client Blazor/API e messaggistica abilitata per eventi di integrazione.

Database ownership

Ogni servizio possiede il proprio database. In una soluzione con più servizi, un servizio può usare PostgreSQL mentre un altro usa SQL Server. Questa decisione può variare per necessità del dominio, performance, operatività, maturità del team o integrazione con infrastruttura esistente.

In un servizio modulare, il database appartiene al servizio come runtime, ma i moduli sono isolati tramite schema, progetti di persistenza e migrations proprie. Un modulo non deve interrogare direttamente le tabelle di un altro modulo, anche quando le tabelle si trovano nello stesso database fisico.

Scelta tra semplice e modulare

Scegli la struttura più piccola che protegge il limite. Un servizio semplice è sufficiente quando esiste un unico limite di dominio. Un servizio modulare ha senso quando diverse capacità devono condividere runtime, deploy o transazione, ma devono comunque mantenere separati modelli, persistenza, API e test.

SceltaQuando usarlaCosto assunto
Servizio sempliceUn dominio focalizzato, pochi limiti interni, API isolata o microservizio diretto.Meno struttura iniziale, ma meno isolamento interno se il dominio cresce molto.
Servizio modularePiù sottodomini nello stesso runtime, monolite modulare, SaaS con aree indipendenti o necessità di schemas per modulo.Più progetti, maggiore disciplina sulle dipendenze e più attenzione ai contratti interni.

Stile architetturale

I servizi generati da Lino seguono Clean Architecture per separare le regole di business dai dettagli tecnici. Il layer di dominio non deve conoscere HTTP, Entity Framework Core, messaggistica, UI o provider esterni; queste responsabilità restano ai bordi dell'applicazione.

  • Disaccoppiamento: le regole centrali non dipendono da framework o meccanismi di delivery.
  • Manutenibilità: le modifiche infrastrutturali tendono a restare isolate dalle regole di business.
  • Testabilità: casi d'uso e codice di dominio possono essere testati con meno dipendenze esterne.
  • Evoluzione: i dettagli tecnici possono essere sostituiti con minore impatto sul core del servizio.

Clean Architecture e Strongly Typed IDs

I servizi generati seguono Clean Architecture affinché il codice di business non dipenda da HTTP, EF Core, messaggistica, UI o dettagli infrastrutturali. Questa separazione rende i test più diretti, riduce l'accoppiamento e permette di sostituire dettagli tecnici senza riscrivere le regole centrali.

Gli Strongly Typed IDs aumentano la sicurezza impedendo la mescolanza accidentale di identificatori. Invece di accettare qualsiasi Guid, long o int, il dominio può lavorare con un tipo specifico per ogni entità o aggregato, di solito nel formato <EntityName>Id. Questo impedisce di passare l'identificatore di un'entità dove ne era atteso un altro e rende le firme più espressive in commands, queries, entità, handlers e mappings.

  • Type safety: impedisce di mescolare accidentalmente identificatori di entità diverse.
  • Chiarezza: rende firme e contratti più espressivi rispetto a tipi primitivi sciolti.
  • Refactoring: concentra modifiche di formato o serializzazione nel tipo ID corrispondente.
  • Riduzione degli errori: rende gli usi errati visibili in compilazione ogni volta che è possibile.

Integrazione tra servizi

Quando un servizio deve reagire a un altro, preferisci eventi di integrazione, integrazioni HTTP esplicite o dati replicati consapevolmente. L'accesso diretto al database di un altro servizio crea accoppiamento strutturale, rende le migrations più difficili e rende i deploy indipendenti più rischiosi.

Prossimi passi per servizi modulari

Dopo aver creato un servizio modulare, il passo successivo è aggiungere moduli con lino module new. Ogni modulo deve rappresentare una propria capacità di business, con dominio, applicazione, persistenza, API, test e integrazioni che preservano il limite interno del servizio.

Creazione e gestione dei moduli

I moduli esistono solo all'interno di servizi modulari. Un modulo rappresenta un limite di business nello stesso runtime: possiede il proprio modello di dominio, casi d'uso, persistenza, superficie API, eventi di integrazione, test e schema di database.

Usa i moduli quando il servizio deve ospitare più di una capacità di business senza mescolare entità, regole, migrations e contratti. In un monolite modulare, tutto può essere eseguito nello stesso processo, ma i limiti interni restano importanti: il vero isolamento è controllo delle dipendenze, non solo separazione delle cartelle.

lino module new --service <ServiceName>

Durante la creazione, la procedura guidata richiede:

  • Servizio: il servizio modulare che ospiterà il modulo. Lino non permette di creare moduli dentro servizi semplici, perché non hanno la struttura necessaria per l'isolamento interno.
  • Namespace del modulo: il nome tecnico usato in cartelle, namespaces, assemblies e progetti.
  • Nome visualizzato: il nome leggibile del modulo usato nei punti pensati per la lettura umana.
  • Strongly Typed IDs: definisce se gli identificatori generati dentro il modulo useranno tipi dedicati, di solito nel formato <EntityName>Id.

Dopo la conferma, Lino aggiunge il modulo dentro il servizio senza indebolire il limite architetturale:

src/Services/<ServiceName>/
├── Host/
├── Infrastructure/
└── Modules/
    └── <ModuleName>/
        ├── Domain/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Domain.csproj
        ├── Application/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Application.csproj
        ├── Infrastructure.Persistence/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.Persistence.csproj
        ├── Infrastructure/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.csproj
        ├── Api/
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Api.csproj
        ├── Integration.Events/              (quando esiste la messaggistica)
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Integration.Events.csproj
        ├── Api.Contracts/                   (quando esiste consumo HTTP tipizzato)
        │   └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Contracts.csproj
        └── Api.Client/                      (quando esiste consumo HTTP tipizzato)
            └── <ProjectName>.<ServiceName>.<ModuleName>.Api.Client.csproj
tests/Services/<ServiceName>/Modules/<ModuleName>/
├── UnitTests/
│   ├── Domain/
│   │   └── <ProjectName>.<ServiceName>.<ModuleName>.Domain.UnitTests.csproj
│   └── Application/
│       └── <ProjectName>.<ServiceName>.<ModuleName>.Application.UnitTests.csproj
└── IntegrationTests/
    └── <ProjectName>.<ServiceName>.<ModuleName>.IntegrationTests.csproj

Responsabilità degli artefatti generati

ArtefattoResponsabilità dentro il modulo
DomainEntità, aggregati, Value Objects, enumerations, eventi di dominio, contratti di repository e invarianti del modulo.
ApplicationCasi d'uso, commands, queries, handlers, validazioni, contratti interni di input e output e orchestrazione delle regole del modulo.
Infrastructure.PersistenceDbContext, configurazioni di Entity Framework Core, repository concreti, Unit of Work e migrations del modulo.
InfrastructureImplementazioni tecniche specifiche del modulo, adattatori, provider e composizione delle dipendenze.
ApiEndpoint HTTP, versioning, filtri, autorizzazione e adattamento tra richieste esterne e casi d'uso.
Api.ContractsGenerato quando esiste consumo HTTP tipizzato, di solito da Web Apps Blazor. Contiene requests, responses, DTO, tipi pubblici e interfacce client condivise tra l'API e il consumatore.
Api.ClientGenerato insieme ai contratti quando esiste consumo HTTP tipizzato. Contiene l'implementazione HTTP delle interfacce, usando HttpClient, providers, options e helpers affinché i progetti Blazor consumino le API generate in modo coerente e fortemente tipizzato.
Integration.EventsGenerato quando il progetto ha messaggistica. Contiene eventi di integrazione pubblicati e consumati da altri moduli, servizi o sistemi, mantenendo il payload come contratto esplicito.

Struttura del database

Il database resta collegato al servizio, non a ciascun modulo in modo isolato. Dentro un servizio modulare, ogni modulo è rappresentato dal proprio schema nel database associato, con progetto di persistenza e migrations propri. Questo offre isolamento e organizzazione senza richiedere più database fisici per ogni modulo.

Isolamento e indipendenza tra moduli

Un modulo non deve accedere direttamente al DbContext, alle entità, ai repository o ai servizi interni di un altro modulo. Ogni modulo possiede il proprio modello e la propria persistenza. Quando un altro modulo ha bisogno di dati, usa un'integrazione esplicita invece di attraversare il limite tramite dettagli interni di implementazione.

Questo evita che una modifica apparentemente locale rompa un'altra area del sistema. Se un modulo consumatore deve consultare dati che appartengono a un altro modulo, non deve dipendere dall'entità completa del modulo di origine. Può mantenere una shadow entity con i dati minimi necessari per il proprio caso d'uso, alimentata da integrazione o evento.

Vantaggi di questo disaccoppiamento:

  • Isolamento: ogni modulo può evolvere regole, persistenza e test senza attraversare dettagli interni di un altro modulo.
  • Organizzazione: l'applicazione rispetta i bounded contexts e rende esplicito l'ownership.
  • Flessibilità: i moduli possono essere aggiunti, rimossi o rifattorizzati con minore impatto sul resto del servizio.
  • Facilità di test: ogni modulo può essere validato in modo più indipendente, aumentando la fiducia nelle modifiche.

Comunicazione tra moduli

Usa contratti espliciti per la comunicazione. Per chiamate dentro lo stesso runtime, un'integrazione interna può esporre contratti in Integration.Contracts e un'implementazione in-process quando generata. Per chiamate tra runtimes, usa integrazione HTTP. Per la propagazione asincrona, pubblica eventi di integrazione in Integration.Events e usa l'infrastruttura di messaggistica/outbox quando la pubblicazione fa parte di un'operazione transazionale.

Schemas, migrations e database

In un servizio modulare, i moduli condividono il provider di database del servizio, ma ogni modulo è rappresentato dal proprio schema e dal proprio progetto Infrastructure.Persistence. Le migrations sono generate per modulo, mantenendo l'evoluzione del database allineata al limite di business.

Questa separazione permette a ogni modulo di evolvere tabelle, seeds, indici e foreign keys senza trasformare il database del servizio in un unico modello condiviso da tutti. Lo schema è un limite tecnico che rafforza il limite di business, anche quando i moduli girano nello stesso processo e usano lo stesso database fisico.

Buoni limiti di modulo

Crea moduli attorno a capacità di business, non attorno a layer tecnici. Un buon nome di modulo deve descrivere una responsabilità riconoscibile dal dominio, come un'area, un processo o una capacità con regole proprie. Evita nomi generici come Common, Core o Utilities per regole di business, perché nascondono ownership e tendono a diventare dipendenze eccessivamente condivise.

  • Coesione: il modulo deve avere linguaggio, regole e dati che cambiano insieme.
  • Autonomia: deve essere possibile testare ed evolvere il modulo senza accedere a entità interne di un altro modulo.
  • Contratti chiari: i dati necessari fuori dal modulo devono essere esposti tramite API, integrazione, evento o shadow entity, non tramite accesso diretto al database.
  • Basso accoppiamento: se due moduli devono modificare sempre le stesse entità, probabilmente il limite deve essere rivisto.

Con i moduli creati, i prossimi argomenti della documentazione mostrano come modellare entità, Value Objects, enumerations, commands, queries, API, eventi, integrazioni e migrations dentro questi limiti.

Si è verificato un errore non gestito. Ricarica 🗙