Abril de 2018

Volume 33 – Número 4

Pontos de dados – Entidades de propriedade do EF Core 2 e soluções temporárias

Por Julie Lerman

Julie LermanO novo recurso de Entidade de Propriedade no EF Core 2.0 substitui o recurso Tipo Complexo do Entity Framework “clássico” (EF até EF6). Entidades de Propriedade permitem o mapeamento de objetos de valor para o armazenamento de dados. É bastante comum ter uma regra de negócios que permita que as propriedades baseadas em objetos de valor sejam nulas. Além disso, como os objetos de valor são imutáveis, também é importante substituir as propriedades que contenham um objeto de valor. A versão atual do EF Core não permite nenhum desses cenários por padrão, mas ambos terão suporte nas próximas iterações. Enquanto isso, em vez de tratar essas limitações como empecilhos para aqueles de nós que amam os benefícios dos padrões do design controlado por domínio (DDD), este artigo mostrará a você como contornar essas limitações. Um profissional de DDD pode ainda rejeitar esses padrões temporários por não seguirem os princípios do DDD de maneira próxima o suficiente, mas meu lado pragmático está satisfeito por saber que eles são, simplesmente, soluções temporárias.

Observe que eu disse “por padrão”. Mas existe uma maneira de fazer com que o EF Core assuma a responsabilidade de impor sua própria regra sobre entidades de propriedade nulas e permitir a substituição de objetos de valor sem afetar drasticamente suas classes de domínio ou suas regras de negócio. Nesta coluna, eu mostrarei como fazer isso.

Há outra maneira de contornar o problema de nulidade, que é simplesmente mapear o tipo do objeto de valor para sua própria tabela e, assim, dividir fisicamente os dados do objeto de valor do restante do objeto ao qual ele pertence. Embora essa possa ser uma boa solução para alguns cenários, geralmente prefiro não usá-la. Assim, prefiro usar a minha solução alternativa, que é o foco desta coluna. Mas, primeiro, quero garantir que você entenda por que esse problema e essa solução são importantes a ponto de eu dedicar esta coluna para esse tópico.

Um breve resumo sobre objetos de valor

Objetos de valor são um tipo que permite o encapsulamento de diversos valores em uma única propriedade. Uma cadeia de caracteres é um ótimo exemplo de um objeto de valor. Elas são compostas de uma coleção de caracteres e são imutáveis: a imutabilidade é uma faceta fundamental de um objeto de valor. A combinação e a ordem das letras “c”, “a” e “r” têm um significado específico. Se você fosse mudá-las, por exemplo, trocando a última letra por um “t”, o significado mudaria completamente.  O objeto é definido pela combinação de todos os seus valores. Assim, o fato de o objeto não poder ser modificado faz parte de seu contrato. E há outro aspecto importante de um objeto de valor: ele não tem sua própria identidade. Ele pode ser usado apenas como uma propriedade de outra classe, como uma cadeia de caracteres. Objetos de valor têm outras regras contratuais, mas, para começar e se você é novo no conceito, essas são as mais importantes.

Dado que um objeto de valor é composto por suas propriedades e, como um todo, usado como uma propriedade em outra classe, é preciso um esforço especial para persistir seus dados. Com um banco de dados não relacional, como um banco de dados de documentos, é fácil apenas armazenar o gráfico de um objeto e seus objetos de valor incorporados. Mas esse não é o caso quando se faz o armazenamento em um banco de dados relacional. Já na primeira versão, o Entity Framework incluiu o ComplexType, que sabia como mapear as propriedades da propriedade para o banco de dados no qual o EF estava persistindo seus dados. Um exemplo de objeto de valor comum é o PersonName, que pode consistir em uma propriedade FirstName e uma propriedade LastName. Caso tenha um tipo Contact com uma propriedade PersonName, por padrão, o EF Core armazenará os valores FirstName e LastName como colunas adicionais na tabela na qual Contact está mapeado.

Um exemplo de um objeto de valor em uso

Descobri que observar vários exemplos de objetos de valor me ajudou a entender melhor o conceito, assim, vou usar outro exemplo: uma entidade SalesOrder e um objeto de valor PostalAddress. Geralmente, um pedido inclui um endereço de entrega e um endereço de faturamento. Embora esses endereços possam existir para outros fins, no contexto do pedido, eles são partes integrantes da definição. Se uma pessoa se mudar para um novo local, você ainda desejará saber para onde dado pedido foi enviado, portanto, faz sentido inserir os endereços ao pedido. Mas, para tratar os endereços consistentemente em meu sistema, prefiro encapsular os valores que compõem um endereço em sua própria classe, PostalAddress, conforme mostrado na Figura 1.

Figura 1 ValueObject PostalAddress

public class PostalAddress : ValueObject<PostalAddress>
{
  public static PostalAddress Create (string street, string city,
                                      string region, string postalCode)   {
    return new PostalAddress (street, city, region, postalCode);
  }
  private PostalAddress () { }
  private PostalAddress (string street, string city, string region,
                         string postalCode)   {
    Street = street;
    City = city;
    Region = region;
    PostalCode = postalCode;
  }
  public string Street { get; private set; }
  public string City { get; private set; }
  public string Region { get; private set; }
  public string PostalCode { get; private set; }
  public PostalAddress CopyOf ()   {
    return new PostalAddress (Street, City, Region, PostalCode);
  }
}

O PostalAddress herda de uma classe base ValueObject criada por Jimmy Bogard (bit.ly/2EpKydG). O ValueObject fornece algumas das lógicas obrigatórias requeridas de um objeto de valor. Por exemplo, ele tem uma substituição do Object.Equals, o que garante que todas as propriedades sejam comparadas. Lembre-se de que ele faz uso intenso de reflexão, o que pode afetar o desempenho de um aplicativo de produção.

Duas outras características importantes do meu objeto de valor PostalAddress são: ele não possui nenhuma propriedade de chave de identidade; e seu construtor força a regra invariável de que cada propriedade deve ser preenchida. No entanto, para uma entidade de propriedade ser capaz de mapear um tipo definido como um objeto de valor, a única regra é que ele não possua uma chave de identidade própria. Uma entidade de propriedade não está preocupada com os outros atributos de um objeto de valor.

Com o PostalAddress definido, agora posso usá-lo como as propriedades Shipping­Address e BillingAddress da minha classe SalesOrder (consulte a Figura 2). Elas não propriedades de navegação para dados relacionados, apenas mais propriedades similares às Notas escalares e OrderDate.

Figura 2 A classe SalesOrder contém propriedades que são tipos PostalAddress

public class SalesOrder {
  public SalesOrder (DateTime orderDate, decimal orderTotal)   {
    OrderDate = orderDate;
    OrderTotal = orderTotal;
    Id = Guid.NewGuid ();
  }
  private SalesOrder () { }
  public Guid Id { get; private set; }
  public DateTime OrderDate { get; private set; }
  public decimal OrderTotal { get; private set; }
  private PostalAddress _shippingAddress;
  public PostalAddress ShippingAddress => _shippingAddress;
  public void SetShippingAddress (PostalAddress shipping)
  {
    _shippingAddress = shipping;
  }
  private PostalAddress _billingAddress;
  public PostalAddress BillingAddress => _billingAddress;
  public void CopyShippingAddressToBillingAddress ()
  {
    _billingAddress = _shippingAddress?.CopyOf ();
  }
  public void SetBillingAddress (PostalAddress billing)
  {
    _billingAddress = billing;
  }
}

Esses endereços agora residem no SalesOrder e podem fornecer informações precisas, independentemente do endereço atual da pessoa que fez o pedido. Eu sempre saberei para onde esse pedido foi.

Mapeando um objeto de valor como uma entidade de propriedade do EF Core

Em versões anteriores, o EF conseguia reconhecer automaticamente as classes que deveriam ser mapeadas usando um ComplexType, descobrindo que a classe foi usada como uma propriedade de outra entidade e que ela não tinha propriedade de chave. O EF Core, porém, não consegue inferir entidades de propriedade automaticamente. Você deve especificar isso nos mapeamentos da API fluente do DbContext no método OnModelCreating usando o novo método OwnsOne para especificar qual propriedade dessa entidade é a entidade de propriedade:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<SalesOrder>().OwnsOne(s=>s.BillingAddress);
  modelBuilder.Entity<SalesOrder>().OwnsOne(s=>s.ShippingAddress);
}

Eu usei as migrações do EF Core para criar um arquivo de migração descrevendo o banco de dados para o qual meu modelo é mapeado. A Figura 3 mostra a seção da migração que representa a tabela SalesOrder. Como você pode ver, o EF Core entendeu que as propriedades PostalAddress de cada um dos dois endereços são parte do SalesOrder. Os nomes das colunas são estipulados conforme a convenção do EF Core, embora você possa mudá-los com a API fluente.

Figura 3 A migração para a tabela SalesOrder, incluindo todas as colunas para as propriedades PostalAddress

migrationBuilder.CreateTable(
  name: "SalesOrders",
  columns: table => new
  {
    Id = table.Column(nullable: false)
              .Annotation("Sqlite:Autoincrement", true),
    OrderDate = table.Column(nullable: false),
    OrderTotal = table.Column(nullable: false),
    BillingAddress_City = table.Column(nullable: true),
    BillingAddress_PostalCode = table.Column(nullable: true),
    BillingAddress_Region = table.Column(nullable: true),
    BillingAddress_Street = table.Column(nullable: true),
    ShippingAddress_City = table.Column(nullable: true),
    ShippingAddress_PostalCode = table.Column(nullable: true),
    ShippingAddress_Region = table.Column(nullable: true),
    ShippingAddress_Street = table.Column(nullable: true)
  }

Além disso, como mencionado anteriormente, os endereços são colocados na tabela SalesOrder por convenção, que é minha preferência. Esse código alternativo vai dividi-los em tabelas separadas e evitar completamente o problema de nulidade:

modelBuilder.Entity<SalesOrder> ().OwnsOne (
  s => s.BillingAddress).ToTable("BillingAddresses");
modelBuilder.Entity<SalesOrder> ().OwnsOne (
  s => s.ShippingAddress).ToTable("ShippingAddresses");

Criando um SalesOrder no código

Inserir um pedido de venda com o endereço de cobrança e o endereço de entrega é simples:

private static void InsertNewOrder()
{
  var order=new SalesOrder{OrderDate=DateTime.Today, OrderTotal=100.00M};
  order.SetShippingAddress (PostalAddress.Create (
    "One Main", "Burlington", "VT", "05000"));
  order.SetBillingAddress (PostalAddress.Create (
    "Two Main", "Burlington", "VT", "05000"));
  using(var context=new OrderContext()){
    context.SalesOrders.Add(order);
    context.SaveChanges();
  }
}

Mas digamos que minhas regras de negócio permitam que um pedido seja armazenado, mesmo que os endereços de envio e de faturamento não tenham sido inseridos ainda e um usuário possa concluir o pedido em outro momento. Eu vou converter em comentário o código que preenche a propriedade BillingAddress:

// order.BillingAddress=new Address("Two Main","Burlington", "VT", "05000");

Quando SaveChanges for chamado, o EF Core tenta descobrir quais são as propriedades do BillingAddress, de modo que possa efetuar o push delas para a tabela SalesOrder. Contudo, haverá falha nesse caso porque BillingAddress é nulo. Internamente, o EF Core tem uma regra estabelecendo que uma propriedade de tipo de propriedade convencionalmente mapeada não pode ser nula.

O EF Core está pressupondo que o tipo de propriedade está disponível para que suas propriedades possam ser lidas. Os desenvolvedores podem ver isso como um empecilho para o uso de objetos de valor ou, ainda pior, para o uso do EF Core, devido a quão fundamental são os objetos de valor para o design de software. Foi assim que me senti no começo, mas acabei conseguindo criar uma solução alternativa.

Solução temporária para permitir objetos de valor nulo

O objetivo da solução alternativa é assegurar que o EF Core receba um ShippingAddress, BillingAddress ou outro tipo de propriedades, caso o usuário tenha fornecido um ou não. Isso significa que o usuário não é forçado a fornecer um endereço de envio ou de faturamento apenas para satisfazer a camada de persistência. Caso ele não os forneça, um objeto PostalAddress com valores nulos em suas propriedades será adicionado pelo DbContext quando for o momento de salvar um SalesOrder.

Fiz uma pequena adaptação na classe PostalAddress, na qual adicionei um segundo método de fábrica, Empty, para permitir que o DbContext crie facilmente um PostalAddress vazio:

public static PostalAddress Empty()
{
  return new PostalAddress(null,null,null,null);
}

Além disso, aprimorei a classe base ValueObject com um novo método, IsEmpty, mostrado na Figura 4, para permitir que o código determine facilmente se um objeto tem todos os valores nulos em suas propriedades. O IsEmpty aproveita um código que já existe na classe ValueObject. Ele faz a iteração entre as propriedades e, se alguma delas tiver um valor, ele retorna “false”, indicando que o objeto não está vazio; caso contrário, retorna “true”.

Figura 4 O método IsEmpty adicionado à classe base ValueObject

public bool IsEmpty ()
{
  Type t = GetType ();
  FieldInfo[] fields = t.GetFields
    (BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
  foreach (FieldInfo field in fields)
  {
    object value = field.GetValue (this);
    if (value != null)
    {
      return false;
    }
  }
  return true;
}

Contudo, minha solução para permitir entidades de propriedade nulas ainda não estava completa. Eu ainda precisava usar toda essa nova lógica para garantir que o novo SalesOrders sempre tivesse um ShippingAddress e um BillingAddress para que o EF Core pudesse armazená-los no banco de dados. Logo que adicionei essa última parte da minha solução, eu não estava feliz com o resultado porque o último trecho do código (o qual eu não me importaria em compartilhar) estava fazendo com que a classe SalesOrder impusesse a regra do EF Core — a ruína do design controlado por domínio.

Voilá! Uma solução elegante

Felizmente, eu estava dando uma palestra na DevIntersection, como faço todos os outonos, e Diego Vega e Andrew Peters, da equipe do EF, também estavam se apresentando. Eu mostrei minha solução alternativa a eles e expliquei o que estava me incomodando — a necessidade de impor ShippingAddress e BillingAddress não nulos no SalesOrder — e eles concordaram. Andrew rapidamente pensou em uma maneira de usar meu trabalho na classe base ValueObject e o ajuste que fiz no PostalAddress para forçar o EF Core a resolver o problema sem colocar o ônus sobre o SalesOrder. A mágica acontece na substituição do método SaveChanges da minha classe DbContext, mostrada na Figura 5.

Figura 5 Substituindo o SaveChanges para fornecer valores para tipos de propriedade nulos

public override int SaveChanges()
{
  foreach (var entry in ChangeTracker.Entries()
             .Where(e => e.Entity is SalesOrder && e.State == EntityState.Added))
  {
    if (entry.Entity is SalesOrder)
    {
      if (entry.Reference("ShippingAddress").CurrentValue == null)
      {
        entry.Reference("ShippingAddress").CurrentValue = PostalAddress.Empty();
      }
      if (entry.Reference("BillingAddress").CurrentValue == null)
      {
        entry.Reference("BillingAddress").CurrentValue = PostalAddress.Empty();
      }
  }
  return base.SaveChanges();
}

A partir da coleção de entradas que o DbContext está acompanhando, o SaveChanges fará a iteração através daquelas sinalizadas como SalesOrders para serem adicionadas ao banco de dados e fará com que elas sejam preenchidas assim como seus equivalentes vazios.

E quanto a fazer consultas àqueles tipos de propriedade vazios?

Satisfeita a necessidade de o EF Core armazenar objetos de valor nulo, agora é hora de consultá-los de volta a partir do banco de dados. Porém, o EF Core resolve essas propriedades em seu estado vazio. Qualquer ShippingAddress ou BillingAddress originalmente nulo retorna como uma instância com valores nulos em suas propriedades. Após qualquer consulta, eu preciso que a minha lógica substitua quaisquer propriedades PostalAddress vazias por “null”.

Eu fiquei um bom tempo procurando uma maneira elegante de conseguir fazer isso. Infelizmente, ainda não existe um gancho de ciclo de vida que consiga modificar objetos enquanto eles estão sendo materializados a partir dos resultados de consulta. Há um serviço substituível no pipeline de consulta chamado CreateReadValueExpression na classe interna EntityMaterializerSource, mas ele só pode ser usado em valores escalares, não em objetos. Eu testei várias outras abordagens que ficavam cada vez mais complicadas e, finalmente, tive uma longa conversa comigo mesma sobre essa ser uma solução temporária, de modo que eu posso aceitar uma solução mais simples, mesmo que tenha um quê de código. E essa não é uma tarefa muito difícil de ser controlada se suas consultas estiverem encapsuladas em uma classe dedicada a fazer chamadas do EF Core para o banco de dados.

Eu nomeei o método como FixOptionalValueObjects:

private static void FixOptionalValueObjects (SalesOrder order) {
  if (order.ShippingAddress.IsEmpty ()) { order.SetShippingAddress (null); }
  if (order.BillingAddress.IsEmpty ()) { order.SetBillingAddress (null); }
}

Agora eu tenho uma solução na qual o usuário pode manter objetos de valor nulo e deixar que o EF Core os armazene e os recupere como não nulos, ainda que os retorne à base de código como nulos no final das contas.

Substituindo objetos de valor

Eu mencionei outra limitação da versão atual do EF Core 2, que é a incapacidade de substituir entidades de propriedade. Objetos de valor são, por definição, imutáveis. Então, caso você precise alterá-los, a única maneira de fazer isso é os substituindo. Logicamente, isso significa que você está modificando o SalesOrder, tal como se você tivesse alterado sua propriedade OrderDate. Porém, devido à maneira como o EF Core acompanha as entidades de propriedade, ele sempre achará que a substituição é adicionada, mesmo que o seu host, SalesOrder, por exemplo, não seja novo.

Eu fiz uma alteração na substituição do SaveChanges para corrigir esse problema (veja a Figura 6). Agora a substituição filtra por SalesOrders que são adicionados ou modificados e, com as duas novas linhas de código que modificam o estado das propriedades de referência, ela faz com que ShippingAddress e BillingAddress tenham o mesmo estado do pedido — que será Adicionado ou Modificado. Agora, os objetos SalesOrder modificados também poderão incluir os valores das propriedades ShippingAddress e BillingAddress em seus comandos UPDATE.

Figura 6 Fazendo o SaveChanges compreender os tipos de propriedade substituídos marcando-os como modificados

public override int SaveChanges () {
  foreach (var entry in ChangeTracker.Entries ().Where (
    e => e.Entity is SalesOrder &&
    (e.State == EntityState.Added || e.State == EntityState.Modified))) {
    if (entry.Entity is SalesOrder order) {
      if (entry.Reference ("ShippingAddress").CurrentValue == null) {
        entry.Reference ("ShippingAddress").CurrentValue = PostalAddress.Empty ();
      }
      if (entry.Reference ("BillingAddress").CurrentValue == null) {
        entry.Reference ("BillingAddress").CurrentValue = PostalAddress.Empty ();
      }
      entry.Reference ("ShippingAddress").TargetEntry.State = entry.State;
      entry.Reference ("BillingAddress").TargetEntry.State = entry.State;
    }
  }
  return base.SaveChanges ();
}

Esse padrão funciona porque estou salvando com uma instância de OrderContext diferente da que consultei, a qual, portanto, não tem nenhuma noção preconcebida do estado dos objetos PostalAddress. Você pode encontrar um padrão alternativo para objetos acompanhados nos comentários do problema do GitHub em bit.ly/2sxMECT.

Solução pragmática para o curto prazo

Se não fosse possível vislumbrar alterações que permitam entidades de propriedade opcionais e a substituição de entidades de propriedade, eu provavelmente tomaria medidas para criar um modelo de dados separado para lidar com a persistência de dados em meu software. Mas essa solução temporária me poupa desse esforço e investimento extras, e eu sei que logo poderei remover as soluções alternativas e mapear facilmente meus modelos de domínio diretamente para meu banco de dados, permitindo que o EF Core defina o modelo de dados. Fiquei feliz por investir meu tempo, esforço e imaginação para criar soluções alternativas que me permitissem usar objetos de valor e o EF Core 2 ao projetar minhas soluções — além de ajudar outras pessoas a conseguirem fazer o mesmo.

Observe que o download que acompanha este artigo está em um aplicativo de console para testar a solução e manter os dados em um banco de dados SQLite. Eu estou usando o banco de dados em vez de apenas fazer testes com o provedor InMemory porque queria inspecionar o banco de dados para ter 100% de certeza de que os dados estavam sendo armazenados da maneira esperada.


Julie Lerman é Diretora Regional da Microsoft, MVP da Microsoft, coach e consultora de equipes de software e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do “Programming Entity Framework”, bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter em @julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Andriy Svyryd
Andriy Svyryd é um desenvolvedor de .NET ucraniano que trabalha na equipe do Entity Framework desde 2010. Veja todos os projetos para os quais ele contribui em github.com/AndriySvyryd.


Discuta esse artigo no fórum do MSDN Magazine