Definizione dei Casi d'Uso dell'Applicazione
I casi d'uso sono i punti di ingresso del livello applicativo che trasformano un'intenzione di business in software eseguibile. In Lino, si trovano in Application/UseCases e sono organizzati attorno a entità, aggregates, enumerazioni, moduli e servizi definiti nelle fasi precedenti del progetto.
Lino segue una struttura orientata a CQRS: Commands rappresentano operazioni che modificano lo stato, mentre Queries rappresentano operazioni che leggono dati. Questa separazione mantiene esplicite e più facili da mantenere le regole di scrittura, i modelli di lettura, le validazioni, il tracing, il logging e i contratti di risposta.
Il Result Pattern standardizza il ritorno delle operazioni, incapsulando successo, fallimento, messaggi di errore e dati risultanti. Invece di usare eccezioni per flussi di business attesi, il handler restituisce un Result o Result<T> con valore, errore o no-content a seconda del caso.
Il CLI genera i file iniziali di un caso d'uso, ma il codice generato non sostituisce l'analisi del dominio. Lo sviluppatore deve comunque rivedere il handler, confermare le proprietà selezionate, aggiungere regole di business, adeguare le validazioni e verificare che il caso d'uso rispetti i limiti del modulo e del servizio.
Importante: i progetti Lino possono essere creati con supporto a CQRS e mediator. Commands e Queries generati usano astrazioni applicative come ICommand, IQuery, ICommandHandler, IQueryHandler e Tolitech.Results per standardizzare la gestione delle richieste e dei risultati delle operazioni.
Panoramica dei Use Cases
Un Use Case rappresenta un'operazione completa dell'applicazione: riceve un modello di input, valida i dati, coordina le dipendenze, invoca il comportamento di dominio quando necessario, persiste o consulta dati e restituisce un risultato standardizzato. È il punto in cui l'applicazione orchestra un flusso di business senza spostare invarianti fuori dal modello di dominio.
Nelle soluzioni generate da Lino, i use cases sono raggruppati nel progetto applicativo usando una struttura prevedibile:
src/Services/<ServiceName>/<ModuleName>/Application/UseCases/<EntityName>/
├── Commands/
│ └── <CommandName>/
└── Queries/
└── <QueryName>/
Questa organizzazione rende chiaro a quale servizio, modulo ed entità appartiene ogni feature. Mantiene inoltre indipendenti il modello di scrittura e il modello di lettura, cosa utile quando una schermata, API, integrazione o processo in background ha bisogno solo di un lato del comportamento.
Command o Query?
| Tipo | Intenzione | Esempi | Risultato comune |
|---|---|---|---|
| Command | Modificare lo stato del sistema. | CreateOrder, UpdateVehicle, DeleteMaintenance, SavePermissionsByRoleId. |
Result, Result<CommandResult> o no-content in caso di successo. |
| Query | Leggere dati senza modificare lo stato. | GetOrderById, ListCustomers, GetVehicleAvailability. |
Result<QueryResult>, lista, pagina o DTO specifico per il consumer. |
Cosa appartiene al caso d'uso applicativo
- Contratto di input: il record di command o query con solo i dati necessari per l'operazione.
- Validazione: regole che verificano formato della richiesta, campi obbligatori, intervalli, identificatori, filtri e altri vincoli di input prima che il handler esegua il flusso.
- Orchestrazione del handler: chiamate a dipendenze, accesso a repository, query al contesto, uso di Unit of Work, composizione del risultato, logging e tracing.
- Contratto di risultato: un piccolo oggetto di risposta o un risultato senza contenuto che consegna al caller esattamente ciò di cui ha bisogno per continuare.
Cosa deve restare fuori dal caso d'uso
- Invarianti di dominio devono restare in entità, Value Objects, domain services e metodi di dominio.
- Dettagli di infrastruttura devono restare dietro astrazioni, repository, contesti di database, servizi di file, integrazioni di messaggistica e clients esterni.
- Preoccupazioni di presentazione, come stato della UI, labels, layouts e comportamento dei componenti, devono restare nel livello web app.
Un buon use case è esplicito, abbastanza piccolo da essere compreso e rigoroso sui confini: coordina il lavoro, ma non diventa un posto in cui tutte le regole del sistema vengono mescolate.
Commands
Un Command è un messaggio immutabile che trasporta solo i dati necessari per eseguire un'azione che modifica lo stato del sistema. Esempi comuni sono CreateCustomer, UpdateVehicle, DeleteMaintenance, ConfirmOrder e SavePermissionsByRoleId. I Commands devono essere nominati come azioni perché dicono all'applicazione di fare qualcosa.
In Lino, i Commands generati sono records che implementano l'astrazione di command e restituiscono un risultato standardizzato. I Commands di creazione normalmente restituiscono un oggetto di risultato con l'identificatore o l'informazione minima richiesta dal caller, mentre i commands di aggiornamento ed eliminazione di solito restituiscono no-content quando l'operazione viene conclusa con successo.
Caratteristiche di un Command
- Immutabilità: implementato come
recordo classe solo conget, senza setters pubblici mutabili. - Nome all'imperativo: riflette l'azione di business, come
CreateOrder,UpdateCustomerAddressoChangeProductPrice. - Dati minimi: contiene solo i campi necessari per eseguire l'operazione, senza trasportare entità intere o grandi volumi di dati.
- Validazione isolata: ogni Command ha regole proprie per garantire che la richiesta sia coerente prima di arrivare al handler.
- Indipendenza dalla UI: il Command rappresenta l'intenzione dell'applicazione, non il pulsante, form o componente che ha avviato l'operazione.
Quando creare un Command
- Usa un Command quando i dati saranno creati, modificati, rimossi, associati, importati, approvati, annullati o mutati in qualsiasi forma.
- Mantieni il payload focalizzato sull'operazione; non passare un'entità intera quando pochi campi sono sufficienti.
- Preferisci nomi che descrivono l'azione di business, non l'evento visuale che l'ha avviata.
- Verifica se l'azione appartiene al modulo corrente o se dovrebbe essere espressa da integrazione, evento o shadow entity in un altro modulo.
Command Validators
I Command Validators garantiscono che il Command sia ben formato e soddisfi i requisiti di input prima di essere inviato al handler. In Lino, queste validazioni sono implementate con FluentValidation, una libreria comune nei progetti .NET perché offre un'API fluente e leggibile per creare regole.
Regole comuni di validazione
NotEmptyeNotNullper campi obbligatori.InclusiveBetweenper intervalli numerici, valori monetari, percentuali, quantità minime e massime.MaximumLength,MinimumLengtheLengthper la lunghezza delle strings.RuleForEachper validare elementi di collections, comeList<T>.Mustper regole custom, come formato di documenti, coerenza di date e combinazioni di campi non valide.
Il validator protegge il bordo di input. Le invarianti reali del dominio, come limiti di stato, transizioni consentite e regole che devono valere indipendentemente dal caller, continuano ad appartenere ad aggregate, entità, Value Object o domain service.
Command Handlers
Il Command Handler esegue la logica applicativa associata al Command. Orchestra repository, contesti, IUnitOfWork, servizi ausiliari, logging, tracing, chiamate di integrazione ed eventi di dominio quando necessario.
Pattern di implementazione di un handler
- Ricevere dipendenze tramite dependency injection, come repository, servizi esterni, contesti e Unit of Work.
- Validare l'esistenza e la disponibilità dei dati necessari per l'operazione.
- Caricare l'aggregate o l'entità corretta quando l'operazione dipende da stato esistente.
- Chiamare metodi di dominio invece di duplicare invarianti nel handler.
- Persistire modifiche tramite
IUnitOfWorko astrazioni di persistenza del progetto. - Registrare eventi di dominio, Outbox o integrazioni quando l'operazione deve comunicare con altre parti del sistema.
- Restituire
ResultoResult<T>con successo, fallimento noto o dati minimi di risposta.
Command Results e Result Pattern
Il Command Result deve essere un DTO semplice con solo i dati necessari affinché il caller possa proseguire. In creazione, di solito include l'Id generato. In aggiornamento o eliminazione, può non esserci payload. Quando l'operazione fallisce per una condizione attesa, il ritorno deve trasportare informazioni di errore standardizzate.
Il Result Pattern incapsula il risultato di un'operazione come successo o fallimento. Invece di lanciare eccezioni per scenari prevedibili, come entità non trovata, stato non valido o validazione di business, il handler restituisce un tipo come Result<T> con valore, errore e metadati quando necessario.
- In caso di successo, il risultato può esporre un
Value, come un piccolo DTO o identificatore creato. - In caso di fallimento, il risultato archivia codici o messaggi standardizzati, spesso definiti in error definitions riutilizzabili.
- Il flusso di gestione degli errori resta coerente tra Application, API, typed clients e UI.
Creazione di un Command con il CLI
Lino semplifica la generazione degli artefatti necessari per un nuovo Command tramite il comando:
lino command new
Il CLI accetta anche opzioni per ridurre le domande nell'assistente:
lino command new --service <ServiceName> --module <ModuleName> --entity <EntityName> --name <CommandName> lino command new --name <CommandName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino command list --service <ServiceName> --module <ModuleName> --entity <EntityName>
-so--service: servizio di destinazione.-mo--module: modulo di destinazione in servizi modularizzati.-eo--entity: entità associata al Command.-n,--name,-co--command: nome del Command.
Durante il flusso interattivo, Lino conferma servizio, modulo, entità, nome del Command, tipo del Command e, per operazioni di creazione o aggiornamento, le proprietà che faranno parte della richiesta. I tipi esposti dal CLI sono Create, Update e Delete.
Dopo la conferma, Lino crea file come:
CreateOrderCommand.csCreateOrderCommandValidator.csCreateOrderCommandHandler.csCreateOrderCommandResult.cs
Esempio di struttura generata
Considera il Command CreatePerson. La struttura generata sarà simile a:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── People/
├── Commands/
│ └── CreatePerson/
│ ├── CreatePersonCommand.cs
│ ├── CreatePersonCommandValidator.cs
│ ├── CreatePersonCommandHandler.cs
│ └── CreatePersonCommandResult.cs
└── Queries/
└── ...
Per un Command custom di flotta, la struttura segue lo stesso pattern:
Application/UseCases/Vehicles/Commands/UpdateVehicle/ ├── UpdateVehicleCommand.cs ├── UpdateVehicleCommandValidator.cs ├── UpdateVehicleCommandHandler.cs └── UpdateVehicleCommandResult.cs
Responsabilità dei file di Command
- Command: contratto immutabile di richiesta con l'input dell'operazione.
- Validator: validazione di input, normalmente con regole di obbligatorietà, lunghezza, intervallo, identificatori e collections.
- Handler: orchestrazione di repository, Unit of Work, contesti, metodi di dominio, logging, tracing e creazione del risultato.
- Result: contratto minimo di risposta per operazioni riuscite che devono restituire dati.
Checklist di implementazione
- Esegui
lino command newe scegli servizio, modulo, entità, tipo e proprietà corrette. - Apri il Command generato e rimuovi qualsiasi campo che non faccia parte del contratto dell'operazione.
- Rafforza il validator con regole di input di business che non possono essere inferite automaticamente.
- Rivedi il handler e assicurati che chiami comportamento di dominio invece di duplicare invarianti nel livello applicativo.
- Usa
IUnitOfWorkper la persistenza e preferisci il salvataggio transazionale quando l'operazione richiede anche eventi o Outbox affidabile. - Restituisci fallimenti tramite errori di
Result, non tramite eccezioni per risultati di business attesi. - Compila e testa l'endpoint, pagina o integrazione che chiamerà il Command.
Regola pratica: il generatore consegna lo scheletro architetturale. La correttezza finale deriva dalla revisione del caso d'uso rispetto al linguaggio del dominio, invarianti, esigenze di persistenza e confini del modulo.
Queries
Una Query rappresenta l'intenzione di ottenere dati senza modificare lo stato del dominio. Le Queries sono progettate per una lettura efficiente, restituendo esattamente i campi necessari al caller, senza caricare entità intere quando non è necessario.
Esempi comuni sono GetCustomerById, ListCustomers, ListPhoneTypes, ListOrdersByDateRange e una query custom come GetVehicleAvailability. Una Query deve rispondere a una domanda specifica dell'applicazione.
Caratteristiche di una Query
- Immutabile: come i Commands, una Query non deve consentire modifiche dopo essere stata creata.
- Nome descrittivo: riflette l'informazione cercata, come
GetCustomerByIdoListOrdersByDateRange. - Filtri e paginazione: può trasportare date, status, pagina, dimensione pagina, testo di ricerca, ordinamento e altri parametri di lettura.
- Proiezione: deve restituire DTO o view model con solo i campi necessari, evitando l'esposizione diretta di entità di dominio.
- Senza effetti collaterali: non deve chiamare metodi che modificano stato né salvare modifiche nel database.
Quando creare una Query
- Usa una Query quando l'operazione legge dati e non deve modificare stato.
- Usa una Query a risultato singolo per schermate di dettaglio, ricerche per identificatore, controlli di disponibilità o risposte di un oggetto.
- Usa una Query di lista per grids, liste di opzioni, collections figlie e controlli di selezione.
- Usa paginazione per grids e insiemi di dati potenzialmente grandi.
- Usa liste semplici per dati di riferimento piccoli, enumerazioni ed endpoints di opzioni.
Query Validators
I Query Validators validano parametri di input come filtri, valori di paginazione, intervalli di data, identificatori e regole di visibilità. Sono implementati anch'essi con FluentValidation.
Regole comuni di validazione nelle Queries
GreaterThanOrEqualToeLessThanOrEqualToper filtri di intervallo, come data iniziale e finale.Length,MaximumLengtheMinimumLengthper filtri di testo, come nome, e-mail o termine di ricerca.InclusiveBetweenper paginazione, limitandopageepageSize.NotEmptyper identificatori obbligatori in query di dettaglio.Mustper combinazioni di filtri non valide, come data finale minore della data iniziale.
Query Handlers
Il Query Handler consulta repository o il contesto del database e restituisce proiezioni ottimizzate. In Lino, si raccomanda di usare proiezioni con Select, filtri espliciti, ordinamento prevedibile e letture senza tracking quando appropriato.
Il handler di Query deve essere read-only: non chiama metodi di dominio che modificano stato, non genera modifiche persistenti e non esegue SaveChanges. Se una lettura deve registrare audit, avviare un'integrazione o ricalcolare stato, questo di solito indica un altro use case o processo separato.
Query Results
In Lino, i risultati delle Queries sono rappresentati da records nominati con il suffisso QueryResult. Questa convenzione vale per risposta singola, lista semplice, lista paginata o DTO specifico per schermata, API, integrazione o processo in background.
I risultati di query devono essere contratti stabili di lettura. Trattali come DTOs progettati per il consumer, non come scorciatoia per esporre direttamente entità di dominio.
Creazione di una Query con il CLI
In modo simile ai Commands, Lino mette a disposizione il comando:
lino query new
Il CLI accetta anche opzioni come:
lino query new --service <ServiceName> --module <ModuleName> --entity <EntityName> --name <QueryName> lino query new --name <QueryName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino query list --service <ServiceName> --module <ModuleName> --entity <EntityName>
-so--service: servizio di destinazione.-mo--module: modulo di destinazione.-eo--entity: entità associata alla Query.-n,--name,-qo--query: nome della Query.
Durante il flusso interattivo, Lino chiede se la Query restituisce un risultato singolo o una lista. Quando la risposta è una lista, chiede anche se deve essere paginata. Infine, consente di selezionare le proprietà che devono essere restituite o usate dalla proiezione generata.
Lino genererà automaticamente:
GetOrderByIdQuery.csGetOrderByIdQueryValidator.csGetOrderByIdQueryHandler.csGetOrderByIdQueryResult.cs
Esempio di struttura generata per Queries
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Orders/
├── Commands/
| └── ...
└── Queries/
└── GetOrderById/
├── GetOrderByIdQuery.cs
├── GetOrderByIdQueryValidator.cs
├── GetOrderByIdQueryHandler.cs
└── GetOrderByIdQueryResult.cs
Per una Query custom di disponibilità del veicolo, la struttura segue lo stesso pattern:
Application/UseCases/Vehicles/Queries/GetVehicleAvailability/ ├── GetVehicleAvailabilityQuery.cs ├── GetVehicleAvailabilityQueryValidator.cs ├── GetVehicleAvailabilityQueryHandler.cs └── GetVehicleAvailabilityQueryResult.cs
Responsabilità dei file di Query
- Query: contratto immutabile di richiesta con identificatori, filtri, ordinamento, paginazione o intervalli di data.
- Validator: validazione di identificatori, paginazione, filtri obbligatori, intervalli di data e criteri di ricerca.
- Handler: orchestrazione di lettura usando contesto applicativo, proiezioni, filtri, ordinamento, paginazione, logging, tracing e creazione del risultato.
- Result: DTO di risposta modellato per il caller, spesso con records interni per elementi di lista.
Checklist di implementazione
- Esegui
lino query newe scegli servizio, modulo, entità, nome della Query, tipo di ritorno, modalità di paginazione e proprietà. - Rivedi il contratto di richiesta e mantieni solo i filtri davvero necessari al caller.
- Rafforza il validator, specialmente per identificatori obbligatori, intervalli di data, limiti di page size e combinazioni di filtri non valide.
- Adegua la proiezione del handler per restituire solo i campi richiesti dalla UI, API, integrazione o processo in background.
- Mantieni la Query read-only: non chiamare metodi di dominio che mutano stato e non salvare modifiche nel handler.
- Propaga fallimenti attesi tramite
Result, usando errori standardizzati esistenti quando appropriato. - Compila e testa il consumer che chiama la Query, come endpoint API, pagina Blazor, integrazione in-process o typed client.
Esempio di Query custom
Nel flusso di integrazione tra moduli di un SaaS, una Query custom di disponibilità del veicolo può essere creata con lino query new. La struttura generata fornisce Query, Handler, Result e Validator. Poi lo sviluppatore aggiunge gli input necessari, come identificatore del veicolo, data iniziale e data finale, valida che l'intervallo sia corretto e implementa la logica del handler. Questo è il flusso atteso: generare prima la struttura coerente e poi completare il comportamento specifico del dominio con codice esplicito.
Orientamento di read model: i risultati di Query devono essere contratti stabili. Trattali come DTOs progettati per il caller, non come scorciatoia per esporre direttamente entità di dominio.
Conclusione
Definire use cases in Lino significa trasformare il modello di dominio in operazioni applicative chiare. Commands gestiscono modifiche di stato, Queries gestiscono letture, validators proteggono il bordo di input, handlers orchestrano il flusso e gli oggetti di risultato standardizzano l'output.
Il flusso più rapido di solito è: modellare il dominio, generare i Commands e le Queries necessari, rivedere i file generati, completare il comportamento di business, esporre il caso d'uso tramite un'API o una pagina quando necessario e validare tutto con build e test. Questo mantiene lo sviluppo rapido senza perdere controllo architetturale.
Man mano che il progetto cresce, mantieni ogni use case focalizzato su un'intenzione di business, rispetta i confini di servizio e modulo e usa eventi, integrazioni o shadow entities quando un altro modulo deve partecipare al processo.
