Skip to main content

Setembro de 2017
Volume 32 - Número 9

Pontos de Dados - EF Core 2.0 mais amigável para DDD

Julie Lerman | Setembro de 2017 | Obtenha o código: C#    VB

Julie LermanSe você vem acompanhando esta coluna há algum tempo, já deve ter lido alguns artigos sobre o uso do Entity Framework (EF) para desenvolver soluções baseadas nos padrões e diretrizes do design controlado por domínio (DDD). Embora o foco do DDD seja o domínio, e não a persistência dos dados, em algum momento você precisará ter dados entrando e saindo do seu software.

Em iterações anteriores do EF, seus padrões (convencionais ou personalizados) permitiam que os usuários mapeassem classes de domínio simples diretamente no banco de dados sem muitos problemas. Para mim, se a camada de mapeamento do EF pudesse gerar modelos de domínio de boa qualidade dentro e fora do banco de dados, sem que fosse necessário criar outros modelos de dados, isso seria suficiente. Mas quando você se vê adaptando as classes e a lógica do seu domínio para que eles funcionem melhor com o Entity Framework, isso indica que é hora de criar um modelo de persistência de dados para depois mapeá-lo das classes do modelo do domínio até as classes do modelo de dados.

Fiquei um pouco surpresa ao descobrir há quanto tempo esses artigos sobre DDD e EF foram publicados. Já faz quatro anos desde que escrevi essa série de três artigos chamada “Codificação para o design controlado por domínio: Dicas para desenvolvedores com foco em dados” nos meses de agosto, setembro e outubro de 2013 da MSDN Magazine. Este é o link para o primeiro artigo da série, que inclui os links para os outros dois artigos: msdn.com/magazine/dn342868.

Há duas colunas específicas que falavam sobre os padrões do DDD e explicavam como o EF poderia ou não fazer o mapeamento com facilidade entre as classes do seu domínio e o banco de dados. A partir do EF6, um dos principais problemas era a impossibilidade de encapsular e, consequentemente, proteger uma coleção “filho”. Usar os padrões conhecidos para proteger a coleção (o que na maioria das vezes significava incluir um IEnumerable) não era compatível com os requisitos do EF, e o EF nem reconheceria que a navegação deveria fazer parte do modelo. Steve Smith e eu pensamos sobre isso por muito tempo quando criamos o curso Domain-Driven Design Fundamentals ( bit.ly/PS-DDD), pela Pluralsight, e Steve encontrou uma ótima solução ( bit.ly/2ufw89D).

O EF Core finalmente resolveu esse problema em sua versão 1.1, e eu escrevi sobre esse novo recurso na coluna de janeiro de 2017 ( msdn.com/magazine/mt745093). O EF Core 1.0 e 1.1 também eliminou outros obstáculos para o DDD, mas deixou algumas lacunas; a mais perceptível delas era a impossibilidade de mapear os objetos de valor do DDD usados em seus tipos de domínio. Sempre foi possível fazer isso com o EF, mas não com o EF Core. Com a chegada do EF Core 2.0, essa limitação já não existe mais.

Neste artigo, falarei sobre os recursos do EF Core 2.0 que estão alinhados a vários conceitos do DDD. O EF Core 2.0 é muito mais amigável para os desenvolvedores que aproveitam esses conceitos, que talvez você ainda não conheça. Mesmo se não pretender trabalhar com DDD, você poderá se beneficiar dos excelentes padrões que ele oferece! E com o EF Core, isso ficou ainda mais fácil.

Um-para-um é melhor

Em seu livro “Domain-Driven Design”, Eric Evans afirma que “Uma associação bidirecional significa que ambos os objetos só podem ser entendidos juntos. Quando os requisitos de um aplicativo não exigem um percurso bidirecional, adicionar uma direção para o percurso reduz a interdependência e simplifica o design”. Seguir essa orientação de fato eliminou efeitos colaterais no meu código. O EF sempre foi capaz de lidar com as relações unidirecionais um-para-muitos e um-para-um. Na verdade, enquanto escrevia este artigo, percebi que por muito tempo acreditei que uma relação um-para-um com duas extremidades obrigatoriamente resultar em uma relação bidirecional era errado. No entanto, você realmente precisava configurar explicitamente essas relações obrigatórias, mas isso não é mais necessário no EF Core, exceto em casos especiais.

Um requisito desfavorável do EF6 para relações um-para-um era que a propriedade principal do tipo dependente precisava ser dobrada como a chave estrangeira para a entidade principal. Isso obrigava você a criar classes de uma forma inconveniente, mesmo se você estivesse habituado a fazer isso. Graças à introdução do suporte para chaves estrangeiras exclusivas no EF Core, agora você pode ter uma propriedade de chave estrangeira explícita na extremidade dependente da relação de um-para-um. Ter uma chave estrangeira explícita é uma abordagem mais natural. E, na maioria dos casos, o EF Core seria capaz de identificar corretamente a extremidade dependente da relação com base na existência da propriedade da chave estrangeira. Se isso não desse certo devido a algum caso especial, você precisaria adicionar uma configuração, que demonstrarei rapidamente ao renomear a propriedade da chave estrangeira.

Para demonstrar uma relação um-para-um, usarei meu domínio preferido do EF Core: as classes do filme “Os sete samurais”:

public class Samurai {
  public int Id { get; set; }
  public string Name { get; set; }
  public Entrance Entrance { get; set; }
}

public class Entrance {
  public int Id { get; set; }
  public string SceneName { get; set; }
  public int SamuraiId { get; set; }
}

Agora, com o EF Core, o par de classes Samurai e Entrance (a primeira aparição do personagem no filme) será identificado corretamente como uma relação um-para-um unidirecional, sendo Entrance o tipo dependente. Não preciso incluir uma propriedade de navegação em Entrance nem de um mapeamento especial na API fluente. A chave estrangeira (SamuraiId) segue a convenção, por isso o EF Core é capaz de reconhecer a relação.

O EF Core entende que, no banco de dados, Entrance.SamuraiId é uma chave estrangeira exclusiva que aponta para Samurai. Lembre-se de algo que eu demorei a entender (e preciso me lembrar sempre): o EF Core não é o EF6! Por padrão, o .NET e o EF Core tratarão Samurai.Entrance como uma propriedade opcional no tempo de execução, exceto se houver uma lógica de domínio para indicar que Entrance é necessária. A partir do EF4.3, você pode aproveitar a API de validação que responderia a uma anotação [Required] na classe ou no mapeamento. Mas não existe (ainda?) uma API de validação no EF Core para resolver esse problema em particular. Sem falar dos outros requisitos relacionados ao banco de dados. Por exemplo, Entrance.SamuraiId será um valor inteiro não permite um valor nulo. Se você tentar inserir uma Entrance sem um valor para SamuraiId, o EF Core não entenderá os dados inválidos, o que também significa que o provedor InMemory não estará em conformidade. Mas seu banco de dados relacional pode enviar um erro para o conflito de restrição.

No entanto, da perspectiva de um DDD, isso não é um problema, porque você não estará usando a camada de persistência para identificar erros na lógica do seu domínio. Se Samurai exigir uma Entrance, isso é uma regra comercial. Se você não puder ter Entrances órfãs, isso também é uma regra comercial. Então, a validação deveria fazer parte da lógica do seu domínio de qualquer forma.

Veja um exemplo para aqueles casos especiais que mencionei anteriormente. Se a chave estrangeira na entidade dependente (Entrance, por exemplo) não seguir a convenção, você pode usar a API fluente para informar isso ao EF Core. Se Entrance.SamuraiId fosse Entrance.SamuraiFK, você pode esclarecer esse FK usando:

modelBuilder.Entity<Samurai>().HasOne(s=>s.Entrance)
  .WithOne().HasForeignKey<Entrance>(e=>e.SamuraiFK);

Se a relação for obrigatória nas duas extremidades (ou seja, Entrance precisa ter um Samurai), você pode adicionar IsRequired depois de WithOne.

As propriedades podem ser mais encapsuladas

O DDD orienta você a criar agregações (gráficos de objeto), onde a raiz da agregação (o objeto principal no gráfico) controla todos os outros objetos no gráfico. Isso significa escrever códigos que impedem outros códigos de utilizar incorretamente ou até mesmo abusar das regras. Encapsular propriedades para que elas não sejam definidas aleatoriamente (e, frequentemente, lidas aleatoriamente) é a principal forma de proteger um gráfico. No EF6 e em suas versões anteriores, sempre era possível adicionar setters privados a propriedades escalares e de navegação, e elas ainda seriam reconhecidas pelo EF, quando ele lesse e atualizasse os dados, mas não era fácil tornar as propriedades privadas. Uma publicação de Rowan Miller mostra uma forma de fazer isso no EF6 e contém links para algumas soluções anteriores ( bit.ly/2eHTm2t). Antes, não era possível proteger verdadeiramente uma coleção de navegação em uma relação um-para-muitos. Muitas pessoas escreveram sobre esse problema. Agora, além de o EF Core trabalhar facilmente com propriedades privadas que têm campos de suporte (ou campos de suporte deduzidos), você também pode encapsular as propriedades da coleção, graças à compatibilidade com o mapeamento de IEnumerable<T>. Escrevi sobre os campos de suporte e o IEnumerable<T> na coluna de janeiro de 2017 que mencionei antes, por isso não vou me aprofundar nisso aqui. Contudo, eles são muito importantes para os padrões do DDD e, consequentemente, é relevante mencioná-los neste artigo.

Embora seja possível ocultar escalares e coleções, existe um outro tipo de propriedade que talvez você queira encapsular: as propriedades de navegação. As coleções de navegação aproveitam a compatibilidade com o IEnumerable<T>, mas as propriedades de navegação privadas, como Samurai.Entrance, não podem ser entendidas pelo modelo. Apesar disso, existe uma forma de configurar o modelo para que ele entenda uma propriedade de navegação oculta na raiz da agregação.

Por exemplo, no código abaixo, declarei Entrance como uma propriedade privada de Samurai (e não estou usando um campo de suporte explícito, embora eu pudesse se precisasse). Você pode criar uma nova Entrance com o método CreateEntrance (que chama um método de fábrica em Entrance) e ler apenas a propriedade SceneName de Entrance. Observe que estou usando o operador condicional nulo do C# 6 para impedir uma exceção, caso eu ainda não tivesse carregado Entrance:

private Entrance Entrance {get;set;}
public void CreateEntrance (string sceneName) {
    Entrance = Entrance.Create (sceneName);
  }
public string EntranceScene => Entrance?.SceneName;

Segundo a convenção, o EF Core não faz uma suposição sobre essa propriedade privada. Mesmo se eu tivesse adicionado o campo de suporte, a Entrance privada não seria descoberta automaticamente, e você não poderia usá-la ao interagir com o armazenamento de dados. Essa é uma API específica para design e ajuda a proteger você de possíveis efeitos colaterais. Mas você pode configurá-la explicitamente. Lembre-se de que quando Entrance for pública, o EF Core será capaz de entender a relação um-para-um. No entanto, porque ela é privada, antes você precisa ter certeza de que o EF está a par disso.

Em OnModelCreating, adicione o mapeamento fluente HasOne/WithOne para informar o EF Core. Por Entrance ser privada, você não pode usar a expressão lambda como o parâmetro de HasOne. Em vez disso, você precisa descrever a propriedade por seu tipo e nome. WithOne geralmente usa a expressão lambda para especificar a propriedade de navegação para a outra extremidade do pareamento. O problema é que Entrance não tem uma propriedade de navegação Samurai, mas apenas a chave estrangeira. Tudo bem! Você pode deixar o parâmetro vazio, porque o EF Core já tem informações suficientes para entender isso:

modelBuilder.Entity<Samurai> ()
  .HasOne (typeof (Entrance), "Entrance").WithOne();

E se você usar uma propriedade de suporte, como _entrance na classe Samurai, como mostram estas alterações?

private Entrance _entrance;
private Entrance Entrance { get{return _entrance;} }
public void CreateEntrance (string sceneName) {
    _entrance = _entrance.Create (sceneName);
  }
public string EntranceScene => _entrance?.SceneName;

O EF Core entenderá que ele precisa usar o campo de suporte quando materializar a propriedade Entrance. Isso acontece porque, como explicou Arthur Vickers em nossa longa conversa sobre o GitHub quando eu estava aprendendo sobre o assunto, "se houver um campo de suporte e nenhum setter, o EF usará apenas o campo de suporte, porque não há nada mais que ele possa usar". Então, isso funciona.

Se o nome do campo de suporte não seguir a convenção, se você chamá-lo de _foo, por exemplo, será necessário adicionar uma configuração de metadados:

modelBuilder.Entity<Samurai> ()
  .Metadata
  .FindNavigation ("Entrance")
  .SetField("_foo");

Agora, as atualizações e as consultas no banco de dados poderão identificar essa relação. Lembre-se de que se quiser usar o carregamento adiantado, você precisará usar uma cadeia de caracteres para Entrance, porque ela não poderá ser descoberta pela expressão lambda. Por exemplo:

var samurai = context.Samurais.Include("Entrance").FirstOrDefault();

Você pode usar a sintaxe padrão para interagir com os campos de suporte para elementos como filtros, conforme mostrador na página de documentação dos campos de suporte em bit.ly/2wJeHQ7.

Objetos de valor agora são aceitos

Os objetos de valor são um conceito importante para o DDD, porque permitem que você defina modelos de domínio como tipos de valor. Um objeto de valor não tem uma identidade própria e se torna parte da entidade que o utiliza como uma propriedade. Considere o tipo de valor de cadeia de caracteres, que é composto por vários caracteres. Como a mudança de um único caractere altera o significado da palavra, as cadeias de caracteres são imutáveis. Para alterar uma cadeia de caracteres, você precisa substituir todo o objeto da cadeia de caracteres. O DDD orienta você a usar objetos de valor sempre que identificar uma relação um-para-um. Você pode obter mais informações sobre objetos de valor no curso DDD Fundamentals que mencionei antes.

O EF sempre permitiu a inclusão de objetos de valor em seu tipo ComplexType. Você poderia definir um tipo sem nenhuma chave e usá-lo como uma propriedade de uma entidade. Isso era suficiente para o EF reconhecê-lo como um ComplexType e mapear sua propriedade na tabela para a qual a entidade estava mapeada. Depois, você poderia estender o tipo para que ele também tivesse os recursos necessários a um objeto de valor, como garantir que o tipo fosse imutável e conseguir avaliar cada propriedade ao determinar a igualdade e substituir o hash. Frequentemente derivo meus tipos da classe base ValueObject de Jimmy Bogard para adotar esses atributos rapidamente.

O nome de uma pessoa é um tipo muito usado como um objeto de valor. Você pode garantir que um conjunto de regras comuns será seguido sempre que alguém quiser usar o nome de uma pessoa como uma entidade. A Figura 1 mostra uma classe PersonName simples com as propriedades First e Last totalmente encapsuladas e uma propriedade para retornar FullName. A classe foi criada para assegurar que as duas partes do nome sejam sempre fornecidas.

Figura 1: O objeto de valor PersonName
public class PersonName : ValueObject<PersonName> {
  public static PersonName Create (string first, string last) {
    return new PersonName (first, last);
  }
  private PersonName () { } 
  private PersonName (string first, string last) {
    First = first;
    Last = last;
  }
  public string First { get; private set; }
  public string Last { get; private set; }
  public string FullName => First + " " + Last;
}

Posso usar PersonName como uma propriedade em outros tipos e continuar adicionando outras lógicas à classe PersonName. A vantagem do objeto de valor em comparação com uma relação um-para-um aqui é que não preciso manter a relação quando eu estiver escrevendo o código. É uma programação orientada por objeto. É apenas mais uma propriedade. Na classe Samurai, adicionei uma nova propriedade desse tipo, a defini como um setter privado e forneci outro método chamado Identify para usar no lugar do setter:

 

public PersonName SecretIdentity{get;private set;}
public void Identify (string first, string last) {
  SecretIdentity = PersonName.Create (first, last);
}

Até o EF Core 2.0, não havia um recurso semelhante a ComplexTypes, por isso não era possível utilizar com facilidade objetos de valor sem adicionar um modelo de dados separado. Em vez de apenas reimplementar ComplexType no EF Core, a equipe do EF criou um conceito chamado entidades proprietárias, que aproveita outro recurso do EF Core, as propriedades sombra. Agora, as entidades proprietárias são reconhecidas como propriedades adicionais dos tipos que as possuem, e o EF Core entende a função delas no esquema do banco de dados e sabe como criar consultas e atualizações que respeitem esses dados.

A convenção do EF Core 2.0 não descobrirá automaticamente que essa nova propriedade SecretIdentity é um tipo que deve ser incorporado aos dados persistentes. Você precisará informar explicitamente para DbContext que a propriedade Samurai.SecretIdentity é uma entidade proprietária em DbContext.OnModelCreating usando o método OwnsOne:

protected override void OnModelCreating (ModelBuilder modelBuilder) {
  modelBuilder.Entity<Samurai>().OwnsOne(s => s.SecretIdentity);
}

Isso obriga as propriedade de PersonName a resolverem as propriedade de Samurai. Embora seu código use o tipo Samurai.SecretIdentity e navegue por ele até as propriedades First e Last, essas duas propriedades serão transformadas em colunas na tabela do banco de dados Samurais. A convenção do EF Core as nomeará com o nome da propriedade em Samurai (SecretIdentity) e o nome da propriedade da entidade proprietária, como mostra a Figura 2.

O esquema da tabela Samurais com as propriedades do valor
Figura 2: O esquema da tabela Samurais com as propriedades do valor

Agora posso identificar o nome secreto de um Samurai e salvá-lo com um código semelhante a este:

using (var context = new SamuraiContext()) {
  var samurai = new Samurai { Name = "HubbieSan" 
  samurai.Identify ("Late", "Todinner");
  context.Samurais.Add (samurai);
  context.SaveChanges ();
}

No armazenamento de dados, "Late" se torna persistente no campo SecretIdentity_First, e "Todinner" no campo SecretIdentity_Last.

Depois, posso simplesmente executar uma consulta para encontrar um Samurai:

var samurai=context.Samurais .FirstOrDefaultAsync (s => s.Name == "HubbieSan")

O EF Core assegura que a propriedade SecretIdentity resultante de Samurai será preenchida, e posso ver a identidade com a solicitação:

samurai.SecretIdentity.FullName

No EF Core, o preenchimento das propriedades que são entidades proprietárias é uma exigência. No download de exemplo, você verá como criei o tipo PersonName para atender a esse requisito.

Classes simples para explicações simples

O que mostrei aqui são classes simples que aproveitam alguns dos principais conceitos de implementação do DDD da forma mais concisa possível para mostrar como o EF Core responde a esses constructos. Como você viu, o EF Core 2.0 é capaz de entender relações um-para-um unidirecionais. Ele pode manter os dados de entidades que contêm propriedades escalares, de navegação e de coleção totalmente encapsuladas. Ele também permite que você use objetos de valor em seu modelo de domínio, bem como é capaz de mantê-los.

Para este artigo, usei classes simples sem lógicas adicionais para restringir melhor as entidades e os objetos de valor usando os padrões do DDD. Essa simplicidade pode ser vista no download de exemplo, que também está disponível no GitHub em bit.ly/2tDRXwi. Lá, você encontrará a versão simples e uma ramificação avançada na qual compactei este modelo de domínio e apliquei outras práticas de DDD à raiz da agregação (Samurai), à sua entidade relacionada (Entrance) e ao objeto de valor (PersonName) para você poder ver como o EF Core 2.0 lida com uma expressão mais realista de uma agregação de DDD. Em um próximo artigo, falarei sobre os padrões avançados aplicados a essa ramificação.

Lembre-se de que usei uma versão do EF Core 2.0 disponibilizada pouco depois do lançamento da sua versão final. Embora a maioria dos comportamentos que descrevi aqui sejam sólidos, é possível que alguns ajustes sejam implementados antes do lançamento da versão 2.0.0.


Julie Lerman é Diretora Regional da Microsoft, MVP da Microsoft, mentora 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.