Partilhar via


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.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

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.

Diagram showing the Address value-object inside the Order Aggregate.

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 AddressValueObject 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