Defining Application Use Cases
In this section, we will explore how to define your application's use cases using the Lino CLI. Use cases represent specific interactions between users or external systems and the application, encapsulating business rules and promoting a domain-driven architecture. We will cover the separation of read and write operations through the CQRS pattern, as well as standardizing response returns using the Result Pattern.
The CQRS (Command Query Responsibility Segregation) pattern proposes separating operations that modify the application's state (Commands) from those that only query data (Queries). This approach allows optimizing each type of operation independently, improving scalability, performance, and system maintainability.
The Result Pattern is used to standardize the return of operations, encapsulating information about success or failure, error messages, and resulting data. This facilitates consistent response handling across different layers of the application.
We will see how Commands and Queries files are structured, data validations, how domain logic is applied in Handlers, and how to return results in a standardized way. All of this is achieved through automatic creation of these components via the Lino CLI.
Note: Although not mandatory, Lino currently offers two options to apply the CQRS pattern at the application layer: using the Mediator pattern through the MediatR
library (by Jimmy Bogard) or the Mediator
library (by Martin Othamar).
Overview of Use Cases
A Use Case represents a complete interaction between users or external systems and the application, describing specific business scenarios. In Lino, each Use Case is divided into:
- Command: represents the intent to modify the system state (create, update, delete, etc.).
- Query: represents the intent to query data without changing the domain state.
This separation promotes code clarity, facilitates testing, allows independent scalability for reading and writing, and aligns with clean architecture principles and best practices of Domain-Driven Design (DDD).
Commands
A Command is an immutable message that carries only the data necessary to perform an action that changes the system state (for example, CreateInvoice
, DeactivateUser
).
It should contain only the properties that represent the essential information needed to perform the operation.
Characteristics of a Command
- Immutability: implemented as a
record
or a class with onlyget
accessors, without public setters. - Imperative naming: reflects the action to be performed, such as
CreateOrder
,UpdateCustomerAddress
. - Minimal data: contains only the fields necessary to perform the operation, without returning large volumes of data.
- Isolated validation: each Command has its own validation rules, ensuring it is consistent before reaching the handler (Handler).
Command Validators
Command Validators ensure that the Command is well-formed and meets business requirements before being sent to the Handler. In Lino, we use the FluentValidation library to implement these validations, as it is widely adopted in .NET projects and provides a fluent API for creating rules.
Common Validation Rules
NotEmpty
,NotNull
for required fields.InclusiveBetween
for numeric ranges (e.g., monetary values, minimum/maximum quantities).MaximumLength
andMinimumLength
for string length constraints.RuleForEach
for validating items in collections (List<T>
).Must
for custom rules (e.g., document format, date validation).
Command Handlers
The Command Handler is responsible for executing the domain logic associated with that Command. It orchestrates repositories, units of work (IUnitOfWork
), auxiliary services, and triggers domain events when necessary.
Handler Implementation Pattern
- Receive dependencies (repositories, external services, UnitOfWork) via dependency injection.
- Map and instantiate domain entities, ensuring initial consistency.
- Apply business rules (additional validations, value calculations, triggering domain events).
- Persist changes through
IUnitOfWork
or direct repositories. - Return a Command Result indicating success or failure (
Result<T>
).
Command Results and Result Pattern
The Command Result should be a simple DTO containing only the minimal data necessary for the caller (e.g., an API or front-end) to proceed. It typically includes the identifier of the created or updated entity (Id
) and, if a failure occurs, standardized error information.
For scenarios where a Command may fail, we use the Result Pattern: an abstraction that encapsulates the outcome of an operation, which can be success or failure. In .NET, it is common to use types like Result<T>
(or similar libraries), which:
- Allow defining a
Value
in case of success (e.g., a simple DTO). - In case of failure, store standardized codes or messages (
Error
), possibly with extra metadata (HTTP codes, validation details, etc.). - Facilitate unifying the error handling flow across the API, maintaining consistency between layers.
Creating a Command with the CLI
Lino simplifies generating all artifacts necessary for a new Command through the command:
lino command new
When running this command, the interactive assistant will ask for:
- Service β The name of the service where the Command will be created.
- Module β In modularized services, specifies the target module.
- Entity β The domain entity related to the Command (optional but recommended).
- Command Name β The final name of the Command (e.g.,
CreateOrder
).
After confirmation, Lino will automatically create the files:
CreateOrderCommand.cs
CreateOrderCommandValidator.cs
CreateOrderCommandHandler.cs
CreateOrderCommandResult.cs
Example of Generated Structure
Consider the Command CreatePerson
. The generated structure will be similar to:
MyApp/ βββ src/ βββ Services/ βββ MyService/ βββ Application/ βββ MyApp.MyService.Application.csproj βββ UseCases/ βββ People/ βββ Commands/ β βββ CreatePerson/ β βββ CreatePersonCommand.cs β βββ CreatePersonCommandValidator.cs β βββ CreatePersonCommandHandler.cs β βββ CreatePersonCommandResult.cs βββ Queries/ βββ ...
Queries
A Query represents the intent to retrieve data without changing the state of the domain. Queries are designed to be read-efficient, returning exactly the required fields without loading entire entities when not requested.
Characteristics of a Query
- Immutable β similar to Commands, a Query should not allow modifications after creation.
- Descriptive Name β reflects the information being sought, for example,
GetCustomerById
,ListOrdersByDateRange
. - Filters and Pagination β can carry parameters to apply specific searches (dates, status, page, sorting).
- Projection β should return a DTO or view model with only the necessary fields, avoiding loading full domain objects.
Query Validators
Query Validators are responsible for validating input parameters such as filters, pagination values (page, pageSize), and security rules (user permissions, data visibility). They are also implemented using FluentValidation.
Common Validation Rules in Queries
GreaterThanOrEqualTo
orLessThanOrEqualTo
for range filters (e.g., dates).Length
for text filters (e.g., search by name, email).InclusiveBetween
for pagination parameters (e.g., minimum/maximum number of items per page).
Query Handlers
The Query Handler is responsible for querying repositories or the database context and returning optimized projections, avoiding loading complete domain entities.
In Lino, it is recommended to use projections with methods like .Select()
to map data directly to the expected DTO format, ensuring efficiency and better performance.
Query Results
In Lino, query results are always represented by named records with the suffix QueryResult
.
This convention applies regardless of the response type β whether a list of items (with or without pagination) or a single detailed item.
Creating a Query with the CLI
Similar to Commands, Lino provides the command:
lino query new
The wizard will prompt for:
- Service β The service where the Query will be created.
- Module β When applicable, defines the domain module.
- Entity β The entity or aggregate to which the Query is associated (optional).
- Query Name β For example,
GetOrderById
orListProductsByCategory
.
Lino will automatically generate:
GetOrderByIdQuery.cs
GetOrderByIdQueryValidator.cs
GetOrderByIdQueryHandler.cs
GetOrderByIdQueryResult.cs
Example of Generated Structure for Queries
MyApp/ βββ src/ βββ Services/ βββ MyService/ βββ Application/ βββ MyApp.MyService.Application.csproj βββ UseCases/ βββ Orders/ βββ Commands/ | βββ ... βββ Queries/ βββ GetOrderById/ βββ GetOrderByIdQuery.cs βββ GetOrderByIdQueryValidator.cs βββ GetOrderByIdQueryHandler.cs βββ GetOrderByIdQueryResult.cs
Conclusion
After understanding how to define and structure Use Cases (Commands and Queries) in Lino, you are ready to create robust and scalable business scenarios by clearly separating write and read logic. Use the CLI to speed up development, but always review and adjust implementations according to your domainβs specific needs.
Next, explore how to handle Persistence, Domain Events, and Infrastructure Services to compose the entire architecture of your application.