## Índice
- B2B
- DDD
- ValueObject
- Entity
- Aggregate
- AggregateRoot
- BoundedContexts
- Modelado del dominio
- Implementación DDD y CQRS
- BoundedContexts
- Dominio
- Aggregates
- AggregateRoots
- Entities
- ValueObjects
- DomainEvents
- Validaciones y dominio completo, puro o eficiente
- Aplicación
- Commands
- Queries
- Listeners Events
- Bus
- Presentation (APIRest Graphql)
- Controllers (ApiRest)
- Graphql
- Requests
- Responses
- CQRS
- Persistencia
- DbContext
- EF core y funcionamiento básico
- Tipos de columnas de base de datos
- Entidades
- Configurations
- Own (OwnsOne, OwnsMany, Owned) vs Has (HasOne, HasMany)
- Conversiones
- Consultas, modificaciones, patrón Repository
- Linq y EfCore
- Persistencia de dominio
## B2B

## DDD
DDD se define como un enfoque de diseño de software que se centra en la creación de un modelo de dominio rico y expresivo, .....
Resumiendo mucho mucho ddd sería el desarrollo centrado en el dominio (nuestras clases, entidades y lógica de negocio), con lo de lenguaje expresivo es que todo esté definido con nombres y conceptos que todo el mundo conoce (lenguaje ubicuo) y que el modelo de dominio sea lo mas puro posible sin contaminarlo con cosas externas (ORM, validaciones, serializaciones,...)
Para el modelado del dominio se utilizan conceptos como Aggregate, Entity, ValueObject, DomainEvents, BoundedContext y nos vamos a centrar solo en estos 4 conceptos.
### ValueObject
La parte mas básica del dominio, son piezas reutilizables que no tienen identidad propia, son inmutables y se comparan por sus propiedades. Ejemplo: Email, Dirección, Precio, Imagen,...
- En el proyecto se han implementado por ejemplo
- CultureVo : Para textos multilenguaje
- EmailVo : Para emails
- AddressVo : Para direcciones
- ImageVo : Para imágenes
- CurrencyVo : Para monedas
- .....
:::info
- Un ValueObject no tiene Id propio
- Si 2 valueobjects tienen las mismas propiedades son iguales (por ejemplo 23 EUR y 23 EUR son lo mismo, aunque hayan 2 instancias diferentes)
- Son inmutables, una vez creados no se pueden cambiar sus propiedades
- Deben tener constructor privado y un método estático Create para crear nuevas instancias (esto es así para que no se puedan crear instancias con new y la mitad de campos vacíos)
- Pueden tener validaciones
- por ejemplo un EmailVo puede validar que el email es correcto
- Un address no puede permitir que el campo dirección esté vacío
- ....
:::
### Entity
Es una clase, objeto, concepto que tiene identidad propia, se identifica por su Id y no por sus propiedades. 2 instancias con el mismo id son la misma entidad.
:::info
- Una entity tiene Id siempre
- 2 entities con el mismo id son la misma entidad aunque sus propiedades sean diferentes
- Pueden tener propiedades mutables, pero únicamente a través de métodos de la propia entidad o del aggregate root (lo vemos más adelante)
:::
Estas con las 2 piezas más básicas del dominio, con estas 2 entidades se tienen que modelar toda funcionalidad o dominio nuevo que se vaya a incluir en la aplicación.
La primera parte y la mas importante es identificar las entidades y los value objects y como se relacionan entre si.
### Aggregate
Un agregado es un concepto mas abstacto y dificil de enteder en DDD, puesto que realmente no es una clase, ni un objeto, ni nada físico, es una agrupación lógica de entidades que se operan como una sola entidad.
Es como que dentro de un agregado todas las entidades y valueobjects que cuelgan de el están siempre disponibles van todos en grupo.
- Salen de la base de datos todos juntos
- Se guardan todos juntos
- Se validan todos juntos
Por ejemplo un agregado en la aplicación puede ser un Cliente,
### AggregateRoot
Es la entidad padre del agregado, cuando se define un agregado se tiene que elegir una entidad que será la entidad principal, esta entidad es la que se conoce como AggregateRoot. Y es la parte visible y operativa del agregado en si.

:::danger
- Sole puede haber un AggregateRoot por agregado
- Todas las entidades y valueobjects cuelgan del root
- Todas las operaciones pasan por el root
- Cualquier accion, lo que sea, no se permite por ejemplo obtener el listado de usuarios y actualizar sus propiedades directamente sin pasar por el root
- client.Users[0].Name = "Nuevo nombre" --> No permitido
- En vez de eso se debe hacer client.UpdateUserName(userId, "Nuevo nombre") --> Permitido
- Los aggregate roots no pueden tener otros aggregate roots dentro, solo entidades y valueobjects
- Si un aggregateroot referencia a otro root se hace mediante su Id
- client.TransportId
- client.MarketId
- Si un root se elimina se eliminan todas las entidades y valueobjects que cuelgan de el
- Los aggregate roots son los únicos que pueden disparar eventos
:::
### BoundedContexts
Son como los agregados pero a un nivel mas alto, son agrupaciones agregados independientes que encapsulan y marcan unos límites de visibilidad con otros BoundedContexts, en la aplicación por ejemplo se ha dividido en varios boundedContexts
- B2B.Auth
- B2B.Clients
- .....
Pensando en módulos que operan de forma independiente, por ejemplo
- auth : Controla usuarios (personas físicas), roles, permisos, tokens de acceso,
- clients : Controla clientes (empresas), usuarios que actuan en nombre del cliente , grupos de clientes, agentes comerciales.
- ....
Los boundedcontexts marca un límite de visibilidad entre ellos, por ejemplo el boundedcontext de auth no sabe nada de clientes, ni de productos, ni de pedidos, solo sabe de usuarios, roles y permisos.
Puede contener ids de otros boundedcontexts pero no referencias directas a sus entidades, ni integridad referencial entre ellos.
Cada boundedContext puede tener entidades repetidas, esto no es ningún problema, las entidades pueden tener campos distintos. Por ejemplo
- En el BC de Orders podemos tener la entidad Client con los campos mínimos necesarios para gestionar pedidos (id, nombre, dirección de envío)
- En el BC de Clients podemos tener la entidad Client con todos los campos necesarios para gestionar clientes (id, nombre, direcciones, condiciones comerciales, usuarios,...)
- En el BC de notificaciones solo podemos tener la entidad Client con los campos necesarios para enviar notificaciones (id, nombre, email)
Cada bounded context tiene su propio almacenamiento de datos independiente, no se permiten queries cruzadas entre boundedcontexts, porque un boundedcontext puede usar Postgresql y otro Elasticsearch, o incluso un boundedcontext puede no tener base de datos propia y obtener los datos de otro boundedcontext mediante eventos.
Esto también pasa con los aggregate root, un root puede guardarse en postgresql y otro en elasticsearch, no hay problema.
### Modelado del dominio
A la hora de modelar el dominio al principio puede ser complejo porque estamos muy acostumbrados a hacer todo pensando en como se guardaran los datos en la base de datos. Así que pensamos en tablas, relaciones, claves foráneas, normalización, en vez de pensar únicamente en las entidades y sus relaciones.
Por ejemplo un caso típico es cliente, usuarios y direcciones.
En un primer momento identificaríamos todos los conceptos involucrados en el dominio, como por ejemplo:
- Cliente
- Usuario
- Dirección
- Población
- Teléfono
- Nombre
- Email
Se determinan las relaciones entre ellos.
- Un cliente tiene varios usuarios
- Un cliente tiene varias direcciones
- Una dirección tiene una población
- Un usuario tiene un email
Se determinan también lo que son entidades y valueobjects
- Cliente --> Entity
- Usuario --> Entity
- Alias de dirección --> ValueObject
- Dirección --> Ambigüedad de momento en si es un ValueObject o una Entity
- Población --> ValueObject
- Teléfono --> ValueObject
- Nombre --> ValueObject
- Email --> ValueObject
Con las direcciones se dan 2 situaciones
- Una dirección es igual a otra si todos lso campos son iguales (calle, número, población, cp,..., si es la misma localización física es la misma dirección)
- Pero un cliente puede tener varias direcciones y asignarles un alias, también puede querer modificar una de ellas por separado mediante un id o alias y eliminar una de ellas.
En estos casos se puede complicar un poco, hasta es posible que se decida qeu dirección es un aggregateroot y tenga su propio id y se gestione por separado.
En estos casos lo que me ha servido es determinar si tiene sentido que una dirección exista sin el cliente, si la respuesta es no, entonces es un valueobject o una entity, si la respuesta es si, entonces es un aggregate root.
En el B2B se ha modelado como una entidad y como un valueobjet pero con conceptos distintos
- ClientShippingAddress : Entity
- Id : Guid
- Alias : string
- AddressVo : ValueObject
por lo que al final el diagrama de clase de cliente queria algo así
```csharp
class Client {
Guid Id
string Name
List<ClientUser> Users
List<ClientShippingAddress> ShippingAddresses
}
class ClientUser {
Guid Id
NameVo Name
EmailVo Email
}
class ClientShippingAddress {
Guid Id
string Alias
AddressVo Address
}
```
Como se puede observar se ha modelado de forma sencilla en clases, sin incluir ids cruzados entre las entidades, no se ha añadido un ClientId en ClientUser o ClientShippingAddress porque estas entidades cuelgan del root Client y no pueden existir sin el root.
## Implementación DDD y CQRS
El B2B se ha desarrollado siguiendo los principios DDD y CQRS, con las capas que define
- Presentación
Capa de presentación que los conceptos
- **Requests**: Clases de .net que representan una petición con los campos
- **Response**: Clases de .net que representan una salida de datos de la aplicación
- **ApiREST**: Controladores usando el MVC de Microsoft
- **GraphQL**: Querys/Mutations usando la librería HotChocolate
- **Validaciones**: Validaciones de datos de entrada usando la DataAnnotations y MicrosoftFluentValidation
- Aplicación
- Comandos : Acciones que se realizan en la aplicación
- Queries : Consultas
- CommandHandlers : Los manejadores de comandos implementados con MediatR
- QueryHandlers : Los manejadores de queries implementados con MediatR
- EventListeners: Los manejadores de eventos implementados con EventListeners
- Dominio
- Aggregates : Agrupaciones de entidades que se operan como una sola entidad
- AggregateRoot : Entidad root del agregado
- Entities : Entidades que cuelgan de root
- ValueObjects : Partes reutilizables sin entidad propia
- DomainEvents: Eventos que se generan en el root
- Infraestructura
Implementación de las conexiones físicas con sistemas externos
- Eventos :
- Hangfire
- RabbitMQ
- Bus
- MediatR
- ORM's
- ElasticNest -> Elasticsearch
- EfCore - NpgSQL.--> PostreSql

Esquema de una acción

### BoundedContexts
Los BoundedContext son agrupaciones de agregados independientes que encapsulan y marca unos límites de visibilidad con otros BoundedContexts
Se ha separado la aplicación en varios BoundedContext
- **B2B.Auth:** Users, Roles, UserActions, VirtualActions, AccessTokens
- **B2B.Clients:** Clients, Sales, Groups
- **B2B.Orders:** DeliveryNotes, Discounts, Invoices, Orders, Paymethods, Satis, ShoppingCarts, Taxes
- **B2B.Inventory:** Markets, Offers, ProductRates, Rates, StockAlerts, Stocks, Transports, Warehouses
- **B2B.Storage:** Folders, Documents, StorageFiles
- **B2B.Catalog:** CatalogModels, Categories, Menus, Modules, Pages
- **B2B.Notifications:** NewOrders, RemindPasswords, NewContact, ClientRegistration, ...
- **B2B.Products:** Models, Attributes, Products, Families, Brands
Cuando un BC necesita de datos de otros boundedContext se utiliza unas entidades especiales sincronizadas, solo con los datos que al boundedContext le interesan.
La sincronización se realiza mediante la captura de eventos
- B2B.Clientes
- synceds:
- brands (id, nombre)
- markets (id)
- payments (id,idOrder, amount)
- paymentMethods (id,nombre)
- B2B.Orders
- synceds
- brands
- clients
- clientGroups
- families
- markets
- models
- products
- shippingaddress
- transports
- user
- B2B.Catalog
- synceds:
- brands
- models
- attributes
- clients
- families
- offers
- orderitems
- products
- productImages
- stockAlert
- stocks
- Cada BoundedContext tiene la estructura de capas de DDD teniendo sus capas de presentación, aplicación, dominio e infrastructura.
- Los boundedContext pueden ser eliminados del sistema si no se van a usar y no causan ningún problema porque son módulos aislados.
- También se deberían poder sacar a otro sistema standalone siempre que se puedan conectar a los gestores de colas (Hangfire o Rabbitmq) y recibir los eventos de sincronización que necesitan.
- El tema de la sincronización de entidades como siempre tiene algunas peculiaridades
- Nos ha funcionado bastante bien el tema de sincronización y errores ya que las entidades synced son aisladas, solo se usan para obtener información puntual o validación de ids, por lo que borrarlas o resincronizarlas no provoca ningún problema
- Es un poco a veces tedioso el no tener un dato de una entidad necesaria y tener que traertelo, resincronizar en ese momento.
- Si la entidad ya la tienes y el campo está en el evento es trivial pero si no existe todavía la entidad o el campo no existe en el evento requiere una programación extra en ese momento
- Al ser un sistema con concurrencia alta , los eventos pueden llegar desordenados y hay que establecer uan prioridad de guardado con Concurrencia pesimista o optimista
- El tema es que no he encontrado forma de hacerlo que no sea un poco manual, no es mucho código añadido pero hay que estar pendiente
- Si no se usa nada funciona pero en algunos casos da errores de concurrencia
- Si se usa la concurrencia optimista (pensar que no va a suceder nada y cuando sucede algo repetir llamada) puede guardar datos mas antiguos uqe los nuevos. Pero el problema es que genera excepciones y ralentiza el sistema por repetición de llamadas
- Si se usa la concurrencia pesimista, hay que hacer un lock del registro correspondiente, esta forma funciona muy bien, no produce resultados erróneos ni exceptions, y tampoco hemos notado ralentizaciones excesivas
- Hemos probado concurrencia 200 con 2 millones de eventos procesados
- Al tener concurrencia hay que controlar también el orden de eventos para no tener problemas si un evento anterior llega mas tarde por lo que sea (job fallido uqe se reencola, ordenación de eventos del gestor de colas, workers atascados,...)
- También se pueden eliminar los boundedcontext y crear un único boundecontext monolítico y no harían falta los synced
- Se pueden unificar BoundedContext que tengan demasiados synceds
### Dominio
##### Aggregates
Los agregados son agrupaciones de entidades que se operan como si fuera una única entidad, tienen un root y todos los cambios pasan por el root, esto es para garantizar la invariabilidad del dominio y sus reglas.
Pueden contener 3 tipos de entidades
- Root : Entidad padre por el que pasan todos los cambios y puede tener eventos de dominio
- Entidades: Entidades que cuelgan del padre, con identificador propio
- ValueObjects : Partes reutilizables (ladrillitos) para componer las entidades
###### AggregateRoots
Clase que hereda de AbstractAggregateRoot, para crear un aggregate root hay que definir una clase.
Para garantizar la invariabilidad de la clase se han utilizado nomenclaturas de c#
- {get; private set;} Esto provoca que solo se pueda setear los valores de forma privada
- private constructor : Constructor privado, para que no se puedan hacer new() {...} con cualquier número de propiedades
- static Create para garantizar que todas las creaciones de la clase pasan por el
```csharp
class Product : AbstractAggregateRoot<Guid> {
public CultureVo Name {get; private set;}
public CultureVo Description {get; private set;}
public List<ImageVo> Images {get; private set;}
private Product() {}
public static Product Create(.....) {
return new() {....}
}
}
```
```csharp
class Client : AbstractAggregateRoot<Guid> {
public string Name {get; private set;}
public List<AddressVo> ShippingAddresses {get; private set;}
public List<ClientUser> Users {get; private set;}
private Client() {}
public static Client Create(....) { return new() {....}}
...
public void AddUser(ClientUser user)
public void DeleteUser(ClentUser user)
...
}
```
Al extender de AbstractAggregateRoot la clase internamente contiene
- OriginatedAt (--> timestamp del evento que generó al entidad)
- List<AbstractDomainEvent> : Eventos que genera el root
###### Entities
Entidades con id propio pero que no pueden coexistir si el root no existe.
Las entidades las definimos como clases que heredan de AbstractEntity
```csharp
public class ClientUser : AbstractEntity<Guid> {
public string Name {get; private set}
private ClientUser() {}
public static ClientUser Create(....) { return new() {....}}
}
```
Por heredar de entity la entidad obtiene CreatedAt, UpdateAt
###### ValueObjects
Son las piezas básicas de la aplicación que se pueden reutilizar, no tienen id propio,
son inmutables.
Los valueobjects son clases que heredan de AbstractValueObject
```csharp
public class EmailVo : AbstractValueObject {
public string Value {get; private set;}
private EmailVo() {}
public static ErrorOr<EmailVo> Create(string value) {
if (!isValid) return Error.
return new {....}
}
public static IsValid(string value) {
return true|false
}
// Sobrecarga de equals para poder comparar 2 emailvo
public override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
```
###### DomainEvents
Los eventos de dominio los registra el aggregateRoot cuando algo muta la entidad, pueden llevar la entidad completa o partes de ella.
En este caso se utilizan los modificadores de acceso de .net (init)
- {get; init;} Esto provoca que una vez creada clase es inmutable
En este caso se ha relajado la norma de hacer privado el constructor porque lo hace el propio aggregateroot
```csharp
public class ClientUpdatedEvent : AbstractDomainEvent {
public Guid id {get; init;}
public string Name {get; init;}
// Métodos sobrecargados que hay que definir
public override string GetIdentifier() => "clients.client.updated";
public override string GetAggregateId() => Id.ToString();
}
```

#### Validaciones y dominio completo, puro o eficiente
https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/

### Aplicación
En esta capa se definen las acciones o consultas que se pueden realizar para cada agregado, para ellos se utilizan Commands, Queries y EventListeners
#### Commands
Son acciones que mutan agregados y entidades por lo que cambian el estado de la fuente de verdad (bases de datos, storages, ...)
Son clases anémicas (que no llevan código) que encapsulan los datos necesarios para realizar la acción
Debe implementar la interfaz ICommand y definir un resultado de salida, normalmente los commands no devuelven nada , si no dan error se supone que ha funcionado.
```csharp
[Action(Id="auth.write.user.delete", IsPublic:false)]
public class UserDeleteCommand : ICommand<EmptyResult> {
public Guid UserId {get; init;}
public string Name {get; init;}
}
[Action(Id="auth.write.user.delete", IsPublic:false)]
public record UserDeleteCommand(
Guid UserId,
sring Name
) : ICommand<EmptyResult>;
```
También puede ser un record, el record tiene la ventaja de ser inmutable de base, y también si se añade una propiedad nueva obliga a todas las intancias a pasar el valor o no compila.
Para realizar la acción se define un handler de la misma
```csharp
public class UserDeleteCommandHandler(
IUserRepository userRepository, // Repositorio para guardar cambios en base de datos
IRequestContext requestContext // Contexto del request (userId, role, ...)
)
: ICommandHandler<UserDeleteCommand, EmptyResult> // Descriptor de command y result
{
public async Task<ErrorOr<EmptyResult>> Handle(UserDeleteCommand command)
{
// Código de eliminación del usuario
if (requestContext.UserId == request.Id) return AuthErrors.UserCannotDeleteItSelf;
// Marcado para eliminación
userRepository.Delete(command.UserId, cancellationToken);
// Persistir cambios en una única transacción a la base de datos
await userRepository.SaveChangesAsync();
// Retornar resultado
return EmptyResult.Value;
}
}
```
Se ha definido que cada acción tiene un :
- id: Identificador único de la acción
- Esto es útil para poder meter/sacar commands del bus y poder encontrarlos y deserializarlos
- También indica la nomenclatura {BD}.{write|read}.{aggregate}.{accion} para poder asignar permisos en modo wildcard **auth.read.*, auth.write.***
- logBody:
- Indica si se puede loguear el body para qeu no aparezcna datos sensibles en logs, etc...
- isPublic:
- Para indicar si el método requiere validación de permisos o no
- Scope: Para limitar la acción a ciertos ámbitos por ejemplo (user, client-api, reset-password,...)
#### Queries
Prácticamente igual que los commands pero para queries
```csharp
[Action("Id="auth.users.read")]
public class UserGetQuery : IQuery<UserGetResult>
{
public Guid? Id { get; init; }
public string? Email { get; init; }
}
// Con un record
public record UserGetQuery(Guid? Id, string? Email) : IQuery<UserGetResult>
```
```chsarp
public class UserGetQueryHandler(
IUserRepository UserRepository,
IRequestContext requestContext)
: IQueryHandler<UserGetQuery, UserGetResult> // IQueryHandler y result
{
public async Task<ErrorOr<UserGetResult>> Handle(UserGetQuery request)
{
var userId = request.Id is not null ? UserId.Create(request.Id.Value) : (ErrorOr<UserId>?)null;
var email = request.Email is not null ? EmailVo.Create(request.Email) : (ErrorOr<EmailVo>?)null;
var errors = B2BErrorOrExtensions.Merge(userId, email);
if (errors.Any()) return errors;
User? user = null;
if (userId is not null) user = await UserRepository.GetUser(userId.Value.Value);
if (email is not null) user = await UserRepository.GetUser(email.Value.Value);
if (user is null) return CoreErrors.NotFound;
return UserGetResult.From(user);
}
}
```
#### Listeners Events
Para los eventos mas de lo mismo pero solo definiendo el listener, ya que el evento se declara en su lugar de origen (*)
```csharp
public class ClientCreditInfoUpdatedListener(IClientRepository clientRepository)
: INotificationHandler<ClientCreditPaymentsUpdatedEvent> // INotificationHandler
{
public async Task Handle(ClientCreditPaymentsUpdatedEvent notification, CancellationToken cancellationToken)
{
// Get client
var clientId = notification.ClientId;
var client = await clientRepository.GetToUpdate(clientId);
if (client is null) throw new Exception(CoreErrors.NotFound.Description);
...
}
}
```
### Bus
El bus se utiliza para transportar los commands desde el lanzador al handler correspondiente.
Se han creado las interfaces ICommandBus, IQueryBus y IEventBus para los commands, queries y events respectivamente.
### Presentation (APIRest Graphql)
En la capa de presentación se ubican lo relativo a entrada salida de ApiREST y GraphQL
#### Controllers (ApiRest)
Para declarar un endpoint se crea una clase que extienda de ApiBaseController, con anotaciones de MVC se configura el endpoint
- ApiExplorerSettings(GroupName): Grupo para agrupar endpoints en swagger
- Route: Ruta del endpoint
- [Authorize] : Opcional para endpoints autenticados
- Constructor con ICommandBus o IQueryBus inyectar el bus correspondiente
- [HttpPut|HttpGet|HttpPOST]
- Task<ActionResult<TipoDeVuelto>> : Siempre devuelve un ActionResult con el response que sea, en el caso de los comandos que no devuelven nada solo ActionResult
- Devuelve un método Ok o Problem según el resultado del error
```csharp
[ApiExplorerSettings(GroupName = "Clients")]
[Route("api/clients/profile/users/profile")]
[Authorize]
public class ProfileUserUpdateController(ICommandBus commandBus) : ApiBaseController
{
[HttpPut]
public async Task<ActionResult> Update(Guid id, [FromBody] ProfileUserUpdateRequest request)
{
var result = await commandBus.DispatchAsync(ProfileUserUpdateRequest.ToCommand(request));
return result.Match(value => Ok(), Problem);
}
}
```
#### Graphql
Para definir una query o mutation de graphql se genera una clase con annotations
- MutationType o QueryType
- Tipo devuelto GraphqlRestults si es vacío o el response que corresponda
- Si ha habido algún error se devuelve Problem
```csharp
[MutationType]
public class ProfileUserUpdateGraphQL
{
[Authorize]
public async Task<GraphQLResults> ClientUserProfileUpdate([FromBody] ProfileUserUpdateRequest request, ICommandBus _commandBus)
{
var result = await _commandBus.DispatchAsync(ProfileUserUpdateRequest.ToCommand(request));
if (result.IsError) GraphQLErrors.Problem(result.Errors);
return GraphQLResults.Success;
}
}
```
#### Requests
Los request son clases tipadas que reciben los requests de la aplicación. Se pueden comentar con comentarios .net para que el swagger o graphql se autodocumente con las entradas y salidas
También se pueden usar validaciones de entrada usando annotations
```csharp
/// <summary>
/// Authentication request
/// </summary>
public class LoginRequest
{
/// <summary>
/// Email of user
/// </summary>
[Email]
[RequiredNotEmpty]
public string Email { get; init; }
/// <summary>
/// Password
/// </summary>
[RequiredNotEmpty]
public string Password { get; init; }
/// <summary>
/// Type
/// </summary>
[RequiredNotEmpty]
public string Type { get; init; } = string.Empty;
/// <summary>
/// Long duration token
/// </summary>
public bool LongDuration { get; init; }
}
```
#### Responses
Son clases tipadas que corresponden con la salida, se pueden comentar igualmente para swagger o graphql
```csharp
/// <summary>
/// Response of get user
/// </summary>
public class UserGetResponse
{
/// <summary>
/// Id of user
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// Roles of user
/// </summary>
public List<string> Roles { get; init; }
/// <summary>
/// Email
/// </summary>
public string Email { get; init; }
/// <summary>
/// Culture
/// </summary>
public string Culture { get; init; }
/// <summary>
/// Name of user
/// </summary>
public string Name { get; init; }
public static UserGetResponse From(UserGetResult result) {
return new UserGetResponse(){
Id = result.Id,
Culture = result.Culture,
Roles = result.Roles,
Email = result.Email,
Name = result.Name
};
}
}
```
### CQRS
Se ha usado el patrón de CQRS para implementar la conexión entre Controllers y Graphql con los handlers correspondientes.
Para ello se han implementado 3 buses
#### ICommandBus
Para ejecutar commands, un command solo puede tener un manejador asociado de lo contrario
da un error.
Se implementar 3 métodos
- DispatchAsync : Usando la librería de mensajería MediatR, se crea un command y se inyecta en el bus, mediatR se encarga de conducir el command hasta su handler correspodniente y devolver la salida.
- Enqueue : El comando se encola en un gestor de colas, se han probado 2
- Hangfire : Librería que permite el encolado/desencolado con prioridad de colas,
- Muy rápido, con 20 workers puede ejecutar 300 o 400 peticiones segundo
- Tiene panel de control para poder mover jobs , ver fallidos, ver exceptions, reencolar, eliminar....
- También tiene API para crear endpoitns específicos
- Puedes configurar crons internos
- Hemos detectado qeu cuando la cola en espera supera el 1.000.000 de jobs se ralentiza bastante, puede ser por utilizar BaseDedatos relacional. Se puede configurar con Redis pero cuesta 400 euros la licencia.
- RabbitMQ :
- Muy rápido también pero no tanto, unos 60 u 80 por seg
- No atasca por tener muchos trabajos fallidos
- Tiene uan gestión de deadletter pasándolos a una cola aparte con posbilidad de recuperación
- Tiene un panel de control mas sencillo con pocas opciones
- Para ver los jobs fallidos es mas rudimentario
- Los trabajos fallidos se mueven en bloque, si hay 10.000 fallidos hay que volver a meter los 10.000
- Schedule : Para programar jobs con un delay o una hora específica
#### IQueryBus
Para ejecutar queries y obtener resultado. Solo puede haber un handler por query.
Solo implementa un método
- *Ask* : Encuentra el handler correspondiente y devuelve el resultado
#### IEventBus
Para inyectar eventos en el bus y que se ejecuten. En este caso si que pueden haber tantos listeners como se necesiten
Los eventos se pueden lanzar desde cualquier sitio pero para facilitar la publicación de los mismos se ha hecho que cuando el ORM guarda satisfactoriamente un AggregateRoot publica en el bus los eventos que el aggregateroot tenga generados.
### Persistencia
Para la persistencia de base de dato se ha usado el enfoque CodeFirst con EFCore. Todas la migraciones son gestionadas desde código usando FluentApi.
Mediante esta forma se respeta DDD porque los modelos con contienen nada de lógica asociada a la base de datos.
Para ello se han utilizado:
#### DbContext
En EfCore cada DbContext puede contener unas entidades de la base de datos y son aisladas de otros DbContext. Se ha definido 1 DbContext por cada BoundedContext
En cada DbContext se añaden propiedades tipadas con DbSet<ClientUser>
```csharp
public class B2BClientContext(IConfiguration configuration, IEventBus? eventBus, ILogger<B2BClientContext>? logger) : BaseContext(configuration, eventBus, logger)
{
public DbSet<Client> Client { get; set; }
public DbSet<ClientShippingAddress> ClientShippingAddress { get; set; }
public DbSet<ClientUser> ClientUser { get; set; }
public DbSet<ClientGroup> ClientGroups { get; set; }
public DbSet<PaymentsSynced> PaymentSynced { get; set; }
...
}
```
#### EF core y funcionamiento básico
Ef core es el ORM utilizado en el proyecto y el encargado de transformar el dominio a persistencia con postgresql
EfCore permite utilizará los DbSet creados en el context para la creación de las tablas de base de datos, para ello
- Creara una tabla para cada entidad creada en dbcontext
- A continuación analizará cada propiedad con acceso pública de la clase para crear las columnas correspondientes
#### Tipos de columnas de base de datos
##### Primitivos (Guid, int, string, List<string>, Dictionary<string>)
Ef core transforma cada propiedad en una columna con el tipo correspondiente
- string --> text
- int --> int
- List<string> --> array[text]
- Dictionary<string, string> -> hstore
#### Entidades
Cuando EfCore encuentra una entidad tiene 2 vías de actuación
- Si es una única entidad
Ef core creará una columna por cada elemento de la entidad
```csharp
class Cliente
public ClienteUser User {get; set;}
```
Crearía una columna prefijada para valor de User
user_name: string, user_id: uuid, user_email:string
- Si es una colección de entidades
Ef core crear una tabla adicional con el nombre de la case y le asignará una clave ajena a la clase padre
```csharp
class Cliente
public List<ClienteUser> User {get; set;}
```
Crearía la tabla client_users con las columnas id, client_id, name y email
Si queremos manualmente definir mas finamente como se generan las migraciones y se comporta efcore podemos definir configuraciones específicas.
#### Configurations
Para cada AggregateRoot se define un archivo configuration que define las columnas que contendrá la base de datos con FluentApi (esto es con builders para no añadir nada extra a clase del dominio)
```csharp
public class ClientConfiguration : IEntityTypeConfiguration<Client>
{
public void Configure(EntityTypeBuilder<Client> builder)
{
builder.HasKey(x => x.Id); // Indica la clave primaria
builder.ToTable("clients"); // forzamos el nombre de la tabla
builder.OwnsMany(x => x.Users).WithOne().HasForeignKey(x => x.ClientId);
builder.OwnsMany(x => x.ShippingAddress).WithOne().IsRequired();
builder.OwnsMany(x => x.CommercialConditions).WithOne().IsRequired();
builder.Property(x => x.Email).HasColumnType("citext");
builder.OwnsMany(x => x.SecondaryEmails, a =>
{
a.ToTable("client_secondary_emails");
a.Property(x => x.Value).HasColumnType("citext").HasColumnName("email");
});
builder.OwnsOne(x => x.FiscalInfo);
}
}
```
#### Own (OwnsOne, OwnsMany, Owned) vs Has (HasOne, HasMany)
Cuando ef core define las relaciones del AggregateRoot con sus entidades tiene 2 formas de hacerlo (own y has), siendo has la forma por defecto
##### HasMany , HasOne
Esta es la forma por defecto y si no indicamos nada es que la efcore aplica de base.
Con esta forma a la hora de recuperar de la base de datos los dominios debemos indicar que entidades relacionadas queremos que se incluyan en la query.
```csharp
// únicamente obtendría la entidad cliente, sin ninguna relacionada.
var clientes = await clientsContext.Clients.ToListAsync();
// Para obtener la entidad junto con todas las relaciones
var clientes = await clientsContext.Clients
.Include(x => x.Users)
.Include(x => x.ShippingAddress)
```
:::danger
Esta forma no es muy indicada para trabajar con Aggregates de DDD, ya que un aggregate se define como una serie de entidades que se comportan como un todo, y esto permitiría obtneer el cliente sin sus usuarios por ejemplo, por lo que tendríamos un dominio incompleto
:::
##### OwnsMany , OwnsOne, Owned
Si se definen las relaciones de esta forma ef core entiende que la entidad principal se compone de todas las entidades y cada vez que se extrae una AggregateRoot se obtiene con toda su información
```
// Utilizando OwnsMany clientes estará completo con todas sus subentidades
var clientes = await clientsContext.Clients.ToListAsync();
```
###### Owned
Existe el caso particular de Owned, el cual se le puede indicar a EFCore que siempre que se encuentre una entidad de este tipo va a ser configurada como owned.
En el proyecto se ha utilizado para indicar que todos los ValueObjects son de tipo Owned, ya que no pueden tener entidad propia y siempre vamos a querer que EfCore los incluya en todas la queries sin tener que indicarlo en cada entidad.
```csharp
BaseContext.cs
modelBuilder.Owned<ColorVo>();
modelBuilder.Owned<CurrencyVo>();
```
###### Conversiones
Cuando una propiedad es muy compleja o no queremos que se expanda en muchas columnas, o simplemente queremos que no cree un prefijo, existe la opción de aplicarle una conversión
```csharp
builder.Property(x => x.EmailVo)
.HasConversion(
v => v.Value, // De dominio a base de datos
v => EmailVo.Create(v).ValueOrThrow() // De base de datos a dominio
)
```
Hay algunos tipos que ya tienen conversiones globales definidas
```csharp
configurationBuilder.Properties<CultureVo>().HaveConversion<CultureVoConverter>();
configurationBuilder.Properties<StringIdVo>().HaveConversion<StringIdVoConverter>();
```
De esta forma EFCore no creará columnas adicionales para cada propiedad del ValueObject sino que usará la conversión definida.
#### Consultas, modificaciones, patrón Repository
Se ha usado el patrón repository, este patrón hace que la capa de aplicación solo conozca una interfaz de repository y los métodos que esta publica. La implementación de la entrada y salida con el ORM se deja en una implementación en la capa de interfaz.
Este patrón hace que se pueda cambiar el repository para en vez de guardar en una base de datos se pueda guardar en otra o cambiar la forma en que se guardan los datos sin afectar a las acciones del dominio.
### Linq y EfCore
Linq es el lenguaje de consultas integrado en .net, que permite hacer consultas tipadas, a cualquier colección que implemente IEnumerable<T> o IQueryable<T>. Por lo que es válido tanto para recorrer colecciones en memoria como para hacer consultas a bases de datos mediante ORM's como EfCore.
Ef Core transforma las consultas Linq a sentencias SQL para ejecutarlas en la base de datos.
```csharp
// Para obtener todos los clientes a un listado
var clients = await clientsContext.Clients.ToListAsync();
// Para obtener todos los clientes a un listado filtrado por nombre
var clients = await clientsContext.Clients
.Where(x => x.Name.Contains("nombre"))
.ToListAsync();
// Para utilizar funciones específicas de postgresql como ilike
var clients = await clientsContext.Clients
.Where(x => EF.Functions.ILike(x.Name, "%nombre%"))
.ToListAsync();
// Si por ejemplo el campo por el que queremos buscar es un ValueObject tenemos que conocer como se convierte a base de datos. Si el valueObject es un StringIdVo o EmailVo que tienen un conversor a string
var clients = await clientsContext.Clients
.Where(x => x.Email == EmailVo.Create("email@ejemplo.com"))
.ToListAsync();
```
:::danger
Siempre debe usarse la versión asíncrona de los métodos de ejecución para evitar bloqueos en el hilo principal de la aplicación.
:::
Las consultas con se ejecutan hasta que se llama a un método de ejecución como ToListAsync(), FirstOrDefaultAsync(), CountAsync(),...
```csharp
var clientsQuery = clientsContext.Clients
.Where(x => x.Name.Contains("nombre"));
var total = await clientsQuery.CountAsync(); // Aquí se ejecuta la consulta
var clients = await clientsQuery
.Skip(0) // offset
.Take(10) // limit
.ToListAsync(); // Aquí se ejecuta la consulta
// Existen extensions definidos en la aplicación para facilitar la paginación
var clientsPaged = await clientsQuery
.ApplyPagination(paginatedRequest)
.ToListAsync();
```
:::danger
Todas las consultas deben finalizarse dentro del repository, nunca devolver IQueryable<T> o IEnumerable<T> y que la consulta se ejecute fuera del repository.
En algunos proyectos o aplicaciones esto es bastante útil, pero en este proyecto el repository puede en cualquier momento cambiar la forma de persistencia, llamando a una api rest, elasticsearch, o leer desde una archivo plano. Donde no tendría cabida que fuera del repository se realizaran wheres, includes, skips, takes,....
:::
### Persistencia de dominio
En la aplicación se ha utilizado el patrón repository para la persistencia de los agregados y entidades del dominio.
Es a través del repository donde se realizan todas las operaciones de guardado, actualización, eliminación y consulta de los agregados y entidades del dominio.
Ef core funciona usando un ChangeTracker, esto es un mecanismo que rastrea los cambios realizados en las entidades obtenidas. Por defecto cuando se obtiene una entidad desde el DbContext, esta queda siendo rastreada por el ChangeTracker, pero en este proyecto se ha optado por deshabilitar este comportamiento por defecto y hacer un tracking manualmente en los repositories.
por lo que
```csharp
// Esto no estaría trackeado por lo que efcore no podría guardar cambios en base de datos
var client = await clientsContext.Clients.FirstOrDefaultAsync(x => x.Id == clientId);
// Esto si estaría trackeado
var client = await clientsContext.Clients
.AsTracking() // Indicamos que queremos que se trackee
.FirstOrDefaultAsync(x => x.Id == clientId);
```
Normalmente para el guardado de los agregados se utiliza el patrón UnitOfWork, que permite agrupar varias operaciones de guardado en una única transacción.
En este proyecto no existe un UnitOfWork explícito, pero si que se ha implementado el mismo concepto dentro de cada repository, con el método SaveChangesAsync().
Por lo que cada vez que se realizan cambios en un agregado o entidad, se debe llamar a este método para persistir los cambios en la base de datos.
```csharp
// Obtener cliente trackeado
var client = await clientsContext.Clients
.AsTracking()
.FirstOrDefaultAsync(x => x.Id == clientId);
// Realizar cambios en el cliente
client.UpdateName("Nuevo Nombre");
// Persist
await clientsContext.SaveChangesAsync(); // Aquí se guardan los cambios en base de datos
```
Aunque por norma general y para que el código sea más limpio, se ha optado por encapsular el SaveChangesAsync() dentro de cada método del repository que realiza cambios en la base de datos.
```csharp
Handler<ClientUpdateCommand>
{
public async Task<ErrorOr<EmptyResult>> Handle(ClientUpdateCommand command)
{
var client = await clientRepository.GetToUpdate(command.ClientId);
if (client is null) return CoreErrors.NotFound;
client.UpdateName(command.Name);
// El SaveChangesAsync() se realiza dentro del método del repository
await clientRepository.SaveChangesAsync();
return EmptyResult.Value;
}
}
:::info
Cuanod se trata de actualizar entidades completas se puede optar por crear una nueva entidad y usar un método AddOrUpdate en el repository para simplificar el código
:::
```csharp
Handler<ClientUpdateCommand>
{
public async Task<ErrorOr<EmptyResult>> Handle(ClientUpdateCommand command)
{
var client = Client.Create(
command.ClientId,
command.Name,
...
);
clientRepository.AddOrUpdate(client);
// El SaveChangesAsync() se realiza dentro del método del repository
await clientRepository.SaveChangesAsync();
return EmptyResult.Value;
}
}
```
```csharp
public interface IClientRepository
{
void AddClient(Client client);
Task<Client?> GetClientByUserID(Guid clientUserId);
Task<Client?> GetToUpdate(Guid clientId);
...
}
```
### Consultas complejas
Hay situaciones en el que las consultas son muy complejas e involucran a varios aggregados por necesidades de negocio.
Para estos casos si la consulta acaba siendo demasiado compleja lo mejor es crear una vista materializada en la base de datos que contenga los datos necesarios para la consulta y mapear esa vista a una entidad en ef core.
Dado que estamos en una aplicación CQRS, estas consultas complejas solo se realizarán en queries.
La vista materializada se haría mediante una entidad o clase nueva, simulando a un aggregateRoot con los campos y entidades necesarias.
Utilizando listeners de eventos podemos mantener la vista actualizada cada vez que se produzca un cambio en los agregados que afectan a la vista.
### Generación migraciones
Para generar una migración el proyecto debe compilar sin errores. Como regla general se programa todo el código nuevo y la migración sería el último paso.
Para generar una migración nueva, por ejemplo en en el boundedContext de Orders
```bash
cd B2B.Orders
dotnet ef migrations add NombreDeLaMigracion
```
Esto genera un archivo de migración que "ef tools" genera en la carpeta Migrations del proyecto correspondiente.
```text
...
Migrations/20251023133814_DeliveryNotesClientNameMigration.Designer.cs
Migrations/20251023133814_DeliveryNotesClientNameMigration.cs
```
### Ejecución migraciones
Para la ejecución de las migraciones es necesario que esté establecida una cadena de conexión válida en las variables de entorno.
Esto siempre en entorno local para las bases de datos de desarrollo locales, para las apliaciones publicadas ya se realiza en el propio desplegador.
```bash
$env:ConnectionStrings__B2BOrdersPostgreSQL="Server=-....."
```
```bash
dotnet ef database update
```
### Rollback de migraciones
Para poder eliminar una migración se puede usar el comando
```bash
dotnet ef migrations remove
```
Pero solo se podrá usar si la migración no se ha aplicado a la base de datos, si ya se ha aplicado habrá que hacer un rollback manualmente.
Como comprueba que la migración ya se ha aplicado a la base de datos, se necesita que la variable de entorno esté establecida previamente.
Si la migración ya se ha aplicado a la base de datos, habrá que hacer un rollback manualmente, para ello se puede realizar en 2 pasos
1- Llevar la base de datos a un punto anterior
```bash
dotnet ef database update MigracionAnterior
```
Esto revierte la base de datos al estado de esa migración. Y ahora ya si se puede eliminar la migración
```bash
dotnet ef migrations remove
```
## Permisos
Toda acción del sistema está identificada mediante un string jerarquico, tipo
orders.write.shopping-cart.confirm
clients.profile.write.info
clients.users.write.delete
clients.read.search
....
Usando la funcionalidad PipeLineBehavior de MediatR se puede poner un validador antes de la ejecución del commmand
```
request->controller->command->bus-> ValidacionPermiso ->handler->...
```
Y en la tabla de roles se asociao lo que cada rol puede hacer
global_admin : *
client_admin : clients.profile.*
auth.profile.*
orders.profile.*
client_seller: clients.profile.read.*
## Metodologías empleadas y descartadas
### Only PUT - Idempotente
Para el desarrollo se ha probado la opción de no tener POST en el sistema, solo PUTS
Tradicionalmente se ha usado siempre un POST y un PUT para crear un recurso y actualizarlo.
El POST genera un id o recibe un id en el sistema y el PUT actualiza un recurso existente, el POST falla si el id ya existe y el PUT falla si el id no existe. Por lo que los actores que conectan con la API, ya sea FRONT, CMS, Importador etc... tienen que manejar estos errores. En el caso de importadores mediante colas en Rabbit es mas dificil de gestionar este tipo de errores.
Se ha utilizado el enfoque de solo PUTS con identificadores generados externamente. Hay 3 tipos de ids
- Guid : Se genera un GUID externamente y se intenta operar con el recurso, si no existe se crea, si existe se acutaliza en el caso de commands y 404 en el caso de consultas
- StringIdVo : Un valueobject que recibe un id, el id debe ser en mayúsculas y cumplir unas reglas para evitar (acnetos, espacios,....)
- StringIdTreeVo: Un valueobject para guardar ids en formato Tree, y poder tener las ventajas de buscar por Jerarquía, Level, parents....
Mediante este enfoque todas los comandos son idempotentes, o funcionan o fallan siempre.
### Carpetas
Para la estructura de carpetas se ha usado un enfoque de agrupación por feature/acción mas que por tipo de carpeta.
Tradicionalmente en DDD o otros sistemas todo se agrupa por tipos archivo
```
┣ Presentation/
┃ ┗ Controllers/
┃ ┃ ┣ Controller1
┃ ┃ ┣ Controller2
┃ ┃ ┣ Controller3
┃ ┃ ┣ Controller4
┃ ┃ ┣ Controller5
┃ ┃ ┣ ...
┃ ┗ Requests/
┃ ┃ ┣ Request
┃ ┃ ┣ Request
┃ ┃ ┣ Request
┃ ┃ ┣ Request
┃ ┃ ┣ Request
┃ ┃ ┣ Request
┃ ┃ ┣ Request
┃ ┗ Responses/
┃ ┃ ┣ Response
┃ ┃ ┣ Response
┃ ┃ ┣ Response
┃ ┃ ┣ Response
┃ ┃ ┣ Response
```
Esta organización de carpetas es buena para tener todo junto pero mala para el desarrollo de una funcionalidad, cuando se desarrolla algo normalemnte se tocan varias capas
api, request, response, command, commmandHandler,repository
La estructura de carpetas que se ha utilizado es enfocada a cada agregado
```
┣ Aggreates/
┃ ┗ Client/
┃ ┃ ┣ Api
┃ ┃ | ┣ Update
┃ ┃ ┃ | ┣ UpdateController.cs
┃ ┃ ┃ | ┣ UpdateRequest.cs
┃ ┃ ┃ | ┣ UpdateResponse.cs
┃ ┃ | ┣ Delete
┃ ┃ ┃ | ┣ DeleteController
┃ ┃ ┃ | ┣ ....
┃ ┃ ┣ Application
┃ ┃ | ┣ Update
┃ ┃ ┃ | ┣ UpdateCommand.cs
┃ ┃ ┃ | ┣ UpdateCommandHandler.cs
┃ ┃ | ┣ Delete
┃ ┃ ┃ | ┣ ....
┃ ┃ ┣ Domain
┃ ┃ ┃ | ┣ Client.cs
┃ ┃ ┃ | ┣ ClientUser.cs
┃ ┃ ┃ | ┣ ClientShippingAddress.cs
┃ ┃ ┣ Infrastructure
┃ ┃ ┃ | ┣ ClientRepository
┃ ┃ ┃ | ┣ ClientConfiguration
```
Esta nos ha funcionado mejor al no tener que movernos en carpetas con demasiados archivos
Una nueva propuesta sería que cada acción encapsule todo lo necesario para la misma, pero de momento solo usamos la anterior
```
┣ Aggreates/
┃ ┗ Client/
┃ ┃ ┣ Actions
┃ ┃ | ┣ Update
┃ ┃ ┃ | ┣ UpdateController.cs
┃ ┃ ┃ | ┣ UpdateRequest.cs
┃ ┃ ┃ | ┣ UpdateResponse.cs
┃ ┃ ┃ | ┣ UpdateCommand.cs
┃ ┃ ┃ | ┣ UpdateCommandHandler.cs
┃ ┃ ┣ Domain
┃ ┃ ┃ | ┣ Client.cs
┃ ┃ ┃ | ┣ ClientUser.cs
┃ ┃ ┃ | ┣ ClientShippingAddress.cs
┃ ┃ ┣ Infrastructure
┃ ┃ ┃ | ┣ ClientRepository
┃ ┃ ┃ | ┣ ClientConfiguration
```
### Generador de templates
Como pueden ser muchos archivos al final es mucho código para escribir, así que se ha desarollado un generador de templates en python para generar archivos
```
Command/
┣ Api/
┃ ┗ {$command}/
┃ ┣ {$aggregate}{$command}Controller.cs
┃ ┣ {$aggregate}{$command}GraphQL.cs
┃ ┗ {$aggregate}{$command}Request.cs
┗ Application/
┗ {$command}/
┃ ┣ {$aggregate}{$command}Command.cs
┃ ┗ {$aggregate}{$command}CommandHandler.cs
```
Para ejecuarlo hay que tener instalado python y se hace desde consola
Ejemplo para generar un command
```bash
# Acceder a la carpeta donde se quiere generar el código
# - Para un aggregado será B2B.Clients/Aggregates
# - Para un command dentro de un aggregado B2B.Clients/Aggregates/Sales
cd B2B.Clients/Aggregates/Sales
py ../../../gen.py Command --boundedContext=Clients --aggregate=Sales --command=UpdateEmail --typeId=string
# Si faltara algún parámetro el generador lo va pidiendo/recomendando
py ../../../gen.py Command
boundedContext (Clients):
aggregate (Sales):
...
```
### Mapeado de entidades
En DDD al final hay una separación muy fuerte de conceptos, Request, Response, Query, QueryResult, Domain,...
Muchas veces los objetos parecen los mismos pero conceptualmente no lo son, cuando se intentan usar los mismos objetos al final hay mucho acoplamiento
- Request: Puede contener muchos datos que no se quiere que se llegen tal cual a la base de datos o a un handler , por lo que al final con anotaciones [NotMapped] [JsonIgnore] se consiguen pero se acaba con un deterioro de dominio y haciendo parches
- Commands: Deben poder serializarse/deserializarse en json puro, para poder inyectar desde muchas fuentes, la serialización se complica cuando el command tiene entidades asociadas, métodos, annotations....
- Dominio : Deberían ser clases que solo contienen los datos que queremos manejar o persistir, no deberían tener restricciones de serlización o anotaciones para swager, grpahql...
La solución adoptada es que los Request --> Command -> Dominio tengan un mapper asociado, normalmente se ha usado un método estático en el propio requset, command
```csharp
public class UpdateRequest {
///
/// UserId
///
[RequiredNotEmpty]
public Guid UserId {get; init;}
///
/// UserId
///
[RequiredNotEmpty]
public string Name {get; init;}
public static ToCommand(UpdateRequest request) {
return new UpdateRequest() {
UserId = request.UserId,
Name = request.Name
}
}
}
```
```csharp
public class UpdateCommand {
public Guid UserId {get; init;}
public string Name {get; init;}
// Los commands no tienen ToDomain porque normalmente requieren de validaciones
}
```
```csharp
public class ClientSearchResult {
public Guid Id {get; init;}
public string Name {get; init;}
public List<ClientUserResult> Users {get; init;}
public static ClientSearchResult FromDomain(Client client) {
return new() {
Id = client.id,
Name = client.Name,
Users = client.Users.ConvertAll(ClientUserResult.FromDomain)
}
}
}
```
```csharp
public class ClientSearchResponse {
public Guid Id {get; init;}
public string Name {get; init;}
public List<ClientUserResponse> Users {get; init;}
public static ClientSearchResponse FromResult(Client client) {
return new() {
Id = client.id,
Name = client.Name,
Users = client.Users.ConvertAll(ClientUserResponse.FromResult)
}
}
}
```
Copilot ayuda mucho en estos casos
### Gestión de errores
Para la gestión de errores se ha evitado el uso de lanzar Exceptions. Es una práctica que parece ser que no es recomendada
Empeoran el rendimiento
- Se han hecho benchrmaks y cuando se lanzan exceptions el compilador tiene que calcular el stacktrace, y hacer una especie de GoTo, esto provoca perdidas de tiempo
- En sistemas donde todas la validaciones se hacen por exceptions por ejemplo, si hay 10.000 jobs fallidos lanzan 10.000 exceptions que provocan ralentización
- Son incomodas para gestionar códigos de error, mensajes ...
- Cuando hay varias exceptions es difícil juntarlas
Enfoque Result o ErrorOr
El enfoque optado ha sido utilizar ErrorOr<TipoDevuelto>, por lo que cada cosa que devuelve errores, devuelve un Error o el resultado esperado.
Por lo que los métodos ne vez de devolver un valor devuelve un ErrorOr
```csharp
/// Con exceptions
public User GetUser() {
var user = await _repository.find()
if (!user) throw new NotFoundException()
return user
}
/// Con ErrorOr
public ErrorOr<User> GetUser() {
var user = await _repository.find()
if (!user) return CoreErrors.NotFound;
return user
}
//
public class GetUserHandler() {
public ErrorOr<User> Handle() {
var email = EmailVo.Create(request.Email)
var password = PasswordVo.Create(request.Password)
var roleType = RoleType.Create(request.RoleType)
var errors = B2BErrorOrExtensions.merge(email, password, roleType);
if (errors.Any()) return errors
}
}
// Exceptions
public class GetUserHandler() {
public ErrorOr<User> Handle() {
var email = EmailVo.Create(request.Email)
var password = PasswordVo.Create(request.Password)
var roleType = RoleType.Create(request.RoleType)
var procesoComplejo = procesarComplejo()
var errors = ErrorOr.merge(email, password, roleType, procesoComplejo);
if (errors.Any()) return errors;
}
}
// Exceptions
public class GetUserHandler() {
public ErrorOr<User> Handle() {
var email;
var password;
var roleType;
var errors = []
try {
email = EmailVo.Create(request.Email)
} catch(e) {
errors.add(e.message)
}
try {
password = PasswordVo.Create(request.Password)
} catch(e) {
errors.add(e.message)
}
if (errors) {
throw new Exception(errors)
}
}
}
```
## Cheatsheets (Guía de programación)
### API
#### Controller

#### Graphql

#### Request

#### Response

### Application
#### Command

#### Command Handler
Es el encargado de ejecutar la lógica del command, y se situa en la misma carpeta que el command.

### Infrastructure
#### Repository

### Configuración para ef core

:::danger
Si la clave primaria es un int o Guid o algún primitivo que ef core pueda generar automáticamente el identificador hay que marcarlo como ValueGeneratedNever().
Porque si no le decimos nada efcore entiende que tiene que generarlo, y cuando se lo asignemos manualmente ef core creerá que es un registro existente y lanzará un error de actualización.
:::
