Skip to content

Exploración Detallada de las Capas

En esta sección, profundizaremos en cada una de las capas del arquetipo, describiendo sus componentes, responsabilidades y cómo interactúan entre sí.

Archetype

Capa de Dominio (Domain)


La capa de Dominio es el corazón de la aplicación, donde se define y reside la lógica de negocio principal. Esta capa se encarga de modelar los conceptos y las reglas del dominio del problema, representando las entidades, agregados, objetos de valor y la lógica de negocio que rige el comportamiento del sistema. Una característica fundamental de la capa de Dominio es su independencia de cualquier tecnología o framework externo. No debe depender de detalles de implementación técnica como bases de datos, interfaces de usuario o servicios de terceros.

Archetype

Responsabilidades de la Capa de Dominio

  • Modelado del Dominio: Representa los conceptos y las reglas de negocio de manera clara y precisa a través de entidades, agregados y objetos de valor.
  • Lógica de Negocio: Contiene la lógica que define cómo se comportan los objetos del dominio y cómo se deben realizar las operaciones en el sistema.
  • Validación de Invariantes: Asegura que los objetos de dominio siempre se encuentren en un estado válido y consistente, aplicando reglas de validación y utilizando guard clauses para proteger la integridad de las entidades y agregados.
  • Generación de Eventos de Dominio: Define y genera eventos de dominio que representan cambios de estado significativos dentro del dominio, permitiendo que otras partes del sistema reaccionen a estos cambios.
  • Definición de Interfaces: Define las interfaces para los repositorios que permiten el acceso a la persistencia de los agregados. Estas interfaces actúan como “puertos” en el contexto del patrón Ports and Adapters.

Relación con Otras Capas

La capa de Dominio es el centro de la aplicación, sin embargo, no tiene conocimiento de cómo se implementan las dependencias externas. La capa de Aplicación orquesta las operaciones de la capa de Dominio y define los casos de uso, y la capa de Infraestructura se encarga de la implementación de las dependencias externas (repositorios, servicios, etc.). Las capas superiores dependen de las abstracciones definidas en la capa de dominio, pero la capa de dominio no depende de las implementaciones de las capas superiores.

Beneficios de la Independencia de la Capa de Dominio

  • Foco en el Negocio: Al ser independiente de la tecnología, se facilita que el equipo de desarrollo se centre en la lógica de negocio y en las necesidades del usuario.
  • Reutilización: Al no depender de tecnologías específicas, el modelo del dominio es reutilizable en diferentes contextos y aplicaciones.
  • Testabilidad: Facilita la realización de pruebas unitarias enfocadas en la lógica de negocio sin depender de la infraestructura.
  • Mantenibilidad: Al estar aislada de los detalles de la implementación, la lógica de dominio es más fácil de entender y mantener a largo plazo.
  • Evolución Ágil: Permite que la lógica de negocio evolucione de manera independiente de las decisiones de implementación técnica

Agregado (OrderAggregate)

El Agregado es un patrón de diseño fundamental en el contexto de Diseño Guiado por el Dominio (DDD). Representa un conjunto de entidades y objetos de valor que se agrupan para formar una unidad coherente y consistente. Los agregados son la principal forma de encapsular la lógica de negocio y asegurar la consistencia de los objetos dentro del dominio.

Archetype

¿Por qué usar el patrón Aggregate?

El patrón Aggregate se utiliza para encapsular la lógica de negocio alrededor de una entidad principal, asegurando la consistencia de las operaciones. En nuestro caso, OrderAggregate gestiona las operaciones sobre un pedido, manteniendo su estado y lógica de forma coherente.

Rol en el Dominio

El agregado OrderAggregate es el núcleo del dominio en este arquetipo. Representa un pedido en el sistema y encapsula toda la lógica y las reglas de negocio relacionadas con los pedidos. El agregado es responsable de mantener la coherencia del estado del pedido, asegurando que las operaciones sobre el pedido se realicen de forma correcta y consistente. El agregado OrderAggregate contiene las entidades ProductEntity y los objetos de valor ClientValueObject y AddressValueObject, que representan los componentes del pedido.

using CodeDesignPlus.Net.Microservice.Domain.ValueObjects;
namespace CodeDesignPlus.Net.Microservice.Domain;
public class OrderAggregate(Guid id) : AggregateRoot(id)
{
public Instant? CompletedAt { get; private set; }
public Instant? CancelledAt { get; private set; }
public ClientValueObject Client { get; private set; } = default!;
public List<ProductEntity> Products { get; private set; } = [];
public OrderStatus Status { get; private set; }
public string? ReasonForCancellation { get; private set; }
public AddressValueObject ShippingAddress { get; private set; } = default!;
public static OrderAggregate Create(Guid id, ClientValueObject client, AddressValueObject shippingAddress, Guid tenant, Guid createdBy)
{
DomainGuard.GuidIsEmpty(id, Errors.IdOrderIsInvalid);
DomainGuard.IsNull(client, Errors.ClientIsNull);
DomainGuard.GuidIsEmpty(tenant, Errors.TenantIsInvalid);
DomainGuard.IsNull(shippingAddress, Errors.AddressIsNull);
var aggregate = new OrderAggregate(id)
{
Client = client,
ShippingAddress = shippingAddress,
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
Status = OrderStatus.Created,
Tenant = tenant,
CreatedBy = createdBy
};
aggregate.AddEvent(OrderCreatedDomainEvent.Create(aggregate.Id, aggregate.Client, aggregate.ShippingAddress, aggregate.Tenant, aggregate.CreatedBy));
return aggregate;
}
public void AddProduct(Guid id, string name, string description, long price, int quantity, Guid updateBy)
{
DomainGuard.GuidIsEmpty(id, Errors.IdProductIsInvalid);
DomainGuard.IsNullOrEmpty(name, Errors.NameProductIsInvalid);
DomainGuard.IsLessThan(price, 0, Errors.PriceProductIsInvalid);
DomainGuard.IsLessThan(quantity, 0, Errors.QuantityProductIsInvalid);
this.UpdatedAt = SystemClock.Instance.GetCurrentInstant();
this.UpdatedBy = updateBy;
var product = new ProductEntity
{
Id = id,
Name = name,
Description = description,
Price = price,
Quantity = quantity
};
Products.Add(product);
AddEvent(ProductAddedToOrderDomainEvent.Create(Id, quantity, product));
}
public void RemoveProduct(Guid productId, Guid updateBy)
{
DomainGuard.GuidIsEmpty(productId, Errors.IdProductIsInvalid);
var product = Products.SingleOrDefault(x => x.Id == productId);
DomainGuard.IsNull(product, Errors.ProductNotFound);
this.UpdatedAt = SystemClock.Instance.GetCurrentInstant();
this.UpdatedBy = updateBy;
Products.Remove(product);
AddEvent(ProductRemovedFromOrderDomainEvent.Create(Id, productId));
}
public void UpdateProductQuantity(Guid productId, int newQuantity, Guid updateBy)
{
DomainGuard.GuidIsEmpty(productId, Errors.IdProductIsInvalid);
DomainGuard.IsLessThan(newQuantity, 0, Errors.QuantityProductIsInvalid);
var product = Products.SingleOrDefault(p => p.Id == productId);
DomainGuard.IsNull(product, Errors.ProductNotFound);
this.UpdatedAt = SystemClock.Instance.GetCurrentInstant();
this.UpdatedBy = updateBy;
product.Quantity = newQuantity;
AddEvent(ProductQuantityUpdatedDomainEvent.Create(Id, productId, newQuantity));
}
public void CompleteOrder(Guid updateBy)
{
DomainGuard.IsTrue(Status == OrderStatus.Cancelled, Errors.OrderAlreadyCancelled);
DomainGuard.IsTrue(Status == OrderStatus.Completed, Errors.OrderAlreadyCompleted);
var @event = OrderCompletedDomainEvent.Create(Id);
this.UpdatedAt = SystemClock.Instance.GetCurrentInstant();
this.UpdatedBy = updateBy;
this.CompletedAt = @event.CompletedAt;
this.Status = OrderStatus.Completed;
AddEvent(OrderCompletedDomainEvent.Create(Id));
}
public void CancelOrder(string reason, Guid updateBy)
{
DomainGuard.IsTrue(Status == OrderStatus.Cancelled, Errors.OrderAlreadyCancelled);
this.UpdatedAt = SystemClock.Instance.GetCurrentInstant();
this.UpdatedBy = updateBy;
this.ReasonForCancellation = reason;
this.CancelledAt = SystemClock.Instance.GetCurrentInstant();
this.Status = OrderStatus.Cancelled;
AddEvent(OrderCancelledDomainEvent.Create(Id, reason));
}
}

Entidades (ProductEntity)

ProductEntity en el contexto de Diseño Guiado por el Dominio (DDD), ProductEntity es una entidad, lo que significa que es un objeto con una identidad única y persistente a través del tiempo dentro del dominio. A diferencia de los objetos de valor, las entidades tienen una identidad que las distingue, y esa identidad es lo que importa, no solo sus atributos. En este caso, un ProductEntity representa un producto individual dentro de un pedido y es esencial para mantener el registro y la coherencia de los productos dentro del mismo. Aunque se almacena dentro de un agregado (OrderAggregate), cada instancia de ProductEntity mantiene su propia identidad única, lo que permite rastrear el producto específico aunque sus atributos cambien.

Archetype

¿Por qué usar Entidades?

El uso de entidades en DDD permite modelar el dominio de manera más precisa, representando objetos que tienen identidad y un ciclo de vida dentro del sistema. A diferencia de los objetos de valor (que son inmutables y se identifican por sus atributos), las entidades se identifican por su identidad, lo que les permite cambiar de estado mientras conservan su identidad. Las entidades son fundamentales para modelar escenarios en los que los objetos tienen una existencia propia y requieren de un registro y seguimiento de sus cambios a lo largo del tiempo.

Rol en el Dominio

La entidad ProductEntity no existe de forma aislada, sino que su ciclo de vida está gestionado por el agregado OrderAggregate. La entidad es parte del estado del agregado, lo que significa que las operaciones sobre ProductEntity (como añadir, eliminar o actualizar la cantidad) se realizan a través de los métodos del OrderAggregate. Esto asegura que las reglas de negocio y la lógica del dominio se apliquen correctamente, manteniendo la consistencia del estado del agregado. La clave aquí es entender que, aunque ProductEntity tiene su propia identidad, su significado y funcionalidad se entienden mejor dentro del contexto de OrderAggregate.

namespace CodeDesignPlus.Net.Microservice.Domain.Entities;
public class ProductEntity : IEntityBase
{
public Guid Id { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public long Price { get; set; }
public int Quantity { get; set; }
}

Objetos de Valor (Value Objects)

En el contexto de Diseño Guiado por el Dominio (DDD), los Objetos de Valor (Value Objects) son objetos inmutables que representan un concepto dentro del dominio, pero sin tener una identidad propia. A diferencia de las entidades, los objetos de valor se definen por sus atributos, y dos objetos de valor se consideran iguales si sus atributos son iguales. Los objetos de valor se crean para representar conceptos que no requieren tener un seguimiento individual, sino que su valor está definido por sus características.

Archetype

Inmutabilidad y Uso

Una característica crucial de los objetos de valor es su inmutabilidad. Una vez que un objeto de valor se crea, sus atributos no pueden ser modificados. Esto garantiza la consistencia y la seguridad del dominio. Si se necesita modificar la información de un objeto de valor, se crea una nueva instancia con los valores modificados.

¿Por qué usar Objetos de Valor?

Los objetos de valor en DDD promueven un diseño más claro y expresivo al definir explícitamente conceptos que no tienen una identidad propia. Su inmutabilidad facilita la razón sobre el dominio y ayuda a evitar inconsistencias y errores. Los value objects permiten que las entidades encapsulen mejor los conceptos del negocio que no son entidades por sí mismas.

Rol en el Dominio

Los objetos de valor son utilizados por las entidades y los agregados para representar conceptos o características. En este ejemplo, ClientValueObject y AddressValueObject son utilizados por el OrderAggregate como parte de su estado, lo que demuestra que los value objects son un componente de un agregado. A pesar de que son parte de un agregado, mantienen sus propias reglas de negocio (que suelen ser reglas de validación y creación).

Representa la información esencial de un cliente asociado a un pedido. Los objetos ClientValueObject se utilizan cuando no se requiere un seguimiento individual del cliente a través de la aplicación, sino que solo se necesita representar sus datos dentro del contexto de una orden. La información del cliente se considera como un todo, y dos clientes son iguales si tienen los mismos valores en sus propiedades.

namespace CodeDesignPlus.Net.Microservice.Domain.ValueObjects;
public sealed partial class ClientValueObject
{
public ClientValueObject(Guid id, string name, string document, string typeDocument)
{
this.Id = id;
this.Name = name;
this.Document = document;
this.TypeDocument = typeDocument;
}
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Document { get; private set; }
public string TypeDocument { get; private set; }
public static ClientValueObject Create(Guid id, string name, string document, string typeDocument)
{
DomainGuard.GuidIsEmpty(id, Errors.IdClientIsInvalid);
DomainGuard.IsNullOrEmpty(name, Errors.NameClientIsInvalid);
DomainGuard.IsNullOrEmpty(document, Errors.DocumentIsNull);
DomainGuard.IsNullOrEmpty(typeDocument, Errors.TypeDocumentIsNull);
return new ClientValueObject(id, name, document, typeDocument);
}
}

Atributos:

  • Id: El identificador del cliente, aunque no define su identidad dentro del dominio, es importante para identificar al cliente en otros contextos (aunque el valor en sí mismo no sea suficiente para definir la igualdad del objeto de valor).
  • Name: El nombre del cliente.
  • Document: El documento del cliente.
  • TypeDocument: El tipo de documento del cliente.

Eventos de Dominio (DomainEvents)

En el contexto de Diseño Guiado por el Dominio (DDD), los Eventos de Dominio son representaciones de algo que ha sucedido en el dominio y que es relevante para otras partes del sistema. A diferencia de los comandos, que representan intenciones de realizar acciones, los eventos representan hechos que ya han ocurrido. Un evento de dominio es un registro de un cambio de estado significativo en el dominio. Los eventos de dominio se utilizan para desacoplar diferentes partes del sistema y permitir la comunicación asíncrona entre ellas. Estos eventos son inmutables, ya que representan un hecho histórico que no puede cambiar.

Archetype

¿Por qué usar Eventos de Dominio?

Los eventos de dominio son fundamentales en DDD para lograr el desacoplamiento, la escalabilidad y la mantenibilidad. Permiten que diferentes partes del sistema reaccionen a los cambios en el dominio sin tener dependencias directas entre ellas. Además, los eventos de dominio se pueden utilizar como registros de cambios en el dominio, lo que facilita la auditoría y el seguimiento de los cambios en el sistema.

Propósito

Los eventos de dominio se utilizan como una forma de notificar otros componentes de la aplicación sobre cambios importantes en el dominio sin crear dependencias directas entre ellos. Esto permite construir un sistema más flexible y mantenible, ya que los componentes pueden reaccionar a los cambios de forma independiente.

Cómo se Usan

En este arquetipo, los eventos de dominio se generan dentro de los agregados cuando se producen cambios de estado relevantes. Por ejemplo, cuando se crea una nueva orden o se añade un producto, el agregado (OrderAggregate) genera un evento que notifica a otros componentes sobre el cambio.

Características Clave

  • Inmutabilidad: Una vez que se crea un evento de dominio, su información no puede cambiar. Esto asegura que el registro del evento sea consistente.
  • Representación de Hechos Pasados: Los eventos de dominio representan algo que ha ocurrido, no una intención de acción.
  • Desacoplamiento: Permiten que diferentes partes del sistema reaccionen a los cambios sin conocerse directamente.
  • Persistencia Los eventos generalmente se persisten para auditar y rastrear los cambios en el dominio.
  • Publicación: Los eventos se publican en un bus de eventos para que otros componentes puedan suscribirse y reaccionar ante ellos.

Rol en el Dominio

Los eventos de dominio sirven para que el agregado comunique los cambios relevantes en su estado a otras partes del sistema que no necesitan estar acopladas directamente al agregado. Es decir, los eventos permiten que otras capas de la aplicación reaccionen a los cambios en el dominio sin conocer los detalles de la lógica interna del agregado.

Se emite cuando se crea una nueva orden. Representa el hecho de que una nueva orden se ha creado con su información asociada (cliente, dirección de envío, etc.).

using CodeDesignPlus.Net.Microservice.Domain.ValueObjects;
namespace CodeDesignPlus.Net.Microservice.Domain.DomainEvents;
[EventKey<OrderAggregate>(1, "OrderCreated")]
public class OrderCreatedDomainEvent(
Guid aggregateId,
OrderStatus orderStatus,
ClientValueObject client,
AddressValueObject shippingAddress,
Instant createdAt,
Guid tenant,
Guid createBy,
Guid? eventId = null,
Instant? occurredAt = null,
Dictionary<string, object>? metadata = null
) : DomainEvent(aggregateId, eventId, occurredAt, metadata)
{
public ClientValueObject Client { get; } = client;
public AddressValueObject ShippingAddress { get; } = shippingAddress;
public OrderStatus OrderStatus { get; } = orderStatus;
public Instant CreatedAt { get; } = createdAt;
public Guid Tenant { get; private set; } = tenant;
public Guid CreateBy { get; private set; } = createBy;
public static OrderCreatedDomainEvent Create(
Guid id,
ClientValueObject client,
AddressValueObject shippingAddress,
Guid tenant,
Guid creaateBy)
{
return new OrderCreatedDomainEvent(
id,
OrderStatus.Created,
client,
shippingAddress,
SystemClock.Instance.GetCurrentInstant(),
tenant,
creaateBy
);
}
}

Repositorios (Repositories)

En el contexto de Diseño Guiado por el Dominio (DDD), los Repositorios son interfaces que actúan como abstracciones para acceder a los agregados del dominio. Un repositorio es responsable de la persistencia y recuperación de las entidades y agregados del dominio, desacoplando la lógica de negocio de los detalles de la implementación de la persistencia. Un repositorio no debe contener lógica de negocio, sino que se centra exclusivamente en proporcionar una interfaz para el acceso a datos.

Archetype

¿Por qué usar Repositorios?

Los repositorios son un patrón clave en DDD para desacoplar el dominio de la infraestructura. Al abstraer la lógica de persistencia, se facilita la testabilidad de la capa de dominio y se permite cambiar la tecnología de almacenamiento sin afectar a la lógica de negocio. Los repositorios promueven la separación de preocupaciones y hacen que la aplicación sea más mantenible y extensible a largo plazo.

Propósito

El propósito principal de un repositorio es abstraer la forma en que los datos se almacenan y recuperan, permitiendo que la lógica del dominio (por ejemplo, en el agregado) no dependa de los detalles específicos de la tecnología de persistencia (como una base de datos o un sistema de archivos). Esto significa que la capa de dominio no debe conocer si los datos se guardan en una base de datos relacional, NoSQL, un sistema de archivos o incluso en memoria.

Rol en el Dominio

Los repositorios en el arquetipo actúan como interfaces que definen las operaciones que se pueden realizar sobre los agregados del dominio, como buscar, crear, actualizar o eliminar. La implementación concreta de estas operaciones se realiza en la capa de infraestructura. Los repositorios permiten que la capa de dominio acceda a los datos de forma independiente de la tecnología de persistencia, lo que facilita la reutilización y la testabilidad de la lógica de negocio.

La interfaz IOrderRepository define las operaciones que se pueden realizar sobre los pedidos en el sistema. Estas operaciones incluyen la creación, actualización, eliminación y recuperación de pedidos, así como la adición, eliminación y actualización de productos en un pedido.

namespace CodeDesignPlus.Net.Microservice.Domain.Repositories;
public interface IOrderRepository : IRepositoryBase
{
Task AddProductToOrderAsync(Guid id, Guid tenant, AddProductToOrderParams parameters, CancellationToken cancellationToken);
Task CancelOrderAsync(CancelOrderParams parameters, Guid tenant, CancellationToken cancellationToken);
Task CompleteOrderAsync(CompleteOrderParams parameters, Guid tenant, CancellationToken cancellationToken);
Task RemoveProductFromOrderAsync(RemoveProductFromOrderParams parameters, Guid tenant, CancellationToken cancellationToken);
Task UpdateQuantityProductAsync(Guid id, Guid tenant, UpdateQuantityProductParams parameters, CancellationToken cancellationToken);
}
Responsabilidades Principales
  • Abstracción de la persistencia: Los repositorios abstraen los detalles de la persistencia para la capa de dominio.
  • Acceso a Agregados: Los repositorios proporcionan un mecanismo para acceder a los agregados del dominio.
  • Persistencia de Agregados: Los repositorios son responsables de persistir los agregados después de que su estado cambia.
  • Recuperación de Agregados: Los repositorios se utilizan para recuperar los agregados del sistema de almacenamiento.

DTOs como Parámetros del Repositorio

En la capa de dominio, definimos interfaces para los repositorios (como IOrderRepository) que actúan como abstracciones para la persistencia de los agregados. Para facilitar la interacción entre la capa de dominio y la implementación del repositorio en la capa de infraestructura, utilizamos DTOs (Data Transfer Objects) como parámetros en los métodos del repositorio.

Estos DTOs encapsulan la información necesaria para llevar a cabo operaciones específicas sobre los agregados, como añadir productos, actualizar cantidades, o cancelar un pedido. Es importante destacar que estos DTOs son diferentes a los que se usan en la capa de aplicación. Los DTOs de la capa de dominio son específicos para las necesidades de persistencia de los agregados y generalmente reflejan los datos que se necesitan para actualizar el estado del agregado.

Archetype

DTO utilizado como parámetro para añadir un producto a un pedido. Contiene la información necesaria para realizar la operación, como el ID del producto, la cantidad y el usuario que realiza la acción.

using System;
namespace CodeDesignPlus.Net.Microservice.Domain.DataTransferObjects;
public class AddProductToOrderParams: IDtoBase
{
public required Guid Id { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
public required long Price { get; set; }
public required int Quantity { get; set; }
public required Instant? UpdatedAt { get; set; }
public required Guid? UpdateBy { get; set; }
}

Errores (Errors.cs)

La clase Errors en la capa de dominio se utiliza para centralizar y definir los códigos y mensajes de error específicos que pueden ocurrir dentro de la lógica del negocio. Esta capa es el corazón de la aplicación y sus errores reflejan problemas en las reglas de negocio y validaciones de datos que deben cumplirse. La clase Errors actúa como un repositorio de constantes de cadena que representan estos errores. Estos errores se utilizan para validar las invariantes del dominio a través de guard clauses (DomainGuard) que proporciona CodeDesignPlus.Net.Exceptions, al momento de realizar operaciones sobre el agregado.

Estructura y Organización de Códigos de Error

Los códigos de error siguen una estructura jerárquica para facilitar la identificación del origen del error:

  • 000: Errores internos genéricos (no aplicable directamente en este caso).
  • 100: Errores específicos de la capa de Dominio.
  • 200: Errores específicos de la capa de Aplicación.
  • 300: Errores específicos de la capa de Infraestructura.

Dentro de cada categoría, los códigos se numeran secuencialmente para distinguir cada tipo de error específico.

Formato de los Mensajes de Error

Los mensajes de error asociados a los códigos deben seguir el formato: <code> : <message>.

  • <code>: Es el código de error, por ejemplo “101”.
  • <message>: Es una descripción legible del error, por ejemplo “Tenant is invalid.”.

Este formato estandarizado ayuda a los desarrolladores a identificar rápidamente la causa del error y a rastrear su origen.

Errores Definidos en la Capa de Dominio

La clase Errors (en la capa de Dominio) contiene las siguientes constantes, que corresponden a diferentes escenarios de error en el dominio de órdenes:

namespace CodeDesignPlus.Net.Microservice.Domain;
public class Errors: IErrorCodes
{
public const string IdOrderIsInvalid = "100 : Id is invalid.";
public const string TenantIsInvalid = "101 : Tenant is invalid.";
public const string ClientIsNull = "102 : Client is null.";
public const string IdClientIsInvalid = "103 : Id client is invalid.";
public const string NameClientIsInvalid = "104 : Name client is invalid.";
public const string ProductIsNull = "105 : Product is null.";
public const string IdProductIsInvalid = "106 : Id product is invalid.";
public const string NameProductIsInvalid = "107 : Name product is invalid.";
public const string PriceProductIsInvalid = "108 : Price product is invalid.";
public const string QuantityProductIsInvalid = "109 : Quantity product is invalid.";
public const string ProductNotFound = "110 : Product not found in the order.";
public const string OrderAlreadyCompleted = "111 : Order already completed.";
public const string OrderAlreadyCancelled = "112 : Order already cancelled.";
public const string DocumentIsNull = "113 : Document is null.";
public const string TypeDocumentIsNull = "114 : Type document is null.";
public const string CountryIsNull = "115 : Country is null.";
public const string StateIsNull = "116 : State is null.";
public const string CityIsNull = "117 : City is null.";
public const string AddressIsNull = "118 : Address is null.";
public const string CodePostalIsInvalid = "119 : Code postal is invalid.";
}

Uso de las Constantes de Error

Estas constantes de error se utilizan en las cláusulas de guardia (guard clauses) de las clases de la capa de dominio (como OrderAggregate), para validar los datos y asegurar que se mantienen las invariantes del dominio. Las clases de guardado, como DomainGuard, lanzan excepciones específicas (del tipo CodeDesignPlusException) cuando una condición no se cumple, incluyendo el código y mensaje de error correspondiente.

public void AddProduct(Guid id, string name, string description, long price, int quantity, Guid updateBy)
{
DomainGuard.GuidIsEmpty(id, Errors.IdProductIsInvalid);
DomainGuard.IsNullOrEmpty(name, Errors.NameProductIsInvalid);
DomainGuard.IsLessThan(price, 0, Errors.PriceProductIsInvalid);
DomainGuard.IsLessThan(quantity, 0, Errors.QuantityProductIsInvalid);
// ...
}

Startup (Startup.cs)

La sección Startup en la capa de dominio, aunque presente para mantener la consistencia estructural del arquetipo, no realiza ninguna configuración específica en este caso. La clase Startup está vacía y no registra servicios, dependencias ni ninguna configuración.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CodeDesignPlus.Net.Microservice.Domain;
public class Startup : IStartup
{
public void Initialize(IServiceCollection services, IConfiguration configuration)
{
}
}
  • Beneficios de un Startup Vacío (Cuando Aplica):

    El objetivo de la capa de dominio es ser lo más pura posible. En la configuración actual del arquetipo, no se usa ningún servicio que requiera ser registrado, sin embargo, esto no quiere decir que en un futuro no se pueda configurar si se llegase a requerir. El no registrar servicios en la capa de dominio promueve su independencia de frameworks o bibliotecas externas, centrándose exclusivamente en la lógica de negocio. Esto facilita la reutilización, portabilidad y testabilidad.

  • ¿Por qué es necesario un Startup (Aunque Esté Vacío)?

    Si bien en este caso el Startup del dominio está vacío, su existencia es importante como referencia para diferentes métodos de extensión del SDK CodeDesignPlus.Net. El SDK utiliza este Startup para escanear los assemblies de la aplicación y registrar automáticamente servicios como repositorios, manejadores de eventos de dominio y otras configuraciones. Es decir, aunque en este caso no haya configuraciones específicas, el Startup es clave para el funcionamiento del SDK.

Capa de Aplicación (Application)


La capa de Aplicación actúa como el orquestrador del sistema, implementando los casos de uso y coordinando las interacciones entre el dominio y las capas externas. Esta capa sigue los principios de CQRS (Command Query Responsibility Segregation), separando las operaciones que modifican el estado del sistema (comandos) de las operaciones que leen información del sistema (consultas). Esta separación permite una mayor flexibilidad y escalabilidad, ya que los comandos y las consultas pueden ser optimizados de manera independiente.

Archetype

Comandos (Commands)

En el contexto de CQRS, los Comandos representan acciones que modifican el estado del sistema. Son objetos que encapsulan la intención de realizar una operación que tendrá un impacto en el dominio, por ejemplo, crear un nuevo pedido, añadir un producto o cancelar un pedido. Los comandos no contienen lógica de negocio, sino que son mensajes que se envían al sistema para indicar que se debe realizar una acción. En este arquetipo, los comandos son objetos simples que contienen la información necesaria para ejecutar la operación, pero la lógica de cómo se realiza la operación está en el handler correspondiente.

Archetype

Representa la intención de crear un nuevo pedido, incluyendo todos los datos necesarios para su creación.

namespace CodeDesignPlus.Net.Microservice.Application.Order.Commands.CreateOrder;
[DtoGenerator]
public record CreateOrderCommand(Guid Id, ClientDto Client, AddressDto Address) : IRequest;
public class Validator : AbstractValidator<CreateOrderCommand>
{
public Validator()
{
RuleFor(x => x.Id).NotEmpty().NotNull();
RuleFor(x => x.Client)
.NotNull()
.DependentRules(() =>
{
RuleFor(x => x.Client.Id).NotEmpty().NotNull();
RuleFor(x => x.Client.Name).NotEmpty().NotNull();
RuleFor(x => x.Client.Document).NotEmpty().NotNull();
RuleFor(x => x.Client.TypeDocument).NotEmpty().NotNull();
});
RuleFor(x => x.Address)
.NotNull()
.DependentRules(() =>
{
RuleFor(x => x.Address.Country).NotEmpty().NotNull();
RuleFor(x => x.Address.State).NotEmpty().NotNull();
RuleFor(x => x.Address.City).NotEmpty().NotNull();
RuleFor(x => x.Address.Address).NotEmpty().NotNull();
RuleFor(x => x.Address.CodePostal).NotEmpty().NotNull();
});
}
}

Lógica de los Comandos

Los comandos son objetos que contienen los datos necesarios para realizar la operación en el dominio. Son contenedores de información y no tienen lógica de negocio en sí mismos.

Handlers de Comandos

Cada comando tiene un Handler de Comando asociado, que es el responsable de ejecutar la lógica de negocio correspondiente en el dominio. El handler recibe el comando, recupera los agregados necesarios del dominio a través de los repositorios, aplica la lógica del dominio para realizar la operación y persistir los cambios, y publica los eventos de dominio correspondientes.

Procesa el comando CreateOrderCommand, creando un nuevo pedido en el sistema con la información proporcionada.

using CodeDesignPlus.Net.Microservice.Domain.ValueObjects;
namespace CodeDesignPlus.Net.Microservice.Application.Order.Commands.CreateOrder;
public class CreateOrderCommandHandler(IOrderRepository orderRepository, IUserContext user, IPubSub pubsub) : IRequestHandler<CreateOrderCommand>
{
public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var exist = await orderRepository.ExistsAsync<OrderAggregate>(request.Id, user.Tenant, cancellationToken);
ApplicationGuard.IsTrue(exist, Errors.OrderAlreadyExists);
var client = ClientValueObject.Create(request.Client.Id, request.Client.Name, request.Client.Document, request.Client.TypeDocument);
var address = AddressValueObject.Create(request.Address.Country, request.Address.State, request.Address.City, request.Address.Address, request.Address.CodePostal);
var order = OrderAggregate.Create(request.Id, client, address, user.Tenant, user.IdUser);
await orderRepository.CreateAsync(order, cancellationToken);
await pubsub.PublishAsync(order.GetAndClearEvents(), cancellationToken);
}
}

Consultas (Queries)

En el contexto de CQRS, las Queries representan solicitudes para obtener información del sistema sin modificar su estado. Son objetos que encapsulan los criterios o datos necesarios para realizar la consulta, como buscar un pedido por ID, obtener todos los pedidos o filtrar por algún criterio específico. Las queries no contienen lógica de negocio, sino que solo encapsulan los criterios de la consulta.

Archetype

Representa la intención de obtener un pedido por su identificador único.

namespace CodeDesignPlus.Net.Microservice.Application.Order.Queries.FindOrderById;
public record FindOrderByIdQuery(Guid Id) : IRequest<OrderDto>;

Lógica de las Queries

Las queries son objetos que contienen los datos o criterios necesarios para realizar la consulta en el dominio. Son contenedores de información y no tienen lógica de negocio en sí mismos.

Handlers de Queries

Cada query tiene un Handler de Query asociado, que es el responsable de ejecutar la consulta en el dominio. El handler recibe la query, recupera los datos necesarios del dominio a través de los repositorios y devuelve la información solicitada en forma de DTOs.

Procesa la consulta FindOrderByIdQuery, buscando un pedido por su identificador único y devolviendo la información asociada.

using CodeDesignPlus.Net.Cache.Abstractions;
namespace CodeDesignPlus.Net.Microservice.Application.Order.Queries.FindOrderById;
public class FindOrderByIdQueryHandler(IOrderRepository orderRepository, IMapper mapper, ICacheManager cacheManager, IUserContext user) : IRequestHandler<FindOrderByIdQuery, OrderDto>
{
public async Task<OrderDto> Handle(FindOrderByIdQuery request, CancellationToken cancellationToken)
{
if(await cacheManager.ExistsAsync(request.Id.ToString()))
return await cacheManager.GetAsync<OrderDto>(request.Id.ToString());
var result = await orderRepository.FindAsync<OrderAggregate>(request.Id, user.Tenant, cancellationToken);
var order = mapper.Map<OrderDto>(result);
await cacheManager.SetAsync(request.Id.ToString(), order);
return order;
}
}

Data Transfer Objects (DTOs)

Los DTOs (Data Transfer Objects) son objetos simples utilizados para transferir información entre las capas, especialmente entre la capa de aplicación y los entrypoints. En el contexto de CQRS, los DTOs se utilizan para encapsular los datos que se obtienen mediante las consultas (queries) y se envían a los clientes, así como para recibir datos en los comandos. Los DTOs evitan la exposición directa de las entidades del dominio a las capas externas, promoviendo un mayor nivel de desacoplamiento. En este arquetipo, la mayoría de los DTOs son generados automáticamente a partir de los comandos utilizando la librería CodeDesignPlus.Net.Generator, que hace uso de los Source Generators de C#. Esto reduce la necesidad de escribir código repetitivo y facilita la mantenibilidad del arquetipo.

Beneficios en CQRS

  • Desacoplamiento: Los DTOs desacoplan las capas de aplicación y presentación de las entidades del dominio, permitiendo que las capas evolucionen independientemente.
  • Especialización: Los DTOs se pueden adaptar específicamente a las necesidades de cada tipo de consulta, por ejemplo, para optimizar los datos que se envían al cliente.
  • Transferencia de datos: Los DTOs se usan para transferir datos tanto de entrada en los comandos como de salida en las consultas.

Data Transfer Objects

En la capa de aplicación, los Data Transfer Objects (DTOs) juegan un papel fundamental en la transferencia de información entre las distintas capas y los puntos de entrada (entrypoints) de la aplicación. Los DTOs son objetos diseñados específicamente para transportar datos, evitando exponer directamente las entidades del dominio o los detalles internos de la aplicación. Esto ayuda a mantener la separación de preocupaciones y la flexibilidad del sistema.

En este arquetipo, los DTOs se utilizan de manera diferenciada en comandos y consultas:

Comandos

Los comandos representan acciones que modifican el estado del sistema. Los comandos que están decorados con el atributo [DtoGenerator] generan automáticamente un DTO, el cual encapsula la información necesaria para realizar la operación.

Generación Automática de DTOs Basada en la Estructura del Comando:

La librería CodeDesignPlus.Net.Generator genera automáticamente el DTO basándose en la estructura del comando. Esto significa que el DTO resultante tendrá propiedades que coincidan con las del comando, facilitando la transferencia de datos de manera precisa y eficiente. Estos DTOs son los que se utilizan como estructura de datos en los entrypoints de la aplicación (como controladores API o servicios de frontend). El entrypoint recibe el DTO, que luego es mapeado a un objeto comando utilizando Mapster para que pueda ser procesado por la capa de aplicación.

Consultas

Las consultas representan acciones que recuperan información del sistema sin modificar su estado. Estas retornan DTOs que contienen la información solicitada. Estos DTOs definen cómo se presentan los datos al cliente y pueden contener información proveniente de múltiples fuentes.

Creación Manual de DTOs:

A diferencia de los comandos, los DTOs para las consultas no se generan automáticamente. El desarrollador debe crear estos DTOs manualmente, diseñándolos de acuerdo con la información que debe ser devuelta y el formato deseado.

Archetype

DTO que representa una dirección, con sus atributos como país, estado, ciudad, dirección y código postal.

namespace CodeDesignPlus.Net.Microservice.Application.Order.DataTransferObjects;
public class AddressDto
{
public required string Country { get; set; }
public required string State { get; set; }
public required string City { get; set; }
public required string Address { get; set; }
public int CodePostal { get; set; }
}

Errores (Errors.cs)

En la capa de aplicación, la clase Errors se utiliza para centralizar y definir los códigos y mensajes de error específicos que pueden ocurrir en la capa de aplicación. Estos errores reflejan problemas en la lógica de la aplicación o en la interacción con otros sistemas, pero no en las reglas de negocio en sí. La clase Errors actúa como un repositorio de constantes de cadena que representan estos errores. Estos errores se utilizan para validar las entradas y salidas de los comandos y consultas, y para manejar excepciones en la capa de aplicación.

Códigos y Mensajes de Error

Los códigos de error para la capa de aplicación seguirán la misma estructura y formato que en la capa de dominio, pero utilizando el rango de códigos 200 para errores de esta capa. Por ejemplo:

  • InvalidInputData ( 201: Invalid input data provided)
  • ServiceUnavailable (202: The service is currently unavailable.)
  • PaymentFailed (203: Payment process failed.)

Errores Definidos en la Capa de Aplicación

La clase Errors (en la capa de Aplicación) contiene las siguientes constantes, que corresponden a diferentes escenarios de error en la capa de aplicación:

using CodeDesignPlus.Net.Core.Abstractions;
namespace CodeDesignPlus.Net.Microservice.Application;
public class Errors: IErrorCodes
{
public const string OrderNotFound = "200 : The order does not exist.";
public const string OrderAlreadyExists = "201 : The order already exists.";
public const string ClientIsNull = "300 : The client is null.";
}

Uso de las Constantes de Error

Estas constantes de error se utilizan en las cláusulas de guardia (guard clauses) de las clases de la capa de aplicación (como los handlers de comandos y consultas), para validar los datos y asegurar que se mantienen las invariantes de la aplicación. Las clases de guardado, como ApplicationGuard, lanzan excepciones específicas (del tipo CodeDesignPlusException) cuando una condición no se cumple, incluyendo el código y mensaje de error correspondiente.

using CodeDesignPlus.Net.Microservice.Domain.ValueObjects;
namespace CodeDesignPlus.Net.Microservice.Application.Order.Commands.CreateOrder;
public class CreateOrderCommandHandler(IOrderRepository orderRepository, IUserContext user, IPubSub pubSub) : IRequestHandler<CreateOrderCommand>
{
public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = await orderRepository.FindAsync(request.Id, cancellationToken);
ApplicationGuard.IsNotNull(order, Errors.OrderAlreadyExists);
// ...
}
}

Startup (Startup.cs)

La sección Startup de la capa de aplicación tiene como objetivo la configuración de la aplicación. A diferencia del dominio, en la capa de aplicación si se realizan configuraciones, como por ejemplo, la configuración del mapper. Las clases Startup en CodeDesignPlus evitan tener toda la configuración centralizada en el Program.cs de .NET Core, haciendo que este archivo no se vuelva extenso e inmanejable.

Configuración en la Capa de Aplicación

En esta capa, el Startup se centra en:

  • Configuraciones personalizadas del comportamiento de la aplicación.
  • Ejecutar la configuración del mapper, a traves de la clase estática MapsterConfigOrder (ejemplo).
Beneficios de esta Configuración

Al centralizar la configuración en el Startup de la capa de aplicación, se evita tener un Program.cs extenso e ilegible. El Startup funciona como punto de entrada para otras configuraciones personalizadas. La configuración del mapper es clave para el buen funcionamiento de la capa de aplicación, evitando hacer asignaciones manuales.

¿Por qué es necesario el uso del Startup en la Capa de Aplicación?

El Startup de la capa de aplicación es necesario para organizar y centralizar configuraciones que son específicas de esta capa. Además, el SDK utiliza esta clase como referencia para métodos de extensión en el escaneo de los assemblies. Aunque el SDK automatiza algunos registros, el desarrollador puede usar el Startup para agregar configuraciones personalizadas.

Setup (Setup)

La carpeta Setup en la capa de aplicación tiene como propósito principal contener configuraciones transversales que se utilizan en toda la capa de aplicación. Es decir, aquellas configuraciones que no pertenecen a un componente específico, sino que son necesarias para el funcionamiento de varios componentes. Esta carpeta sirve como un lugar centralizado para organizar y gestionar la configuración de la aplicación, evitando tenerla dispersa en diferentes partes del código.

En este arquetipo, la clase MapsterConfig dentro de la carpeta Setup es un ejemplo de configuración transversal. Esta clase se utiliza para configurar el mapeo de objetos entre las entidades del dominio y los DTOs de la capa de aplicación. La configuración se realiza utilizando la librería Mapster, una herramienta que simplifica la transformación de datos entre diferentes tipos.

using CodeDesignPlus.Net.Microservice.Domain.ValueObjects;
namespace CodeDesignPlus.Net.Microservice.Application.Setup;
public static class MapsterConfigOrder
{
public static void Configure()
{
TypeAdapterConfig<ClientDto, ClientValueObject>.NewConfig().TwoWays();
TypeAdapterConfig<AddressDto, AddressValueObject>.NewConfig().TwoWays();
TypeAdapterConfig<ProductDto, ProductDto>.NewConfig().TwoWays();
TypeAdapterConfig<OrderAggregate, OrderDto>.NewConfig()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.Client, src => src.Client)
.Map(dest => dest.Products, src => src.Products)
.Map(dest => dest.CompletedAt, src => src.CompletedAt)
.Map(dest => dest.CancelledAt, src => src.CancelledAt)
.Map(dest => dest.Status, src => src.Status)
.Map(dest => dest.CreatedAt, src => src.CreatedAt)
.Map(dest => dest.UpdatedAt, src => src.UpdatedAt)
.Map(dest => dest.CreatedBy, src => src.CreatedBy)
.Map(dest => dest.UpdatedBy, src => src.UpdatedBy)
.Map(dest => dest.ReasonForCancellation, src => src.ReasonForCancellation);
}
}

Capa de Infraestructura (Infrastructure)


La capa de Infraestructura actúa como el puente entre la lógica de negocio (definida en el dominio) y el mundo exterior, siguiendo el patrón de diseño Ports and Adapters (también conocido como Arquitectura Hexagonal). En este enfoque, la capa de dominio es el “núcleo” de la aplicación y se comunica con el exterior a través de “puertos” (interfaces). La capa de infraestructura, por su parte, proporciona las implementaciones concretas de las dependencias externas, actuando como “adaptadores” que se conectan a estos “puertos”. Su principal responsabilidad es, por lo tanto, implementar las tecnologías y los servicios específicos que la aplicación necesita, como bases de datos, servicios de mensajería, APIs externas, sistemas de archivos, etc. En esencia, esta capa se encarga de los detalles de implementación que son específicos de cada tecnología, permitiendo que el dominio se mantenga independiente de estas decisiones.

  • Puertos (Interfaces): Las interfaces como IOrderRepository, definidas en la capa de dominio, actúan como “puertos” que la aplicación utiliza para acceder a la persistencia de datos, sin conocer los detalles de la implementación.

  • Adaptadores (Implementaciones): Las clases de la capa de infraestructura, como OrderRepository, actúan como “adaptadores” que implementan las interfaces de la capa de dominio utilizando tecnologías concretas (en este caso, MongoDB). Esto desacopla el dominio de los detalles de implementación.

Archetype

Responsabilidades de la Capa de Infraestructura

  • Implementación de Repositorios: La capa de infraestructura es responsable de implementar los repositorios, que son las clases que se encargan de la persistencia y recuperación de datos. Estos repositorios interactúan directamente con las bases de datos, sistemas de archivos o cualquier otro mecanismo de almacenamiento.

  • Adaptación a Tecnologías Específicas: La infraestructura se encarga de adaptar la lógica de dominio a las tecnologías específicas de cada entorno. Por ejemplo, si la lógica de dominio requiere una operación de persistencia, la capa de infraestructura se encarga de realizar esta operación utilizando una base de datos específica (MongoDB en este caso).

  • Integración con Servicios Externos: La capa de infraestructura también es responsable de la integración con servicios externos, como APIs de terceros, sistemas de mensajería o servicios de correo electrónico.

  • Implementación de Componentes Técnicos: Además de la persistencia de datos y las integraciones con servicios externos, la capa de infraestructura puede implementar otros componentes técnicos, como servicios de logging, mecanismos de caché o cualquier otro componente de soporte técnico.

  • Transformación de Datos: En muchos casos, es necesario transformar los objetos del dominio a un formato que sea adecuado para la infraestructura. Por ejemplo, al persistir datos en MongoDB, los objetos de dominio se transforman a documentos de MongoDB. La capa de infraestructura es la encargada de realizar estas transformaciones.

El Patrón Repositorio

El patrón Repositorio es una abstracción que se utiliza para desacoplar la lógica de negocio de los detalles específicos de la persistencia de datos. El repositorio actúa como una interfaz entre el dominio y la base de datos, ocultando los detalles de cómo los datos se almacenan y recuperan. El dominio y la aplicación interactúan con el repositorio a través de interfaces, sin preocuparse por la tecnología de persistencia específica (como una base de datos relacional, NoSQL, un sistema de archivos, etc.) o la forma en que se organizan los datos.

Archetype

Beneficios del Uso del Patrón Repositorio:

  • Desacoplamiento: El patrón repositorio desacopla la lógica de dominio de las implementaciones de persistencia, permitiendo que el dominio se centre en las reglas de negocio y la lógica de aplicación, sin tener que conocer cómo se almacenan y recuperan los datos.
  • Testabilidad: Facilita la realización de pruebas unitarias, ya que los repositorios pueden ser reemplazados por implementaciones simuladas, que permiten comprobar la lógica del dominio sin depender de una base de datos concreta.
  • Flexibilidad: Permite cambiar la tecnología de persistencia (como la base de datos) sin tener que modificar la lógica de negocio, ya que los cambios se realizan únicamente en las implementaciones de los repositorios.
  • Mantenibilidad: Centraliza el código de acceso a datos, lo que facilita su mantenimiento y mejora su comprensión.

Implementación del Repositorio OrderRepository

El arquetipo, hace uso de dos patrones clave: Ports and Adapters y el patrón Repositorio. En este contexto, la clase OrderRepository actúa como un “adapter” dentro del patrón Ports and Adapters, implementando la interfaz IOrderRepository (el “port”) que fue definida en la capa de dominio. Para la persistencia de datos, OrderRepository utiliza MongoDB y la librería oficial de MongoDB. Al hacer esto, se adhiere al principio de inversión de dependencias. La capa de aplicación se comunica con la capa de infraestructura a través de la interfaz IOrderRepository de la capa de dominio, desconociendo la implementación concreta del repositorio.

namespace CodeDesignPlus.Net.Microservice.Infrastructure.Repositories;
public class OrderRepository(IServiceProvider serviceProvider, IOptions<MongoOptions> mongoOptions, ILogger<RepositoryBase> logger)
: RepositoryBase(serviceProvider, mongoOptions, logger), IOrderRepository
{
public Task AddProductToOrderAsync(Guid id, Guid tenant, AddProductToOrderParams parameters, CancellationToken cancellationToken)
{
var product = new ProductEntity
{
Id = parameters.Id,
Name = parameters.Name,
Description = parameters.Description,
Price = parameters.Price,
Quantity = parameters.Quantity,
};
var filter = Builders<OrderAggregate>.Filter.And(
Builders<OrderAggregate>.Filter.Eq(x => x.Id, id),
Builders<OrderAggregate>.Filter.Eq(x => x.Tenant, tenant)
);
var update = Builders<OrderAggregate>.Update
.Push(x => x.Products, product)
.Set(x => x.UpdatedAt, parameters.UpdatedAt)
.Set(x => x.UpdatedBy, parameters.UpdateBy);
var collection = base.GetCollection<OrderAggregate>();
return collection.UpdateOneAsync(
filter,
update,
cancellationToken: cancellationToken
);
}
public Task CancelOrderAsync(CancelOrderParams parameters, Guid tenant, CancellationToken cancellationToken)
{
var filter = Builders<OrderAggregate>.Filter.And(
Builders<OrderAggregate>.Filter.Eq(x => x.Id, parameters.Id),
Builders<OrderAggregate>.Filter.Eq(x => x.Tenant, tenant)
);
var update = Builders<OrderAggregate>.Update
.Set(x => x.Status, parameters.OrderStatus)
.Set(x => x.ReasonForCancellation, parameters.Reason)
.Set(x => x.CancelledAt, parameters.CancelledAt)
.Set(x => x.UpdatedAt, parameters.UpdatedAt)
.Set(x => x.UpdatedBy, parameters.UpdateBy);
return base.GetCollection<OrderAggregate>().UpdateOneAsync(filter, update, cancellationToken: cancellationToken);
}
public Task CompleteOrderAsync(CompleteOrderParams parameters, Guid tenant, CancellationToken cancellationToken)
{
var filterId = Builders<OrderAggregate>.Filter.And(
Builders<OrderAggregate>.Filter.Eq(x => x.Id, parameters.Id),
Builders<OrderAggregate>.Filter.Eq(x => x.Tenant, tenant)
);
var update = Builders<OrderAggregate>.Update
.Set(x => x.CompletedAt, parameters.CompletedAt)
.Set(x => x.Status, parameters.OrderStatus)
.Set(x => x.UpdatedAt, parameters.UpdatedAt)
.Set(x => x.UpdatedBy, parameters.UpdateBy);
return base.GetCollection<OrderAggregate>().UpdateOneAsync(filterId, update, cancellationToken: cancellationToken);
}
public Task RemoveProductFromOrderAsync(RemoveProductFromOrderParams parameters, Guid tenant, CancellationToken cancellationToken)
{
var filterId = Builders<OrderAggregate>.Filter.And(
Builders<OrderAggregate>.Filter.Eq(x => x.Id, parameters.Id),
Builders<OrderAggregate>.Filter.Eq(x => x.Tenant, tenant)
);
var update = Builders<OrderAggregate>.Update
.PullFilter(x => x.Products, p => p.Id == parameters.IdProduct)
.Set(x => x.UpdatedAt, parameters.UpdatedAt)
.Set(x => x.UpdatedBy, parameters.UpdateBy);
return base.GetCollection<OrderAggregate>().UpdateOneAsync(filterId, update, cancellationToken: cancellationToken);
}
public Task UpdateQuantityProductAsync(Guid id, Guid tenant, UpdateQuantityProductParams parameters, CancellationToken cancellationToken)
{
var filterId = Builders<OrderAggregate>.Filter.And(
Builders<OrderAggregate>.Filter.Eq(x => x.Id, id),
Builders<OrderAggregate>.Filter.Eq(x => x.Tenant, tenant),
Builders<OrderAggregate>.Filter.ElemMatch(x => x.Products, x => x.Id == parameters.Id)
);
var update = Builders<OrderAggregate>.Update
.Set("Products.$.Quantity", parameters.NewQuantity)
.Set(x => x.UpdatedAt, parameters.UpdatedAt)
.Set(x => x.UpdatedBy, parameters.UpdateBy);
return base.GetCollection<OrderAggregate>().UpdateOneAsync(filterId, update, cancellationToken: cancellationToken);
}
}

Errores (Errors.cs)

La capa de aplicación tambien tiene su propia clase Errors para centralizar y definir los códigos y mensajes de error específicos que pueden ocurrir en la capa de aplicación. Estos errores reflejan problemas en la lógica de la aplicación o en la interacción con otros sistemas, pero no en las reglas de negocio en sí. La clase Errors actúa como un repositorio de constantes de cadena que representan estos errores. Estos errores se utilizan para validar las entradas y salidas de los comandos y consultas, y para manejar excepciones en la capa de aplicación.

Códigos y Mensajes de Error

Los códigos de error para la capa de infraestructura seguirán la misma estructura y formato que en las otras capas, pero utilizando el rango de códigos 300 para errores de esta capa. Por ejemplo:

  • DatabaseConnectionFailed (301: Failed to connect to the database.)
  • FileStorageError (302: Error accessing the file system.)
  • MessageQueueError (303: Failed to publish message.)

Errores Definidos en la Capa de Infraestructura

La clase Errors (en la capa de Infraestructura) contiene las siguientes constantes, que corresponden a diferentes escenarios de error en la capa de infraestructura:

namespace CodeDesignPlus.Net.Microservice.Infrastructure;
public class Errors: IErrorCodes
{
public const string UnknownError = "000 : An unknown error occurred.";
}

Uso de las Constantes de Error

Estas constantes se utilizan en las implementaciones concretas de los repositorios y otros componentes de infraestructura para lanzar excepciones específicas (del tipo CodeDesignPlusException) cuando ocurre algún problema en la capa de infraestructura.

public class OrderRepository : IOrderRepository {
public async Task<OrderAggregate> FindAsync(Guid id, CancellationToken cancellationToken)
{
try{
// código para consultar en base de datos
}catch(Exception ex){
throw new CodeDesignPlusException(Errors.DatabaseConnectionFailed, ex);
}
}
}

Startup (Startup.cs)

Al igual que en la capa de dominio, el Startup en la capa de infraestructura, en este contexto, no realiza ninguna configuración específica en este contexto, manteniendolo vacio. Esto se debe a que el SDK CodeDesignPlus automatiza los registros de los servicios de la infraestructura como los repositorios. Sin embargo, la clase Startup sigue siendo necesaria para el funcionamiento del SDK.

namespace CodeDesignPlus.Net.Microservice.Infrastructure
{
public class Startup : IStartup
{
public void Initialize(IServiceCollection services, IConfiguration configuration)
{
}
}
}
  • Beneficios de un Startup Vacío (Cuando Aplica)

    El principal beneficio de no tener configuraciones en la capa de infraestructura en este caso es la simplicidad de esta capa. Al igual que en el dominio, en esta capa no hay ninguna configuración por defecto que deba llevarse a cabo. El desarrollador puede tener la libertad de configurar a su gusto.

  • **¿Por qué es necesario un Startup (Aunque Esté Vacío)? **

    Si bien la clase Startup en la capa de infraestructura está vacía en este arquetipo, sigue siendo necesaria como referencia para los métodos de extensión del SDK CodeDesignPlus.Net. El SDK utiliza esta clase para escanear los assemblies de la aplicación y registrar automáticamente los servicios de la infraestructura como los repositorios. Así, se automatizan los registros, pero se deja la posibilidad al desarrollador de agregar configuraciones adicionales.

Entrypoints (Puntos de Entrada)


En el contexto de la arquitectura Ports and Adapters (también conocida como arquitectura hexagonal), los entry points representan los puntos de acceso a la aplicación desde el mundo exterior. Son las puertas de entrada por las cuales los usuarios o sistemas externos interactúan con la lógica de negocio. Estos entry points actúan como los “drivers” o “actuadores” del patrón, encargándose de la recepción de las solicitudes del mundo exterior, la orquestación de la lógica de aplicación y el envío de las respuestas adecuadas. En esencia, un entry point es un “adaptador” que se conecta a los “puertos” (interfaces) definidos en la capa de aplicación para que esta última pueda interactuar con el mundo exterior.

En CodeDesignPlus, los entry points manejan las peticiones que vienen de fuentes como eventos de dominio, rest api o grpc y se encargan de realizar el mapeo de los datos de entrada a DTOs que sean adecuados para la capa de aplicación. Es decir, a través de los entry points, los datos de entrada se convierten en el lenguaje que la capa de aplicación entiende. Los entry points no deben realizar validaciones, ni lógica de negocio.

Archetype

CodeDesignPlus.Net.Microservice.AsyncWorker

Este entry point está diseñado para procesar tareas asíncronas en segundo plano. Es ideal para ejecutar procesos que no requieren una respuesta inmediata y que pueden ser procesados de forma diferida, como el procesamiento de datos en lotes, tareas programadas, el envío de correos electrónicos, etc.

CodeDesignPlus.Net.Microservice.AsyncWorker se implementa como un worker service o un proceso en segundo plano que escucha un determinado tipo de mensaje o evento. Al recibir un mensaje, el worker procesa la información utilizando la lógica de la capa de aplicación.

Responsabilidades:

  • Escuchar mensajes de colas de mensajeria (como RabbitMQ, Kafka).
  • Deserializar los mensajes a DTOs de la capa de aplicación.
  • Invocar los comandos de la capa de aplicación para procesar la información recibida.
  • Loggear errores, exepciones y métricas.

Relación con Ports and Adapters:

Actúa como un “adaptador” que recibe eventos y mensajes del mundo exterior (cola de mensajería) y los transforma en solicitudes para la capa de aplicación.

CodeDesignPlus.Net.Microservice.gRpc

Este entry point utiliza el framework gRPC, un moderno framework de código abierto de alto rendimiento para llamadas a procedimientos remotos (RPC). Es ideal para aplicaciones que requieren una comunicación eficiente y de baja latencia, como microservicios o sistemas que necesitan una comunicación en tiempo real.

CodeDesignPlus.Net.Microservice.gRpc se implementa como un servicio gRPC que define un protocolo basado en protobuf para la comunicación. Recibe peticiones a través de canales gRPC, deserializa los mensajes a DTOs de la capa de aplicación y devuelve respuestas utilizando el mismo protocolo.

Responsabilidades:

  • Recibir peticiones gRPC.
  • Deserializar los mensajes protobuf a DTOs de la capa de aplicación.
  • Invocar los comandos y queries de la capa de aplicación.
  • Serializar las respuestas a mensajes protobuf.
  • Manejar canales gRPC.

Relación con Ports and Adapters:

Actúa como un “adaptador” que recibe peticiones a través de gRPC del mundo exterior y las transforma en solicitudes para la capa de aplicación, exponiendo la lógica de la aplicación a través de este protocolo.

CodeDesignPlus.Net.Microservice.Rest

Este entry point expone la lógica de la aplicación como una API REST. Es el entry point adecuado para aplicaciones que necesitan ser accedidas por clientes web, aplicaciones móviles o cualquier otro sistema que utilice el protocolo HTTP.

CodeDesignPlus.Net.Microservice.Rest se implementa como una API REST basada en ASP.NET Core. Recibe peticiones HTTP (GET, POST, PUT, DELETE, etc.), deserializa los datos del request a DTOs de la capa de aplicación, y devuelve las respuestas serializadas en un formato específico (como JSON).

Responsabilidades:

  • Recibir peticiones HTTP.
  • Deserializar el request body a DTOs de la capa de aplicación.
  • Invocar los comandos y queries de la capa de aplicación.
  • Serializar las respuestas a JSON (u otro formato).
  • Manejar peticiones y respuestas HTTP.

Relación con Ports and Adapters:

Actúa como un “adaptador” que recibe peticiones HTTP del mundo exterior y las transforma en solicitudes para la capa de aplicación, exponiendo la lógica del dominio a través de la API.