Entity Framework - API fluida

Fluent API es una forma avanzada de especificar la configuración del modelo que cubre todo lo que pueden hacer las anotaciones de datos, además de alguna configuración más avanzada que no es posible con las anotaciones de datos. Las anotaciones de datos y la API fluida se pueden usar juntas, pero Code First da prioridad a la API fluida> anotaciones de datos> convenciones predeterminadas.

  • La API fluida es otra forma de configurar sus clases de dominio.

  • A la API Code First Fluent se accede con mayor frecuencia anulando el método OnModelCreating en su DbContext derivado.

  • Fluent API proporciona más funciones de configuración que DataAnnotations. Fluent API admite los siguientes tipos de asignaciones.

En este capítulo, continuaremos con el ejemplo simple que contiene las clases de Estudiante, Curso e Inscripción y una clase de contexto con el nombre MyContext como se muestra en el siguiente código.

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 acceder a la API de Fluent, debe anular el método OnModelCreating en DbContext. Echemos un vistazo a un ejemplo simple en el que cambiaremos el nombre de la columna en la tabla de estudiantes de FirstMidName a FirstName como se muestra en el siguiente código.

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 se utiliza para asignar clases CLR a un esquema de base de datos. Es la clase principal y en la que puede configurar todas sus clases de dominio. Este enfoque centrado en el código para crear un modelo de datos de entidad (EDM) se conoce como Code First.

Fluent API proporciona varios métodos importantes para configurar entidades y sus propiedades para anular varias convenciones de Code First. A continuación hay algunos de ellos.

No Señor. Nombre y descripción del método
1

ComplexType<TComplexType>

Registra un tipo como tipo complejo en el modelo y devuelve un objeto que se puede utilizar para configurar el tipo complejo. Este método se puede llamar varias veces para que el mismo tipo realice varias líneas de configuración.

2

Entity<TEntityType>

Registra un tipo de entidad como parte del modelo y devuelve un objeto que se puede utilizar para configurar la entidad. Este método se puede llamar varias veces para que la misma entidad realice varias líneas de configuración.

3

HasKey<TKey>

Configura las propiedades de la clave principal para este tipo de entidad.

4

HasMany<TTargetEntity>

Configura muchas relaciones de este tipo de entidad.

5

HasOptional<TTargetEntity>

Configura una relación opcional de este tipo de entidad. Las instancias del tipo de entidad podrán guardarse en la base de datos sin que se especifique esta relación. La clave externa en la base de datos será anulable.

6

HasRequired<TTargetEntity>

Configura una relación requerida de este tipo de entidad. Las instancias del tipo de entidad no se podrán guardar en la base de datos a menos que se especifique esta relación. La clave externa de la base de datos no admitirá nulos.

7

Ignore<TProperty>

Excluye una propiedad del modelo para que no se asigne a la base de datos. (Heredado de StructuralTypeConfiguration <TStructuralType>)

8

Property<T>

Configura una propiedad de estructura definida en este tipo. (Heredado de StructuralTypeConfiguration <TStructuralType>)

9

ToTable(String)

Configura el nombre de la tabla a la que se asigna este tipo de entidad.

La API fluida le permite configurar sus entidades o sus propiedades, ya sea que desee cambiar algo sobre cómo se asignan a la base de datos o cómo se relacionan entre sí. Existe una gran variedad de mapeos y modelado en los que puede influir utilizando las configuraciones. Los siguientes son los principales tipos de mapeo que admite Fluent API:

  • Mapeo de entidades
  • Mapeo de propiedades

Mapeo de entidades

El mapeo de entidades son solo algunos mapeos simples que afectarán la comprensión de Entity Framework sobre cómo se asignan las clases a las bases de datos. Todo esto lo discutimos en anotaciones de datos y aquí veremos cómo lograr lo mismo usando Fluent API.

  • Entonces, en lugar de ir a las clases de dominio para agregar estas configuraciones, podemos hacerlo dentro del contexto.

  • Lo primero es anular el método OnModelCreating, que permite trabajar con modelBuilder.

Esquema predeterminado

El esquema predeterminado es dbo cuando se genera la base de datos. Puede utilizar el método HasDefaultSchema en DbModelBuilder para especificar el esquema de base de datos que se utilizará para todas las tablas, procedimientos almacenados, etc.

Echemos un vistazo al siguiente ejemplo en el que se aplica el esquema de administración.

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; }
}

Asignar entidad a tabla

Con la convención predeterminada, Code First creará las tablas de la base de datos con el nombre de las propiedades de DbSet en la clase de contexto, como Cursos, Inscripciones y Estudiantes. Pero si desea diferentes nombres de tabla, puede anular esta convención y puede proporcionar un nombre de tabla diferente al de las propiedades de DbSet, como se muestra en el siguiente código.

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");
}

Cuando se genera la base de datos, verá el nombre de las tablas como se especifica en el método OnModelCreating.

División de entidades (Asignar entidad a varias tablas)

Entity Splitting le permite combinar datos provenientes de múltiples tablas en una sola clase y solo se puede usar con tablas que tienen una relación uno a uno entre ellas. Echemos un vistazo al siguiente ejemplo en el que la información del alumno se asigna en dos tablas.

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");
}

En el código anterior, puede ver que la entidad Student se divide en las dos tablas siguientes al asignar algunas propiedades a la tabla StudentData y algunas propiedades a la tabla StudentEnrollmentInfo utilizando el método Map.

  • StudentData - Contiene el nombre y apellido del estudiante.

  • StudentEnrollmentInfo - Contiene EnrollmentDate.

Cuando se genera la base de datos, verá las siguientes tablas en su base de datos como se muestra en la siguiente imagen.

Mapeo de propiedades

El método Property se utiliza para configurar atributos para cada propiedad perteneciente a una entidad o tipo complejo. El método Property se utiliza para obtener un objeto de configuración para una propiedad determinada. También puede mapear y configurar las propiedades de sus clases de dominio usando Fluent API.

Configurar una clave principal

La convención predeterminada para las claves primarias es:

  • La clase define una propiedad cuyo nombre es "ID" o "Id"
  • Nombre de la clase seguido de "ID" o "Id"

Si su clase no sigue las convenciones predeterminadas para la clave principal, como se muestra en el siguiente código de la clase Student:

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; }
}

Luego, para establecer explícitamente una propiedad para que sea una clave principal, puede usar el método HasKey como se muestra en el siguiente código:

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

Configurar columna

En Entity Framework, de forma predeterminada, Code First creará una columna para una propiedad con el mismo nombre, orden y tipo de datos. Pero también puede anular esta convención, como se muestra en el siguiente código.

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 la propiedad MaxLength

En el siguiente ejemplo, la propiedad Título del curso no debe tener más de 24 caracteres. Cuando el usuario especifica un valor de más de 24 caracteres, el usuario obtendrá una excepción DbEntityValidationException.

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

Configurar la propiedad Null o NotNull

En el siguiente ejemplo, se requiere la propiedad Título del curso, por lo que se usa el método IsRequired para crear la columna NotNull. De manera similar, Student EnrollmentDate es opcional, por lo que usaremos el método IsOptional para permitir un valor nulo en esta columna como se muestra en el siguiente código.

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"); 
}

Configurar relaciones

Una relación, en el contexto de las bases de datos, es una situación que existe entre dos tablas de bases de datos relacionales, cuando una tabla tiene una clave externa que hace referencia a la clave principal de la otra tabla. Cuando trabaja con Code First, define su modelo definiendo las clases CLR de su dominio. De forma predeterminada, Entity Framework usa las convenciones de Code First para asignar sus clases al esquema de la base de datos.

  • Si usa las convenciones de nomenclatura de Code First, en la mayoría de los casos puede confiar en Code First para configurar las relaciones entre sus tablas según las claves externas y las propiedades de navegación.

  • Si no cumplen con esas convenciones, también hay configuraciones que puede usar para afectar las relaciones entre clases y cómo esas relaciones se realizan en la base de datos cuando agrega configuraciones en Code First.

  • Algunos de ellos están disponibles en las anotaciones de datos y puede aplicar algunos aún más complicados con una API Fluent.

Configurar la relación uno a uno

Cuando define una relación uno a uno en su modelo, usa una propiedad de navegación de referencia en cada clase. En la base de datos, ambas tablas pueden tener solo un registro en cada lado de la relación. Cada valor de clave principal se relaciona con un solo registro (o ningún registro) en la tabla relacionada.

  • Se crea una relación uno a uno si ambas columnas relacionadas son claves primarias o tienen restricciones únicas.

  • En una relación uno a uno, la clave principal actúa además como clave externa y no hay una columna de clave externa separada para ninguna de las tablas.

  • Este tipo de relación no es común porque la mayor parte de la información relacionada de esta manera estaría toda en una tabla.

Echemos un vistazo al siguiente ejemplo donde agregaremos otra clase a nuestro modelo para crear una relación uno a uno.

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 puede ver en el código anterior, los atributos Key y ForeignKey se utilizan para la propiedad ID en la clase StudentLogIn, para marcarla como Clave principal y como Clave externa.

Para configurar una relación uno a cero o una relación entre Student y StudentLogIn utilizando Fluent API, debe anular el método OnModelCreating como se muestra en el siguiente código.

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
}

En la mayoría de los casos, Entity Framework puede inferir qué tipo es el dependiente y cuál es el principal en una relación. Sin embargo, cuando se requieren ambos extremos de la relación o ambos lados son opcionales, Entity Framework no puede identificar al dependiente y al principal. Cuando se requieren ambos extremos de la relación, puede usar HasRequired como se muestra en el siguiente código.

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);  
}

Cuando se genera la base de datos, verá que se crea la relación como se muestra en la siguiente imagen.

Configurar la relación de uno a varios

La tabla de clave primaria contiene solo un registro que se relaciona con ninguno, uno o muchos registros en la tabla relacionada. Este es el tipo de relación más utilizado.

  • En este tipo de relación, una fila de la tabla A puede tener muchas filas coincidentes en la tabla B, pero una fila de la tabla B solo puede tener una fila coincidente en la tabla A.

  • La clave externa se define en la tabla que representa los muchos extremos de la relación.

  • Por ejemplo, en el diagrama anterior, las tablas de estudiantes y matrículas tienen una relación de uno a muchos, cada estudiante puede tener muchas matrículas, pero cada matrícula pertenece a un solo estudiante.

A continuación se muestran el estudiante y la inscripción, que tiene una relación de uno a varios, pero la clave externa en la tabla de inscripción no sigue las convenciones predeterminadas de 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; }
}

En este caso, para configurar la relación de uno a varios usando la API Fluent, debe usar el método HasForeignKey como se muestra en el siguiente código.

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);  
}

Cuando se genera la base de datos, verá que la relación se crea como se muestra en la siguiente imagen.

En el ejemplo anterior, el método HasRequired especifica que la propiedad de navegación del estudiante debe ser nula. Por lo tanto, debe asignar la entidad Estudiante con inscripción cada vez que agregue o actualice la inscripción. Para manejar esto, necesitamos usar el método HasOptional en lugar del método HasRequired.

Configurar la relación de varios a varios

Cada registro en ambas tablas puede relacionarse con cualquier número de registros (o ningún registro) en la otra tabla.

  • Puede crear una relación de este tipo definiendo una tercera tabla, llamada tabla de unión, cuya clave principal consta de las claves externas de la tabla A y la tabla B.

  • Por ejemplo, la tabla Student y la tabla Course tienen una relación de varios a varios.

A continuación se muestran las clases de Estudiante y Curso en las que Estudiante y Curso tienen una relación de muchos a muchos, porque ambas clases tienen propiedades de navegación Estudiantes y Cursos que son colecciones. En otras palabras, una entidad tiene otra colección 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 la relación de varios a varios entre el alumno y el curso, puede utilizar la API de Fluent como se muestra en el siguiente código.

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);
}

Las convenciones predeterminadas de Code First se utilizan para crear una tabla de combinación cuando se genera la base de datos. Como resultado, la tabla StudentCourses se crea con las columnas Course_CourseID y Student_ID como se muestra en la siguiente imagen.

Si desea especificar el nombre de la tabla de unión y los nombres de las columnas de la tabla, debe realizar una configuración adicional mediante el 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");
   }); 
}

Puede ver cuándo se genera la base de datos, la tabla y el nombre de las columnas se crean como se especifica en el código anterior.

Le recomendamos que ejecute el ejemplo anterior paso a paso para una mejor comprensión.