Skip to content

Instantly share code, notes, and snippets.

@vinicius-stutz
Created April 24, 2018 03:37
Show Gist options
  • Save vinicius-stutz/1d3c4a5595103866df4315c0f3b8fb2a to your computer and use it in GitHub Desktop.
Save vinicius-stutz/1d3c4a5595103866df4315c0f3b8fb2a to your computer and use it in GitHub Desktop.
Mapeamento com Entity Framework Code First (Fluent Api)

Qual a diferença entre Data Annotations e Fluent Api?

Quando usado o mapeamento por Data Annotations, as propriedades das entidades POCO (Plain Old CLR Object) são decoradas com atributos, fazendo necessário referência externa, além da “sujeira” dos metadados.

public int ClienteId {get; set;}
[MaxLength(150), Required]
public string Nome { get; set; }

No Fluent Api usamos métodos encadeados fora das entidades, as mesmas permanecem “limpas”, porém precisamos escrever mais código.

HasKey(x => x.ClienteId);
Property(x => x.Nome)
.HasMaxLength(150)
.HasRequired();

Por que usar Fluent Api?
Falando de sistemas construídos usando DDD (Domain Driven Design), logo vem a primeira preocupação:

Como isolar a camada de domínio de tudo, principalmente da persistência de dados?

Como visto anteriormente, usando Data Annotations, somos obrigados a referenciar a dll System.ComponentModel.DataAnnotations na camada de domínio, pois temos que decorar as propriedades com as anotações. A única forma de usufruir do Code First e manter nossas entidades “limpas” sem referências externas, é mapeando o contexto na abordagem Fluent Api. Com esse modelo, as entidades de domínio se tornam “ignorantes” para as configurações de banco de dados, já que esses detalhes ficam isolados na camada de persistência, que por sua vez, sabe quais as entidades que está configurando.

Tudo que é possível configurar com Data Annotations é possível configurar com o Fluent Api, já o inverso não é possível tornando o aprendizado do Fluent Api OBRIGATÓRIO em sistemas mais complexos, o modo com Data Annotations seria mais apropriado em contextos mais simples permitindo maior acoplamento.

Neste artigo vamos construir um pequeno contexto de uma escola e demonstrar as configurações de banco de dados, mapeamento e relacionamentos entre as entidades, que serão espelhadas como relacionamento de tabela no banco de dados.

Bom, abra o Visual Studio 2013, New Project > Other Project Type > Visual Studio Solution, nomeie como EntityFrameworkEscola, após criar a solução, adicione 3 projetos Visual C#.

Nos projetos EntityFrameworkEscola.Console e EntityFrameworkEscola.DataAccess, instale o Entity Framework via Nuget.

PM> Install-Package EntityFramework

Adicione a referência do projeto EntityFrameworkEscola.Domain nos projetos EntityFrameworkEscola.DataAccess e EntityFrameworkEscola.Console e a referência do projeto EntityFrameworkEscola.DataAccess no projeto EntityFrameworkEscola.Console.

Obs: O projeto EntityFrameworkEscola.Console simula uma camada de apresentação e claro, que em um projeto real, o mesmo não iria referenciar a camada de persistência, teríamos que usar algum container de DI, interfaces e etc**,** porém para simplificar este artigo, vamos trabalhar dessa forma.

No projeto EntityFrameworkEscola.Domain, adicione uma pasta chamada Entities e dentro da pasta, crie as seguintes classes conforme as listagens abaixo:

Aluno

namespace EntityFrameworkEscola.Domain.Entities
{
        public class Aluno: Pessoa
        {
             public Aluno()
             {
                Usuario = new Usuario();
             }
             public int AlunoId { get; set; }
             public virtual Turma Turma { get; set; }
             public virtual Usuario Usuario { get; set; }
        }
}

Listagem 01 – Aluno.cs

Curso

using System.Collections.Generic;
namespace EntityFrameworkEscola.Domain.Entities
{
       public class Curso
       {
             public Curso()
             {
                  ProfessorLista = new List<Professor>();
                  Ativo = true;
             }
             public int CursoId { get; set; }
             public string Numero { get; set; }
             public string Descricao { get; set; }
             public bool Ativo { get; set; }
             public virtual ICollection<Professor> ProfessorLista { get; set; }
             public override string ToString()
             {
                 return Descricao;
             }
       }
}

Listagem 02 – Curso.cs

Pessoa

namespace EntityFrameworkEscola.Domain.Entities
{
      public abstract class Pessoa
      {
          public string Nome { get; set; }
      }
}

Listagem 03 – Pessoa.cs

Professor

using System.Collections.Generic;
namespace EntityFrameworkEscola.Domain.Entities
{
      public class Professor: Pessoa
      {
           public Professor()
           {
              CursoLista = new List<Curso>();
              TurmaLista = new List<Turma>();
           }
           public int ProfessorId { get; set; }
           public virtual ICollection<Curso> CursoLista { get; set; }
           public virtual ICollection<Turma> TurmaLista { get; set; }
           public override string ToString()
           {
               return Nome;
           }
      }
}

Listagem 04 – Professor.cs

Turma

using System;
using System.Collections.Generic;
namespace EntityFrameworkEscola.Domain.Entities
{
      public class Turma
      {
          public Turma()
          {
               AlunoLista = new List<Aluno>();
          }
          public int TurmaId { get; set; }
          public DateTime DataInicio { get; set; }
          public DateTime DataTermino { get; set; }
          public virtual Professor Professor { get; set; }
          public virtual Curso Curso { get; set; }
          public virtual ICollection<Aluno> AlunoLista { get; set; }
          public override string ToString()
          {
                return String.Format("Turma: {0} - Data Início: {1} - Professor: {2}", Curso.Descricao, DataInicio.ToShortDateString(), Professor.Nome);
          }
      }
}

Listagem 05 – Turma.cs

Usuario

namespace EntityFrameworkEscola.Domain.Entities
{
     public class Usuario
     {
          public int UsuarioId { get; set; }
          public string Email { get; set; }
          public string Senha { get; set; }
     }
}

Listagem 06 – Usuario.cs

Bom, temos todas as nossas entidades criadas, agora vamos para o projeto EntityFrameworkEscola.DataAccess, crie uma classe com o nome DataContext, nela vamos realizar algumas configurações válidas para o nosso contexto, veja confrome a Listagem 07

using EntityFrameworkEscola.Domain.Entities;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace EntityFrameworkEscola.DataAccess
{
       public class DataContext : DbContext
       {
             public DataContext()
                     : base("DataContext")
             {
                  Configuration.LazyLoadingEnabled = false;
                  Configuration.ProxyCreationEnabled = false;
             }
             public DbSet<Aluno> Alunos { get; set; }
             public DbSet<Turma> Turmas { get; set; }
             public DbSet<Curso> Cursos { get; set; }
             public DbSet<Professor> Professores { get; set; }
             public DbSet<Usuario> Usuarios { get; set; }
             protected override void OnModelCreating(DbModelBuilder modelBuilder)
             {
                  modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
                  modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
                  modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
                  modelBuilder.Properties()
                         .Where(p => p.Name == p.ReflectedType.Name + "Id")
                         .Configure(p => p.IsKey());
                 modelBuilder.Properties<string>()
                        .Configure(p => p.HasColumnType("varchar"));
                  modelBuilder.Properties<string>()
                        .Configure(p => p.HasMaxLength(100));
             }
       }
}

Listagem 07 – DataContext.cs

Vamos entender o que significa cada uma dessas configurações:

A classe DataContext deve herdar o DbContext do Entity Framework, no construtor da classe base, o nome da connection string deve ser passado por parâmetro, se nada for informado, ele vai procurar uma connection string com o mesmo nome da classe que está configurando os dados, no caso do nosso exemplo, a classe DataContext.

public DataContext()
     : base("DataContext")//Passado por parâmetro o nome da connection string
{
     ...
}

Listagem 08 – Contrutor do DataContext.cs

Connection String que está no projeto EntityFrameworkEscola.Console:

<connectionStrings>
 <add name="DataContext"
      connectionString="Data Source=(localdb)\v11.0;
      Initial Catalog=EFEscola;
      Integrated Security=True;
      Pooling=False"
      providerName="System.Data.SqlClient"/>
 </connectionStrings>

Listagem 09 – Connection String.

Dentro do construtor, eu costumo desabilitar o Lazy Loading, esse mecanismo faz com que o Entity Framework carregue automaticamente os relacionamentos em memória, o que pode causar perda de performance ao fazer um select na base de dados, quando essa opção é desabilitada (deixado como false), ele não carrega as dependências automaticamente e quando for necessário carregar, basta usar o método Include():

Configuration.LazyLoadingEnabled = false;

Listagem 10 – Desabilitando o Lazy Loading

Eu também costumo desabilitar a criação de proxy, o Entity Framework por padrão cria um proxy toda vez que é instanciado uma entidade POCO para que possa ser realizado eventuais mudanças e fazer o carregamento automático das propriedades virtuais, como não estamos usando o Lazy Loading habilitado, não tem muito sentido manter a criação de proxy habilitada também.

Configuration.ProxyCreationEnabled = false;

Listagem 11 – Desabilitando a criação de proxy

Na Listagem 12, temos as propriedades DbSet que vão realizar o mapeamento das tabelas do banco de dados nas entidades POCO

public DbSet<Aluno> Alunos { get; set; }
public DbSet<Turma> Turmas { get; set; }
public DbSet<Curso> Cursos { get; set; }
public DbSet<Professor> Professores { get; set; }
public DbSet<Usuario> Usuarios { get; set; }

Listagem 12 – Mapeamento das entidades POCO

Na classe DataContext, como visto anteriormente, fizemos um override no método OnModelCreating, nele vamos realizar algumas configurações que serão válidas para todo o contexto, como definição de nomenclatura das tabelas, definição de chave primária, entre outras.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    //Aqui vamos remover a pluralização padrão do Etity Framework que é em inglês
    modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
 
    /*Desabilitamos o delete em cascata em relacionamentos 1:N evitando
     ter registros filhos     sem registros pai*/
    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
 
    //Basicamente a mesma configuração, porém em relacionamenos N:N
    modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
 
   /*Toda propriedade do tipo string na entidade POCO
    seja configurado como VARCHAR no SQL Server*/
    modelBuilder.Properties<string>()
              .Configure(p => p.HasColumnType("varchar"));
 
     /*Toda propriedade do tipo string na entidade POCO seja configurado como VARCHAR (150) no banco de dados */
     modelBuilder.Properties<string>()
            .Configure(p => p.HasMaxLength(150));
 
  /*Definimos usando reflexão que toda propriedade que contenha
 "Nome da classe" + Id como "CursoId" por exemplo, seja dada como
 chave primária, caso não tenha sido especificado*/
    modelBuilder.Properties()
       .Where(p => p.Name == p.ReflectedType.Name + "Id")
       .Configure(p => p.IsKey());
}

Listagem 13 – Método OnModelCreating

Depois de toda a configuração do contexto no Entity Framework visto na primeira parte do artigo, vamos focar nas entidades POCO e construir os relacionamentos entre elas com a abordagem Fluent Api.

Antes de montar as relações de tabela, no projeto EntityFramewokEscola.DataAccess adicione uma pasta chamada Map, nessa pasta vamos criar as classes de mapeamento.

Vamos iniciar já com um relacionamento N:N: Curso – Professor

Regra: Um Curso pode ser ministrado por um ou muitos professores e um professor pode ministrar um ou muitos cursos.

Dentro da pasta Map, crie uma classe chamada CursoMap, a mesma deverá ficar como abaixo:

using EntityFrameworkEscola.Domain.Entities;
using System.Data.Entity.ModelConfiguration;
 
namespace EntityFrameworkEscola.DataAccess.Map
{
        public class CursoMap : EntityTypeConfiguration<Curso>
        {
              public CursoMap()
              {
                  /*O método ToTable define qual o nome que será
                  dado a tabela no banco de dados*/
                  ToTable("Curso");
 
                  //Definimos a propriedade CursoId como chave primária
                  HasKey(x => x.CursoId);
 
                  //Descricao terá no máximo 150 caracteres e será um campo "NOT NULL"
                  Property(x => x.Descricao).HasMaxLength(150).IsRequired();
 
                  HasMany(x => x.ProfessorLista)
                            .WithMany(x => x.CursoLista)
                            .Map(m =>
                            {
                               m.MapLeftKey("CursoId");
                               m.MapRightKey("ProfessorId");
                               m.ToTable("CursoProfessor");
                            });
                }
          }
}

Vamos entender o que significa todos esses métodos. Como falamos na primeira parte, a entidade POCO lá no projeto Domain continua “limpa” sem a necessidade de nenhuma referência externa, já no projeto DataAccess, estamos fazendo todo o mapeamento das classes que no SQL Server serão espelhadas como tabelas.

Queria chamar a atenção em um detalhe importante, como vimos na MER, quando temos um relacionamento N:N, sempre teremos que ter uma terceira tabela para relacionar os ID’s dos participantes, como estamos usando um ORM, que tem como premissa trazer o contexto de banco de dados para a POO (Programação Orientada a Objeto), não precisamos criar uma terceira classe chamada CursoProfessor para estar em conformidade com a MER, basta definir a terceira tabela através de métodos encadeados.

/*Curso tem uma lista de Professores*/
 HasMany(x => x.ProfessorLista)
   .WithMany(x => x.CursoLista) //...e professores uma lista de Cursos
   .Map(m => //esse relacionamento será mapeado em uma terceira tabela
      {
         m.MapLeftKey("CursoId"); //chave da esquerda será de CursoId
         m.MapRightKey("ProfessorId"); //Chave da direita será ProfessorId
         m.ToTable("CursoProfessor");// e o nome da tabela será CursoProfessor
      });

Obs: Na verdade, mesmo sem nenhum mapeamento, o Entity Framework tem a inteligência de saber quando duas entidades POCO possuem listas entre si, ele automaticamente cria a terceira tabela, porém vai criar com a nomenclatura padrão. Usando o mapeamento imperativo no C#, definimos o nosso padrão de nomenclatura de campos e tabela.

Na pasta Map, crie uma classe chamada ProfessorMap e veja como a mesma deve estar:

using EntityFrameworkEscola.Domain.Entities;
using System.Data.Entity.ModelConfiguration;
 
namespace EntityFrameworkEscola.DataAccess.Map
{
      public class ProfessorMap : EntityTypeConfiguration<Professor>
      {
          public ProfessorMap()
          {
              ToTable("Professor");
              HasKey(x => x.ProfessorId);
              Property(x => x.Nome).HasMaxLength(150).IsRequired();
          }
      }
}

Agora vamos configurar o relacionamento de Curso, Professor e Turma.

Regra: Uma Turma deve ter obrigatoriamente um Curso e um Professor que ministre aquele curso específico, assim como um professor e um curso pode estar em N Turmas, teremos um relacionamento 1:N.

Legal, crie uma classe dentro da pasta Map chamada TurmaMap e veja suas configurações:

using EntityFrameworkEscola.Domain.Entities;
using System.Data.Entity.ModelConfiguration;
 
namespace EntityFrameworkEscola.DataAccess.Map
{
     public class TurmaMap : EntityTypeConfiguration<Turma>
     {
         public TurmaMap()
         {
            ToTable("Turma");
            HasKey(x => x.TurmaId);
 
           /*Na entidade Turma, a propriedade do tipo Curso é obrigatória*/
            HasRequired(x => x.Curso)
               .WithMany() //Curso pode ter muitas turmas
               .Map(m => m.MapKey("CursoId"));//a chave estrangeira em Turma é CursoId
 
            /*Novamente, em Turma a propriedade do tipo Professor é requerida*/
            HasRequired(x => x.Professor)
               .WithMany(x => x.TurmaLista) //a classe Professor pode ter uma lista de Turma
               .Map(m => m.MapKey("ProfessorId"));//a chave estrangeira é ProfessorId
         }
     }
}

Continuando, crie uma classe chamada AlunoMap dentro da pasta Map.

Regra: Obrigatoriamente, um Aluno deve ter uma Turma e uma Turma pode conter um ou muitos Alunos.

Veja como a classe AlunoMap deve ficar:

using EntityFrameworkEscola.Domain.Entities;
using System.Data.Entity.ModelConfiguration;
 
namespace EntityFrameworkEscola.DataAccess.Map
{
     public class AlunoMap : EntityTypeConfiguration<Aluno>
     {
         public AlunoMap()
         {
            ToTable("Aluno");
            HasKey(x => x.AlunoId);
 
            //1:N - 1 aluno DEVE ter 1 turma e 1 turma pode ter muitos alunos
            HasRequired(x => x.Turma)
              .WithMany(x => x.AlunoLista)
              .Map(m => m.MapKey("TurmaId"));
 
            //1:1 - 1 aluno deve ter apenas 1 usuário
            HasRequired(x => x.Usuario)
              .WithRequiredPrincipal();
         }
     }
}

Regra: Quando um Aluno for criado, deve existir um Usuário e vice versa.

Crie uma classe chamada UsuarioMap dentro da pasta Map, a mesma deve ficar como abaixo:

using EntityFrameworkEscola.Domain.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Infrastructure.Annotations;
using System.Data.Entity.ModelConfiguration;
 
namespace EntityFrameworkEscola.DataAccess.Map
{
     public class UsuarioMap : EntityTypeConfiguration<Usuario>
     {
         public UsuarioMap()
         {
            ToTable("Usuario");
            HasKey(x => x.UsuarioId);
            Property(x => x.Email).IsRequired().HasMaxLength(150);
            Property(u => u.Senha).HasMaxLength(20).IsRequired();
          }
      }
}

Vá até o Package Manager Console e adicione o Migrations no projeto EntityFrameworkEscola.DataAccess com o seguinte comando:

PM> Enable-Migrations

No projeto EntityFrameworkEscola.DataAccess, será criado automaticamente uma pasta chamada Migrations, dentro da pasta terá uma classe chamadaConfiguration, habilite o versionamento automático da base de dados.

public Configuration()
{
     AutomaticMigrationsEnabled = true;
}

No método Seed, vamos inserir alguns dados iniciais…

protected override void Seed(EntityFrameworkEscola.DataAccess.DataContext context)
{
    Professor professor1 = new Professor();
    professor1.Nome = "Professor Um";
 
    Professor professor2 = new Professor();
    professor2.Nome = "Professor Dois";
 
    Curso curso1 = new Curso();
    curso1.Numero = "70-480";
    curso1.Descricao = "Programming in HTML5 with JavaScript and CSS3";
    curso1.ProfessorLista.Add(professor1);
    curso1.ProfessorLista.Add(professor2);
 
    Curso curso2 = new Curso();
    curso2.Numero = "70-486";
    curso2.Descricao = "Developing ASP.NET MVC 4 Web Applications";
    curso2.ProfessorLista.Add(professor2);
 
    context.Cursos.Add(curso1);
    context.Cursos.Add(curso2);
 
}

Volte a classe DataContext, no método OnModelCreating, vamos inserir o mapeamento das nossas entidades POCO**,** inclua dentro do método conforme a listagem abaixo:

     ...
 
modelBuilder.Configurations.Add(new AlunoMap());
modelBuilder.Configurations.Add(new TurmaMap());
modelBuilder.Configurations.Add(new CursoMap());
modelBuilder.Configurations.Add(new ProfessorMap());
modelBuilder.Configurations.Add(new UsuarioMap());

No projeto EntityFrameworkEscola.Console, na classe Program, veja como fica para testarmos tudo o que fizemos, coloque um break point e brinque um pouco para melhor compreensão:

using EntityFrameworkEscola.DataAccess;
using EntityFrameworkEscola.Domain.Entities;
using System;
using System.Linq;
 
namespace EntityFrameworkEscola
{
      class Program
      {
            static void Main(string[] args)
            {
                DataContext db = new DataContext();
 
                var cursos = db.Cursos
                        .Include("ProfessorLista")
                        .ToList();
 
                var curso = cursos.FirstOrDefault(x => x.Numero == "70-486");
                var professor = curso.ProfessorLista.FirstOrDefault(x => x.ProfessorId == 2);
 
                Turma turma = new Turma();
                turma.DataInicio = DateTime.Now;
                turma.DataTermino = DateTime.Now.AddDays(10);
                turma.Curso = curso;
                turma.Professor = professor;
 
                Aluno aluno = new Aluno();
                aluno.Nome = "Diego Neves";
 
                Usuario usuario = new Usuario();
                usuario.Email = "[email protected]";
                usuario.Senha = "123456";
 
                aluno.Usuario = usuario;
 
                Aluno aluno2 = new Aluno();
                aluno2.Nome = "Raquel Mota";
 
                Usuario usuario2 = new Usuario();
                usuario2.Email = "[email protected]";
                usuario2.Senha = "123456";
 
                aluno2.Usuario = usuario2;
 
                turma.AlunoLista.Add(aluno);
                turma.AlunoLista.Add(aluno2);
 
                db.Turmas.Add(turma);
                db.SaveChanges();
 
                var turmas = db.Turmas
                     .Include("Professor")
                     .Include("Curso")
                     .Include("AlunoLista")
                     .ToList();
 
                var usuarios = db.Usuarios
                             .ToList();
 
                foreach (var turmaItem in turmas)
                {
                     Console.WriteLine("Turma: "+ turmaItem.Curso.Descricao);
                     Console.WriteLine("Início: " + turmaItem.DataInicio.ToShortDateString());
                     Console.WriteLine("Previsto: " + turmaItem.DataTermino.ToShortDateString());
                     Console.WriteLine("Professor: "+ turmaItem.Professor.Nome);
                     Console.WriteLine("------------------------ Alunos Matriculados ------------------------");
                foreach (var alunoItem in turmaItem.AlunoLista)
                {
                    Console.WriteLine(String.Format("Nome: {0} - E-mail: {1}", alunoItem.Nome, usuarios.FirstOrDefault(x => x.UsuarioId == alunoItem.AlunoId).Email));
 }
                    Console.WriteLine(Environment.NewLine);
                }
 
                Console.ReadKey();
 
          }
      }
}

Projeto no GitHub

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment