Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Sugerencia
Este contenido es un extracto del libro electrónico, ".NET Microservices Architecture for Containerized .NET Applications" (Arquitectura de microservicios de .NET para aplicaciones de .NET contenedorizadas), disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.
Como se explicó 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 seguimiento de identidad, como los objetos de valor.
Un objeto value puede hacer referencia a otras entidades. Por ejemplo, en una aplicación que genera una ruta que describe cómo obtener 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 hacer referencia a entidades como City, Road, etc.
En la figura 7-13 se muestra el objeto de valor Address dentro del agregado Order.
Figura 7-13. Objeto de valor Dirección en el agregado Pedido
Como se muestra en la figura 7-13, una entidad normalmente se compone de varios atributos. Por ejemplo, la Order
entidad se puede modelar como una entidad con una identidad y compuesta internamente de un conjunto de atributos como OrderId, OrderDate, OrderItems, etc. Pero la dirección, que es simplemente un valor complejo compuesto por país o región, calle, ciudad, etc., y no tiene identidad en este dominio, debe modelarse y tratarse como un objeto de valor.
Características importantes de los objetos de valor
Hay dos características principales para los objetos de valor:
No tienen identidad.
Son inmutables.
Ya se ha analizado la primera característica. La inmutabilidad es un requisito importante. Los valores de un objeto de valor deben ser inmutables una vez creado el objeto. Por lo tanto, cuando se construye el objeto, debe proporcionar los valores necesarios, pero no debe permitirles cambiar durante la vigencia del objeto.
Los objetos value permiten realizar ciertos trucos para el rendimiento, gracias a su naturaleza inmutable. Esto es especialmente cierto en los sistemas en los que puede haber miles de instancias de objeto de valor, muchas de las cuales tienen los mismos valores. Su naturaleza inmutable les permite reutilizarse; pueden ser objetos intercambiables, ya que sus valores son los mismos y no tienen identidad. Este tipo de optimización a veces puede marcar la diferencia entre el software que se ejecuta lentamente y el software con un buen rendimiento. Por supuesto, todos estos casos dependen del entorno de aplicación y del contexto de implementación.
Implementación de objetos de valor en C#
En términos de implementación, puede tener una clase base de objeto de valor que tenga métodos de utilidad básicos como la igualdad en función de la comparación entre todos los atributos (ya que un objeto de valor no debe basarse en la identidad) y otras características fundamentales. En el ejemplo siguiente se muestra una clase base de objeto de valor utilizada en el microservicio de ordenación 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, right) || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// Other utility methods
}
ValueObject
es un tipo abstract class
, pero en este ejemplo, no sobrecarga los operadores ==
y !=
. Puede optar por hacerlo, haciendo que las comparaciones se deleguen en la invalidación Equals
. Por ejemplo, considere las siguientes sobrecargas de operador en el tipo ValueObject
:
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
Puede usar esta clase al implementar su objeto de valor real, como el objeto de valor Address
que se muestra en el ejemplo siguiente:
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() { }
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> GetEqualityComponents()
{
// 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;
}
}
Esta implementación de objeto de valor de Address
no tiene identidad y, por tanto, no se define ningún campo de identificador para él, ya sea en la Address
definición de clase o en la definición de clase ValueObject
.
No era posible tener ningún campo de identificador en una clase que Entity Framework (EF) usara hasta EF Core 2.0, lo que ayuda en gran medida a implementar objetos de mejor valor sin identificador. Es precisamente la explicación de la sección siguiente.
Podría decirse que los objetos de valor, que son inmutables, deben ser de solo lectura (es decir, tener propiedades get-only) y eso es cierto. Sin embargo, los objetos de valor normalmente se serializan y deserializan para pasar por las colas de mensajes y, al ser de solo lectura, se impide que el deserializador asigne valores, por lo que simplemente los deja como private set
, lo que es lo suficientemente de solo lectura como para ser práctico.
Semántica de comparación de objetos de valor
Se pueden comparar dos instancias del Address
tipo mediante todos los métodos siguientes:
var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True
Cuando todos los valores son iguales, las comparaciones se evalúan correctamente como true
. Si no eligió sobrecargar los operadores ==
y !=
, entonces la última comparación de one == two
se evaluaría como false
. Para obtener más información, vea Sobrecarga de los operadores de igualdad de ValueObject.
Cómo conservar objetos de valor en la base de datos con EF Core 2.0 y versiones posteriores
Acaba de ver cómo definir un objeto de valor en el modelo de dominio. Pero ¿cómo puede conservarlo en la base de datos mediante Entity Framework Core, dado que suele tener como destino las entidades con identidad?
Contexto histórico y los enfoques anteriores utilizando EF Core 1.1
Como fondo, una limitación al usar EF Core 1.0 y 1.1 era que no podía usar tipos complejos tal como se define en EF 6.x en .NET Framework tradicional. Por lo tanto, si usa EF Core 1.0 o 1.1, debe almacenar el objeto de valor como una entidad ef con un campo id. A continuación, por lo que parecía un objeto de valor sin identidad, podría ocultar su identificador para aclarar que la identidad de un objeto de valor no es importante en el modelo de dominio. Ese identificador se podía ocultar usándolo como propiedad reemplazada. Dado que esa configuración para ocultar el identificador en el modelo se establece en el nivel de infraestructura de EF, sería bastante transparente para tu modelo de dominio.
En la versión inicial de eShopOnContainers (.NET Core 1.1), el identificador oculto necesario para la infraestructura de EF Core se implementó de la siguiente manera en el nivel dbContext, mediante la API de Fluent en el proyecto de infraestructura. Por lo tanto, el identificador se ha ocultado desde el punto de vista del modelo de dominio, pero sigue 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 realizó como una entidad regular en una tabla diferente.
Con EF Core 2.0 y versiones posteriores, hay nuevas y mejores formas de conservar objetos de valor.
Conservar objetos de valor como tipos de entidad propiedad en EF Core 2.0 y versiones posteriores
Incluso con algunas brechas entre el patrón de objetos de valor canónico en DDD y el tipo de entidad propiedad de EF Core, actualmente es la mejor manera de conservar objetos de valor con EF Core 2.0 y versiones posteriores. Puede ver las limitaciones al final de esta sección.
La característica de tipo de entidad propiedad se agregó a EF Core desde la versión 2.0.
Un tipo de entidad propiedad permite asignar tipos que no tienen su propia identidad definida explícitamente en el modelo de dominio y se usan como propiedades, como un objeto de valor, dentro de cualquiera de las entidades. Un tipo de entidad propiedad comparte el mismo tipo CLR con otro tipo de entidad (es decir, es simplemente una clase normal). La entidad que contiene la navegación definitoria es la entidad propietaria. Al consultar al propietario, los tipos de propiedad se incluyen de forma predeterminada.
Si se examina el modelo de dominio, parece que los tipos de propiedad no tienen ninguna identidad pero, en el fondo, la tienen, aunque la propiedad de navegación del propietario forma parte de esta identidad.
La identidad de las instancias de los tipos de propiedad no es totalmente suya. Consta de tres componentes:
Identidad del propietario
La propiedad de navegación que los señala
En el caso de las colecciones de tipos de propiedad, un componente independiente (compatible con EF Core 2.2 y versiones posteriores).
Por ejemplo, en el modelo de dominio Ordering de eShopOnContainers, como parte de la entidad Order, el objeto Address value se implementa como un tipo de entidad propiedad dentro de la entidad propietaria, que es la entidad Order.
Address
es un tipo sin ninguna propiedad de identidad definida en el modelo de dominio. Se usa como propiedad del tipo Order para especificar la dirección de envío de un pedido en concreto.
Por convención, se crea una clave principal paralela para el tipo de propiedad y se asignará a la misma tabla que el propietario mediante la división de tabla. Esto permite usar tipos de propiedad de forma similar a cómo se usan los tipos complejos en EF6 en .NET Framework tradicional.
Es importante tener en cuenta que los tipos de propiedad nunca se detectan por convención en EF Core, por lo que debe declararlos explícitamente.
En eShopOnContainers, en el archivo OrderingContext.cs, dentro del OnModelCreating()
método , se aplican varias configuraciones de infraestructura. Uno de ellos está relacionado 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 código siguiente, la infraestructura de persistencia se define 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, el orderConfiguration.OwnsOne(o => o.Address)
método especifica que la Address
propiedad es una entidad propiedad del Order
tipo.
De forma predeterminada, las convenciones de EF Core asignan a las columnas de base de datos el nombre de las propiedades del tipo de entidad propiedad como EntityProperty_OwnedEntityProperty
. Por lo tanto, las propiedades internas de Address
aparecerán en la Orders
tabla con los nombres Address_Street
, Address_City
(y así sucesivamente para State
, Country
y ZipCode
).
Puede adjuntar el método fluent Property().HasColumnName()
para cambiar el nombre de esas columnas. En el caso en que Address
es una propiedad pública, las asignaciones serían similares a lo siguiente:
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 un mapeo fluido. En el ejemplo hipotético siguiente, OrderDetails
posee BillingAddress
y ShippingAddress
, que son ambos Address
tipos. Luego, OrderDetails
es propiedad del tipo 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 Address BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
Detalles adicionales sobre los tipos de entidad de propiedad
Los tipos de propiedad se definen al configurar una propiedad de navegación en un tipo determinado mediante la API fluida OwnsOne.
La definición de un tipo propio en nuestro modelo de metadatos es una composición de: el tipo de propietario, la propiedad de navegación y el tipo CLR del tipo propio.
La identidad (clave) de una instancia de tipo de propiedad en nuestra pila es una composición de la identidad del tipo de propietario y la definición del tipo de propiedad.
Capacidades de entidades propias
Los tipos de propiedad pueden hacer referencia a otras entidades, ya sean de propiedad (tipos de propiedad anidados) o de no propiedad (propiedades de navegación de referencia normal a otras entidades).
Se puede asignar el mismo tipo CLR como otros tipos de propiedad en la misma entidad de propietario mediante propiedades de navegación independientes.
La división de tablas se configura por convención, pero puede dejar de usarla si asigna el tipo de propiedad a otra tabla mediante ToTable.
En los tipos de propiedad se efectúa una carga diligente de forma automática; es decir, no es necesario llamar a
.Include()
en la consulta.Se puede configurar con el atributo
[Owned]
, mediante EF Core 2.1 y versiones posteriores.Puede controlar colecciones de tipos de propiedad (con la versión 2.2 y posteriores).
Limitaciones de las entidades de propiedad
No se puede crear un elemento
DbSet<T>
de un tipo de propiedad (por cuestiones de diseño).No se puede llamar a
ModelBuilder.Entity<T>()
en los tipos de propiedad (actualmente por cuestiones de diseño).No se admiten los tipos de propiedad opcionales (es decir, que aceptan valores NULL) que se asignan con el propietario en la misma tabla (es decir, mediante la división de tablas). Esto se debe a que la asignación se realiza para cada propiedad; no hay un centinela independiente para el valor complejo NULL como un todo.
No hay compatibilidad con la asignación de herencia para los tipos de propiedad, pero se deberían poder asignar dos tipos hoja de las mismas jerarquías de herencia como otros tipos de propiedad. EF Core no razonará sobre el hecho de que forman parte de la misma jerarquía.
Principales diferencias con los tipos complejos de EF6
- La división de tablas es opcional (es decir, opcionalmente se pueden asignar a una tabla independiente y seguir siendo tipos de propiedad).
Recursos adicionales
Martin Fowler. ValueObject pattern (El patrón ValueObject)
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. "Domain-Driven Design: Tackling Complexity in the Heart of Software" (Diseño orientado al dominio: abordar la complejidad en el corazón del software). (Libro; incluye una explicación de los objetos de valor)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Implementación del diseño orientado al dominio. (Libro; incluye una explicación de los objetos de valor)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Tipos de entidad propiedad
https://learn.microsoft.com/ef/core/modeling/owned-entitiesPropiedades de sombra
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesTipos complejos o objetos de valor. Descripción en el repositorio de GitHub de EF Core (pestaña Problemas)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Clase de objeto de valor base en eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Clase de objeto de valor base en CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csClase Address. Clase de objeto de valor de ejemplo en eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs