Entity Framework - API Fluent

Fluent API é uma maneira avançada de especificar a configuração do modelo que cobre tudo o que as anotações de dados podem fazer, além de algumas configurações mais avançadas que não são possíveis com as anotações de dados. Anotações de dados e a API fluente podem ser usadas juntas, mas Code First dá preferência a API Fluent> anotações de dados> convenções padrão.

  • A API Fluent é outra maneira de configurar suas classes de domínio.

  • A API Code First Fluent é mais comumente acessada substituindo o método OnModelCreating em seu DbContext derivado.

  • A API Fluent fornece mais funcionalidade para configuração do que DataAnnotations. A API Fluent oferece suporte aos seguintes tipos de mapeamento.

Neste capítulo, continuaremos com o exemplo simples que contém as classes Student, Course e Enrollment e uma classe de contexto com o nome MyContext conforme mostrado no código a seguir.

using System.Data.Entity; 
using System.Linq; 
using System.Text;
using System.Threading.Tasks;  

namespace EFCodeFirstDemo {

   class Program {
      static void Main(string[] args) {}
   }
   
   public enum Grade {
      A, B, C, D, F
   }

   public class Enrollment {
      public int EnrollmentID { get; set; }
      public int CourseID { get; set; }
      public int StudentID { get; set; }
      public Grade? Grade { get; set; }
		
      public virtual Course Course { get; set; }
      public virtual Student Student { get; set; }
   }

   public class Student {
      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
		
      public DateTime EnrollmentDate { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class Course {
      public int CourseID { get; set; }
      public string Title { get; set; }
      public int Credits { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class MyContext : DbContext {
      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
   }

}

Para acessar a API Fluent, você precisa substituir o método OnModelCreating em DbContext. Vamos dar uma olhada em um exemplo simples no qual renomearemos o nome da coluna na tabela do aluno de FirstMidName para FirstName, conforme mostrado no código a seguir.

public class MyContext : DbContext {

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
      .HasColumnName("FirstName");}

      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
}

DbModelBuilder é usado para mapear classes CLR para um esquema de banco de dados. É a classe principal e na qual você pode configurar todas as suas classes de domínio. Essa abordagem centrada no código para construir um Entity Data Model (EDM) é conhecida como Code First.

A API Fluent fornece vários métodos importantes para configurar entidades e suas propriedades para substituir várias convenções Code First. Abaixo estão alguns deles.

Sr. Não. Nome e descrição do método
1

ComplexType<TComplexType>

Registra um tipo como um tipo complexo no modelo e retorna um objeto que pode ser usado para configurar o tipo complexo. Este método pode ser chamado várias vezes para o mesmo tipo para executar várias linhas de configuração.

2

Entity<TEntityType>

Registra um tipo de entidade como parte do modelo e retorna um objeto que pode ser usado para configurar a entidade. Este método pode ser chamado várias vezes para a mesma entidade para executar várias linhas de configuração.

3

HasKey<TKey>

Configura a (s) propriedade (s) de chave primária para este tipo de entidade.

4

HasMany<TTargetEntity>

Configura muitos relacionamentos a partir deste tipo de entidade.

5

HasOptional<TTargetEntity>

Configura um relacionamento opcional a partir deste tipo de entidade. As instâncias do tipo de entidade poderão ser salvas no banco de dados sem que esse relacionamento seja especificado. A chave estrangeira no banco de dados será anulável.

6

HasRequired<TTargetEntity>

Configura um relacionamento necessário a partir deste tipo de entidade. As instâncias do tipo de entidade não poderão ser salvas no banco de dados, a menos que esse relacionamento seja especificado. A chave estrangeira no banco de dados não será anulável.

7

Ignore<TProperty>

Exclui uma propriedade do modelo para que ela não seja mapeada para o banco de dados. (Herdado de StructuralTypeConfiguration <TStructuralType>)

8

Property<T>

Configura uma propriedade de estrutura que é definida neste tipo. (Herdado de StructuralTypeConfiguration <TStructuralType>)

9

ToTable(String)

Configura o nome da tabela para a qual este tipo de entidade está mapeado.

A API Fluent permite que você configure suas entidades ou suas propriedades, se você deseja alterar algo sobre como elas são mapeadas para o banco de dados ou como elas se relacionam umas com as outras. Há uma grande variedade de mapeamentos e modelagem que você pode impactar usando as configurações. A seguir estão os principais tipos de mapeamento que a API Fluent suporta -

  • Mapeamento de Entidade
  • Mapeamento de Propriedades

Mapeamento de Entidade

O mapeamento de entidades é apenas alguns mapeamentos simples que afetarão a compreensão do Entity Framework de como as classes são mapeadas para os bancos de dados. Discutimos tudo isso em anotações de dados e aqui veremos como conseguir as mesmas coisas usando a API Fluent.

  • Portanto, em vez de entrar nas classes de domínio para adicionar essas configurações, podemos fazer isso dentro do contexto.

  • A primeira coisa é substituir o método OnModelCreating, que fornece o modelBuilder para trabalhar.

Esquema Padrão

O esquema padrão é dbo quando o banco de dados é gerado. Você pode usar o método HasDefaultSchema em DbModelBuilder para especificar o esquema do banco de dados a ser usado para todas as tabelas, procedimentos armazenados, etc.

Vamos dar uma olhada no exemplo a seguir no qual o esquema de administração é aplicado.

public class MyContext : DbContext {
   public MyContext() : base("name = MyContextDB") {}

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      //Configure default schema
      modelBuilder.HasDefaultSchema("Admin");
   }
	
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

Mapear Entidade para Tabela

Com a convenção padrão, o Code First criará as tabelas do banco de dados com o nome das propriedades DbSet na classe de contexto, como Cursos, Inscrições e Alunos. Mas se quiser nomes de tabela diferentes, você pode substituir essa convenção e pode fornecer um nome de tabela diferente das propriedades DbSet, conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Map entity to table
   modelBuilder.Entity<Student>().ToTable("StudentData");
   modelBuilder.Entity<Course>().ToTable("CourseDetail");
   modelBuilder.Entity<Enrollment>().ToTable("EnrollmentInfo");
}

Quando o banco de dados for gerado, você verá o nome das tabelas conforme especificado no método OnModelCreating.

Divisão de Entidade (Mapear Entidade para Tabela Múltipla)

A divisão de entidades permite combinar dados provenientes de várias tabelas em uma única classe e só pode ser usada com tabelas que têm uma relação um-para-um entre elas. Vamos dar uma olhada no exemplo a seguir, no qual as informações do aluno são mapeadas em duas tabelas.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Map entity to table
   modelBuilder.Entity<Student>().Map(sd ⇒ {
      sd.Properties(p ⇒ new { p.ID, p.FirstMidName, p.LastName });
      sd.ToTable("StudentData");
   })

   .Map(si ⇒ {
      si.Properties(p ⇒ new { p.ID, p.EnrollmentDate });
      si.ToTable("StudentEnrollmentInfo");
   });

   modelBuilder.Entity<Course>().ToTable("CourseDetail");
   modelBuilder.Entity<Enrollment>().ToTable("EnrollmentInfo");
}

No código acima, você pode ver que a entidade Student é dividida nas duas tabelas a seguir, mapeando algumas propriedades para a tabela StudentData e algumas propriedades para a tabela StudentEnrollmentInfo usando o método Map.

  • StudentData - Contém o nome e o sobrenome do aluno.

  • StudentEnrollmentInfo - Contém data de inscrição.

Quando o banco de dados é gerado, você vê as seguintes tabelas em seu banco de dados, conforme mostrado na imagem a seguir.

Mapeamento de Propriedades

O método Property é usado para configurar atributos para cada propriedade pertencente a uma entidade ou tipo complexo. O método Property é usado para obter um objeto de configuração para uma determinada propriedade. Você também pode mapear e configurar as propriedades de suas classes de domínio usando a API Fluent.

Configurando uma chave primária

A convenção padrão para chaves primárias é -

  • Classe define uma propriedade cujo nome é “ID” ou “Id”
  • Nome da classe seguido por “ID” ou “Id”

Se sua classe não segue as convenções padrão para a chave primária, conforme mostrado no seguinte código da classe Aluno -

public class Student {
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Então, para definir explicitamente uma propriedade como uma chave primária, você pode usar o método HasKey conforme mostrado no código a seguir -

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
	
   // Configure Primary Key
   modelBuilder.Entity<Student>().HasKey<int>(s ⇒ s.StdntID); 
}

Configurar coluna

No Entity Framework, por padrão, o Code First criará uma coluna para uma propriedade com o mesmo nome, ordem e tipo de dados. Mas você também pode substituir essa convenção, conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Configure EnrollmentDate Column
   modelBuilder.Entity<Student>().Property(p ⇒ p.EnrollmentDate)
	
   .HasColumnName("EnDate")
   .HasColumnType("DateTime")
   .HasColumnOrder(2);
}

Configurar propriedade MaxLength

No exemplo a seguir, a propriedade Título do curso não deve ter mais de 24 caracteres. Quando o usuário especifica um valor com mais de 24 caracteres, o usuário obtém uma exceção DbEntityValidationException.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
   modelBuilder.Entity<Course>().Property(p ⇒ p.Title).HasMaxLength(24);
}

Configurar propriedade nula ou não nula

No exemplo a seguir, a propriedade Course Title é necessária, portanto, o método IsRequired é usado para criar a coluna NotNull. Da mesma forma, Student EnrollmentDate é opcional, portanto, usaremos o método IsOptional para permitir um valor nulo nesta coluna, conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
   modelBuilder.Entity<Course>().Property(p ⇒ p.Title).IsRequired();
   modelBuilder.Entity<Student>().Property(p ⇒ p.EnrollmentDate).IsOptional();
	
   //modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
   //.HasColumnName("FirstName"); 
}

Configurando Relacionamentos

Um relacionamento, no contexto de bancos de dados, é uma situação que existe entre duas tabelas de banco de dados relacional, quando uma tabela possui uma chave estrangeira que faz referência à chave primária da outra tabela. Ao trabalhar com Code First, você define seu modelo definindo suas classes CLR de domínio. Por padrão, o Entity Framework usa as convenções Code First para mapear suas classes para o esquema do banco de dados.

  • Se você usar as convenções de nomenclatura Code First, na maioria dos casos você pode contar com Code First para configurar relacionamentos entre suas tabelas com base nas chaves estrangeiras e propriedades de navegação.

  • Se eles não atenderem a essas convenções, também existem configurações que você pode usar para impactar os relacionamentos entre as classes e como esses relacionamentos são realizados no banco de dados quando você adiciona configurações no Code First.

  • Alguns deles estão disponíveis nas anotações de dados e você pode aplicar alguns ainda mais complicados com uma API Fluent.

Configurar relacionamento um para um

Ao definir um relacionamento um para um em seu modelo, você usa uma propriedade de navegação de referência em cada classe. No banco de dados, ambas as tabelas podem ter apenas um registro em cada lado do relacionamento. Cada valor de chave primária está relacionado a apenas um registro (ou nenhum registro) na tabela relacionada.

  • Um relacionamento um para um será criado se ambas as colunas relacionadas forem chaves primárias ou tiverem restrições exclusivas.

  • Em um relacionamento um-para-um, a chave primária atua adicionalmente como uma chave estrangeira e não há coluna de chave estrangeira separada para nenhuma das tabelas.

  • Esse tipo de relacionamento não é comum porque a maioria das informações relacionadas dessa forma estariam todas em uma tabela.

Vamos dar uma olhada no exemplo a seguir, onde adicionaremos outra classe em nosso modelo para criar um relacionamento um para um.

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual StudentLogIn StudentLogIn { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class StudentLogIn {
   [Key, ForeignKey("Student")]
   public int ID { get; set; }
   public string EmailID { get; set; }
   public string Password { get; set; }
	
   public virtual Student Student { get; set; }
}

Como você pode ver no código acima, os atributos Key e ForeignKey são usados ​​para a propriedade ID na classe StudentLogIn, a fim de marcá-la como Chave Primária e também como Chave Estrangeira.

Para configurar um para zero ou um relacionamento entre Student e StudentLogIn usando a API Fluent, você precisa substituir o método OnModelCreating conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure ID as PK for StudentLogIn
   modelBuilder.Entity<StudentLogIn>()
   .HasKey(s ⇒ s.ID);

   // Configure ID as FK for StudentLogIn
   modelBuilder.Entity<Student>()
   
   .HasOptional(s ⇒ s.StudentLogIn) //StudentLogIn is optional
   .WithRequired(t ⇒ t.Student); // Create inverse relationship
}

Na maioria dos casos, o Entity Framework pode inferir qual tipo é o dependente e qual é o principal em um relacionamento. No entanto, quando ambas as extremidades do relacionamento são necessárias ou ambos os lados são opcionais, o Entity Framework não pode identificar o dependente e o principal. Quando ambas as extremidades do relacionamento são necessárias, você pode usar HasRequired conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure ID as PK for StudentLogIn
   modelBuilder.Entity<StudentLogIn>()
   .HasKey(s ⇒ s.ID);

   // Configure ID as FK for StudentLogIn
   modelBuilder.Entity<Student>()
   .HasRequired(r ⇒ r.Student)
   .WithOptional(s ⇒ s.StudentLogIn);  
}

Quando o banco de dados for gerado, você verá que o relacionamento é criado conforme mostrado na imagem a seguir.

Configurar relacionamento um-para-muitos

A tabela de chave primária contém apenas um registro relacionado a nenhum, um ou muitos registros na tabela relacionada. Este é o tipo de relacionamento mais comumente usado.

  • Nesse tipo de relacionamento, uma linha na tabela A pode ter muitas linhas correspondentes na tabela B, mas uma linha na tabela B pode ter apenas uma linha correspondente na tabela A.

  • A chave estrangeira é definida na tabela que representa a extremidade múltipla do relacionamento.

  • Por exemplo, no diagrama acima, as tabelas Aluno e Matrícula têm uma relação de um para muitos, cada aluno pode ter muitas matrículas, mas cada inscrição pertence a apenas um aluno.

Abaixo estão o Aluno e a Inscrição, que têm relação um-para-muitos, mas a chave estrangeira na tabela de Inscrição não segue as convenções padrão do Code First.

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
	
   //StdntID is not following code first conventions name
   public int StdntID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual StudentLogIn StudentLogIn { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Nesse caso, para configurar o relacionamento um para muitos usando a API Fluent, você precisa usar o método HasForeignKey conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Configure FK for one-to-many relationship
   modelBuilder.Entity<Enrollment>()

   .HasRequired<Student>(s ⇒ s.Student)
   .WithMany(t ⇒ t.Enrollments)
   .HasForeignKey(u ⇒ u.StdntID);  
}

Quando o banco de dados for gerado, você verá que o relacionamento é criado conforme mostrado na imagem a seguir.

No exemplo acima, o método HasRequired especifica que a propriedade de navegação do Aluno deve ser Nula. Portanto, você deve atribuir a entidade Aluno com Inscrição sempre que adicionar ou atualizar Inscrição. Para lidar com isso, precisamos usar o método HasOptional em vez do método HasRequired.

Configurar relacionamento muitos para muitos

Cada registro em ambas as tabelas pode se relacionar a qualquer número de registros (ou nenhum registro) na outra tabela.

  • Você pode criar tal relacionamento definindo uma terceira tabela, chamada de tabela de junção, cuja chave primária consiste nas chaves estrangeiras da tabela A e da tabela B.

  • Por exemplo, a tabela Aluno e a tabela Curso têm relacionamento muitos para muitos.

A seguir estão as classes Aluno e Curso nas quais Aluno e Curso têm relação de muitos para muitos, porque ambas as classes têm propriedades de navegação Alunos e Cursos que são coleções. Em outras palavras, uma entidade possui outra coleção de entidades.

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Course> Courses { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Student> Students { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Para configurar o relacionamento muitos-para-muitos entre o Aluno e o Curso, você pode usar a API Fluent conforme mostrado no código a seguir.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure many-to-many relationship
   modelBuilder.Entity<Student>()
   .HasMany(s ⇒ s.Courses) 
   .WithMany(s ⇒ s.Students);
}

As convenções padrão do Code First são usadas para criar uma tabela de junção quando o banco de dados é gerado. Como resultado, a tabela StudentCourses é criada com as colunas Course_CourseID e Student_ID, conforme mostrado na imagem a seguir.

Se você deseja especificar o nome da tabela de junção e os nomes das colunas na tabela, você precisa fazer uma configuração adicional usando o método Map.

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure many-to-many relationship 
   modelBuilder.Entity<Student>()

   .HasMany(s ⇒ s.Courses)
   .WithMany(s ⇒ s.Students)
   
   .Map(m ⇒ {
      m.ToTable("StudentCoursesTable");
      m.MapLeftKey("StudentID");
      m.MapRightKey("CourseID");
   }); 
}

Você pode ver quando o banco de dados é gerado, a tabela e os nomes das colunas são criados conforme especificado no código acima.

Recomendamos que você execute o exemplo acima passo a passo para melhor compreensão.