Manuel Gomez
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    ## Í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 ![image](https://hackmd.io/_uploads/r1GB1wLlR.png) ## 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. ![image](https://hackmd.io/_uploads/HJ_-Jj0Axe.png) :::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 ![image](https://hackmd.io/_uploads/HJT_kPIlA.png) Esquema de una acción ![image](https://hackmd.io/_uploads/ryLOzgWy-l.png) ### 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(); } ``` ![image](https://hackmd.io/_uploads/rkN5kDLxC.png) #### Validaciones y dominio completo, puro o eficiente https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/ ![image](https://hackmd.io/_uploads/S1UWePLlA.png) ### 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 ![image](https://hackmd.io/_uploads/BJljiM81bl.png) #### Graphql ![image](https://hackmd.io/_uploads/BknchGUkZe.png) #### Request ![image](https://hackmd.io/_uploads/SJUW6n_1Ze.png) #### Response ![image](https://hackmd.io/_uploads/BkB6Thd1be.png) ### Application #### Command ![image](https://hackmd.io/_uploads/rkFe8GIybe.png) #### Command Handler Es el encargado de ejecutar la lógica del command, y se situa en la misma carpeta que el command. ![image](https://hackmd.io/_uploads/ryCzuGLkWl.png) ### Infrastructure #### Repository ![image](https://hackmd.io/_uploads/BJlO7p_1We.png) ### Configuración para ef core ![image](https://hackmd.io/_uploads/ryqzBTOJbe.png) :::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. ::: ![image](https://hackmd.io/_uploads/r1lABa_kWg.png)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully