Implementar objetos de valor
Gorjeta
Este conteúdo é um trecho do eBook, .NET Microservices Architecture for Containerized .NET Applications, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
Como discutido nas seções anteriores sobre entidades e agregados, a identidade é fundamental para as entidades. No entanto, há muitos objetos e itens de dados em um sistema que não exigem um rastreamento de identidade e identidade, como objetos de valor.
Um objeto value pode fazer referência a outras entidades. Por exemplo, em um aplicativo que gera uma rota que descreve como ir de um ponto para outro, essa rota seria um objeto de valor. Seria um instantâneo de pontos de uma rota específica, mas essa rota sugerida não teria uma identidade, mesmo que internamente pudesse se referir a entidades como City, Road, etc.
A Figura 7-13 mostra o objeto Address value dentro da agregação Order.
Figura 7-13. Objeto de valor de endereço dentro da agregação Ordem
Como mostrado na Figura 7-13, uma entidade é geralmente composta de vários atributos. Por exemplo, a Order
entidade pode ser modelada como uma entidade com uma identidade e composta internamente por um conjunto de atributos, como OrderId, OrderDate, OrderItems, etc. Mas o endereço, que é simplesmente um valor complexo composto por país/região, rua, cidade, etc., e não tem identidade neste domínio, deve ser modelado e tratado como um objeto de valor.
Características importantes dos objetos de valor
Há duas características principais para objetos de valor:
Não têm identidade.
São imutáveis.
A primeira característica já foi discutida. A imutabilidade é um requisito importante. Os valores de um objeto value devem ser imutáveis depois que o objeto é criado. Portanto, quando o objeto é construído, você deve fornecer os valores necessários, mas não deve permitir que eles sejam alterados durante o tempo de vida do objeto.
Os objetos de valor permitem que você execute certos truques para o desempenho, graças à sua natureza imutável. Isso é especialmente verdadeiro em sistemas onde pode haver milhares de instâncias de objeto de valor, muitas das quais têm os mesmos valores. A sua natureza imutável permite a sua reutilização; Podem ser objetos intercambiáveis, uma vez que os seus valores são os mesmos e não têm identidade. Este tipo de otimização pode, por vezes, fazer a diferença entre software que funciona lentamente e software com bom desempenho. É claro que todos esses casos dependem do ambiente do aplicativo e do contexto de implantação.
Implementação do objeto Value em C#
Em termos de implementação, você pode ter uma classe base de objeto de valor que tenha métodos de utilidade básicos, como igualdade baseada na comparação entre todos os atributos (uma vez que um objeto de valor não deve ser baseado em identidade) e outras características fundamentais. O exemplo a seguir mostra uma classe base de objeto value usada no microsserviço de pedidos do 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
}
O ValueObject
é um abstract class
tipo, mas neste exemplo, não sobrecarrega os ==
operadores e !=
. Você pode optar por fazê-lo, fazendo comparações delegadas à Equals
substituição. Por exemplo, considere as seguintes sobrecargas de operador para o ValueObject
tipo:
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
Você pode usar essa classe ao implementar seu objeto de valor real, como com o Address
objeto de valor mostrado no exemplo a seguir:
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;
}
}
Essa implementação de objeto de valor não tem identidade e, portanto, nenhum campo ID é definido para ela, seja na definição de classe ou na Address
definição de Address
ValueObject
classe.
Não ter nenhum campo ID em uma classe a ser usada pelo Entity Framework (EF) não era possível até o EF Core 2.0, o que ajuda muito a implementar objetos de melhor valor sem ID. Esta é precisamente a explicação da próxima secção.
Pode-se argumentar que os objetos de valor, sendo imutáveis, devem ser somente leitura (ou seja, ter propriedades get-only), e isso é realmente verdade. No entanto, os objetos de valor geralmente são serializados e desserializados para passar por filas de mensagens, e ser somente leitura impede que o desserializador atribua valores, então você apenas os deixa como private set
, o que é somente leitura o suficiente para ser prático.
Semântica de comparação de objetos de valor
Duas instâncias do Address
tipo podem ser comparadas usando todos os seguintes métodos:
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
Quando todos os valores são os mesmos, as comparações são corretamente avaliadas como true
. Se você não optou por sobrecarregar os ==
operadores e !=
, então a última comparação de one == two
avaliaria como false
. Para obter mais informações, consulte Overload ValueObject equality operators.
Como persistir objetos de valor no banco de dados com o EF Core 2.0 e posterior
Você acabou de ver como definir um objeto de valor em seu modelo de domínio. Mas como você pode realmente mantê-lo no banco de dados usando o Entity Framework Core, já que ele geralmente tem como alvo entidades com identidade?
Abordagens em segundo plano e mais antigas usando o EF Core 1.1
Como plano de fundo, uma limitação ao usar o EF Core 1.0 e 1.1 era que você não podia usar tipos complexos conforme definido no EF 6.x no .NET Framework tradicional. Portanto, se estiver usando o EF Core 1.0 ou 1.1, você precisará armazenar seu objeto de valor como uma entidade EF com um campo ID. Então, para que parecesse mais um objeto de valor sem identidade, você poderia ocultar sua ID para deixar claro que a identidade de um objeto de valor não é importante no modelo de domínio. Você pode ocultar essa ID usando a ID como uma propriedade de sombra. Como essa configuração para ocultar o ID no modelo é configurada no nível de infraestrutura do EF, ela seria meio transparente para o seu modelo de domínio.
Na versão inicial do eShopOnContainers (.NET Core 1.1), a ID oculta necessária para a infraestrutura do EF Core foi implementada da seguinte maneira no nível DbContext, usando a API Fluent no projeto de infraestrutura. Portanto, o ID estava oculto do ponto de vista do modelo de domínio, mas ainda presente na infraestrutura.
// 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
}
No entanto, a persistência desse objeto de valor no banco de dados foi executada como uma entidade regular em uma tabela diferente.
Com o EF Core 2.0 e posterior, há novas e melhores maneiras de persistir objetos de valor.
Persistir objetos de valor como tipos de entidade de propriedade no EF Core 2.0 e posterior
Mesmo com algumas lacunas entre o padrão de objeto de valor canônico no DDD e o tipo de entidade de propriedade no EF Core, atualmente é a melhor maneira de persistir objetos de valor com o EF Core 2.0 e posterior. Você pode ver as limitações no final desta seção.
O recurso de tipo de entidade de propriedade foi adicionado ao EF Core desde a versão 2.0.
Um tipo de entidade de propriedade permite mapear tipos que não têm sua própria identidade explicitamente definida no modelo de domínio e são usados como propriedades, como um objeto de valor, dentro de qualquer uma de suas entidades. Um tipo de entidade de propriedade compartilha o mesmo tipo de CLR com outro tipo de entidade (ou seja, é apenas uma classe regular). A entidade que contém a navegação definidora é a entidade proprietária. Ao consultar o proprietário, os tipos de propriedade são incluídos por padrão.
Apenas olhando para o modelo de domínio, um tipo de propriedade parece que não tem nenhuma identidade. No entanto, sob as cobertas, os tipos de propriedade têm a identidade, mas a propriedade de navegação do proprietário faz parte dessa identidade.
A identidade das instâncias de tipos próprios não é completamente própria. É composto por três componentes:
A identidade do proprietário
A propriedade de navegação apontando para eles
No caso de coleções de tipos próprios, um componente independente (suportado no EF Core 2.2 e posterior).
Por exemplo, no modelo de domínio Ordering no eShopOnContainers, como parte da entidade Order, o objeto Address value é implementado como um tipo de entidade de propriedade dentro da entidade owner, que é a entidade Order. Address
é um tipo sem propriedade de identidade definida no modelo de domínio. Ele é usado como uma propriedade do tipo Ordem para especificar o endereço de entrega para um pedido específico.
Por convenção, uma chave primária de sombra é criada para o tipo de propriedade e será mapeada para a mesma tabela que o proprietário usando a divisão de tabela. Isso permite usar tipos de propriedade de forma semelhante à forma como os tipos complexos são usados no EF6 no .NET Framework tradicional.
É importante notar que os tipos de propriedade nunca são descobertos por convenção no EF Core, então você tem que declará-los explicitamente.
No eShopOnContainers, no arquivo OrderingContext.cs, dentro do OnModelCreating()
método, várias configurações de infraestrutura são aplicadas. Um deles está relacionado com a entidade da Ordem.
// 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
}
No código a seguir, a infraestrutura de persistência é definida para a entidade 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...
//...
}
No código anterior, o orderConfiguration.OwnsOne(o => o.Address)
método especifica que a Address
propriedade é uma entidade de propriedade do Order
tipo.
Por padrão, as convenções do EF Core nomeiam as colunas do banco de dados para as propriedades do tipo de entidade de propriedade como EntityProperty_OwnedEntityProperty
. Portanto, as propriedades internas de Address
aparecerão na tabela com os Orders
nomes Address_Street
, Address_City
(e assim por diante para State
, Country
, e ZipCode
).
Você pode acrescentar o Property().HasColumnName()
método fluent para renomear essas colunas. No caso em que Address
é uma propriedade pública, os mapeamentos seriam como os seguintes:
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.Street).HasColumnName("ShippingStreet");
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.City).HasColumnName("ShippingCity");
É possível encadear o OwnsOne
método em um mapeamento fluente. No exemplo hipotético a seguir, OrderDetails
possui BillingAddress
e ShippingAddress
, que são ambos os Address
tipos. Então OrderDetails
é propriedade do Order
tipo.
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; }
}
Detalhes adicionais sobre os tipos de entidades detidas
Os tipos de propriedade são definidos quando você configura uma propriedade de navegação para um tipo específico usando a API fluente OwnsOne.
A definição de um tipo de propriedade em nosso modelo de metadados é composta por: o tipo de proprietário, a propriedade de navegação e o tipo CLR do tipo de propriedade.
A identidade (chave) de uma instância de tipo de propriedade em nossa pilha é uma composição da identidade do tipo de proprietário e a definição do tipo de propriedade.
Capacidades das entidades detidas
Os tipos de propriedade podem fazer referência a outras entidades, de propriedade (tipos de propriedade aninhados) ou não de propriedade (propriedades de navegação de referência regular a outras entidades).
Você pode mapear o mesmo tipo de CLR como diferentes tipos de propriedade na mesma entidade proprietária por meio de propriedades de navegação separadas.
A divisão de tabela é configurada por convenção, mas você pode desativar mapeando o tipo de propriedade para uma tabela diferente usando ToTable.
O carregamento ansioso é realizado automaticamente em tipos próprios, ou seja, não há necessidade de chamar
.Include()
a consulta.Pode ser configurado com atributo
[Owned]
, usando o EF Core 2.1 e posterior.Pode lidar com coleções de tipos de propriedade (usando a versão 2.2 e posterior).
Limitações das entidades detidas
Não é possível criar um
DbSet<T>
de um tipo próprio (por design).Não é possível recorrer
ModelBuilder.Entity<T>()
a tipos próprios (atualmente por design).Não há suporte para tipos de propriedade opcionais (ou seja, anuláveis) que são mapeados com o proprietário na mesma tabela (ou seja, usando a divisão de tabela). Isso ocorre porque o mapeamento é feito para cada propriedade, não há sentinela separada para o valor complexo nulo como um todo.
Não há suporte a mapeamento de herança para tipos de propriedade, mas você deve ser capaz de mapear dois tipos de folha das mesmas hierarquias de herança como tipos de propriedade diferentes. O EF Core não raciocinará sobre o fato de que eles fazem parte da mesma hierarquia.
Principais diferenças com os tipos complexos do EF6
- A divisão de tabelas é opcional, ou seja, eles podem opcionalmente ser mapeados para uma tabela separada e ainda serem tipos de propriedade.
Recursos adicionais
Martin Fowler. Padrão ValueObject
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. Design orientado por domínio: Lidando com a complexidade no coração do software. (Livro; inclui uma discussão de objetos de valor)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Implementação de Design Orientado por Domínio. (Livro; inclui uma discussão de objetos de valor)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Tipos de entidades detidas
https://learn.microsoft.com/ef/core/modeling/owned-entitiesPropriedades de sombra
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesTipos complexos e/ou objetos de valor. Discussão no repositório GitHub do EF Core (guia Problemas)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Classe de objeto de valor base em eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Classe de objeto de valor base em CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csClasse de endereço. Classe de objeto de valor de exemplo em eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs