Entity Framework: API fluente

L'API fluente è un modo avanzato per specificare la configurazione del modello che copre tutto ciò che le annotazioni dei dati possono fare oltre a una configurazione più avanzata non possibile con le annotazioni dei dati. Le annotazioni dei dati e l'API fluente possono essere utilizzate insieme, ma Code First dà la precedenza a API fluente> annotazioni dei dati> convenzioni predefinite.

  • L'API fluente è un altro modo per configurare le classi di dominio.

  • L'API Code First Fluent è più comunemente accessibile sovrascrivendo il metodo OnModelCreating nel tuo DbContext derivato.

  • L'API Fluent fornisce più funzionalità per la configurazione rispetto a DataAnnotations. L'API Fluent supporta i seguenti tipi di mapping.

In questo capitolo, continueremo con il semplice esempio che contiene le classi Student, Course e Enrollment e una classe di contesto con il nome MyContext come mostrato nel codice seguente.

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

}

Per accedere all'API Fluent è necessario eseguire l'override del metodo OnModelCreating in DbContext. Diamo un'occhiata a un semplice esempio in cui rinomineremo il nome della colonna nella tabella degli studenti da FirstMidName a FirstName come mostrato nel codice seguente.

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 viene utilizzato per mappare le classi CLR a uno schema di database. È la classe principale e su cui puoi configurare tutte le tue classi di dominio. Questo approccio incentrato sul codice alla creazione di un Entity Data Model (EDM) è noto come Code First.

L'API Fluent fornisce una serie di metodi importanti per configurare le entità e le sue proprietà per sovrascrivere varie convenzioni Code First. Di seguito sono riportati alcuni di loro.

Sr. No. Nome e descrizione del metodo
1

ComplexType<TComplexType>

Registra un tipo come tipo complesso nel modello e restituisce un oggetto che può essere utilizzato per configurare il tipo complesso. Questo metodo può essere chiamato più volte affinché lo stesso tipo esegua più righe di configurazione.

2

Entity<TEntityType>

Registra un tipo di entità come parte del modello e restituisce un oggetto che può essere utilizzato per configurare l'entità. Questo metodo può essere chiamato più volte affinché la stessa entità esegua più righe di configurazione.

3

HasKey<TKey>

Configura le proprietà della chiave primaria per questo tipo di entità.

4

HasMany<TTargetEntity>

Configura una relazione a molti da questo tipo di entità.

5

HasOptional<TTargetEntity>

Configura una relazione facoltativa da questo tipo di entità. Le istanze del tipo di entità potranno essere salvate nel database senza che venga specificata questa relazione. La chiave esterna nel database sarà nullable.

6

HasRequired<TTargetEntity>

Configura una relazione richiesta da questo tipo di entità. Le istanze del tipo di entità non potranno essere salvate nel database a meno che non venga specificata questa relazione. La chiave esterna nel database sarà non annullabile.

7

Ignore<TProperty>

Esclude una proprietà dal modello in modo che non venga mappata al database. (Ereditato da StructuralTypeConfiguration <TStructuralType>)

8

Property<T>

Configura una proprietà struct definita su questo tipo. (Ereditato da StructuralTypeConfiguration <TStructuralType>)

9

ToTable(String)

Configura il nome della tabella a cui è mappato questo tipo di entità.

L'API fluente ti consente di configurare le tue entità o le loro proprietà, sia che tu voglia cambiare qualcosa sul modo in cui si associano al database o su come si relazionano tra loro. Esiste un'enorme varietà di mappature e modelli su cui puoi influire utilizzando le configurazioni. Di seguito sono riportati i principali tipi di mappatura supportati da Fluent API:

  • Mappatura delle entità
  • Mappatura delle proprietà

Mappatura delle entità

Il mapping di entità è solo alcuni semplici mapping che avranno un impatto sulla comprensione di Entity Framework di come le classi vengono mappate ai database. Di tutto ciò abbiamo discusso nelle annotazioni dei dati e qui vedremo come ottenere le stesse cose utilizzando Fluent API.

  • Quindi, invece di entrare nelle classi di dominio per aggiungere queste configurazioni, possiamo farlo all'interno del contesto.

  • La prima cosa è sovrascrivere il metodo OnModelCreating, che consente a modelBuilder di lavorare.

Schema predefinito

Lo schema predefinito è dbo quando viene generato il database. È possibile utilizzare il metodo HasDefaultSchema su DbModelBuilder per specificare lo schema del database da utilizzare per tutte le tabelle, le stored procedure, ecc.

Diamo un'occhiata al seguente esempio in cui viene applicato lo schema di amministrazione.

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

Mappa entità su tabella

Con la convenzione predefinita, Code First creerà le tabelle del database con il nome delle proprietà DbSet nella classe di contesto come Courses, Enrollments e Students. Ma se si vogliono nomi di tabella diversi, è possibile ignorare questa convenzione e fornire un nome di tabella diverso rispetto alle proprietà DbSet, come illustrato nel codice seguente.

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 il database viene generato, vedrai il nome delle tabelle come specificato nel metodo OnModelCreating.

Divisione entità (mappa entità su più tabelle)

La suddivisione dell'entità consente di combinare i dati provenienti da più tabelle in una singola classe e può essere utilizzata solo con tabelle che hanno una relazione uno a uno tra di loro. Diamo un'occhiata al seguente esempio in cui le informazioni sugli studenti sono mappate in due tabelle.

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

Nel codice precedente, puoi vedere che l'entità Student è suddivisa nelle due tabelle seguenti mappando alcune proprietà alla tabella StudentData e alcune proprietà alla tabella StudentEnrollmentInfo utilizzando il metodo Map.

  • StudentData - Contiene Student FirstMidName e Last Name.

  • StudentEnrollmentInfo - Contiene EnrollmentDate.

Quando il database viene generato, vengono visualizzate le seguenti tabelle nel database, come mostrato nell'immagine seguente.

Mappatura delle proprietà

Il metodo Property viene utilizzato per configurare gli attributi per ciascuna proprietà appartenente a un'entità o un tipo complesso. Il metodo Property viene utilizzato per ottenere un oggetto di configurazione per una determinata proprietà. Puoi anche mappare e configurare le proprietà delle tue classi di dominio utilizzando Fluent API.

Configurazione di una chiave primaria

La convenzione predefinita per le chiavi primarie è:

  • La classe definisce una proprietà il cui nome è "ID" o "Id"
  • Nome della classe seguito da "ID" o "ID"

Se la tua classe non segue le convenzioni predefinite per la chiave primaria come mostrato nel seguente codice della classe 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; }
}

Quindi, per impostare esplicitamente una proprietà come chiave primaria, è possibile utilizzare il metodo HasKey come mostrato nel codice seguente:

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

Configura colonna

In Entity Framework, per impostazione predefinita Code First creerà una colonna per una proprietà con lo stesso nome, ordine e tipo di dati. Ma puoi anche ignorare questa convenzione, come mostrato nel codice seguente.

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

Configura proprietà MaxLength

Nell'esempio seguente, la proprietà Titolo del corso non dovrebbe contenere più di 24 caratteri. Quando l'utente specifica un valore più lungo di 24 caratteri, l'utente riceverà un'eccezione DbEntityValidationException.

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

Configurare la proprietà Null o NotNull

Nell'esempio seguente, la proprietà Titolo del corso è obbligatoria, quindi il metodo IsRequired viene utilizzato per creare la colonna NotNull. Allo stesso modo, Student EnrollmentDate è facoltativo, quindi useremo il metodo IsOptional per consentire un valore null in questa colonna, come mostrato nel codice seguente.

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

Configurazione delle relazioni

Una relazione, nel contesto dei database, è una situazione che esiste tra due tabelle di database relazionali, quando una tabella ha una chiave esterna che fa riferimento alla chiave primaria dell'altra tabella. Quando si lavora con Code First, si definisce il modello definendo le classi CLR del dominio. Per impostazione predefinita, Entity Framework usa le convenzioni Code First per mappare le classi allo schema del database.

  • Se si utilizzano le convenzioni di denominazione Code First, nella maggior parte dei casi è possibile fare affidamento su Code First per impostare le relazioni tra le tabelle in base alle chiavi esterne e alle proprietà di navigazione.

  • Se non soddisfano queste convenzioni, esistono anche configurazioni che puoi utilizzare per influire sulle relazioni tra le classi e sul modo in cui tali relazioni vengono realizzate nel database quando aggiungi configurazioni in Code First.

  • Alcuni di essi sono disponibili nelle annotazioni dei dati e puoi applicarne altri ancora più complicati con un'API Fluent.

Configurare la relazione uno a uno

Quando si definisce una relazione uno-a-uno nel modello, si utilizza una proprietà di navigazione di riferimento in ogni classe. Nel database, entrambe le tabelle possono avere un solo record su entrambi i lati della relazione. Ogni valore di chiave primaria si riferisce a un solo record (o nessun record) nella tabella correlata.

  • Viene creata una relazione uno a uno se entrambe le colonne correlate sono chiavi primarie o hanno vincoli univoci.

  • In una relazione uno-a-uno, la chiave primaria funge anche da chiave esterna e non esiste una colonna di chiave esterna separata per nessuna delle due tabelle.

  • Questo tipo di relazione non è comune perché la maggior parte delle informazioni correlate in questo modo sarebbero tutte in una tabella.

Diamo un'occhiata al seguente esempio in cui aggiungeremo un'altra classe al nostro modello per creare una relazione 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; }
}

Come puoi vedere nel codice sopra, gli attributi Key e ForeignKey vengono utilizzati per la proprietà ID nella classe StudentLogIn, al fine di contrassegnarla come chiave primaria oltre che come chiave esterna.

Per configurare una relazione da uno a zero o una relazione tra Student e StudentLogIn utilizzando l'API Fluent, è necessario eseguire l'override del metodo OnModelCreating come mostrato nel codice seguente.

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
}

Nella maggior parte dei casi, Entity Framework può dedurre quale tipo è dipendente e quale è l'entità in una relazione. Tuttavia, quando sono richieste entrambe le estremità della relazione o entrambe le parti sono facoltative, Entity Framework non può identificare il dipendente e il principale. Quando sono necessarie entrambe le estremità della relazione, è possibile utilizzare HasRequired come illustrato nel codice seguente.

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 il database viene generato, vedrai che la relazione viene creata come mostrato nell'immagine seguente.

Configurare la relazione uno-a-molti

La tabella della chiave primaria contiene un solo record che non si riferisce a nessuno, uno o più record nella tabella correlata. Questo è il tipo di relazione più comunemente usato.

  • In questo tipo di relazione, una riga nella tabella A può avere molte righe corrispondenti nella tabella B, ma una riga nella tabella B può avere solo una riga corrispondente nella tabella A.

  • La chiave esterna è definita nella tabella che rappresenta le molte estremità della relazione.

  • Ad esempio, nel diagramma precedente le tabelle Studente e Iscrizione hanno una relazione da uno a più, ogni studente può avere molte iscrizioni, ma ciascuna iscrizione appartiene a un solo studente.

Di seguito sono riportati lo studente e l'iscrizione che ha una relazione uno-a-molti, ma la chiave esterna nella tabella di iscrizione non segue le convenzioni Code First predefinite.

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

In questo caso, per configurare la relazione uno-a-molti utilizzando Fluent API, è necessario utilizzare il metodo HasForeignKey come mostrato nel codice seguente.

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 il database viene generato, vedrai che la relazione viene creata come mostrato nell'immagine seguente.

Nell'esempio precedente, il metodo HasRequired specifica che la proprietà di navigazione Student deve essere Null. Quindi è necessario assegnare a Studente l'entità Iscrizione ogni volta che si aggiunge o si aggiorna l'iscrizione. Per gestire ciò, è necessario utilizzare il metodo HasOptional anziché il metodo HasRequired.

Configura relazione molti-a-molti

Ogni record in entrambe le tabelle può riguardare un numero qualsiasi di record (o nessun record) nell'altra tabella.

  • È possibile creare tale relazione definendo una terza tabella, chiamata tabella di giunzione, la cui chiave primaria è costituita dalle chiavi esterne sia della tabella A che della tabella B.

  • Ad esempio, la tabella Studente e la tabella Corso hanno una relazione molti-a-molti.

Di seguito sono riportate le classi Studente e Corso in cui Studente e Corso hanno una relazione molti-più, poiché entrambe le classi hanno proprietà di navigazione Studenti e Corsi che sono raccolte. In altre parole, un'entità ha un'altra raccolta di entità.

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

Per configurare la relazione molti-a-molti tra Studente e Corso, puoi usare l'API Fluent come mostrato nel codice seguente.

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

Le convenzioni Code First predefinite vengono utilizzate per creare una tabella di join quando viene generato il database. Di conseguenza, la tabella StudentCourses viene creata con le colonne Course_CourseID e Student_ID come mostrato nell'immagine seguente.

Se si desidera specificare il nome della tabella di join ei nomi delle colonne nella tabella è necessario eseguire una configurazione aggiuntiva utilizzando il metodo 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");
   }); 
}

È possibile vedere quando viene generato il database, il nome della tabella e delle colonne viene creato come specificato nel codice sopra.

Si consiglia di eseguire l'esempio precedente in modo graduale per una migliore comprensione.