No post anterior falei sobre resolver o conflito de configurações ao se usar Value Objects (VO) junto com o Entity Framework e hoje vou falar sobre o como configurar chaves estrangeiras quando usamos VO.

Vou dar sequência com o mesmo exemplo contendo uma entidade Cliente e dois Value Objects (TelefoneFixo e TelefoneComercial), mas desta vez vou acrescentar uma classe chamada DDD que servirá para restringir que apenas números de DDDs cadastrados sejam associados aos telefones.

/* 
* para efeito de simplicidade o construtor e as propriedades 
* não foram protegidas com com os modificadores de acesso protected e private 
*/
public class Cliente
{
    public int IdCliente { get; set; }
    public string Nome { get; set; }
    public virtual TelefoneFixo TelefoneFixo { get; set; }
    public virtual TelefoneCelular TelefoneCelular { get; set; }
}

public class TelefoneCelular : Telefone
{
    public TelefoneCelular() : base() { }
    public TelefoneCelular(string ddd, string numero) : base(ddd, numero) { }
}

public class TelefoneFixo : Telefone
{
    protected TelefoneFixo() : base() { }
    public TelefoneFixo(string ddd, string numero) : base(ddd, numero) { }
}

// Value Object Telefone
public class Telefone
{
    // construtor usado pelo EF 
    protected Telefone() { }

    public Telefone(string ddd, string numero)
    {
        if (!ValidarDDD(ddd) && !ValidarNumero(numero))
            throw new InvalidOperationException("Telefone incorreto");

        PrefixoDdd = ddd;
        Numero = numero;
    }

    private bool ValidarDDD(string ddd)
    {
        var regex = new Regex("^[0]{0,1}[0-9]{2}$");
        return regex.IsMatch(ddd);
    }

    private bool ValidarNumero(string numero)
    {
        var regex = new Regex("^[0-9]{4,5}-[0-9]{4}$");
        return regex.IsMatch(numero);
    }

    public string PrefixoDdd { get; set; }
    public string Numero { get; set; }
}

// Value Object para DDD
public class DDD
{
    public string Prefixo { get; set; }
    public string Regiao { get; set; }
}

O VO de DDD acabou ficando anêmico e sem métodos porque o intuito é que ele seja sempre preenchido por meio dos dados pre-cadastrados diretamente na tabela, portanto não há necessidade de validações ou construtores especiais. Para simplificar o exemplo o construtor da classe cliente e suas propriedades também não foram protegidos com os modificadores de acesso (protected e private) como normalmente é feito para as entidades de um Domain Driven Design.

Ainda não vou me preocupar com as propriedades de navegação de Cliente e DDD, vou apenas configurar a entidade DDD e a chave estrangeira entre a propriedade.

// Chave Primária - DDD
modelBuilder.Entity<DDD>().HasKey(x => x.Prefixo);

// Propriedades - DDD
modelBuilder.Entity<DDD>().Property(x => x.Prefixo)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
    .HasMaxLength(3).IsRequired().HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute() { IsUnique = true }));
modelBuilder.Entity<DDD>().Property(x => x.Regiao).HasMaxLength(100).IsRequired();

// ERRO!
modelBuilder.Entity<Cliente>()
    .HasRequired(x => x.TelefoneCelular.DDD);

A mensagem de erro a seguir é apresentada quando tentamos configurar a propriedade DDD para seja uma chave estrangeira da tabela DDD:

The expression ‘x => x.TelefoneCelular.DDD’ is not a valid property expression. The expression should represent a property: C#: ‘t => t.MyProperty’ VB.Net: ‘Function(t) t.MyProperty’.

Embora a mensagem não seja totalmente clara, ela nos dá a entender que o valor (x.TelefoneCelular.DDD), informado como propriedade para o método HasRequired, não é válido.

Para contornar esse problema vamos mapear o TelefoneFixo e o TelefoneCelular para tabelas separadas e configurá-las de maneira independente da entidade Cliente, porém, para isso, teremos que fazer uma pequena alteração na classe Telefone.

// Value Object Telefone
public class Telefone
{
    // construtor usado pelo EF
    protected Telefone() { }

    public Telefone(string ddd, string numero)
    {
        if (!ValidarDDD(ddd) && !ValidarNumero(numero))
            throw new InvalidOperationException("Telefone incorreto");

        PrefixoDdd = ddd;
        Numero = numero;
    }

    private bool ValidarDDD(string ddd)
    {
        var regex = new Regex("^[0]{0,1}[0-9]{2}$");
        return regex.IsMatch(ddd);
    }

    private bool ValidarNumero(string numero)
    {
        var regex = new Regex("^[0-9]{4,5}-[0-9]{4}$");
        return regex.IsMatch(numero);
    }

    public int IdCliente { get; set; }

    public string PrefixoDdd { get; set; }

    public string Numero { get; set; }

    // propriedades de navegação
    public virtual DDD DDD { get; set; }
    public virtual Cliente Cliente { get; set; }

}
  • O DDD foi incluído como uma propriedade virtual usado para a navegação entre os telefones e a entidade DDD.
  • O relacionamento entre Cliente e seus telefones será de um para zero-ou-um e portanto foi adicionada a propriedade IdCliente para ser chave primária do telefone.
  • A propriedade de navegação Cliente também foi adicionada.

Agora basta reconfigurar o mapeamento das entidades no Entity Framework

class AppDbContext : DbContext
{

    public AppDbContext()
    {
        Database.SetInitializer<AppDbContext>(new DbInitializer());
    }
        
    public virtual DbSet<Cliente> Clientes { get; set; }
    public virtual DbSet<DDD> DDDs { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        // Chave Primária - Cliente
        modelBuilder.Entity<Cliente>().HasKey(x => x.IdCliente);

        // Propriedades - Cliente
        modelBuilder.Entity<Cliente>().Property(x => x.Nome).HasMaxLength(50).IsRequired();

        // Chave Primária - DDD
        modelBuilder.Entity<DDD>().HasKey(x => x.Prefixo);

        // Propriedades - DDD
        modelBuilder.Entity<DDD>().Property(x => x.Prefixo)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
            .HasMaxLength(3).IsRequired().HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute() { IsUnique = true }));
        modelBuilder.Entity<DDD>().Property(x => x.Regiao).HasMaxLength(100).IsRequired();


        // Chave Primária - TelefoneCelular
        modelBuilder.Entity<TelefoneCelular>()
            .ToTable("ClienteTelefoneCelular")
            .HasKey(x => x.IdCliente);

        // FK entre TelefoneCeluar e DDD
        modelBuilder.Entity<TelefoneCelular>()
            .HasRequired(x => x.DDD)
            .WithMany()
            .HasForeignKey(x => x.PrefixoDdd);

        // FK entre TelefoneCelular e Cliente
        modelBuilder.Entity<TelefoneCelular>()
            .HasRequired(x => x.Cliente);

        // Chave Primária - TelefoneFixo
        modelBuilder.Entity<TelefoneFixo>()
            .ToTable("ClienteTelefoneFixo")
            .HasKey(x => x.IdCliente);

        // FK entre TelefoneFixo e DDD
        modelBuilder.Entity<TelefoneFixo>()
            .HasRequired(x => x.DDD)
            .WithMany()
            .HasForeignKey(x => x.PrefixoDdd);

        // FK entre TelefoneFixo e Cliente
        modelBuilder.Entity<TelefoneFixo>()
            .HasRequired(x => x.Cliente);
    }
}

Ao atualizar o banco com o migrations (Update-Database) você notará que serão criadas mais duas tabelas (ClienteTelefoneCelular e ClienteTelefoneFixo) contendo IdCliente como chave primária e PrefixoDDD como chave estrangeira de DDD.

Na classe Configuration eu fiz o Seed para incluir os DDDs de seis regiões

internal sealed class Configuration : DbMigrationsConfiguration<ExemploValueObjects.EntityFramework.AppDbContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = true;
    }

    protected override void Seed(ExemploValueObjects.EntityFramework.AppDbContext context)
    {

        context.DDDs.AddOrUpdate(
            x => x.Prefixo,
            new DDD() { Prefixo = "011", Regiao = "São Paulo/Região Metropolitana" },
            new DDD() { Prefixo = "012", Regiao = "São José dos Campos e Vale do Paraíba" },
            new DDD() { Prefixo = "021", Regiao = "Rio de Janeiro, Região Metropolitana e Teresópolis" },
            new DDD() { Prefixo = "022", Regiao = "Campos dos Goytacazes/Nova Friburgo/Macaé/Cabo Frio" },
            new DDD() { Prefixo = "071", Regiao = "Salvador e Região Metropolitana" },
            new DDD() { Prefixo = "081", Regiao = "Recife e Região Metropolitana/Caruaru" });
    }
}

O código abaixo é um exemplo de como um novo cliente pode ser criado.

using (var context = new AppDbContext())
{
    var cliente = new Cliente() { Nome = "Fulano" };

    cliente.TelefoneCelular = new TelefoneCelular("011", "11111-1111");
    cliente.TelefoneFixo = new TelefoneFixo("021", "2222-2222");

    context.Clientes.Add(cliente);
    context.SaveChanges();
}

Se examinar o conteúdo das tabelas Cliente, ClienteTelefoneFixo e ClienteTelefoneCelular você verá que os dados da Entidade Cliente foram corretamente gravados nas respectivas tabelas de acordo com o mapeamento feito. Se tentar gravar um cliente com um DDD inválido (ex.: 070) o entity retornará uma Exception informando que a inserção não foi possível porque há uma violação de chave estrangeira (o prefixo 070 não existe na tabela DDD).

O exemplo dado também é interessante para demonstrar que alguns VO (TelefoneFixo e TelefoneCeluar) podem ser gravados em tabelas sem perder a característica de não possuírem identidades próprias (nesse caso são parte da entidade Cliente e usam o IdCliente como chave primária).

Assim como no post anterior, novamente foi necessário alterar as entidades para que o Entity Framework pudesse ser corretamente configurado e, muito embora o EF seja bastante flexível, o uso de entidades complexas é um tanto quanto limitado e exige algumas alterações no modelo de domínio.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *