Manejando la Complejidad del Negocio con los patrones DDD y CQRS

Visión

Diseñar un modelo de dominio para cada microservicio o Bounded Context que refleje la comprensión del dominio del negocio.

Esta sección se enfoca en los microservicios más avanzados que usted implementa cuando necesita manejar subsistemas complejos, o microservicios derivados del conocimiento de expertos de un dominio, con reglas del negocio en cambio constante. Los patrones de arquitectura utilizados en esta sección se basan en el diseño orientado por el dominio (DDD) y los enfoques de Segregación de las Responsabilidades de Comando y Consulta (CQRS), como se ilustra en la Figura 7-1.

Image

Figura 7-1. Arquitectura externa de microservicios versus patrones arquitectónicos para cada microservicio

Sin embargo, la mayoría de las técnicas usadas en microservicios de datos, como la implementación de un servicio ASP.NET Core Web API, o exponer los metadatos Swagger con Swashbuckle, también son aplicables a los microservicios más avanzados implementados con DDD. Esta sección es una extensión de las secciones anteriores, porque la mayoría de las prácticas explicadas anteriormente también se aplican aquí o para cualquier tipo de microservicio.

Esta sección primero proporciona detalles sobre los patrones simplificados CQRS usados en la aplicación de referencia eShopOnContainers. Más adelante, veremos una descripción general de las técnicas de DDD que le permiten encontrar patrones comunes que puede reutilizar en sus aplicaciones.

DDD es un tema amplio, con un gran conjunto de recursos para el aprendizaje. Puede comenzar con libros como Domain-Driven Design de Eric Evans y materiales adicionales de Vaughn Vernon, Jimmy Nilsson, Greg Young, Udi Dahan, Jimmy Bogard y muchos otros expertos de DDD/CQRS. Pero, sobre todo, debe intentar aprender a aplicar las técnicas de DDD a partir de las conversaciones, los dibujos en las pizarras y las sesiones de modelado con los expertos en el dominio concreto de su negocio.

Recursos adicionales

DDD (Domain-Driven Design)
Libros sobre DDD

 

Formación sobre DDD

Aplicando patrones simplificados de CQRS y DDD en un microservicio

CQRS es un patrón arquitectónico que separa los modelos para leer y escribir datos. El término relacionado Command Query Separation (CQS) fue originalmente definido por Bertrand Meyer en su libro Object Oriented Software Construction. La idea básica es que se pueden dividir las operaciones de un sistema en dos categorías muy diferentes:

CQS es un concepto simple: se trata de métodos dentro del mismo objeto que son consultas o comandos. Cada método devuelve el estado o cambia el estado, pero no ambos. Incluso un objeto del patrón de repositorio puede cumplir con CQS. CQS se puede considerar un principio fundamental para CQRS.

Command and Query Responsibility Segregation (CQRS) fue presentada por Greg Young y fuertemente promovida por Udi Dahan y otros. Se basa en el principio CQS, aunque es más detallado. Se puede considerar un patrón basado en comandos y eventos, además de, opcionalmente, mensajes asíncronos. En muchos casos, CQRS se relaciona con escenarios más avanzados, como tener una base de datos física diferente para lecturas (consultas) que para escrituras (actualizaciones). Además, un sistema CQRS más evolucionado podría implementar Event-Sourcing (ES) para su base de datos de actualizaciones, por lo que sólo almacenaría eventos en el modelo de dominio en lugar de almacenar los datos del estado actual. Sin embargo, este no es el enfoque utilizado en esta guía. Estamos utilizando el enfoque más simple de CQRS, que consiste en separar las consultas de los comandos.

El aspecto de separación de CQRS se logra agrupando las operaciones de consulta en una capa y los comandos en otra capa. Cada capa tiene su propio modelo de datos (nótese que decimos modelo, no necesariamente una base de datos diferente) y está construido usando su propia combinación de patrones y tecnologías. Lo que es más importante, las dos capas pueden estar dentro del mismo nivel o microservicio, como en el ejemplo (microservicio de pedidos) utilizado para esta guía. O podrían implementarse en diferentes microservicios o procesos para que puedan optimizarse y escalarse por separado sin afectarse mutuamente.

CQRS significa tener dos objetos para una operación de lectura/escritura donde en otros contextos hay uno. Existen razones para tener una base de datos de consulta desnormalizada, sobre los que puede aprender en la literatura de CQRS más avanzada. Pero no estamos usando ese enfoque aquí, donde el objetivo es tener más flexibilidad en las consultas en vez de limitar las consultas a las restricciones de los patrones DDD como los agregados.

Un ejemplo de este tipo de servicio es el microservicio de pedidos de la aplicación eShopOnContainers. Este servicio implementa un microservicio basado en un enfoque simplificado de CQRS. Utiliza una fuente única de datos o base de datos, pero dos modelos lógicos y patrones DDD para el dominio transaccional, como se muestra en la Figura 7-2.

Image

Figura 7-2. Microservicio basado en DDD y CQRS - Simplificado

La capa de aplicación puede ser la API Web en sí misma. El aspecto importante del diseño aquí es que el microservicio ha dividido las consultas y ViewModels (modelos de datos especialmente creados para las aplicaciones cliente) de los comandos, modelo de dominio y transacciones que siguen el patrón CQRS. Este enfoque mantiene las consultas independientes de las limitaciones y restricciones provenientes de los patrones DDD que sólo tienen sentido para las transacciones y actualizaciones, como se explica en secciones posteriores.

Aplicando los patrones CQRS y CQS en un microservicio DDD en eShopOnContainers

El diseño del microservicio de pedidos en la aplicación de referencia eShopOnContainers, se basa en los principios de CQRS. Sin embargo, utiliza el enfoque más simple, que es simplemente separar las consultas de los comandos y usar la misma base de datos para ambas acciones.

La esencia de esos patrones y el punto importante aquí, es que las consultas son idempotentes: no importa cuántas veces consulte un sistema, el estado de ese sistema no cambiará. Incluso podría usar un modelo de datos de "consulta" diferente al modelo que es “actualizado” por la lógica transaccional, aunque el microservicio de pedidos esté usando la misma base de datos. Por lo tanto, este es un enfoque simplificado de CQRS.

Por otro lado, los comandos, que activan transacciones y actualizaciones de datos, cambian de estado en el sistema. Hay que ser cuidadoso con los comandos, al manejar la complejidad y las reglas cambiantes del negocio. Aquí es donde se deben aplicar las técnicas de DDD para tener un sistema mejor modelado.

Los patrones DDD presentados en esta guía no deben aplicarse universalmente. Introducen restricciones en su diseño. Esas restricciones proporcionan beneficios tales como una mayor calidad en el tiempo, especialmente en los comandos y otras operaciones que modifican el estado del sistema. Sin embargo, esas restricciones agregan complejidad y limitaciones a la hora de leer y consultar los datos.

Uno de estos patrones es el patrón Agregado, que examinaremos con más detalle en secciones posteriores. En resumen, en el patrón Agregado, trata muchos objetos del dominio como una sola unidad, producto de sus relaciones en el dominio. Es posible que no siempre obtenga ventajas de este patrón en las consultas, de hecho, puede aumentar la complejidad de las consultas. Para las consultas no aporta ninguna ventaja el tratar varios objetos como un agregado único. Sólo agrega complejidad.

Como se muestra en la Figura 7-2, esta guía sugiere el uso de patrones DDD sólo en el área transaccional/actualizaciones de su microservicio (es decir, las que se producen a consecuencia de los comandos). Las consultas pueden seguir un enfoque más simple y deben separarse de los comandos, siguiendo un enfoque CQRS.

Para implementar el "lado de consulta", puede elegir entre muchos enfoques, desde un ORM completo como EF Core, proyecciones de AutoMapper, procedimientos almacenados, vistas, vistas materializadas o un micro ORM.

En esta guía y en eShopOnContainers (específicamente en el microservicio de pedidos) elegimos implementar consultas directas utilizando un micro ORM como Dapper. Esto le permite implementar cualquier consulta basada en sentencias de SQL para obtener el mejor rendimiento, gracias a un framework ligero y muy eficiente.

Tenga en cuenta que cuando utiliza este enfoque, cualquier actualización de su modelo que afecte a la persistencia de las entidades en una base de datos SQL también necesita actualizaciones separadas para las consultas SQL utilizadas por Dapper o cualquier otro enfoque separado (donde no use EF) para realizar consultas.

CQRS y los patrones DDD no son arquitecturas de alto nivel

Es importante entender que CQRS y la mayoría de los patrones DDD (como las capas DDD o un modelo de dominio con agregados) no son estilos arquitectónicos, sino sólo patrones de arquitectura. Microservicios, SOA y arquitectura basada en eventos (EDA) son ejemplos de estilos arquitectónicos. Describen un sistema de muchos componentes, como muchos microservicios. CQRS y los patrones DDD describen algo dentro de un solo sistema o componente, en este caso, algo dentro de un microservicio.

Bounded Contexts (BC) diferentes emplearán patrones diferentes. Tienen diferentes responsabilidades, y eso lleva a soluciones diferentes. Vale la pena enfatizar que forzar el mismo patrón en todas partes conduce al fracaso. No use CQRS y patrones DDD en todas partes. Muchos subsistemas, BC o microservicios son más simples y se pueden implementar más fácilmente usando servicios CRUD simples u otro enfoque.

Sólo hay una arquitectura de aplicación: la arquitectura del sistema o la aplicación completa, de punta a punta, que está diseñando (por ejemplo, la arquitectura de microservicios). Sin embargo, el diseño de cada Bounded Context o microservicio, dentro de esa aplicación, refleja sus propios compromisos y decisiones de diseño interno, en un nivel de patrones de arquitectura. No intente aplicar los mismos patrones arquitectónicos como CQRS o DDD en todas partes.

Recursos adicionales

https://martinfowler.com/bliki/CQRS.html

http://codebetter.com/gregyoung/2009/08/13/command-query-separation/

https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/

http://udidahan.com/2009/12/09/clarified-cqrs/

http://codebetter.com/gregyoung/2010/02/20/why-use-event-sourcing/

Implementando consultas en un microservicio CQRS

Para las lecturas/consultas, el microservicio de pedidos de la aplicación de referencia eShopOnContainers, implementa las consultas independientemente del modelo DDD y del área transaccional. Esto se hizo principalmente porque las exigencias de consultas y transacciones son drásticamente diferentes. Las actualizaciones implican transacciones que deben cumplir con la lógica del dominio. Las consultas, por otro lado, son idempotentes y se pueden distanciar de las reglas de dominio.

El enfoque es simple, como se muestra en la Figura 7-3. La API es implementada por los controladores de API Web, usando cualquier infraestructura (con un micro ORM como Dapper) y devolviendo ViewModels dinámicos, dependiendo de las necesidades de la interfaz de usuario.

Image

Figura 7-3. El enfoque más sencillo posible para consultas en un microservicio CQRS

Este es el enfoque más simple posible para las consultas. Las definiciones de consulta acceden a la base de datos y devuelven un ViewModel dinámico, creado sobre la marcha, para cada consulta. Dado que las consultas son idempotentes, los resultados no cambiarán, sin importar cuántas veces ejecute una consulta. Por lo tanto, no necesita estar restringido por ningún patrón DDD utilizado en el lado de las transacciones, como agregados y otros patrones y es por eso que las consultas están separadas del área transaccional. Simplemente consulte la base de datos para obtener la información que necesita la interfaz de usuario y devuelva un ViewModel dinámico que no necesita estar declarado estáticamente en ningún lugar (no hay clases para los ViewModels) excepto en las propias sentencias SQL.

Dado que este es un enfoque simple, el código requerido en el lado de las consultas (como el código que usa un micro ORM como Dapper) se puede implementar dentro del mismo proyecto de API Web. La figura 7-4 muestra esto. Las consultas se definen en el proyecto de microservicio Ordering.API dentro de la solución eShopOnContainers.

Image

Figura 7-4. Las consultas en el microservicio de pedidos en eShopOnContainers

Usando ViewModels hechos específicamente para las aplicaciones cliente, independientes de las restricciones del modelo de dominio

Dado que las consultas se realizan para obtener los datos que necesitan las aplicaciones cliente, el tipo devuelto se puede hacer específicamente para éstas, en función de los datos devueltos por las consultas. Estos modelos, o Data Transfer Objects (DTO), se llaman ViewModels.

Los datos devueltos (ViewModel) pueden ser el resultado de unir datos de múltiples entidades o tablas en la base de datos o, incluso, a partir de múltiples agregados definidos en el modelo de dominio para el área transaccional. En este caso, debido a que está creando consultas independientes del modelo de dominio, los límites y restricciones de los agregados se ignoran por completo y puede consultar cualquier tabla y columna que pueda necesitar. Este enfoque proporciona una gran flexibilidad y productividad para los desarrolladores que crean o actualizan las consultas.

Los ViewModels pueden ser tipos estáticos definidos en clases, o se pueden crear dinámicamente en función de las consultas realizadas (como se implementa en el microservicio de pedidos), lo que resulta muy ágil para los desarrolladores.

Usando Dapper como micro ORM para realizar las consultas

Puede usar cualquier micro ORM, Entity Framework Core o, incluso, simplemente ADO.NET para realizar consultas. En la aplicación de referencia, seleccionamos Dapper para el microservicio de pedidos en eShopOnContainers, como un buen ejemplo de un micro ORM popular. Puede ejecutar consultas SQL simples con gran rendimiento, porque es un framework muy ligero. Con Dapper, puede escribir una consulta SQL que pueda acceder y hacer joins entre varias tablas.

Dapper es un proyecto open source (original creado por Sam Saffron) y es parte de los componentes utilizados en Stack Overflow. Para usar Dapper, sólo necesita instalarlo a través del paquete Dapper NuGet, como se muestra en la siguiente figura.

Image

También necesitará agregar una declaración using para que su código tenga acceso a los extension methods de Dapper.

Cuando use Dapper en su código, se usa directamente la clase SqlClient disponible en el namespace System.Data.SqlClient. A través del método QueryAsync y otros extension methods que extienden la clase SqlClient, puede simplemente ejecutar consultas de una manera directa y rápida.

ViewModels dinámicos versus estáticos

Al devolver ViewModels desde el lado del servidor a las aplicaciones cliente, puede pensar en esos ViewModels como DTO (Data Transfer Objects) que pueden ser diferentes a las entidades del dominio, porque los ViewModels manejan los datos como los necesita la aplicación cliente. Por lo tanto, en muchos casos, puede agregar datos provenientes de múltiples entidades de dominio y componer los ViewModels precisamente de la forma en que la aplicación cliente los necesita.

Esos ViewModels o DTO se pueden definir explícitamente (como clases portadoras de datos), como la clase OrderSummary mostrada en un fragmento de código más adelante, o simplemente se pueden devolver ViewModles dinámicos, en función de los atributos obtenidos con las consultas.

El ViewModel como un tipo dinámico

Como se muestra en el siguiente código, las consultas pueden devolver directamente un ViewModel basado en los atributos devueltos por el query, simplemente usando el tipo dynamic, como parámetro genérico de QueryAsync<>(). Eso significa que la lista de atributos devueltos se basa en la consulta misma. Si agrega una nueva columna a la consulta o join, esa información se agrega dinámicamente al ViewModel resultante.

using Dapper;

using Microsoft.Extensions.Configuration;

using System.Data.SqlClient;

using System.Threading.Tasks;

using System.Dynamic;

using System.Collections.Generic;

public class OrderQueries : IOrderQueries

{

public async Task<IEnumerable<dynamic>> GetOrdersAsync()

{

using (var connection = new SqlConnection(_connectionString))

{

connection.Open();

return await connection.QueryAsync<dynamic>(@"SELECT o.[Id] as ordernumber,

o.[OrderDate] as [date],os.[Name] as [status],

SUM(oi.units*oi.unitprice) as total

FROM [ordering].[Orders] o

LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid

LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id

GROUP BY o.[Id], o.[OrderDate], os.[Name]");

}

}

}

El punto importante es que, al usar un tipo dinámico, la colección de datos devuelta se ensamblará dinámicamente como el ViewModel.

Ventajas: Este enfoque reduce la necesidad de modificar ViewModels estáticos cada vez que actualiza la sentencia de SQL de una consulta, lo que hace que este enfoque de diseño sea bastante ágil al codificar, directo y rápido para evolucionar con respecto a cambios futuros.

Inconvenientes: A largo plazo, los tipos dinámicos pueden afectar negativamente la claridad e, incluso, afectar la compatibilidad de un servicio con las aplicaciones cliente. Además, el software de middleware como Swagger, no puede ofrecer el mismo nivel de documentación en los tipos devueltos si se usan tipos dinámicos.

Los ViewModels como clases DTO predefinidas

Ventajas: Tener clases estáticas predefinidas como ViewModel, como los "contratos" basados en clases explícitas de DTO, es definitivamente mejor para las API públicas, pero también para los microservicios a largo plazo, incluso si sólo son utilizados por la misma aplicación.

Si desea especificar tipos de respuesta para Swagger, debe usar clases DTO explícitas como tipo devuelto. Por lo tanto, las clases DTO predefinidas le permiten ofrecer una información más detallada con Swagger. Eso beneficiará la documentación de la API y la compatibilidad cuando la consuma.

Inconvenientes: Como se mencionó, cuando se actualiza el código, se requieren algunos pasos más para actualizar las clases de DTO.

Sugerencia basada en nuestra experiencia: en las consultas implementadas en el microservicio de pedido en eShopOnContainers, comenzamos a desarrollar mediante el uso de ViewModels dinámicos, ya que era muy sencillo y ágil cuando se desarrollaba. Pero, una vez que el desarrollo se estabilizó, optamos por refactorizar esto y usar DTO estáticos para los ViewModels, debido a las ventajas mencionadas.

En el siguiente código, puede ver cómo, en este caso, la consulta está devolviendo datos mediante el uso de una clase DTO de ViewModel explícita: la clase OrderSummary.

using Dapper;

using Microsoft.Extensions.Configuration;

using System.Data.SqlClient;

using System.Threading.Tasks;

using System.Dynamic;

using System.Collections.Generic;

 

public class OrderQueries : IOrderQueries

{

public async Task<IEnumerable<OrderSummary>> GetOrdersAsync()

{

using (var connection = new SqlConnection(_connectionString))

{

connection.Open();

var result = await connection.QueryAsync< OrderSummary>(

@"SELECT o.[Id] as ordernumber,

o.[OrderDate] as [date],os.[Name] as [status],

SUM(oi.units*oi.unitprice) as total

FROM [ordering].[Orders] o

LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid

LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id

GROUP BY o.[Id], o.[OrderDate], os.[Name]

ORDER BY o.[Id]");

}

}

}

Describiendo los tipos devueltos por las APIs Web

Los desarrolladores que consumen APIs Web y microservicios, están más preocupados por lo que se devuelve, específicamente los tipos de respuesta y los códigos de error (si no son estándar). Estos se manejan en los comentarios de XML y en las anotaciones de datos.

Sin la documentación adecuada en la interfaz de usuario de Swagger, el consumidor no tiene conocimiento de qué tipo se devuelve o cuáles pueden ser los códigos Http resultantes. Ese problema se soluciona agregando el atributo ProducesResponseType (definido en Microsoft.AspNetCore.Mvc), para que Swagger genere una información más completa sobre el modelo y los valores devueltos por la API, como se muestra en el siguiente código:

namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers

{

[Route("api/v1/[controller]")]

[Authorize]

public class OrdersController : Controller

{

//Additional code...

[Route("")]

[HttpGet]

[ProducesResponseType(typeof(IEnumerable<OrderSummary>),

(int)HttpStatusCode.OK)]

public async Task<IActionResult> GetOrders()

{

var orderTask = _orderQueries.GetOrdersAsync();

var orders = await orderTask;

return Ok(orders);

}

}

}

Sin embargo, el atributo ProducesResponseType no puede usar dynamic como tipo, sino que requiere el uso de tipos explícitos, como OrderSummary, el ViewModel DTO que se muestra en el siguiente fragmento de código.

public class OrderSummary

{

public int ordernumber { get; set; }

public DateTime date { get; set; }

public string status { get; set; }

public double total { get; set; }

}

Esta es otra razón por la cual, a largo plazo, es mejor devolver tipos explícitos que dinámicos.

Al utilizar el atributo ProducesResponseType, también puede especificar cuál es el resultado esperado con respecto a posibles errores/códigos HTTP, como 200,400, etc.

En la siguiente imagen, puede ver cómo la interfaz de usuario de Swagger muestra la información del ResponseType.

C:\Users\CESARDL\AppData\Local\Microsoft\Windows\INetCache\Content.Word\image-new.png

Figura 7-5. Swagger UI mostrando los tipos de respuesta y los posibles estatus Http de una API Web

Puede ver en la imagen de arriba algunos valores de ejemplo basados en los tipos de ViewModel, más los posibles códigos Http que se pueden devolver.

Recursos adicionales

https://github.com/StackExchange/dapper-dot-net

https://msdn.microsoft.com/magazine/mt703432.aspx

https://docs.microsoft.com/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studio

Diseñando un microservicio orientado a DDD

El diseño impulsado por dominio (DDD) aboga por el modelado basado en la realidad del negocio, como lo relevante para sus casos de uso. En el contexto de la creación de aplicaciones, DDD se refiere a los problemas como “dominios”. Describe las áreas problemáticas independientes como Bounded Contexts (cada Bounded Context se correlaciona con un microservicio) y enfatiza el uso de un lenguaje común (lenguaje ubicuo) para hablar de estos problemas o dominios. También sugiere muchos conceptos y patrones técnicos, como entidades de dominio con modelos enriquecidos (sin usar modelos de dominio anémicos), objetos de valor (value objects), agregados y raíz de agregación (o entidad raíz) para soportar la implementación interna. Esta sección presenta el diseño y la implementación de esos patrones internos.

Algunas veces, estas reglas y patrones técnicos de DDD se perciben como obstáculos, que tienen una curva de aprendizaje pronunciada para implementar enfoques de DDD. Pero la parte importante no son los patrones en sí mismos, sino la organización del código para que esté alineado con los problemas del negocio y usando los mismos términos del negocio (lenguaje ubicuo). Además, los enfoques DDD deberían aplicarse sólo si está implementando microservicios complejos con reglas del negocio significativas. Las responsabilidades más simples, como un servicio CRUD, se pueden gestionar con enfoques más sencillos.

La tarea clave para diseñar y definir un microservicio, es dónde dibujar los límites. Los patrones DDD le ayudan a comprender la complejidad del dominio. Para el modelo de dominio de cada Bounded Context (BC), debe identificar y definir las entidades, los value objects y los agregados que modelan su dominio. Usted debe crear y refinar un modelo de dominio que esté dentro del límite que define su contexto. Y eso es muy explícito en la forma de un microservicio. Los componentes dentro de esos límites terminan siendo sus microservicios, aunque en algunos casos un BC o microservicio del negocio puede estar compuesto por varios servicios físicos. DDD es sobre límites, así como también lo son los microservicios.

Mantener los límites del contexto del microservicio relativamente pequeños

Determinar dónde colocar los límites entre contextos delimitados, es lograr un balance entre dos objetivos contrapuestos. En primer lugar, se desea crear los microservicios más pequeños posibles, aunque ese no debería ser el principal motivo, sino crear un límite alrededor de las cosas que necesitan cohesión. En segundo lugar, se desea evitar las comunicaciones excesivas (chatty, “chismorreras”) entre microservicios. Estos objetivos se pueden contradecir entre sí. Debe equilibrarlos descomponiendo el sistema en tantos microservicios pequeños como sea posible, hasta que vea que los límites de comunicación crecen rápidamente con cada intento adicional de separar un nuevo Bounded Context. La cohesión es clave dentro de cada Bounded Context.

Es similar al Inappropriate Intimacy code smell (problema por intimidad inapropiada) al implementar clases. Si dos microservicios necesitan colaborar mucho entre ellos, probablemente deberían ser el mismo microservicio.

Otra forma de ver esto es la autonomía. Si un microservicio debe confiar en otro servicio para atender directamente una solicitud, no es verdaderamente autónomo.

Las capas en los microservicios DDD

La mayoría de las aplicaciones empresariales, con una complejidad técnica y del negocio significativa, están definidas por varias capas. Las capas son un artefacto lógico y no están relacionadas con el despliegue del servicio. Son un recurso para ayudar a los desarrolladores a administrar la complejidad del código. Las diferentes capas (como la capa de modelo de dominio frente a la capa de presentación, etc.) pueden manejar diferentes tipos (clases), lo que obliga a realizar traducciones entre esos tipos.

Por ejemplo, una entidad se podría cargar desde la base de datos. Entonces parte de esa información, o una agregación de información que incluya datos adicionales de otras entidades, se puede enviar a la interfaz de usuario del cliente a través de una API REST Web. El punto aquí es que la entidad de dominio está contenida dentro de la capa de modelo de dominio y no se debe propagar a otras áreas a las que no pertenece, como la capa de presentación.

Además, debe tener entidades siempre válidas (consulte la sección Diseñar validaciones en la capa de modelo de dominio, más adelante) controladas por raíces de agregación (entidades raíz). Por lo tanto, las entidades no se deben usar directamente en las vistas del cliente, porque en el nivel de interfaz de usuario hay datos que no se pueden validar. Para esto es el ViewModel. El ViewModel es un modelo de datos exclusivo para las necesidades de la capa de presentación. Las entidades de dominio no pertenecen directamente a un ViewModel. En su lugar, debe traducir entre ViewModels y entidades de dominio y viceversa.

Al abordar la complejidad, es importante tener un modelo de dominio controlado por raíces de agregación (profundizaremos en esto más adelante) que asegure que todas las invariantes y reglas relacionadas con ese grupo de entidades (agregado) se realicen a través de un solo punto de entrada, la raíz (o entidad) de agregación.

La Figura 7-5 muestra cómo se implementa un diseño en capas en la aplicación eShopOnContainers.

Image

Figura 7-5. Capas DDD del microservicio de pedidos en eShopOnContainers

Debe diseñar el sistema para que cada capa se comunique sólo con ciertas otras capas. Eso puede ser más fácil de aplicar si las capas se implementan como librerías diferentes, porque puede identificar claramente qué dependencias se establecen entre ellas. Por ejemplo, la capa del modelo de dominio no debe tomar una dependencia de ninguna otra capa (las clases de modelo de dominio deben ser Plain Old CLR Objects o POCO). Como se muestra en la Figura 7-6, la librería de la capa Ordering.Domain tiene dependencias solo de las librerías .NET Core o paquetes NuGet, pero no en ninguna otra librería personalizada (como datos, persistencia, etc.).

 

Image

Figura 7-6. Implementar las capas como librerías, simplifica el control de las dependencias entre ellas

La capa del modelo de dominio

El excelente libro de Eric Evans, Domain-Driven Design, dice lo siguiente sobre la capa del modelo de dominio y la capa de aplicación.

Capa del modelo de dominio: Es la responsable de representar conceptos del negocio, información sobre la situación y las reglas del negocio. El estado que refleja la situación del negocio se controla y se usa aquí, aunque los detalles técnicos de su almacenamiento se delegan en la infraestructura. Esta capa es el corazón del software del negocio.

La capa del modelo de dominio es donde se expresa el negocio. Cuando implementa una capa del modelo de dominio como un microservicio en .NET, esa capa se prepara como una librería de clases, con las entidades de dominio que capturan los datos más el comportamiento (métodos con lógica).

Siguiendo los principios de Ignorancia de la Persistencia e Ignorancia de la Infraestructura, esta capa debe ignorar por completo los detalles de persistencia de datos. Estas tareas de persistencia deben ser realizadas por la capa de infraestructura. Por lo tanto, esta capa no debe tomar dependencias directas de la infraestructura, lo que significa que una regla importante es que las clases de entidad del modelo de dominio deben ser POCOs.

Las entidades de dominio no deben tener ninguna dependencia directa (como derivar de una clase base) de ningún framework de infraestructura de acceso a datos como Entity Framework o NHibernate. Idealmente, las entidades de su dominio no deberían derivar o implementar ningún tipo definido en ningún framework de infraestructura.

La mayoría de los frameworks ORM modernos como Entity Framework Core permiten este enfoque, de modo que las clases de su modelo de dominio no estén acopladas a la infraestructura. Sin embargo, tener entidades POCO no siempre es posible cuando se utilizan ciertas bases de datos y frameworks NoSQL, como Actores y Reliable Collections en Azure Service Fabric.

Incluso cuando es importante seguir el principio de Ignorancia de la Persistencia para su modelo de Dominio, no debe ignorar los problemas de persistencia. Es muy importante comprender el modelo de datos físicos y cómo se correlaciona con el modelo de objetos de su entidad. De lo contrario, puede crear diseños imposibles de implementar.

Además, esto no significa que pueda tomar un modelo diseñado para una base de datos relacional y moverlo directamente a una base de datos NoSQL u orientada a documentos. En algunos modelos de entidades, el modelo podría ajustarse, pero por lo general no es así. Todavía existen restricciones que su modelo de entidades debe cumplir, basadas tanto en la tecnología de almacenamiento como en la tecnología ORM.

La capa de la aplicación

Pasando a la capa de aplicación, podemos citar nuevamente el libro Domain-Driven Design, de Eric Evans:

Capa de la aplicación: Define los trabajos que el software debe hacer y dirige los objetos del dominio para resolver problemas. Las tareas de las que esta capa es responsable son significativas para la empresa o necesarias para la interacción con las capas de aplicación de otros sistemas. Esta capa se mantiene delgada. No contiene reglas ni conocimiento del negocio, sino que sólo coordina tareas y delega el trabajo a colaboraciones de objetos de dominio en la siguiente capa, hacia abajo. No tiene un estado que refleje la situación del negocio, pero puede tener un estado que refleje el progreso de una tarea para el usuario o el programa.

La capa de aplicación de un microservicio en .NET suele codificarse como un proyecto ASP.NET Core Web API. El proyecto implementa la interacción del microservicio, el acceso remoto a la red y las API Web externas utilizadas desde la interfaz de usuario o las aplicaciones cliente. Incluye consultas, comandos aceptados por el microservicio, si se usa un enfoque CQRS e, incluso, la comunicación basada en eventos entre microservicios (eventos de integración). La API Web ASP.NET Core que representa la capa de aplicación no debe contener reglas del negocio o conocimiento de dominio (especialmente reglas de dominio para transacciones o actualizaciones), estos deben ser propiedad de la librería del modelo de dominio. La capa de aplicación sólo debe coordinar tareas y no debe contener ni definir ningún estado del dominio (modelo de dominio). Delega la ejecución de las reglas de negocio en las propias clases del modelo de dominio (agregados raíz y entidades del dominio), lo que finalmente actualizará los datos dentro de esas entidades.

Básicamente, la lógica de la aplicación es donde se implementa todos los casos de uso que dependen de un front-end determinado. Por ejemplo, la implementación relacionada con un servicio de API Web.

El objetivo es que la lógica de dominio en la capa del modelo de dominio, sus invariantes, el modelo de datos y las reglas de negocio relacionadas sean completamente independientes de las capas de presentación y aplicación. Más que nada, la capa de modelo de dominio no debe depender directamente de ningún framework de infraestructura.

La capa de infraestructura

La capa de infraestructura es la forma en que los datos que se mantienen inicialmente en las entidades de dominio (en la memoria) se conservan en bases de datos u otro almacén persistente. Un ejemplo es el usar Entity Framework Core para implementar las clases del patrón Repository que usan un DBContext para persistir datos en una base de datos relacional.

De acuerdo con los principios de Ignorancia de la Persistencia e Ignorancia de la Infraestructura mencionados anteriormente, la capa de infraestructura no debe "contaminar" la capa de modelo de dominio. Debe mantener las entidades de modelo de dominio independientes de la infraestructura que utiliza para conservar los datos (EF o cualquier otro framework) al no tener dependencias fuertes sobre los frameworks. La librería de la capa del modelo de dominio sólo debe tener código del dominio, sólo las entidades POCO que implementan el núcleo de su software y están completamente desacopladas de las tecnologías de infraestructura.

Por lo tanto, sus capas o librería y proyectos deberían depender, en última instancia, de la capa (librería) de su modelo de dominio y no viceversa, como se muestra en la Figura 7-7.

Image

Figura 7-7. Dependencies between layers in DDD

Este diseño de capa debe ser independiente para cada microservicio. Como se señaló anteriormente, puede implementar los microservicios más complejos siguiendo patrones DDD, mientras que puede implementas microservicios más simples basados en datos (microservicios CRUD simples en una sola capa) de una manera más sencilla.

Recursos adicionales

Diseñando un modelo de dominio como un microservicio

Definir un modelo de dominio expresivo para cada microservicio o Bounded Context

Su objetivo es crear un modelo de dominio único y cohesivo para cada microservicio del negocio o Bounded Context (BC). Tenga en cuenta, sin embargo, que un BC o microservicio del negocio a veces podría estar compuesto por varios servicios físicos que comparten un modelo de dominio único. El modelo de dominio debe capturar las reglas, el comportamiento, el lenguaje del negocio y las restricciones del Bounded Context único o microservicio del negocio que representa.

El patrón Entidad del Dominio

Las entidades representan objetos del dominio y se definen principalmente por su identidad, continuidad y persistencia en el tiempo y nó solo por los atributos que las componen. Como dice Eric Evans, "un objeto definido principalmente por su identidad se llama Entidad". Las entidades son muy importantes en el modelo de dominio, ya que son la base del modelo. Por lo tanto, debe identificarlas y diseñarlas cuidadosamente.

La identidad de una entidad puede pasar por múltiples microservicios o Bounded Contexts.

La misma identidad (es decir, el valor del Id que identifica la entidad, aunque no la misma entidad) se puede modelar a través de múltiples Bounded Contexts o microservicios. Sin embargo, eso no implica que la misma entidad, con los mismos atributos y lógica, se implemente en múltiples Bounded Contexts. En cambio, las entidades en cada Bounded Context limitan sus atributos y comportamientos a los requeridos en el dominio del Bounded Context.

Por ejemplo, la entidad “Comprador” puede tener la mayoría de los atributos de una persona que se definen como entidad “Usuario” en el microservicio de identidad o perfil, incluida la identidad. Pero la entidad “Comprador” en el microservicio de pedidos puede tener menos atributos, porque sólo ciertos datos del comprador están relacionados con el proceso de pedido. El contexto de cada microservicio o Bounded Context afecta su modelo de dominio.

Las entidades de dominio deben implementar comportamiento además de implementar atributos de datos

Una entidad de dominio en DDD debe implementar la lógica de dominio o el comportamiento relacionado con los datos de la entidad (el objeto al que se accede en la memoria). Por ejemplo, como parte de una clase de la entidad “Pedido”, debe tener la lógica de negocios y las operaciones implementadas como métodos para tareas tales como agregar un artículo de pedido, validar los datos y calcular el total. Los métodos de la entidad se ocupan de las invariantes y las reglas de la entidad, en lugar de tener esas reglas repartidas en la capa de la aplicación.

La figura 7-8 muestra una entidad del dominio que implementa no sólo atributos de datos, sino también operaciones o métodos con lógica relacionada del dominio.

 

Image

Figura 7-8. Ejemplo del diseño de una entidad del dominio, implementando datos y comportamiento

Por supuesto, a veces puede tener entidades que no implementan ninguna lógica como parte de la clase de entidad. Esto puede ocurrir con entidades secundarias dentro de un agregado, si la entidad hija no tiene ninguna lógica especial porque la mayoría de la lógica se define en la raíz del agregado. Si tiene un microservicio complejo que tiene mucha lógica implementada en las clases de servicio en lugar de en las entidades de dominio, podría caer en el modelo de dominio anémico, que se explica en la siguiente sección.

Modelo de dominio expresivo (rico) versus modelo de dominio anémico

En su publicación AnemicDomainModel, Martin Fowler describe un modelo de dominio anémico de esta forma:

El síntoma básico de un Modelo de Dominio Anémico es que a primera vista parece real. Hay objetos, muchos de ellos nombrados usando los nombres en el espacio del dominio y estos objetos están conectados con las relaciones expresivas y la estructura que tienen los modelos del dominio verdaderos. El problema viene cuando se observa el comportamiento y es evidente que casi no hay ningún comportamiento en estos objetos, convirtiéndolos en poco más que bolsas de getters y setters.

Por supuesto, cuando utiliza un modelo de dominio anémico, esos modelos de datos se usarán a partir de un conjunto de objetos de servicio (tradicionalmente denominado la capa del negocio) que captura todo el dominio o lógica del negocio. La capa del negocio se ubica por encima del modelo de dominio y lo usa sólo como datos.

El modelo de dominio anémico es sólo un diseño de estilo procedimental. Los objetos de entidades anémicas no son objetos reales, porque carecen de comportamiento (métodos). Sólo tienen propiedades de datos y, por lo tanto, no es un diseño orientado a objetos. Al poner todo el comportamiento en objetos de servicio (la capa de negocios) esencialmente se termina con código espagueti o scripts de transacción y, por lo tanto, se pierden las ventajas que ofrece un modelo de dominio.

De todos modos, si su microservicio o Bounded Context es muy simple (un servicio CRUD), el modelo de dominio anémico, en forma de objetos que sólo tienen propiedades de datos, podría ser lo suficientemente bueno y podría no ser útil implementar patrones DDD más complejos. En ese caso, será simplemente un modelo de persistencia, porque usted ha creado intencionalmente una entidad con sólo datos, para los propósitos de un CRUD.

Es por eso que las arquitecturas de microservicios son perfectas para un enfoque multi-arquitectónico, que depende de cada Bounded Context. Por ejemplo, en eShopOnContainers, el microservicio de pedidos implementa patrones DDD, pero el microservicio de catálogo, que es un servicio CRUD simple, no lo hace.

Algunas personas dicen que el modelo de dominio anémico es un anti patrón. Realmente depende de lo que se esté implementando. Si el microservicio que está creando es lo suficientemente simple (por ejemplo, un servicio CRUD), seguir el modelo de dominio anémico no es un anti patrón. Sin embargo, si necesita abordar la complejidad del dominio de un microservicio que tiene muchas reglas del negocio en cambio constante, el modelo de dominio anémico sí podría ser un anti patrón para ese microservicio. En ese caso, diseñarlo como un modelo rico y expresivo, con entidades que contengan datos y comportamientos, así como implementar patrones DDD adicionales (agregados, value objects, etc.) podría tener enormes beneficios para el éxito a largo plazo de ese microservicio.

Recursos adicionales

http://deviq.com/entity/

https://martinfowler.com/eaaCatalog/domainModel.html

https://martinfowler.com/bliki/AnemicDomainModel.html

El patrón Objeto de Valor (Value Object)

Como ha señalado Eric Evans, "muchos objetos no tienen identidad conceptual. Estos objetos describen ciertas características de una cosa".

Una entidad requiere una identidad, pero hay muchos objetos en un sistema que no la tienen, como el patrón Value Object. Un Value Object es un objeto sin identidad conceptual, que describe un aspecto de dominio. Estos son objetos que crea una instancia para representar elementos de diseño que sólo le interesan temporalmente. En ese caso, importa lo que son, no quiénes son. Los ejemplos incluyen números y strings, pero también pueden ser conceptos de nivel superior como grupos de atributos.

Algo que es una entidad en un microservicio podría no serlo en otro microservicio, porque en el segundo caso, el Bounded Context podría tener un significado diferente. Por ejemplo, una dirección en una aplicación de comercio electrónico podría no tener una identidad, ya que podría sólo representar un grupo de atributos del perfil del cliente para una persona o empresa. En este caso, la dirección debe clasificarse como un objeto de valor. Sin embargo, en una solicitud para una empresa de servicios de energía eléctrica, la dirección del cliente podría ser importante para el dominio del negocio. Por lo tanto, la dirección debe tener una identidad para que el sistema de facturación pueda vincularse directamente con la dirección. En ese caso, una dirección debe clasificarse como una entidad de dominio.

Una persona con un nombre y un apellido suele ser una entidad porque una persona tiene identidad, incluso si el nombre y el apellido coinciden con otro conjunto de valores, porque esos nombres se refieren a una persona diferente.

Los value objects son difíciles de administrar en bases de datos relacionales y ORM como EF, mientras que en las bases de datos orientadas a documentos son más fáciles de implementar y usar.

EF Core 2.0, incluye la característica Owned Entities, que facilita el manejo de Value Objects, como veremos en detalle más adelante.

Recursos adicionales

El patrón Agregado

Un modelo de dominio contiene grupos de diferentes entidades y procesos que pueden controlar un área importante de funcionalidad, como la entrega de pedidos o el inventario. Una unidad DDD de grano más fino es el agregado, que describe un grupo o grupo de entidades y comportamientos que pueden tratarse como una unidad cohesiva.

Por lo general, se define un agregado según las transacciones que necesita. Un ejemplo clásico es un pedido que también contiene una lista de artículos del pedido. Un artículo de pedido generalmente será una entidad. Pero será una entidad hija dentro del agregado de pedido, que también contendrá la entidad de pedido como su entidad raíz, típicamente denominada raíz de agregación (aggregate root).

La identificación de agregados puede ser difícil. Un agregado es un grupo de objetos que deben ser consistentes entre sí, pero no se puede simplemente seleccionar un grupo de objetos y etiquetarlos como un agregado. Debe comenzar con un concepto de dominio y pensar en las entidades que se utilizan en las transacciones más comunes relacionadas con ese concepto. Aquellas entidades que necesitan ser consistentes transaccionalmente son lo que forma un agregado. Pensar en las transacciones es probablemente la mejor manera de identificar agregados.

El patrón Raíz de Agregación o Entidad Raíz

Un agregado se compone de al menos una entidad: la raíz del agregado, también llamada entidad raíz o entidad primaria. Además, puede tener múltiples entidades secundarias y value objects, con todas las entidades y objetos trabajando juntos para implementar el comportamiento y las transacciones requeridas.

El propósito de una entidad raíz es garantizar la consistencia del agregado; debe ser el único punto de entrada para las actualizaciones del agregado, a través de métodos u operaciones en la clase de entidad raíz. Sólo se deben realizar cambios de las entidades dentro del agregado, a través de la entidad raíz. Es el guardián de la consistencia del agregado, teniendo en cuenta todas las invariantes y las reglas de consistencia que necesitan cumplir en su conjunto. Si cambia una entidad secundaria u objeto de valor de forma independiente, la entidad raíz no puede garantizar que el agregado se encuentre en un estado válido. Sería como una mesa con una pierna suelta. Mantener la consistencia es el objetivo principal de la entidad raíz.

En la figura 7-9, puede ver agregados de ejemplo como el agregado del comprador, que contiene una sola entidad (la entidad raíz Buyer). El agregado de pedido contiene múltiples entidades y un objeto de valor.

Image

Figura 7-9. Ejemplo de agregados con una sola entidad y múltiples entidades

Tenga en cuenta que el agregado Buyer podría tener entidades secundarias adicionales, dependiendo de su dominio, como lo hace en el microservicio de pedidos en la aplicación eShopOnContainers. La Figura 7-9 simplemente ilustra un caso en el que el comprador tiene una sola entidad, como un ejemplo de un agregado que contiene sólo la entidad raíz.

Para mantener la separación de agregados y mantener los límites claros entre ellos, es una buena práctica en un modelo de dominio DDD, rechazar la navegación directa entre agregados y sólo tener el campo de clave foránea (FK), como se implementó en el modelo de dominio del microservicio de pedidos en eShopOnContainers. La entidad Order sólo tiene un campo FK para el comprador, pero no una propiedad de navegación EF Core, como se muestra en el siguiente código:

public class Order : Entity, IAggregateRoot

{

private DateTime _orderDate;

public Address Address { get; private set; }

private int? _buyerId; //FK pointing to a different aggregate root

public OrderStatus OrderStatus { get; private set; }

private readonly List<OrderItem> _orderItems;

public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

 

//… Additional code

}

 

Identificar y trabajar con agregados requiere investigación y experiencia. Para obtener más información, consulte la siguiente lista de recursos adicionales.

Recursos adicionales

Implementando un microservicio del modelo de dominio con .NET Core

En la sección anterior, se explicaron los principios y patrones de diseño fundamentales para desarrollar un modelo de dominio. Ahora es el momento de explorar posibles formas de implementar el modelo de dominio utilizando .NET Core (código simple en C#) y EF Core. Tenga en cuenta que su modelo de dominio estará compuesto simplemente por su código. Sólo tendrá los requisitos del modelo EF Core, pero no dependencias reales de EF. No debe tener dependencias ni referencias fuertes a EF Core ni a ningún otro ORM en su modelo de dominio.

Estructura del modelo de dominio en una librería particular de .NET Standard Library

La organización de carpetas utilizada para la aplicación de referencia eShopOnContainers, demuestra el modelo DDD para la aplicación. Es posible que descubra que una organización de carpetas diferente comunica más claramente las elecciones de diseño realizadas para su aplicación. Como puede ver en la Figura 7-10, en el modelo de dominio de pedidos hay dos agregados, el agregado del pedido y el agregado de comprador. Cada agregado es un grupo de entidades de dominio y value objects, aunque también podría tener un agregado compuesto por una entidad de dominio única (la raíz de agregación o la entidad raíz).

Image

Figura 7-10. Estructura del modelo de dominio en el microservicio de pedidos de eShopOnContainers

Además, la capa del modelo de dominio incluye los contratos de los repositorios (interfaces) que son los requisitos de infraestructura de su modelo de dominio. En otras palabras, estas interfaces expresan qué repositorios y qué métodos debe implementar la capa de infraestructura Es fundamental que la implementación de los repositorios se coloque fuera de la capa de modelo de dominio, en la librería de la capa de infraestructura, para que la capa de modelo de dominio no esté "contaminada" por APIs o clases de infraestructura, como Entity Framework.

También puede ver una carpeta SeedWork que contiene clases base personalizadas que puede usar como base para las entidades de su dominio y value objects, para no tener código redundante en cada clase del dominio.

Estructurando los agregados en una librería personalizada de .NET Standard

Un agregado hace referencia a varios objetos del dominio agrupados para que sean coherentes la consistencia transaccional. Esos objetos podrían ser instancias de entidades (una de las cuales es la entidad raíz o raíz de agregación) más cualquier objeto de valor adicional.

La consistencia transaccional significa que se garantiza que un agregado sea consistente y esté actualizado al final de una acción del negocio. Por ejemplo, el agregado de pedido del modelo de dominio está compuesto como se muestra en la Figura 7-11.

Image

Figura 7-11. El agragado Order en el proyecto de Visual Studio

Si abre alguno de los ficheros en una carpeta del agregado, puede ver cómo está marcada como una clase base o interfaz personalizada, como entidad u objeto de valor, tal como se implementó en la carpeta Seedwork.

Implementando entidades del dominio como clases POCO

Para implementar un modelo de dominio en .NET, se crean clases POCO que corresponden a las entidades de su dominio. En el siguiente ejemplo, la clase Order se define como una entidad y también como una raíz de agregación. Como la clase Order se deriva de la clase base Entity, puede reutilizar el código común relacionado con las entidades. Tenga en cuenta que estas clases base e interfaces las define usted en el proyecto de modelo de dominio, por lo que es su código, no el código de infraestructura de un ORM como EF.

// COMPATIBLE WITH ENTITY FRAMEWORK CORE 2.0

// Entity is a custom base class with the ID

public class Order : Entity, IAggregateRoot

{

private DateTime _orderDate;

public Address Address { get; private set; }

private int? _buyerId;

 

public OrderStatus OrderStatus { get; private set; }

private int _orderStatusId;

 

private string _description;

private int? _paymentMethodId;

 

private readonly List<OrderItem> _orderItems;

public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

public Order(string userId, Address address, int cardTypeId,

string cardNumber, string cardSecurityNumber,

string cardHolderName, DateTime cardExpiration,

int? buyerId = null, int? paymentMethodId = null)

{

_orderItems = new List<OrderItem>();

_buyerId = buyerId;

_paymentMethodId = paymentMethodId;

_orderStatusId = OrderStatus.Submitted.Id;

_orderDate = DateTime.UtcNow;

Address = address;

 

// ...Additional code ...

}

 

public void AddOrderItem(int productId, string productName,

decimal unitPrice, decimal discount,

string pictureUrl, int units = 1)

{

//...

// Domain rules/logic for adding the OrderItem to the order

// ...

var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);

_orderItems.Add(orderItem);

}

// ...

// Additional methods with domain rules/logic related to the Order aggregate

// ...

}

Es importante tener en cuenta que esta es una entidad de dominio implementada como una clase POCO. No tiene ninguna dependencia directa a Entity Framework Core ni en ningún otro framework de infraestructura. Esta es una implementación adecuada para DDD, solo código C# implementando un modelo de dominio.

Además, la clase está decorada con una interfaz llamada IAggregateRoot. Esa es una interfaz vacía, a veces llamada interfaz de marcador (marker interface), que se usa solo para indicar que esta clase también es una raíz de agregación.

Una interfaz de marcador a veces se considera como un anti patrón, sin embargo, también es una manera limpia de marcar una clase, especialmente cuando esa interfaz podría estar evolucionando. Un atributo podría ser la alternativa para el marcador, pero es más rápido ver la clase base (Entity) junto a la interfaz IAggregate en lugar de poner un atributo Aggregate encima de la clase. En todo caso, es más un asunto de preferencias.

Tener una raíz de agregación significa, que la mayoría del código relacionado con la consistencia y las reglas de negocio de las entidades del agregado, deben implementarse como métodos en la clase raíz del agregado Order, es decir, la clase Order (por ejemplo, AddOrderItem() al añadir un OrderItem al agregado). No debe crear ni actualizar objetos OrderItem de forma directa; la clase AggregateRoot debe mantener el control y la coherencia de cualquier operación de actualización contra sus entidades “hijas”.

Encapsulando data en las Entidades del Dominio

Un problema común en los modelos de entidades, es que exponen las propiedades de navegación de sus colecciones como listas con acceso público. Esto permite que cualquier desarrollador manipule directamente los contenidos de esas colecciones, lo que puede obviar reglas del negocio importantes relacionadas con la colección, posiblemente dejando al objeto en un estado no válido. La solución a esto es exponer el acceso a las colecciones relacionadas como sólo lectura y proporcionar métodos explícitos que definan cómo se pueden manipular.

En el código anterior, observe que muchos atributos son de sólo lectura o privados y sólo actualizables por los métodos de clase, por lo que cualquier actualización tiene en cuenta invariantes y la lógica del dominio de negocio, especificadas dentro de los métodos de clase.

Por ejemplo, siguiendo DDD, no debe hacer lo siguiente desde ningún método manejador de comandos o clase de capa de aplicación:

// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR

// COMMAND HANDLERS

 

// Code in command handler methods or Web API controllers

 

//... (WRONG) Some code with business logic out of the domain classes ...

OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,

pictureUrl, unitPrice, discount, units);

 

//... (WRONG) Accessing the OrderItems colletion directly from the application layer // or command handlers

 

myOrder.OrderItems.Add(myNewOrderItem);

 

//...

 

En este caso, el método Add es únicamente una operación para agregar datos, con acceso directo a la colección OrderItems. Por lo tanto, la mayor parte de la lógica de dominio, las reglas o las validaciones relacionadas con esa operación con las entidades secundarias, estarán dispersas en la capa de aplicación (manejadores de comandos y controladores de API Web).

Si obviamos la raíz de agregación, esta no puede garantizar sus invariantes, su validez o su consistencia. Eventualmente tendremos código spaghetti o código de script transaccional.

Para seguir patrones DDD, las entidades no deben aceptar asignaciones públicas en ninguna propiedad de la entidad. Los cambios en una entidad deben ser realizados por métodos explícitos, con lenguaje ubicuo explícito, sobre el cambio que están realizando en la entidad.

Además, las colecciones dentro de la entidad (como los ítems del pedido) deben ser propiedades de sólo lectura (el método AsReadOnly se explica más adelante). Debería poder actualizarlo sólo desde los métodos de la raíz de agregación o los métodos de la entidad hija.

Como puede ver en el código para la raíz de agregación Order, todos los setters (lo que permite actualizar las propiedades de la clase) deben ser privados o de sólo lectura externamente, de modo que cualquier operación contra los datos de la entidad o sus entidades secundarias se deba realizar a través de métodos en la clase de entidad. Esto mantiene la coherencia de forma controlada y orientada a objetos en lugar de implementar código de script transaccional.

El siguiente fragmento de código muestra la forma correcta de codificar la tarea de agregar un objeto OrderItem al agregado de Order.

// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS

 

// The code in command handlers or WebAPI controllers, related only to application stuff

// There is NO code here related to OrderItem object’s business logic

 

myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units);

 

// The code related to OrderItem params validations or domain rules should

// be WITHIN the AddOrderItem method.

En este fragmento, la mayoría de las validaciones o lógica relacionadas con la creación de un objeto OrderItem estarán bajo el control de la raíz de agregación Order en el método AddOrderItem, especialmente las validaciones y la lógica relacionadas con otros elementos en el agregado. Por ejemplo, puede obtener el mismo artículo de producto como resultado de múltiples llamadas a AddOrderItem. En ese método, puede examinar los artículos del producto y consolidar los mismos artículos del producto en un único objeto OrderItem con varias unidades. Además, si hay diferentes montos de descuento, pero la identificación del producto es la misma, es probable que aplique el mayor descuento. Este principio se aplica a cualquier otra lógica de dominio para el objeto OrderItem.

Además, la operación new OrderItem(params) también será controlada y ejecutada por el método AddOrderItem desde la raíz del agregado Order. Por lo tanto, la mayor parte de la lógica o validaciones relacionadas con esa operación (especialmente cualquier cosa que afecte la consistencia entre otras entidades secundarias) estará en un solo lugar, dentro de la raíz de agregación. Ese es el objetivo final del patrón de Agregado.

Cuando utiliza Entity Framework Core 1.1, 2.0 o posterior, se puede expresar mejor una entidad DDD, porque una de las características de EF Core 1.1 o 2.0 es que permite mapear campos además de las propiedades. Esto es útil cuando se protegen colecciones de entidades secundarias o value objects. Con esta mejora, puede usar campos privados simples en lugar de propiedades y puede implementar cualquier actualización de la colección en métodos públicos y proporcionar acceso de solo lectura a través del método AsReadOnly.

En DDD, sólo queremos actualizar la entidad a través de métodos en la entidad (o el constructor) para controlar cualquier invariante y la consistencia de los datos, por lo que las propiedades se definen sólo con un acceso de lectura. Las propiedades están respaldadas por campos privados. Sólo se puede acceder a los miembros privados desde dentro de la clase. Sin embargo, hay una excepción: EF Core sí puede asignar estos campos (para poder devolvern un objeto con los valores adecuados).

Mapeando propiedades de sólo lectura a campos en la tabla

La asignación de propiedades a las columnas de la tabla de la base de datos no es responsabilidad del dominio, sino parte de la capa de infraestructura y persistencia. Mencionamos esto aquí para que esté al tanto de las nuevas capacidades en EF Core 1.1, 2.0 o posterior, relacionadas con la forma en que puede modelar entidades. Detalles adicionales sobre este tema se explican en la sección de infraestructura y persistencia.

Cuando utiliza EF Core 1.0, dentro del DbContext necesita mapear las propiedades que están definidas sólo lectura a campos reales en la tabla. Esto se hace con el método HasField de la clase PropertyBuilder.

Mapeando campos sin propiedades

Con EF Core 1.1 y 2.0 puede mapear columnas a campos de la clase (privados), así, también es posible no usar propiedades para esto. Un caso de uso común para esto es, usar campos privados para un estado interno, al que no es necesario acceder desde fuera de la entidad.

Por ejemplo, en el ejemplo del código anterior de OrderAggregate, hay varios campos privados, como el campo _paymentMethodId, que no tienen ninguna propiedad relacionada para un setter o getter. Ese campo también se puede calcular dentro de la lógica del negocio de la orden y se puede usar a partir de los métodos de la orden, pero también se debe persistir en la base de datos. Para esto, en EF Core (desde v1.1) hay una forma de mapear un campo sin una propiedad relacionada a una columna en la base de datos. Esto también se explica en la sección Capa de infraestructura de esta guía.

Recursos adicionales

 

SeedWork (clases base e interfaces reutilizables para el modelo de dominio)

Como se mencionó, en la carpeta de soluciones también puede ver una carpeta SeedWork. Esta carpeta contiene clases base personalizadas que puede usar como base para sus entidades de dominio y value objects, para evitar código redundante en las clases de objetos del dominio. La carpeta para este tipo de clases se llama SeedWork y no es algo así como framework, porque la carpeta contiene sólo un pequeño grupo de clases reutilizables, que realmente no pueden considerarse un framework. Seedwork es un término introducido por Michael Feathers y popularizado por Martin Fowler, pero también podría nombrar esa carpeta Common, SharedKernel o similar.

La Figura 7-12 muestra las clases que forman la carpeta SeedWork del modelo de dominio en el microservicio de pedidos. Tiene algunas clases básicas personalizadas como Entity, ValueObject y Enumeration, además de algunas interfaces. Estas interfaces (IRepository e IUnitOfWork) informan a la capa de infraestructura sobre lo que debe implementarse. Esas interfaces también se utilizan a través de inyección de dependencias (DI) desde la capa de la aplicación.

Image

Figura 7-12. Un ejemplo de de las clases base e interfaces “SeedWork” del modelo de dominio

Este es el tipo de reutilización de “copiar y pegar” que muchos desarrolladores comparten entre proyectos, no un framework formal. Puede tener SeedWork en cualquier capa o librería. Sin embargo, si el conjunto de clases e interfaces es lo suficientemente grande, es posible que desee crear una sola librería de clases.

La clase base personalizada Entity

El siguiente código es un ejemplo de una clase Entidad base, donde puede colocar código que sea usado de la misma manera por cualquier entidad de dominio, como la ID de entidad, operadores de igualdad, una lista de eventos de dominio por entidad, etc.

// COMPATIBLE WITH ENTITY FRAMEWORK CORE (1.1, 2.0 or later)

public abstract class Entity

{

int? _requestedHashCode;

int _Id;

private List<INotification> _domainEvents;

public virtual int Id

{

get

{

return _Id;

}

protected set

{

_Id = value;

}

}

 

public List<INotification> DomainEvents => _domainEvents;

public void AddDomainEvent(INotification eventItem)

{

_domainEvents = _domainEvents ?? new List<INotification>();

_domainEvents.Add(eventItem);

}

public void RemoveDomainEvent(INotification eventItem)

{

if (_domainEvents is null) return;

_domainEvents.Remove(eventItem);

}

 

public bool IsTransient()

{

return this.Id == default(Int32);

}

 

public override bool Equals(object obj)

{

if (obj == null || !(obj is Entity))

return false;

if (Object.ReferenceEquals(this, obj))

return true;

if (this.GetType() != obj.GetType())

return false;

Entity item = (Entity)obj;

if (item.IsTransient() || this.IsTransient())

return false;

else

return item.Id == this.Id;

}

 

public override int GetHashCode()

{

if (!IsTransient())

{

if (!_requestedHashCode.HasValue)

_requestedHashCode = this.Id.GetHashCode() ^ 31;

// XOR for random distribution. See:

//

// http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx

return _requestedHashCode.Value;

}

else

return base.GetHashCode();

}

public static bool operator ==(Entity left, Entity right)

{

if (Object.Equals(left, null))

return (Object.Equals(right, null)) ? true : false;

else

return left.Equals(right);

}

public static bool operator !=(Entity left, Entity right)

{

return !(left == right);

}

}

El código anterior, que utiliza una lista de eventos por entidad, se explicará en las próximas secciones, cuando nos enfoquemos en los eventos del dominio.

Contratos de repositorio (interfaces) en la capa del modelo de dominio

Los contratos de repositorio son simplemente interfaces .NET que expresan los requisitos del contrato de los repositorios que se utilizarán para cada agregado.

Los repositorios en sí, con código EF Core o cualquier otra dependencia de infraestructura o librerías (Linq, SQL, etc.), no se deben implementar dentro del modelo de dominio, los repositorios sólo deberían implementar las interfaces que defina en el modelo de dominio.

Un patrón relacionado con esta práctica (colocar las interfaces de repositorios en la capa del modelo de dominio) es el patrón de Interfaz Separada. Como explica Martin Fowler, "Use Interfaz Separada para definir una interfaz en un paquete pero implementarla en otro. De esta forma, un cliente que necesita la dependencia de la interfaz puede desconocer por completo la implementación ".

Seguir el patrón de interfaz separada permite que la capa de aplicación (en este caso, el proyecto de API web para el microservicio) dependa de los requisitos definidos en el modelo de dominio, pero no de una dependencia directa a la capa de infraestructura/persistencia. Además, puede usar Inyección de Dependencias (DI) para aislar la implementación, que está incluida en la capa de infraestructura/persistencia mediante repositorios.

Por ejemplo, en el siguiente ejemplo con la interfaz IOrderRepository, se define qué operaciones deberá implementar la clase OrderRepository en la capa de infraestructura. En la implementación actual de la aplicación, el código sólo necesita agregar o actualizar pedidos a la base de datos, ya que las consultas se dividen siguiendo el enfoque simplificado de CQRS.

// Defined at IOrderRepository.cs

public interface IOrderRepository : IRepository<Order>

{

Order Add(Order order);

void Update(Order order);

 

Task<Order> GetAsync(int orderId);

}

 

// Defined at IRepository.cs (Part of the Domain Seedwork)

public interface IRepository<T> where T : IAggregateRoot

{

IUnitOfWork UnitOfWork { get; }

}

Recursos adicionales

Implementando objetos de valor (value objects)

Como se discutió en secciones anteriores sobre entidades y agregados, la identidad es fundamental para las entidades. Sin embargo, hay muchos objetos y elementos de datos en un sistema que no requieren identidad ni hacer un seguimiento de ésta, como los objetos de valor (value objects).

Un value object puede hacer referencia a otras entidades. Por ejemplo, en una aplicación que genera una ruta que describe cómo llegar de un punto a otro, esa ruta sería un objeto de valor. Sería una instantánea de puntos en una ruta específica, pero esta ruta sugerida no tendría una identidad, aunque internamente podría referirse a otras entidades como City, Road, etc.

La Figura 7-13 muestra el objeto valor Address dentro del agregado Order.

Image

Figura 7-13. Objeto de valor Address dentro del agregado Order

Como se muestra en la figura 7-13, una entidad generalmente se compone de múltiples atributos. Por ejemplo, la entidad Order puede modelarse como una entidad con una identidad, compuesta internamente de un conjunto de atributos como OrderId, OrderDate, OrderItems, etc. Pero la dirección, que es simplemente un valor complejo compuesto de Country, Street, City, etc. y no tiene identidad en este dominio, por lo tanto, debe modelarse y tratarse como un objeto de valor.

Características importantes de los value objects

Hay dos características principales para los value objects:

La primera característica ya fue discutida. La inmutabilidad es un requisito importante. Los valores de un objeto de valor deben ser inmutables una vez que se crea el objeto. Por lo tanto, cuando se construye el objeto, debe proporcionar los valores requeridos, pero no debe permitir que cambien durante la vida del objeto.

Los value objects le permiten realizar ciertos trucos para mejorar el rendimiento, gracias a su naturaleza inmutable. Esto es especialmente cierto en sistemas donde puede haber miles de instancias de value objects, muchas de las cuales tienen los mismos valores. Su naturaleza inmutable les permite ser reutilizados, pueden ser objetos intercambiables, ya que sus valores son los mismos y no tienen identidad. Este tipo de optimización a veces puede hacer una diferencia entre el software que se ejecuta lentamente y el software con buen rendimiento. Por supuesto, todos estos casos dependen del entorno de la aplicación y del contexto de despliegue.

Implementación de value objects en C#

En términos de implementación, puede tener una clase base de value objects que tenga métodos utilitarios básicos como igualdad basada en la comparación entre todos los atributos (ya que un objeto de valor no debe basarse en la identidad) y otras características fundamentales. El siguiente ejemplo muestra una clase base de objeto de valor utilizada en el microservicio de pedidos de eShopOnContainers.

public abstract class ValueObject

{

protected static bool EqualOperator(ValueObject left, ValueObject right)

{

if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))

{

return false;

}

return ReferenceEquals(left, null) || left.Equals(right);

}

 

protected static bool NotEqualOperator(ValueObject left, ValueObject right)

{

return !(EqualOperator(left, right));

}

 

protected abstract IEnumerable<object> GetAtomicValues();

 

public override bool Equals(object obj)

{

if (obj == null || obj.GetType() != GetType())

{

return false;

}

ValueObject other = (ValueObject)obj;

IEnumerator<object> thisValues = GetAtomicValues().GetEnumerator();

IEnumerator<object> otherValues = other.GetAtomicValues().GetEnumerator();

while (thisValues.MoveNext() && otherValues.MoveNext())

{

if (ReferenceEquals(thisValues.Current, null) ^

ReferenceEquals(otherValues.Current, null))

{

return false;

}

if (thisValues.Current != null &&

!thisValues.Current.Equals(otherValues.Current))

{

return false;

}

}

return !thisValues.MoveNext() && !otherValues.MoveNext();

}

// Other utilility methods

}

Puede utilizar esta clase al implementar su objeto de valor real, como el Address que se muestra en el siguiente ejemplo:

public class Address : ValueObject

{

public String Street { get; private set; }

public String City { get; private set; }

public String State { get; private set; }

public String Country { get; private set; }

public String ZipCode { get; private set; }

public Address(string street, string city, string state,

string country, string zipcode)

{

Street = street;

City = city;

State = state;

Country = country;

ZipCode = zipcode;

}

 

protected override IEnumerable<object> GetAtomicValues()

{

// Using a yield return statement to return each element one at a time

yield return Street;

yield return City;

yield return State;

yield return Country;

yield return ZipCode;

}

}

Puede ver cómo esta implementación del objeto de valor Address no tiene identidad y, por lo tanto, no se ha registrado ID, ni en la clase Address ni en la clase ValueObject.

Antes de EF Core 2.0 no era posible manejar una clase (en EF) sin campo de identidad (ID), pero esta nueva facilidad ayuda en gran medida para implementar value objects, como veremos en la sección siguiente.

Cómo persistir value objects en la base de datos con EF Core 2.0

Acabamos de ver cómo definir un objeto de valor en un modelo de dominio. Pero, ¿cómo se puede persistir en la base de datos a través de Entity Framework Core, que generalmente apunta a entidades con identidad?

Antecedentes y enfoques anteriores, usando EF Core 1.1

A modo de historia, una limitación al usar EF Core 1.0 y 1.1 era que no se podían usar tipos complejos como lo permite EF 6.x en el .NET Framework tradicional. Por lo tanto, si usa EF Core 1.0 o 1.1, necesita almacenar un objeto de valor como una entidad EF con un campo ID. Entonces, para que pareciera más un objeto de valor sin identidad, podía ocultar su ID para dejar claro que la identidad de un objeto de valor no es importante en el modelo de dominio. Puede ocultar esa ID usándola como una shadow property. Dado que la configuración para ocultar el ID en el modelo se hace en el nivel de infraestructura de EF, sería algo transparente para su modelo de dominio.

En la versión inicial de eShopOnContainers (.NET Core 1.1), la identificación oculta que necesita la infraestructura de EF Core se implementó de la siguiente manera en el nivel de DbContext, utilizando la Fluent API en el proyecto de infraestructura. Por lo tanto, el ID estaba oculto desde el punto de vista del modelo de dominio, pero aún estaba presente en la infraestructura.

// Old approach with EF Core 1.1

// Fluent API within the OrderingContext:DbContext in the Infrastructure project

void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)

{

addressConfiguration.ToTable("address", DEFAULT_SCHEMA);

 

addressConfiguration.Property<int>("Id") // Id is a shadow property

.IsRequired();

addressConfiguration.HasKey("Id"); // Id is a shadow property

}

 

Sin embargo, la persistencia de ese objeto de valor en la base de datos se realizaba como una entidad regular en una tabla diferente.

Con EF Core 2.0 hay nuevas y mejores formas de persistir los value objects.

Persistiendo los value objects como “owned entities” en EF Core 2.0

Aun cuando todavía hay algunas brechas entre el patrón canónico de value objects en DDD y las owned entities (la traducción de “owned entity” sería como una entidad sin identidad, que le pertenece o está supeditada a otra) en EF Core, actualmente es la mejor forma de persistir value objects con EF Core 2.0. Puede ver las limitaciones al final de esta sección.

La característica de owned entity se agregó a EF Core en la versión 2.0.

El tipo owned entity le permite mapear tipos que no tienen su propia identidad definida explícitamente en el modelo del dominio y se usan como propiedades (como un objeto de valor) dentro de cualquiera de sus entidades. Un tipo owned entity comparte el mismo tipo del CLR que cualquier otro tipo de entidad (es decir, es una clase como cualquier otra). La entidad propietaria contiene la propiedad de navegación que define una owned entity. Cuando se hace una consulta a la entidad propietaria, se incluyen, por defecto, las owned entities como cualquier otra propiedad.

Con sólo mirar el modelo de dominio, parece que una owned entity no tiene ninguna identidad, pero, por debajo, si la tienen, lo que pasa es que la propiedad de navegación del propietario es parte de la identidad.

La identidad de instancias de tipos owned entities no es completamente suya, precisamente porque están supeditadas a la entidad propietaria. Consta de tres componentes:

Por ejemplo, en el modelo de dominio de pedidos en eShopOnContainers, como parte de la entidad Order, el objeto de valor Address se implementa como una propiedad owned entity dentro de la entidad propietaria, que es Order. Address es un tipo sin identidad definida en el modelo de dominio. Se utiliza como una propiedad del tipo de orden para especificar la dirección de envío para un pedido en particular.

Por convención, se creará una clave principal paralela para la owned entity y se asignará a la misma tabla que el propietario, usando table splitting. Esto permite usar owned entities de forma similar a como se usan los tipos complejos en EF6 en el .NET Framework tradicional.

Es importante tener en cuenta que los owned entities nunca se descubren por convención en EF Core, por lo que debe declararlos explícitamente.

En eShopOnContainers, en OrderingContext.cs, dentro del método OnModelCreating(), se están aplicando varias configuraciones de la infraestructura. Una de ellas está relacionada con la entidad Order.

// Part of the OrderingContext.cs class at the Ordering.Infrastructure project

//

protected override void OnModelCreating(ModelBuilder modelBuilder)

{

modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());

modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());

modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());

modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());

//...Additional type configurations

}

En el siguiente código es donde se define la infraestructura de persistencia para la entidad Order.

// Part of the OrderEntityTypeConfiguration.cs class

//

public void Configure(EntityTypeBuilder<Order> orderConfiguration)

{

orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);

orderConfiguration.HasKey(o => o.Id);

orderConfiguration.Ignore(b => b.DomainEvents);

orderConfiguration.Property(o => o.Id)

.ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);

 

//Address value object persisted as owned entity in EF Core 2.0

orderConfiguration.OwnsOne(o => o.Address);

 

orderConfiguration.Property<DateTime>("OrderDate").IsRequired();

//...Additional validations, constraints and code...

//...

}En el código anterior, la línea orderConfiguration.OwnsOne (o => o.Address) especifica que la propiedad Address es una propiedad owned entity del tipo Order.

Según la convención de EF Core, las columnas de la base de datos para las propiedades de la owned entity, se llamarán como EntityProperty_OwnedEntityProperty. Por lo tanto, las propiedades internas de Dirección aparecerán en la tabla Orders con los nombres Address_Street, Address_City (y así sucesivamente para State, Country y ZipCode).

Puede agregar el método Property().HasColumnName() a la configuración, para cambiar el nombre de esas columnas. En este caso, como Address es una propiedad pública, las asignaciones serían como las siguientes.

 

orderConfiguration.OwnsOne(p => p.Address)

.Property(p=>p.Street).HasColumnName("ShippingStreet");

 

orderConfiguration.OwnsOne(p => p.Address)

.Property(p=>p.City).HasColumnName("ShippingCity");

 

Es posible encadenar el método OwnsOne en con la Fuent API. En el siguiente ejemplo hipotético, OrderDetails posee BillingAddress y ShippingAddress, que son ambas del tipo Address. Entonces OrderDetails es una owned entity de Order.

 

orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>

{

cb.OwnsOne(c => c.BillingAddress);

cb.OwnsOne(c => c.ShippingAddress);

});

//...

//...

public class Order

{

public int Id { get; set; }

public OrderDetails OrderDetails { get; set; }

}

 

public class OrderDetails

{

public StreetAddress BillingAddress { get; set; }

public StreetAddress ShippingAddress { get; set; }

}

 

public class StreetAddress

{

public string Street { get; set; }

public string City { get; set; }

}

Detalles adicionales de los tipos owned entities

Posibilidades de las Owned Entities:

Limitaciones de las Owned Entities:

Diferencias principales con los tipos complejos de EF6

Recursos adicionales

Usando clases de Enumeración en vez de los enums de C#

Las enumeraciones (enums para abreviar) son un wrapper de lenguaje alrededor de un tipo entero. Es posible que desee limitar su uso a cuando está almacenando un valor de un conjunto cerrado de valores. La clasificación basada en el género (por ejemplo, masculino, femenino, desconocido) o los tamaños (S, M, L, XL) son buenos ejemplos. Usar enumeraciones para el flujo de control o abstracciones más robustas puede ser un code smell (una parte del código que no huele bien). Este tipo de uso conducirá a un código frágil, con muchas sentencias de control de flujo que verifican los valores de la enumeración.

En cambio, puede crear clases de enumeración que habiliten todas las características expresivas de un lenguaje orientado a objetos.

Sin embargo, este no es un tema crítico y, en muchos casos, por simplicidad, aún puede usar tipos enum regulares si lo prefiere.

Implementando clases de Enumeración

El microservicio de pedidos en eShopOnContainers proporciona una implementación de la clase base Enumeration de ejemplo, como se muestra en el siguiente código:

public abstract class Enumeration : IComparable

{

public string Name { get; private set; }

 

public int Id { get; private set; }

 

protected Enumeration()

{

}

protected Enumeration(int id, string name)

{

Id = id;

Name = name;

}

public override string ToString()

{

return Name;

}

public static IEnumerable<T> GetAll<T>() where T : Enumeration, new()

{

var type = typeof(T);

var fields = type.GetTypeInfo().GetFields(BindingFlags.Public |

BindingFlags.Static |

BindingFlags.DeclaredOnly);

foreach (var info in fields)

{

var instance = new T();

var locatedValue = info.GetValue(instance) as T;

 

if (locatedValue != null)

{

yield return locatedValue;

}

}

}

 

 

public override bool Equals(object obj)

{

var otherValue = obj as Enumeration;

 

if (otherValue == null)

{

return false;

}

 

var typeMatches = GetType().Equals(obj.GetType());

var valueMatches = Id.Equals(otherValue.Id);

 

return typeMatches && valueMatches;

}

 

public int CompareTo(object other)

{

return Id.CompareTo(((Enumeration)other).Id);

}

 

// Other utility methods ...

}

Puede usar esta clase como un tipo base en cualquier entidad u objeto de valor, como para la siguiente clase de Enumeración CardType.

public class CardType : Enumeration

{

public static CardType Amex = new CardType(1, "Amex");

public static CardType Visa = new CardType(2, "Visa");

public static CardType MasterCard = new CardType(3, "MasterCard");

 

protected CardType() { }

public CardType(int id, string name)

: base(id, name)

{

}

 

public static IEnumerable<CardType> List()

{

return new[] { Amex, Visa, MasterCard };

}

// Other util methods

}

Recursos adicionales

Diseñando las validaciones en la capa de dominio

En DDD, las reglas de validación se pueden considerar invariantes. La principal responsabilidad de un agregado es imponer invariantes entre los cambios de estado, para todas las entidades dentro de ese agregado.

Las entidades de dominio siempre deben ser entidades válidas. Hay un cierto número de invariantes para un objeto que siempre debería ser cierto. Por ejemplo, un objeto de artículo de pedido siempre debe tener una cantidad que debe ser un entero positivo, más el nombre y el precio del artículo. Por lo tanto, hacer cumplir las invariantes es responsabilidad de las entidades de dominio (especialmente de la raíz de agregación) y un objeto de entidad no debería poder existir sin ser válido. Las reglas invariables simplemente se expresan como contratos y las excepciones o notificaciones se generan cuando se infringen.

El razonamiento detrás de esto es que se producen muchos errores porque los objetos están en un estado al que nunca debieron haber llegado. La siguiente es una buena explicación de Greg Young en una discusión en línea:

Propongamos que ahora tenemos un SendUserCreationEmailService que toma un UserProfile ... ¿cómo podemos racionalizar en ese servicio que Name no sea nulo? ¿Lo revisamos de nuevo? O más probablemente ... simplemente no nos molestamos en verificar y "esperamos lo mejor" -espera que alguien se haya tomado la molestia de validarlo antes de enviárselo. Por supuesto, al usar TDD, una de las primeras pruebas que deberíamos escribir es que si envío un cliente con un nombre nulo debería generar un error. Pero una vez que comenzamos a escribir este tipo de pruebas una y otra vez nos damos cuenta ... "espere si nunca permitiéramos que el nombre se vuelva nulo, no tendríamos todas estas pruebas"

Implementado validaciones en la capa del modelo de dominio

Las validaciones generalmente se implementan en constructores de entidades de dominio o en métodos que pueden actualizar la entidad. Existen múltiples formas de implementar validaciones, como verificar datos y generar excepciones si la validación falla. También hay patrones más avanzados, como el uso del patrón de Especificación para las validaciones y el patrón de Notificación para devolver una colección de errores en lugar de elevar una excepción para cada validación a medida que se produce.

Validando condiciones y elevando excepciones

El siguiente ejemplo de código muestra el enfoque más simple para la validación en una entidad de dominio al generar una excepción. En la tabla de referencias al final de esta sección, puede ver enlaces a implementaciones más avanzadas basadas en los patrones que hemos discutido anteriormente.

public void SetAddress(Address address)

{

_shippingAddress = address?? throw new ArgumentNullException(nameof(address));

}

 

Un mejor ejemplo demostraría la necesidad de garantizar que el estado interno no cambiara o que todas las mutaciones de un método ocurrieran. Por ejemplo, la siguiente implementación dejaría el objeto en un estado no válido:

Public void SetAddress(string line1, string line2,

string city, string state, int zip)

{

_shipingAddress.line1 = line1 ?? throw new ...

_shippingAddress.line2 = line2;

_shippingAddress.city = city ?? throw new ...

_shippingAddress.state = (IsValid(state) ? state : throw new …);

}

 

Si el valor del estado no es válido, la primera línea de dirección y la ciudad ya se han cambiado. Eso podría hacer que la dirección sea inválida.

Se puede usar un enfoque similar en el constructor de la entidad, generando una excepción para garantizar que sea válida una vez que se haya creado.

Usando atributos de validación en el modelo, basados en anotaciones de datos

Los data annotations, como los atributos Required o MaxLength, se pueden utilizar para configurar las propiedades de los campos de la base de datos en EF Core, como se explica en detalle en la sección Mapeo de tablas, pero ya no funcionan para la validación de entidades en EF Core (ni el método IValidatableObject.Validate tampoco), como lo han hecho desde EF 4.x en .NET Framework.

Los data annotations y la interfaz IValidatableObject se pueden seguir utilizando para la validación del modelo durante el model binding, antes de la invocación de las acciones del controlador como siempre, pero ese modelo está pensado para ser un ViewModel o DTO y eso es una preocupación de MVC o API no del modelo de dominio.

Una vez aclarada la diferencia conceptual, todavía puede utilizar data annotations e IValidatableObject en la clase de entidad para la validación, si sus acciones reciben como parámetro un objeto de clase de entidad, lo que no recomendamos. En ese caso, la validación se producirá en el momento del model binding, justo antes de invocar la acción y se puede revisar la propiedad ModelState.isValid del controlador para comprobar el resultado, pero eso también ocurre en el controlador, no antes de persistir el objeto de la entidad en el DbContext, como se ha hecho desde EF 4.x.

Aún así, puede implementar una validación personalizada en la clase de entidad utilizando los data annotations y el método IValidatableObject.Validate, reemplazando el método SaveChanges del DbContext.

Puede ver un ejemplo de implementación de validación de entidades IValidatableObject en este comentario de GitHub. Ese ejemplo no hace validaciones basadas en atributos, pero deberían ser fáciles de implementar usando reflection en el mismo override.

Sin embargo, desde un punto de vista DDD, el modelo de dominio se mantiene mejor con el uso de excepciones en los métodos de comportamiento de su entidad, o implementando los patrones de Especificación y Notificación para hacer cumplir las reglas de validación.

Puede tener sentido utilizar anotaciones de datos en la capa de aplicación en clases de ViewModel (en lugar de entidades de dominio) que aceptarán entrada, para permitir la validación del modelo dentro de la capa de la interfaz de usuario. Sin embargo, esto no exime de hacer las validaciones dentro del modelo de dominio.

Validando entidades implementando los patrones de Especificación y Notificación

Finalmente, un enfoque más elaborado para implementar validaciones en el modelo de dominio es implementar el patrón de Especificación junto con el patrón de Notificación, como se explica en algunos de los recursos adicionales que se enumeran más adelante.

Vale la pena mencionar que también puede usar sólo uno de esos patrones, por ejemplo, validando manualmente con las sentencias de control, pero usando el patrón de notificación para apilar y devolver una lista de errores de validación.

Usando validación diferida en el dominio

Hay varios enfoques para tratar con validaciones diferidas en el dominio. Vaughn Vernon discute estos en su libro Implementing Domain-Driven Design.

Validación en dos pasos

Considere también la validación en dos pasos. Use la validación a nivel de campo en los DTOs de comandos (Objetos de Transferencia de Datos, por ejemplo, un ViewModel de actualización) y la validación a nivel de dominio dentro de sus entidades. Puede hacer esto devolviendo un objeto como resultado en lugar de elevar excepciones, para facilitar el tratamiento de los errores de validación.

Al usar validaciones de campo con anotaciones de datos, no se duplica la definición de validación. La ejecución, sin embargo, puede ocurrir tanto del lado del servidor como del lado del cliente en el caso de los DTOs (comandos y ViewModels).

Recursos adicionales

Validación en el lado del cliente (en las capas de presentación)

Incluso cuando la fuente de la verdad es el modelo de dominio y, en última instancia, debe tener validación en el nivel de modelo de dominio, la validación aún se puede manejar tanto en el nivel de modelo de dominio (del lado del servidor) como en el lado del cliente.

La validación del lado del cliente es una gran conveniencia para los usuarios. Ahorra tiempo que de lo contrario pasarían esperando un viaje de ida y vuelta al servidor que podría devolver errores de validación. En términos del negocio, incluso unas pocas fracciones de segundos se multiplican cientos de veces al día, lo que se traduce en mucho tiempo, gastos y frustración. La validación directa e inmediata permite a los usuarios trabajar de manera más eficiente y producir entradas y salidas de mejor calidad.

Así como el ViewModel y el modelo de dominio son diferentes, la validación del ViewModel y la validación del modelo de dominio pueden ser similares, pero tienen un propósito diferente. Si le preocupa el principio DRY (Don’t Repeat Yourself - no repetirse), tenga en cuenta que en este caso la reutilización del código también puede implicar acoplamiento y, en las aplicaciones empresariales, es más importante no acoplar el lado del servidor al lado del cliente que seguir el principio DRY

Incluso cuando usa la validación del lado del cliente, siempre debe validar sus comandos o DTOs de entrada en el código del servidor, ya que las API del servidor son un posible vector de ataque. Por lo general, hacer ambas cosas es la mejor opción porque si tiene una aplicación cliente, desde la perspectiva de la experiencia del usuario (UX), es mejor ser proactivo y no permitir que el usuario ingrese información inválida.

Por lo tanto, valide los ViewModels en el código del lado del cliente. También puede validar los DTOs o comandos de salida del cliente antes de enviarlos a los servicios.

La implementación de la validación del lado del cliente depende del tipo de aplicación cliente que esté creando. Sería diferente si está validando datos en una aplicación web MVC, con la mayor parte del código en .NET, una aplicación web SPA con esa validación codificada en JavaScript o TypeScript, o una aplicación móvil nativa con Xamarin y C#.

Recursos adicionales

Validación en aplicaciones móviles con Xamarin
Validación en aplicaciones ASP.NET Core
Validación en aplicaciones Web SPA (Angular 2, TypeScript, JavaScript)

 

En resumen, estos son los conceptos más importantes en relación con las validaciones:

Eventos del dominio: diseño e implementación

Use eventos de dominio para implementar explícitamente los efectos secundarios de los cambios dentro de su dominio. En otras palabras, y usando la terminología DDD, use eventos de dominio para implementar explícitamente efectos secundarios en múltiples agregados. Opcionalmente, para una mejor escalabilidad y un menor impacto por los bloqueos de bases de datos, use la consistencia eventual entre los agregados dentro del mismo dominio.

¿Qué es un evento de dominio?

Un evento es algo que sucedió en el pasado. Un evento de dominio es, lógicamente, algo que sucedió en un dominio en particular y algo que desea que otras partes del mismo dominio (en el mismo proceso) conozcan y a lo que puedan reaccionar.

Un beneficio importante de los eventos de dominio es que, los efectos secundarios después de que algo sucedió en un dominio, se pueden expresar explícitamente en lugar de implícitamente. Esos efectos secundarios deben ser consistentes, por lo que todas las operaciones relacionadas con la tarea negocio sucedan, o no ocurra ninguna de ellas. Además, los eventos de dominio permiten una mejor separación de las responsabilidades entre las clases dentro del mismo dominio.

Por ejemplo, si sólo está utilizando Entity Framework y entidades o incluso agregados, si tiene que haber efectos secundarios provocados por un caso de uso, estos se implementarán como un concepto implícito en el código relacionado después de que algo sucedió (es decir, debe ver el código para saber que hay efectos secundarios). Pero, si sólo ve ese código, es posible que no sepa si el efecto que está viendo es parte de la operación principal o si realmente es un efecto secundario. Por otro lado, el uso de eventos de dominio hace que el concepto sea explícito y parte del lenguaje ubicuo. Por ejemplo, en la aplicación eShopOnContainers, crear un pedido no es sólo sobre el pedido, también se actualiza o crea un agregado de comprador, basado en el usuario original, porque el usuario no es un comprador hasta que hace un pedido. Si usa eventos de dominio, puede expresar explícitamente esa regla de dominio basada en el lenguaje ubicuo proporcionado por los expertos de dominio.

Los eventos de dominio son algo similares a los eventos de mensajería, con una diferencia importante. Con mensajería real, colas de mensajes, intermediarios de mensajes o un bus de servicio que usa AMPQ, un mensaje siempre se envía de forma asíncrona y se realiza entre procesos e, incluso, máquinas. Esto es útil para integrar múltiples Bounded Contexts, microservicios o incluso diferentes aplicaciones. Sin embargo, con eventos de dominio, desea generar un evento a partir de la operación que está ejecutando actualmente, pero desea que se produzcan efectos secundarios dentro del mismo dominio.

Los eventos de dominio y sus efectos secundarios (las acciones desencadenadas posteriormente que son administradas por manejadores de eventos) deben ocurrir casi de inmediato, generalmente en el mismo proceso y dentro del mismo dominio. Por lo tanto, los eventos de dominio podrían ser síncronos o asíncronos. Los eventos de integración, sin embargo, siempre deben ser asíncronos.

Eventos de dominio versus eventos de integración

Semánticamente, los eventos de dominio e integración son la misma cosa: notificaciones sobre algo que acaba de suceder. Sin embargo, su implementación debe ser diferente. Los eventos de dominio son sólo mensajes enviados a un despachador de eventos de dominio, que podría implementarse como un mediador en memoria basado en un contenedor IoC o cualquier otro método.

Por otro lado, el propósito de los eventos de integración es propagar transacciones y actualizaciones confirmadas a subsistemas adicionales, ya sean otros microservicios, Bounded Contexts o incluso aplicaciones externas. Por lo tanto, deben ocurrir sólo si la entidad se persiste exitosamente, ya que en muchos escenarios si esto falla, la operación, en realidad, nunca ocurrió efectivamente.

Además, y como se mencionó, los eventos de integración se deben basar en la comunicación asíncrona entre múltiples microservicios (otros Bounded Contexts) o incluso sistemas/aplicaciones externas. Por lo tanto, la interfaz del bus de eventos necesita alguna infraestructura que permita la comunicación distribuida entre procesos y servicios, potencialmente remotos. Puede basarse en un bus de servicio comercial, colas, una base de datos compartida utilizada como buzón de correo o cualquier otro sistema de mensajería distribuido e, idealmente, de tipo push.

Eventos de dominio como la forma preferida de iniciar efectos secundarios entre múltiples agregados dentro del mismo dominio

Si la ejecución de un comando relacionado con una instancia agregada requiere que se ejecuten reglas de dominio adicionales en uno o más agregados adicionales, debería diseñar e implementar los efectos secundarios para que ser desencadenarán por eventos de dominio. Como se muestra en la figura 7-14, y como uno de los casos de uso más importantes, un evento de dominio debe usarse para propagar cambios de estado a través de múltiples agregados dentro del mismo modelo de dominio.

Image

Figura 7-14. Eventos del dominio para reforzar la consistencia entre agregados en el mismo dominio

En la figura, cuando el usuario inicia un pedido, el evento de dominio OrderStarted activa la creación de un objeto de Buyer en el microservicio de pedidos, basado en la información de usuario original del microservicio de identidad (con información proporcionada en el comando CreateOrder). El evento de dominio es generado, en primera instancia, por el agregado de orden cuando se crea ésta.

Alternativamente, puede hacer que la raíz de agregación se suscriba a eventos generados por miembros de sus entidades secundarias. Por ejemplo, cada entidad secundaria OrderItem puede generar un evento cuando el precio del artículo es más alto que un monto específico, o cuando la cantidad del artículo del producto es demasiado alta. La raíz de agregación puede recibir esos eventos y realizar un cálculo o agregación global.

Es importante entender que esta comunicación basada en eventos no se implementa directamente dentro de los agregados; necesita implementar manejadores de eventos de dominio. El manejo de los eventos del dominio es responsabilidad de la aplicación. La capa de modelo de dominio sólo debe centrarse en la lógica de dominio, cosas que un experto en dominio entendería, no la infraestructura de aplicaciones como los manejadores y las acciones de persistencia y de efectos secundarios que usan repositorios. Por lo tanto, el nivel de la capa de aplicación es donde debe tener manejadores de eventos que desencadenen acciones cuando se genere un evento de dominio.

Los eventos de dominio también se pueden utilizar para desencadenar cualquier cantidad de acciones de la aplicación y, lo que es más importante, debe estar abierto para aumentar esa cantidad en el futuro de una manera desacoplada. Por ejemplo, cuando se inicia el pedido, es posible que desee publicar un evento de dominio para propagar esa información a otros agregados o incluso para generar acciones de aplicación, como notificaciones.

El punto clave es la cantidad abierta de acciones que se ejecutarán cuando se produzca un evento de dominio. Eventualmente, las acciones y reglas en el dominio y la aplicación crecerán. La complejidad o la cantidad de acciones con efectos secundarios cuando algo ocurra aumentará, pero si su código se combina con "pegamento" (es decir, simplemente instanciando objetos con new en C#), cada vez que necesite agregar una nueva acción necesitarás cambiar el código original. Esto podría generar nuevos errores, ya que con cada nuevo requerimiento necesitaría cambiar el flujo de código original. Esto va en contra del principio Open/Closed de SOLID. Además, la clase original que estaba orquestando las operaciones crecería y crecería, lo que va en contra del Principio de Responsabilidad Única (SRP).

Por otro lado, si usa eventos de dominio, puede crear una implementación desacoplada y detallada, segregando responsabilidades usando este enfoque:

  1. Enviar un comando (por ejemplo, CreateOrder).
  2. Recibir el comando en un manejador de comando.
  1. Manejar eventos de dominio (dentro del proceso actual) que ejecutarán una cantidad abierta de efectos secundarios en múltiples agregados o acciones de aplicación. Por ejemplo:

Como se muestra en la figura 7-15, a partir del mismo evento de dominio, puede manejar múltiples acciones relacionadas con otros agregados en el dominio o acciones de otras aplicaciones que necesita realizar, a través de microservicios que se conectan con eventos de integración y el bus de eventos.

Image

Figura 7-15. Manejando múltiples acciones por dominio

Los manejadores de eventos están normalmente en la capa de aplicación, porque usará objetos de infraestructura como repositorios o una API de aplicación para el comportamiento del microservicio. En ese sentido, los manejadores de eventos son similares a los manejadores de comandos, por lo que ambos son parte de la capa de aplicación. La diferencia importante es que un comando se debe procesar sólo una vez. Un evento de dominio podría procesarse cero o muchas veces, porque podrían recibirlo múltiples receptores o manejadores de eventos con un propósito diferente cada uno.

La posibilidad de una cantidad abierta de manejadores por evento de dominio le permite agregar muchas más reglas de dominio sin afectar su código actual. Por ejemplo, la implementación de la siguiente regla comercial que debe ocurrir inmediatamente después de un evento puede ser tan fácil como agregar algunos manejadores de eventos (o, a lo mejor, sólo uno):

Cuando la cantidad total comprada por un cliente en la tienda, en cualquier cantidad de pedidos, exceda los $ 6,000, aplique un descuento del 10% en cada nuevo pedido y notifique al cliente con un correo electrónico sobre ese descuento para futuros pedidos.

Implementando eventos de dominio

En C#, un evento de dominio es simplemente una estructura o clase que contiene datos, como un DTO, con toda la información relacionada con lo que acaba de ocurrir en el dominio, como se muestra en el siguiente ejemplo:

public class OrderStartedDomainEvent : INotification

{

public string UserId { get; private set; }

public int CardTypeId { get; private set; }

public string CardNumber { get; private set; }

public string CardSecurityNumber { get; private set; }

public string CardHolderName { get; private set; }

public DateTime CardExpiration { get; private set; }

public Order Order { get; private set; }

 

public OrderStartedDomainEvent(Order order,

int cardTypeId, string cardNumber,

string cardSecurityNumber, string cardHolderName,

DateTime cardExpiration)

{

Order = order;

CardTypeId = cardTypeId;

CardNumber = cardNumber;

CardSecurityNumber = cardSecurityNumber;

CardHolderName = cardHolderName;

CardExpiration = cardExpiration;

}

}

Esta es esencialmente una clase que contiene todos los datos relacionados con el evento OrderStarted.

En términos del lenguaje ubicuo del dominio, dado que un evento es algo que sucedió en el pasado, el nombre de clase del evento debería representarse como un verbo en tiempo pasado, como OrderStartedDomainEvent u OrderShippedDomainEvent. Así es como se implementa el evento de dominio en el microservicio de pedido en eShopOnContainers.

Como hemos señalado, una característica importante de los eventos es que, dado que un evento es algo que sucedió en el pasado, no debería cambiar. Por lo tanto, debe ser una clase inmutable. Puede ver en el código anterior que las propiedades son de sólo lectura desde fuera del objeto. La única forma de actualizar el objeto es mediante el constructor cuando se crea el objeto de evento.

Disparando eventos de dominio

La siguiente pregunta es, cómo disparar un evento de dominio para que llegue a los manejadores de eventos relacionados. Puede usar múltiples enfoques.

Udi Dahan originalmente propuso (por ejemplo, en varias publicaciones relacionadas, como Domain Events – Take 2) el uso de una clase estática para administrar y disparar los eventos. Esto podría incluir una clase estática llamada DomainEvents que dispararía eventos de dominio inmediatamente cuando se invoca, usando sintaxis como DomainEvents.Raise(Event myEvent). Jimmy Bogard escribió una publicación en su blog (Strengthening your domain: Domain Events) que recomienda un enfoque similar.

Sin embargo, cuando la clase de eventos de dominio es estática, también se distribuye a los manejadores de inmediato. Esto hace que las pruebas y la depuración sean más difíciles, ya que los controladores de eventos con lógica de efectos secundarios se ejecutan inmediatamente después de que se genera el evento. Cuando está probando y depurando, realmente quiere enfocarse sólo en lo que está ocurriendo en las clases del agregado actual, no desea ser redireccionado repentinamente a otros manejadores de eventos para realizar efectos secundarios relacionados con otros agregados o lógica de aplicación. Por esto han evolucionados otros enfoques, como se explica en la sección siguiente.

El enfoque diferido para disparar y enviar eventos

En lugar de enviar inmediatamente eventos de dominio a un manejador, un mejor enfoque es agregarlos a una colección y luego enviar esos eventos justo antes o justo después de la transacción (como con SaveChanges en EF). Este enfoque fue descrito por Jimmy Bogard en este artículo A better domain events pattern (Un patrón mejorado para eventos de dominio).

Es importante decidir si envía los eventos de dominio justo antes o después de realizar la transacción, ya que determina si incluirá los efectos secundarios como parte de la misma transacción o en transacciones diferentes. En el último caso, debe manejar la consistencia eventual entre múltiples agregados. Este tema se trata en la siguiente sección.

El enfoque diferido es lo que usa eShopOnContainers. Primero, agrega los eventos que ocurren en sus entidades en una colección o lista de eventos por entidad. Esa lista debe ser parte del objeto de la entidad, o mejor aún, parte de su clase de entidad base, como se muestra en el siguiente ejemplo de la clase base Entity:

public abstract class Entity

{

//...

private List<INotification> _domainEvents;

public List<INotification> DomainEvents => _domainEvents;

 

public void AddDomainEvent(INotification eventItem)

{

_domainEvents = _domainEvents ?? new List<INotification>();

_domainEvents.Add(eventItem);

}

 

public void RemoveDomainEvent(INotification eventItem)

{

if (_domainEvents is null) return;

_domainEvents.Remove(eventItem);

}

//... Additional code

}

Cuando desee disparar un evento, simplemente agréguelo a la colección de eventos del código en cualquier método de la entidad de raíz del agregado, como se muestra en el siguiente código, que es parte de la raíz de agregación Order en eShopOnContainers:

var orderStartedDomainEvent = new OrderStartedDomainEvent(this, //Order object

cardTypeId, cardNumber,

cardSecurityNumber,

cardHolderName,

cardExpiration);

this.AddDomainEvent(orderStartedDomainEvent);

 

Tenga en cuenta que lo único que está haciendo el método AddDomainEvent es agregar un evento a la lista. Aún no se envía ningún evento y aún no se invoca ningún manejador de eventos.

De hecho, los eventos se van a envíar más tarde, cuando guarde la transacción en la base de datos. Si está utilizando Entity Framework Core, eso significa en el método SaveChanges de su DbContext, como en el siguiente código:

// EF Core DbContext

public class OrderingContext : DbContext, IUnitOfWork

{

// ...

public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken =

default(CancellationToken))

{

// Dispatch Domain Events collection.

// Choices:

// A) Right BEFORE committing data (EF SaveChanges) into the DB. This makes

// a single transaction including side effects from the domain event

// handlers that are using the same DbContext with Scope lifetime

// B) Right AFTER committing data (EF SaveChanges) into the DB. This makes

// multiple transactions. You will need to handle eventual consistency and

// compensatory actions in case of failures.

await _mediator.DispatchDomainEventsAsync(this);

 

// After this line runs, all the changes (from the Command Handler and Domain

// event handlers) performed through the DbContext will be commited

var result = await base.SaveChangesAsync();

}

// ...

}

Con este código, se envían los eventos de la entidad a sus respectivos manejadores. El resultado general es que ha desacoplado la creación de un evento de dominio (un simple agregado en una lista en la memoria) para que no sea enviado a un manejador de eventos. Además, dependiendo del tipo de emisor que esté utilizando, puede distribuir los eventos de forma síncrona o asíncrona.

Tenga en cuenta que los límites transaccionales tienen un rol significativo aquí. Si su unidad de trabajo y transacción puede abarcar más de un agregado (como cuando se usa EF Core y una base de datos relacional), esto puede funcionar bien. Pero si la transacción no puede abarcar agregados, como cuando está utilizando una base de datos NoSQL como Azure DocumentDB, debe implementar pasos adicionales para lograr consistencia. Esta es otra razón por la cual la ignorancia de la persistencia no es universal; depende del sistema de almacenamiento que use.