Definizione dei casi d'uso dell'applicazione
In questa sezione esploreremo come definire i casi d’uso della tua applicazione utilizzando Lino CLI. I casi d’uso rappresentano interazioni specifiche tra utenti o sistemi esterni e l’applicazione, incapsulando le regole di business e promuovendo un’architettura orientata al dominio. Tratteremo la separazione tra operazioni di lettura e scrittura tramite il pattern CQRS, oltre a standardizzare le risposte utilizzando il Result Pattern.
Il pattern CQRS (Command Query Responsibility Segregation) propone la separazione tra le operazioni che modificano lo stato dell’applicazione (Commands) e quelle che si limitano a leggere i dati (Queries). Questo approccio consente di ottimizzare ciascun tipo di operazione in modo indipendente, migliorando la scalabilità , le prestazioni e la manutenibilità del sistema.
Il Result Pattern viene utilizzato per standardizzare i risultati delle operazioni, incapsulando informazioni su successo o fallimento, messaggi di errore e dati risultanti. Questo facilita una gestione coerente delle risposte nei diversi livelli dell’applicazione.
Vedremo come sono strutturati i file di Commands e Queries, le validazioni dei dati, come viene applicata la logica di dominio nei Handlers e come restituire i risultati in modo standardizzato. Tutto questo grazie alla creazione automatica di questi componenti tramite il CLI di Lino.
Nota: Sebbene non sia obbligatorio, attualmente Lino offre due opzioni per applicare il pattern CQRS nel livello applicativo: utilizzando il pattern Mediator tramite la libreria MediatR
(di Jimmy Bogard) oppure la libreria Mediator
(di Martin Othamar).
Panoramica dei Use Case
Un Use Case rappresenta un’interazione completa tra utenti o sistemi esterni e l’applicazione, descrivendo scenari di business specifici. In Lino, ogni Use Case è suddiviso in:
- Command: rappresenta l’intenzione di modificare lo stato del sistema (creare, aggiornare, eliminare, ecc.).
- Query: rappresenta l’intenzione di interrogare i dati senza modificare lo stato del dominio.
Questa separazione favorisce la chiarezza del codice, facilita i test, consente scalabilitĂ indipendente per lettura e scrittura, e si allinea ai principi di architettura pulita e alle buone pratiche del Domain-Driven Design (DDD).
Comandi
Un Command è un messaggio immutabile che contiene solo i dati necessari per eseguire un'azione che modifica lo stato del sistema (ad esempio, CreateInvoice
, DeactivateUser
).
Deve contenere solo le proprietĂ che rappresentano le informazioni essenziali per eseguire l'operazione.
Caratteristiche di un Command
- ImmutabilitĂ : implementato come un
record
o una classe con sologet
e senza setter pubblici. - Nome all’imperativo: riflette l’azione che sarà eseguita, per esempio
CreateOrder
,UpdateCustomerAddress
. - Dati minimi: contiene solo i campi necessari per eseguire l’operazione, senza restituire grandi quantità di dati.
- Validazione isolata: ogni Command possiede regole di validazione proprie, garantendo che sia coerente prima di arrivare al gestore (Handler).
Validator dei Command
I Command Validators assicurano che il Command sia ben formato e rispetti i requisiti di business prima di essere inviato all’Handler. In Lino utilizziamo la libreria FluentValidation per implementare queste validazioni, perché è ampiamente adottata nei progetti .NET e offre una Fluent API per la creazione delle regole.
Regole comuni di validazione
NotEmpty
,NotNull
per campi obbligatori.InclusiveBetween
per intervalli numerici (es.: valori monetari, quantitĂ minima/massima).MaximumLength
eMinimumLength
per la lunghezza delle stringhe.RuleForEach
per la validazione degli elementi nelle collezioni (List<T>
).Must
per regole personalizzate (es.: formato documenti, validazione date).
Handler dei Command
Il Command Handler è responsabile di eseguire la logica di dominio associata a quel Command. Orchestri repository, unità di lavoro (IUnitOfWork
), servizi ausiliari e genera eventi di dominio quando necessario.
Pattern di implementazione di un Handler
- Ricevere le dipendenze (repository, servizi esterni, UnitOfWork) tramite dependency injection.
- Mappe e istanzi le entitĂ di dominio, garantendo la coerenza iniziale.
- Applichi le regole di business (validazioni aggiuntive, calcolo dei valori, generazione di eventi di dominio).
- Persisti le modifiche tramite
IUnitOfWork
o repository diretti. - Ritorni un Command Result che indica successo o fallimento (
Result<T>
).
Risultati dei Command e Result Pattern
Il Command Result deve essere un DTO semplice contenente solo i dati minimi necessari affinché il chiamante (es., API o front-end) possa proseguire. Generalmente include l’identificatore dell’entità creata o aggiornata (Id
) e, in caso di fallimento, informazioni standardizzate sull’errore.
Per scenari in cui il Command può fallire, utilizziamo il Result Pattern: un’astrazione che incapsula il risultato di un’operazione, che può essere successo o fallimento. In .NET è comune usare tipi come Result<T>
(o librerie simili), che:
- Permettono di definire un
Value
in caso di successo (es., un DTO semplice). - In caso di fallimento, memorizzano codici o messaggi standardizzati (
Error
), possibilmente con metadati extra (codici HTTP, dettagli di validazione, ecc.). - Facilitano l’unificazione del flusso di gestione degli errori in tutta l’API, mantenendo coerenza tra i livelli.
Creare un Command con il CLI
Lino semplifica la generazione di tutti gli artefatti necessari per un nuovo Command tramite il comando:
lino command new
Al lancio di questo comando, l’assistente interattivo chiederà :
- Servizio — Nome del servizio in cui verrà creato il Command.
- Modulo — Nei servizi modulari, definisce il modulo di destinazione.
- Entità — Entità di dominio correlata al Command (opzionale ma consigliata).
- Nome del Command — Nome finale del Command (es.,
CreateOrder
).
Dopo la conferma, Lino creerĂ automaticamente i file:
CreateOrderCommand.cs
CreateOrderCommandValidator.cs
CreateOrderCommandHandler.cs
CreateOrderCommandResult.cs
Esempio di struttura generata
Considera il Command CreatePerson
. La struttura generata sarĂ simile a:
MyApp/ └── src/ └── Services/ └── MyService/ └── Application/ ├── MyApp.MyService.Application.csproj └── UseCases/ └── People/ ├── Commands/ │ └── CreatePerson/ │ ├── CreatePersonCommand.cs │ ├── CreatePersonCommandValidator.cs │ ├── CreatePersonCommandHandler.cs │ └── CreatePersonCommandResult.cs └── Queries/ └── ...
Query
Una Query rappresenta l’intento di ottenere dati senza modificare lo stato del dominio. Le Query sono progettate per essere efficienti in lettura, restituendo esattamente i campi necessari, senza caricare intere entità se non richiesto.
Caratteristiche di una Query
- Immutabile — come i Comandi, una Query non deve essere modificabile dopo la sua creazione.
- Nome descrittivo — riflette le informazioni ricercate, ad esempio:
GetCustomerById
,ListOrdersByDateRange
. - Filtri e paginazione — può accettare parametri per ricerche specifiche (date, stato, pagina, ordinamento).
- Proiezione — deve restituire un DTO o view model con solo i campi necessari, evitando di caricare oggetti dominio completi.
Validatori di Query
I Validatori di Query sono responsabili della validazione dei parametri di input, come i filtri, i valori di paginazione (page, pageSize) e le regole di sicurezza (permessi utente, visibilità dei dati). Sono anch’essi implementati con FluentValidation.
Regole comuni di validazione nelle Query
GreaterThanOrEqualTo
oLessThanOrEqualTo
— per filtri di intervallo (es.: date).Length
— per filtri di testo (es.: ricerca per nome o email).InclusiveBetween
— per parametri di paginazione (es.: numero minimo/massimo di elementi per pagina).
Handler di Query
L’Handler di Query è responsabile dell’interrogazione dei repository o del contesto del database e del ritorno di proiezioni ottimizzate, evitando di caricare completamente le entità di dominio.
In Lino, si raccomanda l’uso di proiezioni con metodi come .Select()
per mappare direttamente i dati nel formato previsto dal DTO, garantendo efficienza e migliori prestazioni.
Risultati delle Query
In Lino, i risultati delle query sono sempre rappresentati da record con il suffisso QueryResult
.
Questa convenzione si applica indipendentemente dal tipo di risposta — che si tratti di una lista di elementi (con o senza paginazione) o di un singolo elemento dettagliato.
Creare una Query con il CLI
Come per i Comandi, Lino mette a disposizione il comando:
lino query new
La procedura guidata richiederĂ :
- Servizio — il servizio in cui sarà creata la Query.
- Modulo — se applicabile, definisce il modulo di dominio.
- Entità — l'entità o aggregato a cui la Query è associata (opzionale).
- Nome della Query — ad esempio:
GetOrderById
oListProductsByCategory
.
Lino genererĂ automaticamente:
GetOrderByIdQuery.cs
GetOrderByIdQueryValidator.cs
GetOrderByIdQueryHandler.cs
GetOrderByIdQueryResult.cs
Esempio di struttura generata per le Query
MyApp/ └── src/ └── Services/ └── MyService/ └── Application/ ├── MyApp.MyService.Application.csproj └── UseCases/ └── Orders/ ├── Commands/ | └── ... └── Queries/ └── GetOrderById/ ├── GetOrderByIdQuery.cs ├── GetOrderByIdQueryValidator.cs ├── GetOrderByIdQueryHandler.cs └── GetOrderByIdQueryResult.cs
Conclusione
Dopo aver compreso come definire e strutturare i Use Cases (Commands e Queries) in Lino, sei pronto a creare scenari di business robusti e scalabili, segmentando chiaramente la logica di scrittura e lettura. Usa il CLI per accelerare lo sviluppo, ma rivedi e adatta sempre le implementazioni in base alle esigenze specifiche del tuo dominio.
Successivamente, esplora come gestire la persistenza, gli eventi di dominio e i servizi di infrastruttura per comporre l’intera architettura della tua applicazione.