Transacciones únicas entre agregados versus consistencia eventual entre agregados

La cuestión de si realizar una sola transacción o confiar en la consistencia eventual entre los agregados, es controvertida. Muchos autores de DDD como Eric Evans y Vaughn Vernon defienden la regla de que una transacción = un agregado y, por lo tanto, argumentan a favor de la consistencia eventual entre los agregados. Por ejemplo, en su libro Domain-Driven Design, Eric Evans dice esto:

No se espera que las reglas que abarcan agregados estén actualizadas en todo momento. A través del procesamiento de eventos, el procesamiento por lotes u otros mecanismos de actualización, otras dependencias se pueden resolver dentro de un tiempo específico. (pg. 128)

Vaughn Vernon dice lo siguiente en Effective Aggregate Design. Part II: Making Aggregates Work Together (Diseño efectivo de agregados, parte II, haciendo que los agregados trabajen juntos):

Por lo tanto, si la ejecución de un comando en un agregado requiere que se ejecuten reglas del negocio adicionales en uno o más agregados, use consistencia eventual […] Hay una forma práctica de soportar la consistencia eventual en un modelo DDD. Un método de un agregado publica un evento de dominio que es entregado a tiempo a uno o más suscriptores asíncronos.

Este razonamiento se basa en manejar transacciones pequeñas, detalladas, en lugar de transacciones que abarcan muchos agregados o entidades. La idea es que, en el segundo caso, la cantidad de bloqueos de base de datos puede ser significativo en aplicaciones de gran escala con altas necesidades de escalabilidad. Aceptar el hecho de que las aplicaciones de alta escalabilidad no necesitan tener una consistencia transaccional instantánea entre múltiples agregados, ayuda a aceptar el concepto de consistencia eventual. Los cambios atómicos a menudo no son necesarios para la empresa y, en cualquier caso, es responsabilidad de los expertos del dominio decir si determinadas operaciones necesitan transacciones atómicas o no. Si una operación siempre necesita una transacción atómica entre múltiples agregados, debería preguntarse si su agregado debería ser más grande o si no fue diseñado correctamente.

Sin embargo, otros desarrolladores y arquitectos como Jimmy Bogard están de acuerdo en abarcar una sola transacción en varios agregados, pero sólo cuando esos agregados adicionales están relacionados con los efectos secundarios del mismo comando original. Por ejemplo, en un mejor patrón de eventos de dominio, Bogard dice esto:

Normalmente, deseo que los efectos secundarios de un evento de dominio ocurran dentro de la misma transacción lógica, pero no necesariamente en el mismo ámbito de disparar el evento de dominio […] justo antes de guardar nuestra transacción, enviamos nuestros eventos a sus respectivos manejadores.

Si envía los eventos de dominio justo antes de guardar la transacción original, es porque desea que los efectos secundarios de esos eventos se incluyan en la misma transacción. Por ejemplo, si el método SaveChanges del DbContext falla, la transacción revertirá todos los cambios, incluido el resultado de cualquier operación de efecto secundario implementada por los manejadores de eventos de dominio relacionados. Esto se debe a que la vida de DbContext está definido por defecto como "scoped (en el ámbito)". Por lo tanto, el objeto DbContext se comparte entre todos los repositorios que se instancien dentro del mismo ámbito o grafo del objeto. Esto coincide con el ámbito del HttpRequest al desarrollar aplicaciones API Web o MVC.

En realidad, ambos enfoques (transacción atómica única y consistencia eventual) pueden ser correctos. Realmente depende de los requisitos de su dominio o negocio y de lo que dicen los expertos del dominio. También depende de qué tan escalable necesite que sea el servicio (las transacciones más granulares tienen menos impacto con respecto a los bloqueos de base de datos). Y depende de qué tanto quiera invertir en su código, ya que la consistencia eventual tiene más complejidad para detectar posibles inconsistencias entre los agregados y la necesidad de implementar acciones compensatorias. Tenga en cuenta que, si realiza cambios en el agregado original y luego, cuando se envían los eventos, hay un problema y los manejadores no pueden guardar sus efectos secundarios, tendrá inconsistencias entre los agregados.

Una forma de permitir acciones compensatorias sería almacenar los eventos de dominio en tablas de base de datos adicionales, para que puedan ser parte de la transacción original. Después, puede tener un proceso por lotes que detecta inconsistencias y ejecuta acciones compensatorias al comparar la lista de eventos con el estado actual de los agregados. Las acciones compensatorias son parte de un tema complejo que requerirá un análisis profundo de su parte, lo que incluye debatirlo con el usuario del negocio y los expertos en el dominio.

En cualquier caso, puede elegir el enfoque que necesite. Pero el enfoque diferido propuesto (disparar los eventos antes de guardar, para usar una sola transacción) es el enfoque más simple cuando se usa EF Core y una base de datos relacional. Es más fácil de implementar y válido en muchos casos del negocio. También es el enfoque utilizado en el microservicio de pedidos en eShopOnContainers.

¿Pero cómo enviar esos eventos a sus respectivos manejadores de eventos? ¿Cuál es el objeto _mediator mostrado en el ejemplo anterior? Eso tiene que ver con las técnicas y artefactos que puede usar para mapear entre los eventos y sus manejadores de eventos.

El dispatcher de eventos de dominio: mapeando eventos a manejadores

Una vez que pueda enviar o publicar los eventos, necesita algún tipo de artefacto que publique el evento para que cada manejador relacionado pueda obtenerlo y procesar los efectos secundarios en función de ese evento.

Un enfoque es un sistema de mensajería real o incluso un bus de eventos, posiblemente basado en un bus de servicio en lugar de eventos en memoria. Sin embargo, para el primer caso, la mensajería real sería excesiva para procesar eventos de dominio, ya que sólo necesita procesar esos eventos dentro del mismo proceso (es decir, dentro del mismo dominio y la misma capa de aplicación).

Otra forma de asignar eventos a varios manejadores es mediante el uso de registros de tipos en un contenedor IoC, para que pueda inferir dinámicamente a dónde enviar los eventos. En otras palabras, necesita saber qué manejadores de eventos necesitan recibir un evento específico. La figura 7-16 muestra un enfoque simplificado para eso.

Image

Figura 7-16. Canalizador de eventos del dominio usando IoC

Puede construir toda la infraestructura necesaria para implementar ese enfoque usted mismo. Sin embargo, también puede usar librerías disponibles como MediatR, que por debajo usa su contenedor IoC. Por lo tanto, puede usar directamente las interfaces predefinidas y los métodos de publicación/envío del objeto _mediator.

En este código, primero necesita registrar los tipos de manejadores de eventos en su contenedor IoC, como se muestra en el siguiente ejemplo en el microservicio de pedidos (Ordering) en eShopOnContainers:

public class MediatorModule : Autofac.Module

{

protected override void Load(ContainerBuilder builder)

{

// Other registrations ...

// Register the DomainEventHandler classes (they implement

// IAsyncNotificationHandler<>) in assembly holding the Domain Events

builder.RegisterAssemblyTypes(

typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler)

.GetTypeInfo().Assembly)

.AsClosedTypesOf(typeof(IAsyncNotificationHandler<>));

// Other registrations ...

//...

}

 

}

 

El código identifica primero el ensamblado que contiene los manejadores de eventos de dominio al ubicar el ensamblado que contiene cualquiera de los manejadores (usando typeof(ValidateOrAddBuyerAggregateWhenXxxx), pero podría haber elegido cualquier otro manejador de eventos para localizar el ensamblado). Como todos los manejadores de eventos implementan la interfaz IAysncNotificationHandler, el código simplemente busca esos tipos y registra todos los manejadores de eventos.

Cómo suscribirse a eventos de dominio

Cuando utiliza MediatR, cada manejador de eventos debe usar un tipo de evento que se indica en el parámetro genérico de la interfaz IAsyncNotificationHandler, como puede ver en el siguiente código:

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler

: IAsyncNotificationHandler<OrderStartedDomainEvent>

 

En función de la relación entre el evento y el manejador de eventos, que se puede considerar como la suscripción, el artefacto MediatR puede descubrir todos los manejadores de eventos para cada evento y activar cada uno de ellos.

Cómo manejar eventos de dominio

Finalmente, el manejador de eventos usualmente implementa código de la capa de aplicación, que usa repositorios de infraestructura para obtener los agregados adicionales requeridos, para ejecutar la lógica de dominio de los efectos secundarios. El siguiente código de manejador de eventos de dominio en eShopOnContainers, muestra un ejemplo de implementación.

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler

: INotificationHandler<OrderStartedDomainEvent>

{

private readonly ILoggerFactory _logger;

private readonly IBuyerRepository<Buyer> _buyerRepository;

private readonly IIdentityService _identityService;

 

public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(

ILoggerFactory logger,

IBuyerRepository<Buyer> buyerRepository,

IIdentityService identityService)

{

// ...Parameter validations...

}

 

public async Task Handle(OrderStartedDomainEvent orderStartedEvent)

{

var cardTypeId = (orderStartedEvent.CardTypeId != 0) ?

orderStartedEvent.CardTypeId : 1;

var userGuid = _identityService.GetUserIdentity();

var buyer = await _buyerRepository.FindAsync(userGuid);

bool buyerOriginallyExisted = (buyer == null) ? false : true;

if (!buyerOriginallyExisted)

{

buyer = new Buyer(userGuid);

}

buyer.VerifyOrAddPaymentMethod(cardTypeId,

$"Payment Method on {DateTime.UtcNow}",

orderStartedEvent.CardNumber,

orderStartedEvent.CardSecurityNumber,

orderStartedEvent.CardHolderName,

orderStartedEvent.CardExpiration,

orderStartedEvent.Order.Id);

 

var buyerUpdated = buyerOriginallyExisted ? _buyerRepository.Update(buyer) :

_buyerRepository.Add(buyer);

await _buyerRepository.UnitOfWork.SaveEntitiesAsync();

// Logging code using buyerUpdated info, etc.

}

}

El código de manejador de eventos anterior se considera código de la capa de aplicación, porque usa repositorios de infraestructura, como se explica en la siguiente sección de la capa de persistencia. Los manejadores de eventos también podrían usar otros componentes de infraestructura.

Los eventos de dominio pueden generar eventos de integración que se publiquen fuera del microservicio

Finalmente, es importante mencionar que a veces es posible que desee propagar eventos entre múltiples microservicios. Esto se considera un evento de integración y podría publicarse a través de un bus de eventos desde cualquier manejador de eventos.

Conclusiones sobre los eventos de dominio

Como se dijo, use eventos de dominio para implementar explícitamente los efectos secundarios de los cambios dentro de su dominio. Para usar la terminología DDD, use eventos de dominio para implementar explícitamente efectos secundarios en uno o varios agregados. Además, para una mejor escalabilidad y un menor impacto en los bloqueos de bases de datos, use la consistencia eventual entre los agregados dentro del mismo dominio.

Recursos adicionales

Diseñando la capa de infraestructura de persistencia

Los componentes de persistencia de datos proporcionan acceso a los datos alojados dentro de los límites de un microservicio (es decir, una base de datos del microservicio). Contienen la implementación real de componentes como las clases de repositorios y Unidades de Trabajo, como EF DBContexts personalizados.

El patrón de Repositorio

Los repositorios son clases o componentes que encapsulan la lógica necesaria para acceder a las fuentes de datos. Centralizan la funcionalidad común de acceso a datos, facilitando el mantenimiento y desacoplando la infraestructura o tecnología utilizada para acceder a las bases de datos desde las capas del modelo de dominio y de la aplicación. Si usa un ORM como Entity Framework, se simplifica el código que debe implementar, gracias a LINQ y el tipado fuerte. Esto le permite enfocarse en la lógica de persistencia de datos en lugar de en la fontanería del acceso a datos.

El patrón Repositorio es una forma de trabajar con una fuente de datos, que está bien documentada. En el libro Patterns of Enterprise Application Architecture, Martin Fowler describe un repositorio de la siguiente manera:

Un repositorio realiza las tareas de un intermediario entre las capas de modelo de dominio y el mapeo de datos, actuando de forma similar a un conjunto de objetos de dominio en la memoria. Los objetos cliente pueden construir consultas de forma declarativa y luego enviarlas a los repositorios para obtener los resultados. Conceptualmente, un repositorio encapsula un conjunto de objetos almacenados en la base de datos y las operaciones que se pueden realizar sobre ellos, proporcionando una forma más cercana a la capa de persistencia. Los repositorios también soportan el propósito de separar, claramente y en una dirección, la dependencia entre el dominio de trabajo y el mapeo de datos.

Defina un repositorio por agregado

Debe crear un repositorio para cada agregado o raíz de agregación. En un microservicio basado en patrones DDD, el único canal que debe usar para actualizar la base de datos deben ser los repositorios. Esto se debe a que tienen una relación de uno a uno con la raíz de agregación, que controla las invariantes del agregado y la consistencia transaccional. Está bien consultar la base de datos a través de otros canales (como puede hacer siguiendo un enfoque CQRS), porque las consultas no cambian el estado de la base de datos. Sin embargo, el área transaccional (las actualizaciones) siempre debe estar controlada por los repositorios y las raíces de agregación.

Básicamente, un repositorio le permite llenar datos en la memoria, en la forma de entidades del dominio, que provienen de la base de datos. Una vez que las entidades están en la memoria, se pueden cambiar y luego volver a la base de datos mediante transacciones.

Como se señaló anteriormente, si está utilizando el patrón arquitectónico CQS/CQRS, las consultas iniciales se realizarán a través consultas secundarias fuera del modelo de dominio, realizadas mediante sentencias SQL simples utilizando Dapper. Este enfoque es mucho más flexible que los repositorios, porque puede consultar y hacer join con cualquier tabla que necesite y estas consultas no están restringidas por las reglas de los agregados. Esa información irá a la capa de presentación o aplicación cliente.

Si el usuario realiza cambios, los datos que se actualizarán vendrán desde la aplicación cliente o la capa de presentación hasta la capa de la aplicación (como un servicio API Web). Cuando recibe un comando (con datos) en un manejador de comandos, se usan repositorios para obtener los datos que desea actualizar desde la base de datos. Usted lo actualiza en la memoria con la información pasada con los comandos y luego agrega o actualiza los datos (en las entidades de dominio) hacia la base de datos a través de una transacción.

Debemos enfatizar nuevamente que sólo se debe definir un repositorio para cada raíz de agregación, como se muestra en la Figura 7-17. Para lograr el objetivo de que la raíz de agregación debe mantener la consistencia transaccional entre todos los objetos dentro del agregado, nunca debe crear un repositorio para cada tabla en la base de datos.

Image

Figura 7-17. La relación entre repositorios, agregados y tablas de la base de datos

Reforzando la regla de una raíz de agregación por repositorio

Puede aportar valor el implementar su diseño de repositorio para asegurar que se cumpla la regla de que sólo las raíces de agregación deben tener repositorios. Puede crear un tipo de depósito genérico o base que restrinja el tipo de entidades con las que trabaja para garantizar que tengan la interfaz del marcador IAggregateRoot.

Por lo tanto, cada clase de repositorio en la capa de infraestructura implementa su propio contrato o interfaz, como se muestra en el siguiente código:

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories

{

public class OrderRepository : IOrderRepository

{

 

Cada interfaz de repositorio específico implementa la interfaz genérica de IRepository:

public interface IOrderRepository : IRepository<Order>

{

Order Add(Order order);

// ...

}

 

Sin embargo, una mejor forma de hacer que el código haga cumplir la convención de que cada repositorio debe estar relacionado con un agregado único sería implementar un tipo de repositorio genérico, por lo que es explícito que está utilizando un repositorio para apuntar a un agregado específico. Eso se puede hacer fácilmente implementando ese genérico en la interfaz base de IRepository, como en el siguiente código:

public interface IRepository<T> where T : IAggregateRoot

El patrón de repositorio simplifica las pruebas de la lógica de la aplicación

El patrón Repositorio le permite probar fácilmente su aplicación con pruebas unitarias. Recuerde que las pruebas unitarias sólo prueban su código, no la infraestructura, por lo que las abstracciones del repositorio hacen que sea más fácil lograr ese objetivo.

Como se señaló en una sección anterior, se recomienda definir y colocar las interfaces del repositorio en la capa del modelo de dominio para que la capa de la aplicación (por ejemplo, su servicio de API Web) no dependa directamente de la capa de infraestructura donde haya implementado los repositorios. Al hacer esto y usar Inyección de Dependencias en los controladores de su API Web, puede implementar repositorios simulados que devuelvan datos falsos en lugar de datos de la base de datos. Ese enfoque desacoplado le permite crear y ejecutar pruebas unitarias para verificar sólo la lógica de la aplicación, sin requerir conectividad con la base de datos.

Las conexiones a las bases de datos pueden fallar y, lo que es más importante, ejecutar cientos de pruebas en una base de datos es malo por dos razones. En primer lugar, puede llevar mucho tiempo debido a la gran cantidad de pruebas. En segundo lugar, los registros de la base de datos pueden afectar los resultados de sus pruebas, de modo que no sean consistentes. Las pruebas contra la base de datos no son pruebas unitarias, sino pruebas de integración. Debería ejecutar muchas pruebas unitarias rápidamente, pero menor cantidad de pruebas de integración contra las bases de datos.

En términos de separación de intereses para las pruebas unitarias, su lógica opera en entidades de dominio en la memoria. Se supone que el repositorio las ha entregado. Una vez que su lógica modifica las entidades de dominio, se supone que el repositorio las almacenará correctamente. El punto importante aquí es crear pruebas unitarias contra su modelo y su lógica de dominio. Las raíces de agregación son los límites principales de consistencia en DDD.

La diferencia entre el patrón de repositorio y el viejo patrón de las clases de acceso a datos (clases DAL)

Un objeto de acceso a datos realiza directamente el acceso a los datos y las operaciones de persistencia contra el almacenamiento. Un repositorio marca los datos con las operaciones que desea realizar en la memoria de una unidad de trabajo (como al usar el DbContext en EF), pero estas actualizaciones no se realizarán de inmediato.

Una unidad de trabajo se refiere a una transacción única que incluye múltiples operaciones de inserción, actualización o eliminación. En términos simples, significa que para una acción específica del usuario (por ejemplo, el registro en un sitio web), todas las transacciones de inserción, actualización y eliminación se manejan en una sola transacción. Esto es más eficiente que manejar múltiples transacciones de bases de datos.

Estas operaciones de persistencia múltiple se realizarán más adelante en una sola acción, cuando el código de la capa de aplicación lo ordene. La decisión sobre la aplicación de los cambios en memoria en el almacenamiento real de la base de datos generalmente se basa en el patrón de Unidad de Trabajo. En EF, el patrón de unidad de trabajo se implementa como el DBContext.

En muchos casos, este patrón o forma de aplicar operaciones contra el almacenamiento puede aumentar el rendimiento de la aplicación y reducir la posibilidad de inconsistencias. Además, reduce el bloqueo de transacciones en las tablas de la base de datos, porque todas las operaciones previstas se guardan como parte de una transacción. Esto es más eficiente en comparación con la ejecución de muchas operaciones aisladas en la base de datos. Por lo tanto, el ORM seleccionado podrá optimizar la ejecución contra la base de datos agrupando varias acciones de actualización dentro de la misma transacción, a diferencia realizar muchas de transacciones pequeñas y separadas.

Los repositorios no son obligatorios

Los repositorios personalizados son útiles por las razones citadas anteriormente y ese es el enfoque para el microservicio de pedido en eShopOnContainers. Sin embargo, no es un patrón esencial para implementar en un diseño DDD o incluso en desarrollo general en .NET.

Por ejemplo, Jimmy Bogard, al hacer comentarios directos para esta guía, dijo lo siguiente:

Esta será probablemente mi opinión más larga. Realmente no soy fan de los repositorios, principalmente porque ocultan los detalles importantes del mecanismo de persistencia subyacente. Es por eso que también voy por la vía de comandos con MediatR. Puedo usar todo el poder de la capa de persistencia e insertar todo ese comportamiento de dominio en mis raíces de agregación. Normalmente no quiero usar maquetas de mis repositorios, aún necesito tener esa prueba de integración con la realidad. Ir con CQRS significaba que ya no necesitábamos repositorios.

Encontramos que los repositorios son útiles, pero reconocemos que no son críticos para su diseño DDD, de la forma como lo son el patrón Agregado y el modelo de dominio expresivo. Por lo tanto, use el patrón Repositorio o no, como le parezca mejor.

Recursos adicionales

El patrón de repositorio
El patrón Unidad de Trabajo

El patrón de Especificación

El patrón de Especificación (su nombre completo sería Patrón de especificación de consulta) es un patrón de Diseño Orientado por el Dominio, diseñado como el lugar donde se puede poner la definición de una consulta con lógica de ordenación y paginación opcional. El patrón de especificación define una consulta en un objeto. Por ejemplo, para encapsular una consulta paginada que busque algunos productos, puede crear una especificación PagedProduct que tome los parámetros de entrada necesarios (pageNumber, pageSize, filter, etc.). Entonces, cualquier método del repositorio (generalmente una sobrecarga de List()) aceptaría una ISpecification y ejecutaría la consulta esperada basada en esa especificación.

Hay varios beneficios para este enfoque.

La especificación tiene un nombre (a diferencia de sólo un montón de expresiones LINQ) sobre la que se puede discutir.
Se pueden hacer pruebas unitarias de la especificación para garantizar que sea correcta. también se puede reutilizar fácilmente si necesita un comportamiento similar (por ejemplo, en una vista MVC View o una acción API Web, así como en diversos servicios).
También se puede usar una especificación para describir la forma de los datos que se devolverán, de modo que las consultas puedan devolver sólo los datos que solicitaron. Esto elimina la necesidad de lazy loading (ejecutar las consultas SQL en la medida que se van necesitando versus consolidar las consultas para eficiencia y rendimiento) en las aplicaciones web (que generalmente no es una buena idea) y ayuda a evitar que las implementaciones del repositorio se llenen de estos detalles.

Un ejemplo de una interfaz de especificación genérica es el siguiente código de eShopOnWeb.

// https://github.com/dotnet-architecture/eShopOnWeb

public interface ISpecification<T>

{

    Expression<Func<T, bool>> Criteria { get; }

    List<Expression<Func<T, object>>> Includes { get; }

    List<string> IncludeStrings { get; }

}

En las próximas secciones se explica cómo implementar el patrón de Especificación con Entity Framework Core 2.0 y cómo usarlo desde cualquier clase de Repositorio.

Nota importante: el patrón de especificación es un patrón antiguo que se puede implementar de muchas maneras diferentes, como en los recursos adicionales a continuación. Como patrón/idea, es bueno conocer los enfoques anteriores, pero tenga en cuenta que las implementaciones más antiguas no aprovechan las capacidades modernas del lenguaje como Linq y expresiones.

Recursos adicionales

http://deviq.com/specification-pattern/

https://www.martinfowler.com/apsupp/spec.pdf

Implementando la persistencia en la capa de infraestructura con Entity Framework Core

Cuando utiliza bases de datos relacionales como SQL Server, Oracle o PostgreSQL, un enfoque recomendado es implementar la capa de persistencia basada en Entity Framework (EF). EF soporta LINQ y proporciona objetos fuertemente tipados para su modelo, así como una persistencia simplificada en su base de datos.

Entity Framework tiene una larga historia como parte de .NET Framework. Cuando usa .NET Core, también debería usar Entity Framework Core, que se ejecuta tanto en Windows como en Linux de la misma forma. EF Core se reescribió por completo desde cero, no es una migración del Entity Framework tradicional, se implementó con una huella de memoria mucho más pequeña y mejoras importantes en el rendimiento.

Introducción a Entity Framework Core

Entity Framework (EF) Core es una versión ligera, extensible y multiplataforma de la popular tecnología de acceso a datos de Entity Framework. Fue presentado con .NET Core a mediados de 2016.

Ya que existe una introducción a EF Core disponible en la documentación de Microsoft, aquí simplemente proporcionamos enlaces a esa información.

Recursos adicionales

La infraestructura en Entity Framework Core desde la perspectiva DDD

Desde un punto de vista DDD, una capacidad importante de EF es la capacidad de usar entidades de dominio POCO, también conocidas, en la terminología EF, como entidades POCO code-first. Si usa entidades de dominio POCO, las clases de su modelo de dominio son ignorantes de persistencia, por lo que siguen los principios de Ignorancia de la Persistencia e Ignorancia de la Infraestructura.

Según los patrones DDD, debe encapsular el comportamiento y las reglas del dominio dentro de la propia clase de entidad, de modo que pueda controlar invariantes, validaciones y reglas al acceder a cualquier colección. Por lo tanto, no es una buena práctica en DDD permitir el acceso público a colecciones de entidades secundarias o value objects. En su lugar, debe exponer métodos que controlan cómo y cuándo se pueden actualizar sus campos y colecciones y qué comportamiento y acciones deberían ocurrir cuando pase eso.

A partir de EF Core 1.1, se pueden tener campos simples en las entidades, en vez de propiedades públicas, para satisfacer esos requisitos de DDD. Si no desea que un campo de una entidad sea accesible externamente, puede simplemente crear el atributo o campo en lugar de una propiedad. También puede usar setters privados.

De forma similar, ahora puede tener acceso de sólo lectura a las colecciones, al usar una propiedad pública tipeada como IReadOnlyCollection<T>, que está respaldada por un campo privado para la colección (como una Lista<T>) en su entidad, que utiliza EF para la persistencia. Las versiones anteriores de Entity Framework requerían propiedades que soportaran ICollection<T>, para manejar colecciones, lo que significaba que cualquier desarrollador que utilizara la clase de la entidad padre podía agregar o eliminar elementos a través de sus colecciones. Esa posibilidad es contraria a los patrones recomendados en DDD.

Ahora puede usar una colección privada mientras expone un objeto IReadOnlyCollection<T> de sólo lectura, como se muestra en el siguiente ejemplo de código:

public class Order : Entity

{

// Using private fields, allowed since EF Core 1.1

private DateTime _orderDate;

// Other fields ...

 

private readonly List<OrderItem> _orderItems;

public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

protected Order() { }

public Order(int buyerId, int paymentMethodId, Address address)

{

// Initializations ...

}

 

public void AddOrderItem(int productId, string productName,

decimal unitPrice, decimal discount,

string pictureUrl, int units = 1)

{

// Validation logic...

 

var orderItem = new OrderItem(productId, productName,

unitPrice, discount,

pictureUrl, units);

_orderItems.Add(orderItem);

}

}

Tenga en cuenta que sólo se puede acceder a la propiedad OrderItems como de sólo lectura utilizando IReadOnlyCollection<OrderItem>. Como este tipo es de solo lectura, está protegido contra actualizaciones externas regulares.

EF Core proporciona una forma de mapear el modelo de dominio a la base de datos física sin "contaminar" el modelo de dominio. Es un código .NET POCO puro, porque la acción de mapeo se implementa en la capa de persistencia. En esa acción de mapeo, necesita configurar la asignación de campos a la base de datos. En el siguiente ejemplo de un método OnModelCreating, el código resaltado le dice a EF Core que acceda a la propiedad OrderItems a través de su campo.

// At OrderingContext.cs from eShopOnContainers

protected override void OnModelCreating(ModelBuilder modelBuilder)

{

// ...

modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());

// Other entities’ configuration ...

// ...

}

 

 

// At OrderEntityTypeConfiguration.cs from eShopOnContainers

class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>

{

public void Configure(EntityTypeBuilder<Order> orderConfiguration)

{

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

// Other configuration

// ...

 

var navigation =

orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));

 

//EF access the OrderItem collection property through its backing field

navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

 

// Other configuration

// ...

}

}

Cuando utiliza campos en lugar de propiedades, la entidad OrderItem se conserva igual que si tuviera una propiedad List<OrderItem>. Sin embargo, expone un único acceso, el método AddOrderItem, para agregar nuevos elementos al pedido. Como resultado, el comportamiento y los datos se unen y serán consistentes a través de cualquier código de aplicación que use el modelo de dominio.

Implementando repositorios personalizados con Entity Framework Core

A nivel de implementación, un repositorio es simplemente una clase con código de persistencia de datos, coordinada por una unidad de trabajo (DBContext en EF Core) cuando se realizan actualizaciones, como se muestra en la siguiente clase:

// using statements...

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories

{

public class BuyerRepository : IBuyerRepository

{

private readonly OrderingContext _context;

public IUnitOfWork UnitOfWork

{

get

{

return _context;

}

}

 

public BuyerRepository(OrderingContext context)

{

_context = context ?? throw new ArgumentNullException(nameof(context));

}

 

public Buyer Add(Buyer buyer)

{

return _context.Buyers.Add(buyer).Entity;

}

 

public async Task<Buyer> FindAsync(string BuyerIdentityGuid)

{

var buyer = await _context.Buyers

.Include(b => b.Payments)

.Where(b => b.FullName == BuyerIdentityGuid)

.SingleOrDefaultAsync();

 

return buyer;

}

}

}

Tenga en cuenta que la interfaz IBuyerRepository proviene de la capa del modelo de dominio como un contrato. Sin embargo, la implementación del repositorio se realiza en la capa de persistencia e infraestructura.

El DbContext viene a través del constructor a través de Inyección de Dependencias. Se comparte entre varios repositorios dentro del mismo ámbito de la petición HTTP, gracias a su duración por defecto (ServiceLifetime.Scoped) en el contenedor IoC (que también se puede establecer explícitamente con services.AddDbContext<>).

Métodos a implementar en un repositorio (actualizaciones o transacciones versus consultas)

Dentro de cada repositorio, debe colocar los métodos de persistencia que actualicen el estado de las entidades contenidas en el agregado. Recuerde que existe una relación uno-a-uno entre un agregado y su repositorio relacionado. Tenga en cuenta que un objeto de la raíz de agregación podría tener entidades hijo dentro de su grafo en EF. Por ejemplo, un comprador puede tener varios métodos de pago como entidades secundarias relacionadas.

Como el enfoque para el microservicio de pedidos en eShopOnContainers también se basa en CQS/CQRS, la mayoría de las consultas no se implementan en repositorios personalizados. Los desarrolladores tienen libertad de crear las consultas, incluyendo joins, que necesitan para la capa de presentación, sin las restricciones impuestas por los agregados, los repositorios y DDD en general. La mayoría de los repositorios personalizados sugeridos por esta guía tienen varios métodos de actualización o transaccionales, pero sólo los métodos de consulta necesarios para actualizar los datos. Por ejemplo, el repositorio BuyerRepository implementa un método FindAsync, porque la aplicación necesita saber si existe un comprador en particular antes de crear un nuevo comprador relacionado con el pedido.

Sin embargo, los métodos de consulta reales para enviar datos a la capa de presentación o aplicaciones de cliente se implementan, como se mencionó, en las consultas de CQRS basadas en consultas flexibles que usan Dapper.

Usando un repositorio personalizado versus usar el DbContext de EF directamente

La clase DbContext de Entity Framework se basa en los patrones de Unidad de trabajo y Repositorio, y se puede usar directamente desde su código, por ejemplo, desde un controlador ASP.NET Core MVC. Esa es la forma en que puede crear el código más simple, como en el microservicio del catálogo CRUD en eShopOnContainers. En los casos en que desee el código más simple posible, es posible que desee utilizar directamente la clase DbContext, como lo hacen muchos desarrolladores.

Sin embargo, la implementación de repositorios personalizados proporciona varios beneficios al implementar microservicios o aplicaciones más complejos. Los patrones de Unidad de trabajo y Repositorio están destinados a encapsular la capa de persistencia de la infraestructura, por lo que está desacoplada de las capas del modelo de aplicación y dominio. La implementación de estos patrones puede facilitar el uso de maquetas de repositorios para las pruebas unitarias del sistema.

En la Figura 7-18 puede ver las diferencias entre no usar repositorios (usar directamente el DbContext de EF) versus usar repositorios que hacen que sea más fácil crear maquetas de esos repositorios.

Image

Figura 7-18. Usando repositorios personalizados versus un DbContext sencillo

Hay múltiples alternativas para hacer sustitutos para las pruebas. Se podrían hacer los repositorios o de toda una unidad de trabajo. Por lo general es suficiente hacerlas de los repositorios, ya que la complejidad para abstraer y simular una unidad de trabajo completa generalmente no es necesaria.

Más adelante, cuando nos enfoquemos en la capa de aplicación, veremos cómo funciona la Inyección de Dependencias en ASP.NET Core y cómo se implementa al usar repositorios.

En resumen, usar repositorios personalizados le facilita realizar pruebas unitarias, que no se ven afectadas por el estado de la capa de datos. Si ejecutara pruebas que también accedan a la base de datos real a través de Entity Framework, entonces no serían pruebas unitarias sino pruebas de integración, que son mucho más lentas.

Si estuviese utilizando el DbContext directamente tendría que manejar un substituto o ejecutar pruebas mediante un servidor SQL en memoria, con datos predecibles para las pruebas. Pero manejar los substitutos del DbContext o controlar los datos falsos para las pruebas, requiere más trabajo que manejar sustitutos a nivel del repositorio. Por supuesto, siempre se pueden probar los controladores MVC.

El ciclo de vida del DbContext y la IUnitOfWork en el contenedor de IoC

El objeto DbContext (expuesto como un objeto IUnitOfWork) debería compartirse entre varios repositorios dentro del mismo ámbito de la petición HTTP. Por ejemplo, cuando la operación que se está ejecutando debe manejar múltiples agregados o simplemente porque está utilizando múltiples instancias de repositorio. También es importante mencionar que la interfaz IUnitOfWork forma parte de su capa de dominio, no es un tipo EF Core.

Para hacer eso, la instancia del objeto DbContext debe tener la duración del servicio establecida como ServiceLifetime.Scoped. Esta es la duración predeterminada cuando se registra un DbContext con services.AddDbContext en su contenedor IoC, desde el método ConfigureServices del fichero Startup.cs en su proyecto ASP.NET Core Web API. El siguiente código ilustra esto.

public IServiceProvider ConfigureServices(IServiceCollection services)

{

// Add framework services.

services.AddMvc(options =>

{

options.Filters.Add(typeof(HttpGlobalExceptionFilter));

}).AddControllersAsServices();

services.AddEntityFrameworkSqlServer()

.AddDbContext<OrderingContext>(options =>

{

options.UseSqlServer(Configuration["ConnectionString"],

sqlOptions => sqlOptions.MigrationsAssembly(typeof(Startup).

GetTypeInfo().

Assembly.GetName().Name));

},

ServiceLifetime.Scoped // Note that Scoped is the default choice

// in AddDbContext. It is shown here only for

// pedagogic purposes.

);

}

La duración de las instancias de DbContext no se debe configurar como ServiceLifetime.Transient o ServiceLifetime.Singleton.

El ciclo de vida de la instancia del repositorio en el contenedor IoC

De manera similar, el ciclo de vida del repositorio generalmente se debe establecer como por ámbito (InstancePerLifetimeScope en Autofac). También podría ser transitorio (InstancePerDependency en Autofac), pero su servicio será más eficiente en lo que respecta a la memoria cuando utilice la duración por ámbito.

// Registering a Repository in Autofac IoC container

builder.RegisterType<OrderRepository>()

.As<IOrderRepository>()

.InstancePerLifetimeScope();

 

Tenga en cuenta que el uso del tiempo de vida de singleton para el repositorio podría causarle serios problemas de concurrencia cuando su DbContext se establece por ámbito (InstancePerLifetimeScope), que es el tiempo de vida por defecto para un DBContext.

Recursos adicionales

Mapeo de tablas

El mapeo de una tabla identifica los datos de la tabla a consultar y guardar en la base de datos. Anteriormente vimos cómo se pueden usar las entidades de dominio (por ejemplo, un dominio de productos o pedidos) para generar un esquema de base de datos relacionado. EF está fuertemente diseñado en torno al concepto de convenciones. Las convenciones abordan preguntas como "¿Cuál será el nombre de una tabla?" O "¿Qué propiedad es la clave principal?" Las convenciones generalmente se basan en nombres frecuentes, por ejemplo, es típico que la clave principal sea una propiedad que termina Id.

Por convención, cada entidad se configurará para mapearse a una tabla con el mismo nombre que la propiedad DbSet<TEntity> que expone a la entidad en el contexto derivado. Si no se proporciona un valor DbSet<TEntity> para la entidad dada, se utiliza el nombre de la clase.

Anotaciones de datos versus Fluent API

Existen muchas convenciones adicionales de EF Core y la mayoría de ellas se pueden cambiar mediante el uso de anotaciones de datos o de la Fluent API, implementadas dentro del método OnModelCreating.

Las anotaciones de datos se deben usar en las propias clases del modelo de entidad, lo cual es una forma más intrusiva desde un punto de vista DDD. Esto se debe a que está contaminando su modelo con anotaciones de datos relacionadas con la base de datos de infraestructura. Por otro lado, la Fluent API es una forma conveniente de cambiar la mayoría de las convenciones y mapeos dentro de su capa de infraestructura de persistencia de datos, por lo que el modelo de entidad estará limpio y desacoplado de la infraestructura de persistencia.

Fluent API y el método OnModelCreating

Como se mencionó, para cambiar convenciones y mapeos, puede usar el método OnModelCreating en la clase DbContext.

El microservicio de pedidos en eShopOnContainers implementa mapeo y configuración explícitos, cuando es necesario, como se muestra en el siguiente código.

// At OrderingContext.cs from eShopOnContainers

protected override void OnModelCreating(ModelBuilder modelBuilder)

{

// Other entities’ configuration ...

modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());

}

 

// At OrderEntityTypeConfiguration.cs from eShopOnContainers

class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>

{

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 supported since EF Core 2.0

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

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

orderConfiguration.Property<int?>("BuyerId").IsRequired(false);

orderConfiguration.Property<int>("OrderStatusId").IsRequired();

orderConfiguration.Property<int?>("PaymentMethodId").IsRequired(false);

orderConfiguration.Property<string>("Description").IsRequired(false);

 

var navigation =

orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));

navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

 

orderConfiguration.HasOne<PaymentMethod>()

.WithMany()

.HasForeignKey("PaymentMethodId")

.IsRequired(false)

.OnDelete(DeleteBehavior.Restrict);

orderConfiguration.HasOne<Buyer>()

.WithMany()

.IsRequired(false)

.HasForeignKey("BuyerId");

orderConfiguration.HasOne(o => o.OrderStatus)

.WithMany()

.HasForeignKey("OrderStatusId");

}

}

Puede establecer todas las asignaciones de la Fluent API dentro del mismo método OnModelCreating, pero es aconsejable dividir ese código y tener múltiples clases de configuración, una por entidad, como se muestra en el ejemplo. Especialmente para modelos particularmente grandes, es aconsejable tener clases de configuración separadas para cada tipo de entidad.

El código en el ejemplo muestra algunas declaraciones y mapeos explícitos. Sin embargo, las convenciones de EF Core realizan muchas de esas asignaciones automáticamente, por lo que el código real que necesitaría en su caso podría ser más pequeño.

El algoritmo Hi/Lo en EF Core

Un aspecto interesante del código en el ejemplo anterior es que utiliza el algoritmo Hi/Lo como la estrategia de generación de claves.

El algoritmo Hi/Lo es útil cuando necesita claves únicas. Como resumen, el algoritmo Hi/Lo asigna identificadores únicos a las filas de la tabla, pero no depende de guardar la fila en la base de datos de inmediato. Esto le permite comenzar a utilizar los identificadores de inmediato, como sucede con las ID de bases de datos secuenciales regulares.

El algoritmo Hi/Lo describe un mecanismo para generar identificaciones seguras en el lado del cliente, en lugar de en la base de datos. Seguro en este contexto significa sin colisiones. Este algoritmo es interesante por estas razones:

EF Core soporta HiLo con el método ForSqlServerUseSequenceHiLo, como se muestra en el ejemplo anterior.

Mapeando campos en vez de propiedades

Con esta característica, disponible a partir de EF Core 1.1, puede asignar directamente columnas a campos. Es posible no usar propiedades en la clase de entidad y sólo asignar columnas de una tabla a campos. Un uso común para eso sería campos privados para cualquier estado interno al que no se necesite acceder desde fuera de la entidad.

Puede hacerlo con campos individuales o también con colecciones, como un campo List<>. Este punto se mencionó anteriormente cuando discutimos el modelado de las clases de modelo de dominio, pero aquí se puede ver cómo se realiza esa asignación con la configuración de PropertyAccessMode.Field resaltada en el código anterior.

Usando propiedades shadow en EF Core, escondidas a nivel de infraestructura

Las propiedades shadow en EF Core son propiedades que no existen en el modelo de clase de entidad. Los valores y estados de estas propiedades se mantienen puramente en la clase ChangeTracker en el nivel de infraestructura.

Implementando el patrón de Especificación

Como se presentó anteriormente en la sección de diseño, el patrón de Especificación (su nombre completo sería Patrón de especificación de consulta) es un patrón de DDD, diseñado como el lugar donde puede poner la definición de una consulta con ordenamiento opcional y lógica de paginación.

El patrón de especificación define una consulta en un objeto. Por ejemplo, para encapsular una consulta paginada que busca algunos productos, puede crear una especificación de PagedProduct que tome los parámetros de entrada necesarios (pageNumber, pageSize, filter, etc.). Luego, dentro de cualquier método de repositorio (generalmente una sobrecarga de List()), aceptaría una ISpecification y ejecutaría la consulta esperada basada en esa especificación.

Un ejemplo de una interfaz de especificación genérica es el siguiente código de eShopOnWeb.

// GENERIC SPECIFICATION INTERFACE

// https://github.com/dotnet-architecture/eShopOnWeb

 

public interface ISpecification<T>

{

    Expression<Func<T, bool>> Criteria { get; }

    List<Expression<Func<T, object>>> Includes { get; }

    List<string> IncludeStrings { get; }

}

Entonces, la implementación de una clase base de especificación genérica es la siguiente:

// GENERIC SPECIFICATION IMPLEMENTATION (BASE CLASS)

// https://github.com/dotnet-architecture/eShopOnWeb

public abstract class BaseSpecification<T> : ISpecification<T>

{

public BaseSpecification(Expression<Func<T, bool>> criteria)

{

Criteria = criteria;

}

public Expression<Func<T, bool>> Criteria { get; }

 

public List<Expression<Func<T, object>>> Includes { get; } =

new List<Expression<Func<T, object>>>();

 

public List<string> IncludeStrings { get; } = new List<string>();

protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)

{

Includes.Add(includeExpression);

}

// string-based includes allow for including children of children

// e.g. Basket.Items.Product

protected virtual void AddInclude(string includeString)

{

IncludeStrings.Add(includeString);

}

}

La siguiente especificación carga una entidad de carrito de compras individual, ya sea con la identificación del carrito o la identificación del comprador al que pertenece el carrito y se cargará de una vez (eager loading) la colección de artículos del carrito.

// SAMPLE QUERY SPECIFICATION IMPLEMENTATION

 

public class BasketWithItemsSpecification : BaseSpecification<Basket>

{

public BasketWithItemsSpecification(int basketId)

: base(b => b.Id == basketId)

{

AddInclude(b => b.Items);

}

public BasketWithItemsSpecification(string buyerId)

: base(b => b.BuyerId == buyerId)

{

AddInclude(b => b.Items);

}

}

Por último, puede ver a continuación cómo un repositorio de EF genérico puede usar dicha especificación para filtrar y cargar de una vez los datos relacionados con una entidad dada tipo T.

// GENERIC EF REPOSITORY WITH SPECIFICATION

// https://github.com/dotnet-architecture/eShopOnWeb

 

public IEnumerable<T> List(ISpecification<T> spec)

{

// fetch a Queryable that includes all expression-based includes

var queryableResultWithIncludes = spec.Includes

.Aggregate(_dbContext.Set<T>().AsQueryable(),

(current, include) => current.Include(include));

// modify the IQueryable to include any string-based include statements

var secondaryResult = spec.IncludeStrings

.Aggregate(queryableResultWithIncludes,

(current, include) => current.Include(include));

// return the result of the query using the specification's criteria expression

return secondaryResult

.Where(spec.Criteria)

.AsEnumerable();

}

 

Además de encapsular la lógica de filtrado, la especificación puede especificar la forma de los datos que se devolverán, incluidas las propiedades que se rellenarán.

Aunque no se recomienda devolver IQueryable desde un repositorio, está perfectamente bien usarlos dentro del repositorio para crear un conjunto de resultados. Puede ver este enfoque, usado en el método de Lista anterior, que utiliza expresiones IQueryable intermedias para crear la lista de inclusiones de la consulta antes de ejecutarla con los criterios de la especificación en la última línea.

Recursos adicionales

Usando bases de datos NoSQL como infraestructura de persistencia

Cuando utiliza bases de datos NoSQL para la infraestructura de datos, normalmente no utiliza un ORM como Entity Framework Core. En su lugar, utiliza la API proporcionada por el motor NoSQL, como Azure Cosmos DB, MongoDB, Cassandra, RavenDB, CouchDB o Azure Storage Tables.

Sin embargo, cuando utiliza una base de datos NoSQL, especialmente una base de datos orientada a documentos como Azure Cosmos DB, CouchDB o RavenDB, la forma en que diseña su modelo con agregados DDD es parcialmente similar a como puede hacerlo en EF Core, con respecto a la identificación de raíces de agregación, entidades hijo y value objects. Pero, en última instancia, la selección de la base de datos tendrá un impacto en su diseño.

Cuando utiliza una base de datos orientada a documentos, se implementa un agregado como un documento único, serializado en JSON u otro formato. Sin embargo, el uso de la base de datos es transparente desde el punto de vista de un código de modelo de dominio. Al usar una base de datos NoSQL, todavía está usando clases de entidad y clases raíz de agregados, pero con más flexibilidad que cuando usa EF Core porque la persistencia no es relacional.

La diferencia está en cómo se persiste ese modelo. Si implementó su modelo de dominio basado en clases de entidad POCO, ignorantes de infraestructura de persistencia, podría parecer que se debería poder pasar a una infraestructura de persistencia diferente, incluso de relacional a NoSQL. Sin embargo, ese no debería ser su objetivo. Siempre hay restricciones en las diferentes bases de datos que le harán retroceder, por lo que no podrá tener el mismo modelo para bases de datos relacionales o NoSQL. Cambiar los modelos de persistencia no sería trivial, porque las transacciones y las operaciones de persistencia serán muy diferentes.

Por ejemplo, en una base de datos orientada a documentos, está bien que una raíz de agregación tenga múltiples colecciones de entidades secundarias. En una base de datos relacional, consultar varias de colecciones de este tipo es muy poco eficiente, porque se obtiene una sentencia SQL UNION ALL de EF. Tener el mismo modelo de dominio para bases de datos relacionales o bases de datos NoSQL no es simple y no debe intentarlo. Realmente debe diseñar su modelo con una comprensión de cómo se usarán los datos en cada base de datos en particular.

Un beneficio al usar bases de datos NoSQL es que las entidades están más desnormalizadas, por lo que no se establece una asignación de tablas. Su modelo de dominio puede ser más flexible que cuando usa una base de datos relacional.

Cuando diseñe su modelo de dominio basado en agregados, moverse a NoSQL y bases de datos orientadas a documentos podría ser incluso más fácil que usar una base de datos relacional, porque los agregados que diseña son similares a los documentos serializados en una base de datos orientada a documentos. Luego puede incluir en esas "bolsas" toda la información que pueda necesitar para ese agregado.

Por ejemplo, el siguiente código JSON es un ejemplo de un agregado de pedido, cuando se usa una base de datos orientada a documentos. Es similar al agregado de pedido que implementamos en el ejemplo de eShopOnContainers, pero sin usar EF Core.

{

"id": "2017001",

"orderDate": "2/25/2017",

"buyerId": "1234567",

"address": [

{

"street": "100 One Microsoft Way",

"city": "Redmond",

"state": "WA",

"zip": "98052",

"country": "U.S."

}

],

"orderItems": [

{"id": 20170011, "productId": "123456", "productName": ".NET T-Shirt",

"unitPrice": 25, "units": 2, "discount": 0},

{"id": 20170012, "productId": "123457", "productName": ".NET Mug",

"unitPrice": 15, "units": 1, "discount": 0}

]

}