Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Dica
Esse conteúdo é um trecho do eBook, arquitetura de microsserviços do .NET para aplicativos .NET em contêineres, disponível em do .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
Conforme discutido em seções anteriores sobre entidades e agregações, a identidade é fundamental para entidades. No entanto, existem muitos objetos e itens de dados em um sistema que não exigem uma identidade e um acompanhamento de identidade, como objetos de valor.
Um objeto de valor pode referenciar outras entidades. Por exemplo, em um aplicativo que gera uma rota que descreve como obter de um ponto para outro, essa rota seria um objeto de valor. Seria um instantâneo de pontos em uma rota específica, mas essa rota sugerida não teria uma identidade, embora internamente possa se referir a entidades como Cidade, Estrada etc.
A Figura 7-13 mostra o objeto de valor Endereço dentro da agregação Ordem.
Figura 7-13. Tratar objeto de valor no agregado de Ordem
Conforme mostrado na Figura 7-13, uma entidade geralmente é composta por 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 de país/região, rua, cidade etc., e não tem identidade nesse 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:
Eles não têm identidade.
Eles são imutáveis.
A primeira característica já foi discutida. A imutabilidade é um requisito importante. Os valores de um objeto de valor 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 mudem durante o tempo de vida do objeto.
Objetos de valor permitem que você execute determinados truques para desempenho, graças à natureza imutável. Isso é especialmente verdadeiro em sistemas em que pode haver milhares de instâncias de objeto de valor, muitas das quais têm os mesmos valores. Sua natureza imutável permite que eles sejam reutilizados; eles podem ser objetos intercambiáveis, pois seus valores são os mesmos e não têm identidade. Esse tipo de otimização às vezes pode fazer a diferença entre o software que é executado lentamente e o software com bom desempenho. Claro, 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 tem métodos utilitários básicos, como igualdade, com base na comparação entre todos os atributos (uma vez que um objeto de valor não deve ser baseado na identidade) e outras características fundamentais. O exemplo a seguir mostra uma classe base de objeto de valor usada no microsserviço de ordenação 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
é do tipo abstract class
, mas, neste exemplo, ele não sobrecarrega os operadores ==
e !=
. Você pode fazer isso comparando o delegado com a substituição Equals
. Por exemplo, considere as seguintes sobrecargas de operador para o 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);
}
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 valor Address
em objeto não tem identidade e, portanto, nenhum campo de ID é definido para ele, tanto na definição da classe Address
quanto na da classe ValueObject
.
Não era possível ter nenhum campo de ID em uma classe a ser usada pelo EF (Entity Framework) até o EF Core 2.0, o que ajuda muito a implementar objetos de melhor valor sem ID. Essa é precisamente a explicação da próxima seção.
Seria possível argumentar que, já que os objetos de valor são imutáveis, deveriam 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 pelas filas de mensagens e, sendo somente leitura, impedem o desserializador de atribuir valores. Portanto, basta deixá-los como private set
, que é somente leitura o suficiente para ser prático.
Semântica de comparação de objeto 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 iguais, as comparações são avaliadas corretamente como true
. Se você não escolheu sobrecarregar os operadores ==
e !=
, então a última comparação de one == two
seria avaliada como false
. Para obter mais informações, consulte Sobrecarregar operadores de igualdade ValueObject.
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 persistê-lo no banco de dados usando o Entity Framework Core, pois ele geralmente tem como destino entidades com identidade?
Abordagens anteriores e 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 de ID. Em seguida, para que ele se parecesse mais com 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 a ID no modelo está definida no nível de infraestrutura do EF, seria bastante transparente para o modelo de domínio.
Na versão inicial do eShopOnContainers (.NET Core 1.1), o ID oculto necessário pela infraestrutura do EF Core foi implementado da seguinte maneira no nível DbContext, usando Fluent API no projeto de infraestrutura. Portanto, a ID estava oculta do ponto de vista do modelo de domínio, mas ainda estava 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á maneiras novas e melhores de persistir objetos de valor.
Manter objetos de valor como tipos de entidade própria 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 proprietária no EF Core, atualmente é a melhor maneira de persistir objetos de valor no EF Core 2.0 e versões posteriores. Você pode ver 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 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 próprios serão incluídos por padrão.
Apenas observando o modelo de domínio, um tipo de propriedade parece não ter identidade alguma. No entanto, nos bastidores, tipos próprios têm identidade, mas a propriedade de navegação do proprietário faz parte dessa identidade.
A identidade de instâncias de tipos próprios não é completamente própria. Ele consiste em três componentes:
A identidade do proprietário
A propriedade de navegação apontando para elas
No caso de coleções de tipos próprios, um componente independente (compatível com o EF Core 2.2 e posterior).
Por exemplo, no modelo de domínio Ordenação em eShopOnContainers, como parte da entidade de Ordem, o objeto de valor de endereço é implementado como um tipo de entidade própria dentro da entidade de proprietário, que é a entidade Ordem.
Address
é um tipo sem nenhuma propriedade de identidade definida no modelo de domínio. Ele é usado como uma propriedade do tipo Ordem para especificar o endereço para entrega para uma ordem específica.
Por convenção, uma chave primária de sombra será criada para o tipo próprio e será mapeada para a mesma tabela que a do proprietário usando a divisão de tabela. Isso permite usar tipos de propriedade de forma semelhante à forma como tipos complexos são usados no EF6 no .NET Framework tradicional.
É importante observar que os tipos de propriedade nunca são descobertos por convenção no EF Core, portanto, você precisa 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 à entidade 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
}
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 nomeam as colunas de banco de dados para as propriedades do tipo de entidade de propriedade como EntityProperty_OwnedEntityProperty
. Portanto, as propriedades internas de Address
serão exibidas na Orders
tabela com os nomes Address_Street
, Address_City
(e assim por diante para State
, Country
e ZipCode
).
Você pode acrescentar o Property().HasColumnName()
método fluente para renomear essas colunas. No caso Address
de uma propriedade pública, os mapeamentos seriam semelhantes aos 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 método OwnsOne
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
pertence ao 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; }
}
Detalhes adicionais sobre tipos de entidade de propriedade
Os tipos próprios são definidos quando você configura uma propriedade de navegação com um tipo específico usando a API fluente OwnsOne.
A definição de um tipo pertencente em nosso modelo de metadados é uma composição de: o tipo proprietário, a propriedade de navegação e o tipo CLR do tipo pertencente.
A identidade (chave) de uma instância de tipo próprio na nossa pilha é uma composição da identidade do tipo de proprietário e a definição do tipo próprio.
Capacidades de entidades próprias
Os tipos próprios podem referenciar outras entidades, tanto próprias (tipos próprios aninhados) quanto não próprias (propriedades de navegação de referência comuns para outras entidades).
Você pode mapear o mesmo tipo CLR como diferentes tipos próprios na mesma entidade de proprietário por meio de propriedades de navegação separadas.
A divisão de tabela está configurada por convenção, mas você pode recusá-la mapeando o tipo próprio até uma tabela diferente usando ToTable.
O carregamento adiantado é executado automaticamente em tipos próprios, ou seja, não há necessidade de chamar
.Include()
na consulta.Pode ser configurado com o atributo
[Owned]
usando o EF Core 2.1 e posterior.Pode processar coleções de tipos próprios (usando a versão 2.2 e posterior).
Limitações de entidades próprias
Não é possível criar um
DbSet<T>
de um tipo próprio (por design).Você não pode chamar
ModelBuilder.Entity<T>()
em tipos próprios (atualmente por design).Não há suporte para tipos próprios opcionais (ou seja, que permitem valor nulo) 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 de mapeamento de herança para tipos próprios, mas você deve conseguir mapear dois tipos de folha das mesmas hierarquias de herança como tipos próprios diferentes. O EF Core não argumentará sobre o fato de que eles fazem parte da mesma hierarquia.
Principais diferenças com os tipos complexos do EF6
- A divisão de tabela é opcional, ou seja, podem opcionalmente ser mapeados até uma tabela separada e ainda serem tipos próprios.
Recursos adicionais
Martin Fowler. Padrão ValueObject
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. Domain-Driven Design: Abordando a complexidade no coração do software. (Livro; inclui uma discussão sobre objetos de valor)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Implementação do Domain-Driven Design. (Livro; inclui uma discussão sobre objetos de valor)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Tipos de entidade de propriedade
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 do 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ços. Exemplo de classe de objeto de valor em eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs