Definiendo Casos de Uso de la Aplicación
Los casos de uso son los puntos de entrada de la capa de aplicación que transforman una intención de negocio en software ejecutable. En Lino, quedan en Application/UseCases y se organizan alrededor de las entidades, aggregates, enumeraciones, módulos y servicios definidos en las etapas anteriores del proyecto.
Lino sigue una estructura orientada a CQRS: Commands representan operaciones que alteran estado, mientras que Queries representan operaciones que leen datos. Esta separación mantiene reglas de escritura, modelos de lectura, validaciones, rastreo, logging y contratos de respuesta explícitos y más fáciles de mantener.
El Result Pattern estandariza el retorno de las operaciones, encapsulando éxito, fallo, mensajes de error y datos resultantes. En vez de usar excepciones para flujos esperados de negocio, el handler devuelve un Result o Result<T> con valor, error o no-content según el caso.
El CLI genera los archivos iniciales de un caso de uso, pero el código generado no sustituye el análisis del dominio. El desarrollador todavía debe revisar el handler, confirmar las propiedades seleccionadas, agregar reglas de negocio, ajustar validaciones y verificar si el caso de uso respeta los límites del módulo y del servicio.
Importante: los proyectos Lino pueden crearse con soporte para CQRS y mediator. Los Commands y Queries generados usan abstracciones de aplicación como ICommand, IQuery, ICommandHandler, IQueryHandler y Tolitech.Results para estandarizar el tratamiento de solicitudes y resultados de operaciones.
Visión General de Use Cases
Un Use Case representa una operación completa de la aplicación: recibe un modelo de entrada, valida los datos, coordina dependencias, invoca comportamiento de dominio cuando es necesario, persiste o consulta datos y devuelve un resultado estandarizado. Es el lugar donde la aplicación orquesta un flujo de negocio sin mover invariantes fuera del modelo de dominio.
En soluciones generadas por Lino, los use cases se agrupan en el proyecto de aplicación usando una estructura previsible:
src/Services/<ServiceName>/<ModuleName>/Application/UseCases/<EntityName>/
├── Commands/
│ └── <CommandName>/
└── Queries/
└── <QueryName>/
Esta organización deja claro a qué servicio, módulo y entidad pertenece cada feature. También mantiene el modelo de escritura y el modelo de lectura independientes, lo que es útil cuando una pantalla, API, integración o proceso en background necesita solo un lado del comportamiento.
Command o Query?
| Tipo | Intención | Ejemplos | Resultado común |
|---|---|---|---|
| Command | Alterar el estado del sistema. | CreateOrder, UpdateVehicle, DeleteMaintenance, SavePermissionsByRoleId. |
Result, Result<CommandResult> o no-content en caso de éxito. |
| Query | Leer datos sin alterar estado. | GetOrderById, ListCustomers, GetVehicleAvailability. |
Result<QueryResult>, lista, página o DTO específico para el consumidor. |
Qué pertenece al caso de uso de la aplicación
- Contrato de entrada: el record de command o query con solo los datos necesarios para la operación.
- Validación: reglas que verifican formato de la solicitud, campos obligatorios, intervalos, identificadores, filtros y otras restricciones de entrada antes de que el handler ejecute el flujo.
- Orquestación del handler: llamadas a dependencias, acceso a repositorios, consultas al contexto, uso de Unit of Work, composición de resultado, logging y tracing.
- Contrato de resultado: un objeto pequeño de respuesta o un resultado sin contenido que entrega al caller exactamente lo que necesita para continuar.
Qué debe quedar fuera del caso de uso
- Las invariantes de dominio deben permanecer en entidades, Value Objects, domain services y métodos de dominio.
- Los detalles de infraestructura deben permanecer detrás de abstracciones, repositorios, contextos de base de datos, servicios de archivos, integraciones de mensajería y clients externos.
- Las preocupaciones de presentación, como estado de UI, labels, layouts y comportamiento de componentes, deben permanecer en la capa de web app.
Un buen use case es explícito, lo suficientemente pequeño para entenderse y riguroso con las fronteras: coordina el trabajo, pero no se convierte en un lugar donde se mezclan todas las reglas del sistema.
Commands
Un Command es un mensaje inmutable que transporta solo los datos necesarios para ejecutar una acción que modifica el estado del sistema. Ejemplos comunes son CreateCustomer, UpdateVehicle, DeleteMaintenance, ConfirmOrder y SavePermissionsByRoleId. Los Commands deben nombrarse como acciones porque le indican a la aplicación que haga algo.
En Lino, los Commands generados son records que implementan la abstracción de command y devuelven un resultado estandarizado. Los Commands de creación normalmente devuelven un objeto de resultado con el identificador o la información mínima requerida por el caller, mientras que los commands de actualización y eliminación generalmente devuelven no-content cuando la operación se completa correctamente.
Características de un Command
- Inmutabilidad: implementado como
recordo clase solo conget, sin setters públicos mutables. - Nombre en imperativo: refleja la acción de negocio, como
CreateOrder,UpdateCustomerAddressoChangeProductPrice. - Datos mínimos: contiene solamente los campos necesarios para ejecutar la operación, sin transportar entidades completas ni grandes volúmenes de datos.
- Validación aislada: cada Command tiene reglas propias para garantizar que la solicitud sea consistente antes de llegar al handler.
- Independencia de la UI: el Command representa la intención de la aplicación, no el botón, formulario o componente que disparó la operación.
Cuándo crear un Command
- Use un Command cuando los datos se creen, modifiquen, eliminen, asocien, importen, aprueben, cancelen o muten de cualquier forma.
- Mantenga el payload centrado en la operación; no pase una entidad completa cuando pocos campos son suficientes.
- Prefiera nombres que describan la acción de negocio, no el evento visual que la inició.
- Revise si la acción pertenece al módulo actual o si debería expresarse mediante integración, evento o shadow entity en otro módulo.
Command Validators
Los Command Validators garantizan que el Command esté bien formado y cumpla los requisitos de entrada antes de enviarse al handler. En Lino, estas validaciones se implementan con FluentValidation, una biblioteca común en proyectos .NET porque ofrece una API fluida y legible para crear reglas.
Reglas comunes de validación
NotEmptyyNotNullpara campos obligatorios.InclusiveBetweenpara rangos numéricos, valores monetarios, porcentajes, cantidades mínimas y máximas.MaximumLength,MinimumLengthyLengthpara el tamaño de strings.RuleForEachpara validar elementos de colecciones, comoList<T>.Mustpara reglas personalizadas, como formato de documentos, consistencia de fechas y combinaciones inválidas de campos.
El validator protege el borde de entrada. Las invariantes reales del dominio, como límites de estado, transiciones permitidas y reglas que deben valer independientemente del caller, siguen perteneciendo al aggregate, entidad, Value Object o domain service.
Command Handlers
El Command Handler ejecuta la lógica de aplicación asociada al Command. Orquesta repositorios, contextos, IUnitOfWork, servicios auxiliares, logging, tracing, llamadas de integración y eventos de dominio cuando es necesario.
Patrón de implementación de un handler
- Recibir dependencias por inyección de dependencias, como repositorios, servicios externos, contextos y Unit of Work.
- Validar la existencia y disponibilidad de los datos necesarios para la operación.
- Cargar el aggregate o entidad correcta cuando la operación depende de estado existente.
- Llamar métodos de dominio en vez de duplicar invariantes en el handler.
- Persistir cambios mediante
IUnitOfWorko abstracciones de persistencia del proyecto. - Registrar eventos de dominio, Outbox o integraciones cuando la operación necesite comunicar otras partes del sistema.
- Devolver
ResultoResult<T>con éxito, fallo conocido o datos mínimos de respuesta.
Command Results y Result Pattern
El Command Result debe ser un DTO simple con solo los datos necesarios para que el caller continúe. En creación, suele incluir el Id generado. En actualización o eliminación, puede no haber payload. Cuando la operación falla por una condición esperada, el retorno debe transportar información de error estandarizada.
El Result Pattern encapsula el resultado de una operación como éxito o fallo. En vez de lanzar excepciones para escenarios previsibles, como entidad no encontrada, estado inválido o validación de negocio, el handler devuelve un tipo como Result<T> con valor, error y metadatos cuando es necesario.
- En caso de éxito, el resultado puede exponer un
Value, como un DTO pequeño o identificador creado. - En caso de fallo, el resultado almacena códigos o mensajes estandarizados, frecuentemente definidos en error definitions reutilizables.
- El flujo de tratamiento de errores queda consistente entre Application, API, typed clients y UI.
Creando un Command con el CLI
Lino simplifica la generación de los artefactos necesarios para un nuevo Command mediante el comando:
lino command new
El CLI también acepta opciones para reducir preguntas en el asistente:
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: servicio de destino.-mo--module: módulo de destino en servicios modularizados.-eo--entity: entidad asociada al Command.-n,--name,-co--command: nombre del Command.
Durante el flujo interactivo, Lino confirma servicio, módulo, entidad, nombre del Command, tipo del Command y, para operaciones de creación o actualización, las propiedades que formarán parte de la solicitud. Los tipos expuestos por el CLI son Create, Update y Delete.
Después de confirmar, Lino crea archivos como:
CreateOrderCommand.csCreateOrderCommandValidator.csCreateOrderCommandHandler.csCreateOrderCommandResult.cs
Ejemplo de estructura generada
Considere el Command CreatePerson. La estructura generada será similar a:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── People/
├── Commands/
│ └── CreatePerson/
│ ├── CreatePersonCommand.cs
│ ├── CreatePersonCommandValidator.cs
│ ├── CreatePersonCommandHandler.cs
│ └── CreatePersonCommandResult.cs
└── Queries/
└── ...
Para un Command personalizado de flota, la estructura sigue el mismo patrón:
Application/UseCases/Vehicles/Commands/UpdateVehicle/ ├── UpdateVehicleCommand.cs ├── UpdateVehicleCommandValidator.cs ├── UpdateVehicleCommandHandler.cs └── UpdateVehicleCommandResult.cs
Responsabilidades de los archivos de Command
- Command: contrato inmutable de solicitud con la entrada de la operación.
- Validator: validación de entrada, normalmente con reglas de obligatoriedad, tamaño, rango, identificadores y colecciones.
- Handler: orquestación de repositorios, Unit of Work, contextos, métodos de dominio, logging, tracing y creación del resultado.
- Result: contrato mínimo de respuesta para operaciones exitosas que necesitan devolver datos.
Checklist de implementación
- Ejecute
lino command newy elija servicio, módulo, entidad, tipo y propiedades correctas. - Abra el Command generado y elimine cualquier campo que no forme parte del contrato de la operación.
- Fortalezca el validator con reglas de entrada de negocio que no puedan inferirse automáticamente.
- Revise el handler y garantice que llame comportamiento de dominio en vez de duplicar invariantes en la capa de aplicación.
- Use
IUnitOfWorkpara persistencia y prefiera guardado transaccional cuando la operación también exija eventos u Outbox confiable. - Devuelva fallos mediante errores de
Result, no mediante excepciones para resultados de negocio esperados. - Compile y pruebe el endpoint, página o integración que llamará al Command.
Regla práctica: el generador entrega el esqueleto arquitectónico. La corrección final viene de revisar el caso de uso contra el lenguaje del dominio, invariantes, necesidades de persistencia y fronteras del módulo.
Queries
Una Query representa la intención de obtener datos sin alterar el estado del dominio. Las Queries están diseñadas para lectura eficiente, devolviendo exactamente los campos necesarios para el caller, sin cargar entidades completas cuando eso no es necesario.
Ejemplos comunes son GetCustomerById, ListCustomers, ListPhoneTypes, ListOrdersByDateRange y una query personalizada como GetVehicleAvailability. Una Query debe responder una pregunta específica de la aplicación.
Características de una Query
- Inmutable: al igual que Commands, una Query no debe permitir cambios después de creada.
- Nombre descriptivo: refleja la información buscada, como
GetCustomerByIdoListOrdersByDateRange. - Filtros y paginación: puede transportar fechas, status, página, tamaño de página, texto de búsqueda, ordenación y otros parámetros de lectura.
- Proyección: debe devolver DTO o view model con solo los campos necesarios, evitando exposición directa de entidades de dominio.
- Sin efectos colaterales: no debe llamar métodos que alteren estado ni guardar cambios en la base de datos.
Cuándo crear una Query
- Use una Query cuando la operación lee datos y no debe modificar estado.
- Use una Query de resultado único para pantallas de detalle, búsquedas por identificador, verificaciones de disponibilidad o respuestas de un objeto.
- Use una Query de lista para grids, listas de opciones, colecciones hijas y controles de selección.
- Use paginación para grids y conjuntos de datos potencialmente grandes.
- Use listas simples para datos de referencia pequeños, enumeraciones y endpoints de opciones.
Query Validators
Los Query Validators validan parámetros de entrada como filtros, valores de paginación, intervalos de fecha, identificadores y reglas de visibilidad. También se implementan con FluentValidation.
Reglas comunes de validación en Queries
GreaterThanOrEqualToyLessThanOrEqualTopara filtros de intervalo, como fechas inicial y final.Length,MaximumLengthyMinimumLengthpara filtros de texto, como nombre, e-mail o término de búsqueda.InclusiveBetweenpara paginación, limitandopageypageSize.NotEmptypara identificadores obligatorios en consultas de detalle.Mustpara combinaciones inválidas de filtros, como fecha final menor que fecha inicial.
Query Handlers
El Query Handler consulta repositorios o el contexto de la base de datos y devuelve proyecciones optimizadas. En Lino, se recomienda usar proyecciones con Select, filtros explícitos, ordenación previsible y lecturas sin tracking cuando sea apropiado.
El handler de Query debe ser read-only: no llama métodos de dominio que alteran estado, no dispara cambios persistentes y no ejecuta SaveChanges. Si una lectura necesita registrar auditoría, iniciar una integración o recalcular estado, eso generalmente indica otro use case o proceso separado.
Query Results
En Lino, los resultados de Queries se representan por records nombrados con el sufijo QueryResult. Esta convención vale para respuesta única, lista simple, lista paginada o DTO específico para pantalla, API, integración o proceso en background.
Los resultados de consulta deben ser contratos estables de lectura. Trátelos como DTOs diseñados para el consumidor, no como atajo para exponer entidades de dominio directamente.
Creando una Query con el CLI
Similar a los Commands, Lino disponibiliza el comando:
lino query new
El CLI también acepta opciones como:
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: servicio de destino.-mo--module: módulo de destino.-eo--entity: entidad asociada a la Query.-n,--name,-qo--query: nombre de la Query.
Durante el flujo interactivo, Lino pregunta si la Query devuelve un único resultado o una lista. Cuando la respuesta es una lista, también pregunta si debe ser paginada. Por último, permite seleccionar las propiedades que deben devolverse o usarse por la proyección generada.
Lino generará automáticamente:
GetOrderByIdQuery.csGetOrderByIdQueryValidator.csGetOrderByIdQueryHandler.csGetOrderByIdQueryResult.cs
Ejemplo de estructura generada para Queries
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Orders/
├── Commands/
| └── ...
└── Queries/
└── GetOrderById/
├── GetOrderByIdQuery.cs
├── GetOrderByIdQueryValidator.cs
├── GetOrderByIdQueryHandler.cs
└── GetOrderByIdQueryResult.cs
Para una Query personalizada de disponibilidad de vehículo, la estructura sigue el mismo patrón:
Application/UseCases/Vehicles/Queries/GetVehicleAvailability/ ├── GetVehicleAvailabilityQuery.cs ├── GetVehicleAvailabilityQueryValidator.cs ├── GetVehicleAvailabilityQueryHandler.cs └── GetVehicleAvailabilityQueryResult.cs
Responsabilidades de los archivos de Query
- Query: contrato inmutable de solicitud con identificadores, filtros, ordenación, paginación o intervalos de fecha.
- Validator: validación de identificadores, paginación, filtros obligatorios, intervalos de fecha y criterios de búsqueda.
- Handler: orquestación de lectura usando contexto de aplicación, proyecciones, filtros, ordenación, paginación, logging, tracing y creación del resultado.
- Result: DTO de respuesta modelado para el caller, frecuentemente con records internos para elementos de lista.
Checklist de implementación
- Ejecute
lino query newy elija servicio, módulo, entidad, nombre de la Query, tipo de retorno, modo de paginación y propiedades. - Revise el contrato de solicitud y mantenga solo filtros realmente necesarios para el caller.
- Fortalezca el validator, especialmente para identificadores obligatorios, intervalos de fecha, límites de page size y combinaciones inválidas de filtros.
- Ajuste la proyección del handler para devolver solo los campos exigidos por la UI, API, integración o proceso en background.
- Mantenga la Query read-only: no llame métodos de dominio que mutan estado y no guarde cambios en el handler.
- Propague fallos esperados mediante
Result, usando errores estandarizados existentes cuando sea apropiado. - Compile y pruebe el consumidor que llama la Query, como endpoint de API, página Blazor, integración in-process o typed client.
Ejemplo de Query personalizada
En el flujo de integración entre módulos de un SaaS, una Query personalizada de disponibilidad de vehículo puede crearse con lino query new. La estructura generada proporciona Query, Handler, Result y Validator. Después, el desarrollador agrega las entradas necesarias, como identificador del vehículo, fecha inicial y fecha final, valida si el intervalo es correcto e implementa la lógica del handler. Este es el flujo esperado: generar primero la estructura consistente y después completar el comportamiento específico del dominio con código explícito.
Orientación de read model: los resultados de Query deben ser contratos estables. Trátelos como DTOs diseñados para el caller, no como atajo para exponer entidades de dominio directamente.
Conclusión
Definir use cases en Lino es transformar el modelo de dominio en operaciones claras de aplicación. Commands tratan cambios de estado, Queries tratan lecturas, validators protegen el borde de entrada, handlers orquestan el flujo y los objetos de resultado estandarizan la salida.
El flujo más rápido suele ser: modelar el dominio, generar los Commands y Queries necesarios, revisar los archivos generados, completar el comportamiento de negocio, exponer el caso de uso mediante una API o página cuando sea necesario y validar todo con build y pruebas. Esto mantiene el desarrollo rápido sin perder control arquitectónico.
A medida que el proyecto crece, mantenga cada use case centrado en una intención de negocio, respete las fronteras de servicio y módulo y use eventos, integraciones o shadow entities cuando otro módulo necesite participar en el proceso.
