Esercitazione: Creare un modello di dati complesso - ASP.NET MVC con EF Core

Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. In questa esercitazione si aggiungeranno altre entità e relazioni e si personalizzerà il modello di dati specificando regole di formattazione, convalida e mapping del database.

Al termine dell'operazione le classi di entità verranno incluse nel modello di dati completato, illustrato nella figura seguente:

Entity diagram

In questa esercitazione:

  • Personalizzare il modello di dati
  • Apportare modifiche all'entità Student
  • Creare l'entità Instructor
  • Creare l'entità OfficeAssignment
  • Modificare l'entità Course
  • Creare l'entità Department
  • Modificare l'entità Enrollment
  • Aggiornare il contesto di database
  • Eseguire il seeding del database con dati di test
  • Aggiungere una migrazione
  • Modificare la stringa di connessione
  • Aggiornare il database

Prerequisiti

Personalizzare il modello di dati

In questa sezione si apprenderà come personalizzare il modello di dati usando attributi che specificano regole di formattazione, convalida e mapping del database. Quindi nelle sezioni seguenti si creerà il modello di dati School (Istituto scolastico) completo, aggiungendo attributi alle classi già create e creando nuove classi per i tipi di entità rimanenti nel modello.

Attributo DataType

Per le date di iscrizione degli studenti, tutte le pagine Web attualmente visualizzano l'ora oltre alla data, anche se l'unico elemento rilevante di questo campo è la data. Mediante gli attributi di annotazione dei dati è possibile modificare il codice per correggere il formato di visualizzazione in tutte le visualizzazioni che visualizzano i dati. Per un esempio di come eseguire questa operazione si aggiunge un attributo alla proprietà EnrollmentDate nella classe Student.

In Models/Student.csaggiungere un'istruzione using per lo spazio dei System.ComponentModel.DataAnnotations nomi e aggiungere DataType attributi e DisplayFormat alla EnrollmentDate proprietà , come illustrato nell'esempio seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

L'attributo DataType viene usato per specificare un tipo di dati che è più specifico del tipo intrinseco del database. In questo caso si vuole tenere traccia solo della data e non di data e ora. L'enumerazione DataType fornisce per molti tipi di dati, ad esempio Date, Time, PhoneNumber, Currency, EmailAddress e altro ancora. L'attributo DataType può anche consentire all'applicazione di fornire automaticamente le funzionalità specifiche del tipo. Ad esempio, è possibile creare un collegamento mailto: per DataType.EmailAddress e fornire un selettore data per DataType.Date nei browser che supportano HTML5. L'attributo DataType produce attributi HTML5 data- supportati dai browser HTML5. Gli attributi DataType non garantiscono alcuna convalida.

DataType.Date non specifica il formato della data visualizzata. Per impostazione predefinita il campo dati viene visualizzato in base ai formati predefiniti derivanti dal valore CultureInfo del server.

L'attributo DisplayFormat viene usato per specificare in modo esplicito il formato della data:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

L'impostazione ApplyFormatInEditMode specifica che la formattazione deve essere applicata anche quando il valore viene visualizzato in una casella di testo per la modifica. Per determinati campi questa scelta non è consigliabile. Ad esempio per i valori di valuta può non risultare opportuno che il simbolo di valuta sia incluso nella casella di testo per la modifica.

È possibile usare l'attributo DisplayFormat da solo, ma in genere è consigliabile usare anche l'attributo DataType. L'attributo DataType esprime la semantica dei dati anziché la modalità di rendering dei dati in una schermata e offre i seguenti vantaggi che non si ottengono con DisplayFormat:

  • Il browser può abilitare le funzionalità HTML5, ad esempio per visualizzare un controllo di calendario, il simbolo della valuta appropriato per le impostazioni locali, i collegamenti alla posta elettronica, alcune istanze di convalida lato client e così via.

  • Per impostazione predefinita, il browser eseguirà il rendering dei dati usando il formato corretto in base alle impostazioni locali del sistema.

Per altre informazioni, vedere la documentazione dell'helper< tag di input>.

Eseguire l'app, passare alla pagina Students Index (Indice studenti) e verificare che l'ora non viene più visualizzata nelle date di iscrizione. Lo stesso vale per tutte le viste che usano il modello Student (Studente).

Students index page showing dates without times

Attributo StringLength

È anche possibile specificare regole di convalida dei dati e messaggi di errore di convalida mediante gli attributi. L'attributo StringLength imposta la lunghezza massima nel database e offre la convalida lato client e lato server per ASP.NET Core MVC. È anche possibile specificare la lunghezza minima della stringa in questo attributo, ma il valore minimo non ha alcun effetto sullo schema del database.

Ad esempio si supponga di voler limitare a 50 il numero massimo di caratteri che gli utenti possono immettere per un nome. Per aggiungere questa limitazione aggiungere attributi StringLength alle proprietà LastName e FirstMidName, come illustrato nell'esempio seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

L'attributo StringLength non impedisce a un utente di immettere spazi vuoti per un nome. È possibile usare l'attributo RegularExpression per applicare restrizioni all'input. Ad esempio il codice seguente richiede che il primo carattere sia maiuscolo e i caratteri rimanenti siano caratteri alfabetici:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

L'attributo MaxLength offre funzionalità simili a quelle dell'attributo StringLength ma non offre la convalida lato client.

La modifica apportata al modello di database ora richiede un aggiornamento dello schema del database. Si userà migrations per aggiornare lo schema senza perdere i dati eventualmente aggiunti al database tramite l'interfaccia utente dell'applicazione.

Salvare le modifiche e compilare il progetto. Quindi aprire una finestra dei comandi nella cartella di progetto ed eseguire i comandi seguenti:

dotnet ef migrations add MaxLengthOnNames
dotnet ef database update

Il comando migrations add segnala che può verificarsi la perdita di dati, perché la modifica riduce la lunghezza massima per due colonne. Le migrazioni creano un file denominato <timeStamp>_MaxLengthOnNames.cs. Il metodo Up di questo file contiene codice che aggiorna il database per adattarlo al modello di dati corrente. Il comando database update ha eseguito tale codice.

Il timestamp che precede il nome del file delle migrazioni viene usato da Entity Framework per ordinare le migrazioni. È possibile creare più migrazioni prima di eseguire il comando update-database, dopodiché tutte le migrazioni vengono applicate nell'ordine in cui sono state create.

Eseguire l'app, selezionare la scheda Students (Studenti), fare clic su Crea nuovo e provare a immettere un nome con più di 50 caratteri. L'applicazione dovrebbe impedire questa operazione.

Attributo Column

È possibile usare gli attributi anche per controllare il mapping delle classi e delle proprietà nel database. Si supponga di aver usato il nome FirstMidName per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome. Tuttavia si vuole che la colonna di database sia denominata FirstName, perché gli utenti che scrivono query ad hoc per il database sono abituati a tale nome. Per eseguire questo mapping è possibile usare l'attributo Column.

L'attributo Column specifica che quando viene creato il database, la colonna della tabella Student mappata sulla proprietà FirstMidName verrà denominata FirstName. In altri termini, quando il codice fa riferimento a Student.FirstMidName i dati provengono dalla colonna FirstName della tabella Student o vengono aggiornati in tale colonna. Se non si specificano nomi di colonna, le colonne avranno lo stesso nome della proprietà.

Student.cs Nel file aggiungere un'istruzione using per System.ComponentModel.DataAnnotations.Schema e aggiungere l'attributo del nome di colonna alla FirstMidName proprietà , come illustrato nel codice evidenziato seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

L'aggiunta dell'attributo Column modifica il modello che supporta SchoolContext e che pertanto non corrisponderà al database.

Salvare le modifiche e compilare il progetto. Quindi aprire una finestra dei comandi nella cartella di progetto ed eseguire i comandi seguenti per creare un'altra migrazione:

dotnet ef migrations add ColumnFirstName
dotnet ef database update

In Esplora oggetti di SQL Server aprire il designer della tabella Student (Studente) facendo doppio clic sulla tabella.

Students table in SSOX after migrations

Prima dell'applicazione delle prime due migrazioni, le colonne del nome erano di tipo nvarchar(MAX). Ora sono di tipo nvarchar(50) e il nome colonna è cambiato da FirstMidName a FirstName.

Nota

Se si prova a compilare prima di aver creato tutte le classi di entità delle sezioni seguenti, possono verificarsi errori di compilazione.

Modifiche all'entità Student

Student entity

In Models/Student.cssostituire il codice aggiunto in precedenza con il codice seguente. Le modifiche sono evidenziate.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50)]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Attributo Required

L'attributo Required rende obbligatori i campi delle proprietà del nome. L'attributo Required non è necessario per i tipi non nullable quali i tipi del valore (DateTime, int, double, float e così via). I tipi che non possono essere null vengono considerati automaticamente come campi obbligatori.

L'attributo Required deve essere usato con MinimumLength per l'applicazione di MinimumLength.

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

Attributo Display

L'attributo Display specifica che la didascalia delle caselle di testo deve essere "First Name" (Nome), "Last Name" (Cognome), "Full Name" (Nome e cognome) ed "Enrollment Date" (Data di iscrizione) anziché il nome della proprietà (senza spazi tra le parole) in ogni istanza.

Proprietà calcolata FullName

FullName è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. Di conseguenza ha una sola funzione di accesso get e nel database non viene generata nessuna colonna FullName.

Creare l'entità Instructor

Instructor entity

Creare Models/Instructor.cs, sostituendo il codice del modello con il codice seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Si noti che molte proprietà sono uguali nelle entità Student e Instructor. Nell'esercitazione Implementing Inheritance (Implementazione dell'ereditarietà) più avanti in questa serie si effettuerà il refactoring di questo codice per eliminare la ridondanza.

È possibile posizionare più attributi su una riga, pertanto è anche possibile scrivere gli attributi HireDate come indicato di seguito:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Proprietà di navigazione CourseAssignments e OfficeAssignment

Le proprietà CourseAssignments e OfficeAssignment sono proprietà di navigazione.

Un insegnante può tenere un numero qualsiasi di corsi, pertanto CourseAssignments è definita come raccolta.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Se una proprietà di navigazione può contenere più entità, il tipo della proprietà deve essere un elenco in cui le voci possono essere aggiunte, eliminate e aggiornate. È possibile specificare ICollection<T> o un tipo come List<T> o HashSet<T>. Se si specifica ICollection<T>, per impostazione predefinita EF crea una raccolta HashSet<T>.

Il motivo per cui queste sono entità CourseAssignment viene illustrato più avanti nella sezione relativa alle relazioni molti-a-molti.

Le regole business di Contoso University specificano che un insegnante non può avere più di un ufficio, pertanto la proprietà OfficeAssignment contiene un'unica entità OfficeAssignment (che può essere null se non è assegnato alcun ufficio).

public OfficeAssignment OfficeAssignment { get; set; }

Creare l'entità OfficeAssignment

OfficeAssignment entity

Creare Models/OfficeAssignment.cs con il codice seguente:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Attributo Key

Esiste una relazione uno-a-zero-o-uno tra le Instructor entità e OfficeAssignment . Un'assegnazione di ufficio esiste solo in relazione all'insegnante a cui è assegnata e pertanto la chiave primaria è anche la chiave esterna all'entità Instructor . Entity Framework non è tuttavia in grado di riconoscere InstructorID automaticamente come chiave primaria di questa entità perché il nome non segue la ID convenzione di denominazione o classnameID . Per identificare l'entità come chiave viene usato l'attributo Key:

[Key]
public int InstructorID { get; set; }

È anche possibile usare l'attributo Key se l'entità dispone di una chiave primaria propria ma si vuole assegnare alla proprietà un nome diverso da classnameID o ID.

Per impostazione predefinita, EF considera la chiave come non generata dal database, perché la colonna è destinata a una relazione di identificazione.

Proprietà di navigazione Instructor

L'entità Instructor dispone di una proprietà di navigazione OfficeAssignment nullable (perché un docente potrebbe non avere un ufficio assegnato) e l'entità OfficeAssignment dispone di una proprietà di navigazione Instructor non nullable (perché un'assegnazione di ufficio non può esistere senza un insegnante: InstructorID è non nullable). Quando un'entità Instructor dispone di un'entità OfficeAssignment correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.

È possibile inserire un attributo [Required] nella proprietà di navigazione Instructor per specificare che deve essere presente un insegnante correlato, ma questa operazione non è necessaria perché la chiave esterna InstructorID (che è anche la chiave per questa tabella) è di tipo non nullable.

Modificare l'entità Course

Course entity

In Models/Course.cssostituire il codice aggiunto in precedenza con il codice seguente. Le modifiche sono evidenziate.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

L'entità Course ha una proprietà di chiave esterna DepartmentID che fa riferimento all'entità Department correlata e include una proprietà di navigazione Department.

In Entity Framework non è necessario aggiungere una proprietà di chiave esterna al modello di dati se è disponibile una proprietà di navigazione per un'entità correlata. EF crea automaticamente chiavi esterne nel database ogni volta che sono necessarie e crea proprietà nascoste per tali chiavi. Tuttavia il fatto di avere la chiave esterna nel modello di dati può rendere più semplici ed efficienti gli aggiornamenti. Ad esempio, quando si recupera un'entità Course da modificare, l'entità Department è Null se non la si carica, quindi quando si aggiorna l'entità Course , è necessario recuperare prima l'entità Department . Quando la proprietà DepartmentID della chiave esterna è inclusa nel modello di dati, non è necessario recuperare l'entità prima dell'aggiornamento Department .

Attributo DatabaseGenerated

L'attributo DatabaseGenerated con il parametro None per la proprietà CourseID indica che i valori di chiave primaria vengono specificati dall'utente anziché essere generati dal database.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Per impostazione predefinita, Entity Framework presuppone che i valori di chiave primaria vengano generati dal database. Questa è la condizione ottimale nella maggior parte degli scenari. Per le entità, tuttavia Course , si userà un numero di corso specificato dall'utente, ad esempio una serie 1000 per un reparto, una serie 2000 per un altro reparto e così via.

Anche l'attributo DatabaseGenerated può essere usato anche per generare valori predefiniti, come nel caso delle colonne di database usate per registrare la data di creazione o aggiornamento di una riga. Per altre informazioni, vedere Generated Properties (Proprietà generate).

Proprietà chiave esterna e di navigazione

Le proprietà della chiave esterna e le proprietà di navigazione nell'entità Course riflettono le relazioni seguenti:

Un corso viene assegnato a un solo reparto, pertanto sono presenti una chiave esterna DepartmentID e una proprietà di navigazione Department per i motivi indicati in precedenza.

public int DepartmentID { get; set; }
public Department Department { get; set; }

Un corso può avere un numero qualsiasi di studenti iscritti, pertanto la proprietà di navigazione Enrollments è una raccolta:

public ICollection<Enrollment> Enrollments { get; set; }

Un corso può essere impartito da più insegnanti, pertanto la proprietà di navigazione CourseAssignments è una raccolta (il tipo CourseAssignment è illustrato più avanti):

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Creare l'entità Department

Department entity

Creare Models/Department.cs con il codice seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Attributo Column

In precedenza l'attributo Column è stato usato per modificare il mapping del nome di colonna. Nel codice per l'entità l'attributo Department viene usato per modificare il Column mapping dei tipi di dati SQL in modo che la colonna venga definita usando il tipo SQL Server money nel database:

[Column(TypeName="money")]
public decimal Budget { get; set; }

In genere il mapping della colonna non è necessario, perché Entity Framework sceglie il tipo di dati SQL Server appropriato in base al tipo CLR definito per la proprietà. Il tipo CLR decimal esegue il mapping a un tipo SQL Server decimal. In questo caso tuttavia si sa che la colonna includerà importi in valuta, pertanto il tipo di dati money è più appropriato.

Proprietà chiave esterna e di navigazione

Le proprietà di chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:

Un reparto può avere o meno un amministratore e un amministratore è sempre un insegnante. Di conseguenza la proprietà InstructorID è inclusa come chiave esterna per l'entità Instructor e dopo l'indicazione del tipo int viene aggiunto un punto interrogativo, per contrassegnare la proprietà come nullable. La proprietà di navigazione è denominata Administrator ma contiene un'entità Instructor:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

Un reparto può avere molti corsi, pertanto è disponibile una proprietà di navigazione Courses:

public ICollection<Course> Courses { get; set; }

Nota

Per convenzione, Entity Framework consente l'eliminazione a catena per le chiavi esterne non nullable e per le relazioni molti-a-molti. Ciò può determinare regole di eliminazione a catena circolari, che generano un'eccezione quando si prova ad aggiungere una migrazione. Ad esempio, se la Department.InstructorID proprietà non è stata definita come nullable, EF configurerà una regola di eliminazione a catena per eliminare il reparto quando si elimina l'insegnante, che non è quello che si vuole avere. Se le regole business richiedono che la proprietà InstructorID sia non nullable, è necessario usare la seguente istruzione API Fluent per disattivare l'eliminazione a catena nella relazione:

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Modificare l'entità Enrollment

Enrollment entity

In Models/Enrollment.cssostituire il codice aggiunto in precedenza con il codice seguente:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    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; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Proprietà chiave esterna e di navigazione

Le proprietà di chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:

Un record di iscrizione è relativo a un singolo corso, pertanto sono presenti una proprietà di chiave esterna CourseID e una proprietà di navigazione Course:

public int CourseID { get; set; }
public Course Course { get; set; }

Un record di iscrizione è relativo a un singolo studente, pertanto sono presenti una proprietà di chiave esterna StudentID e una proprietà di navigazione Student:

public int StudentID { get; set; }
public Student Student { get; set; }

Relazioni molti-a-molti

Esiste una relazione molti-a-molti tra le Student entità e Course e l'entità Enrollment funziona come tabella di join molti-a-molti con payload nel database. "Con payload" indica che la Enrollment tabella contiene dati aggiuntivi oltre alle chiavi esterne per le tabelle unite in join (in questo caso, una chiave primaria e una Grade proprietà).

La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Il diagramma è stato generato con Entity Framework Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione e il diagramma viene visualizzato solo come esempio.

Student-Course many to many relationship

Ogni riga della relazione inizia con un 1 e termina con un asterisco (*), per indicare una relazione uno-a-molti.

Se la Enrollment tabella non include informazioni di grado, sarebbe necessario contenere solo le due chiavi CourseID esterne e StudentID. In questo caso sarebbe una tabella di join molti-a-molti senza payload (o tabella di join pura) del database. Le Instructor entità e Course hanno quel tipo di relazione molti-a-molti e il passaggio successivo consiste nel creare una classe di entità per funzionare come tabella di join senza payload.

EF Core supporta tabelle di join implicite per relazioni molti-a-molti, ma questo tutoral non è stato aggiornato per usare una tabella di join implicita. Vedere Relazioni molti-a-molti, la Razor versione Pages di questa esercitazione che è stata aggiornata.

Entità CourseAssignment

CourseAssignment entity

Creare Models/CourseAssignment.cs con il codice seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Nomi delle entità di join

È necessaria una tabella di join del database per la relazione molti-a-molti Instructor-Courses e tale relazione deve essere rappresentata da un set di entità. È pratica comune assegnare a un'entità di join il nome EntityName1EntityName2, che in questo caso sarebbe CourseInstructor. È tuttavia consigliabile scegliere un nome che descrive la relazione. I modelli di dati sono inizialmente semplici, quindi crescono e diventano più complessi. In molti casi ai join senza payload vengono assegnati payload in un secondo momento. Se si inizia con un nome di entità descrittivo, non sarà necessario modificarlo successivamente. Idealmente l'entità di join dovrebbe avere il proprio nome naturale (se possibile composto da un'unica parola) nel dominio di business. Ad esempio Books (Documentazione) e Customers (Clienti) potrebbero essere collegati mediante Ratings (Valutazioni). Per questa relazione CourseAssignment è una soluzione migliore rispetto a CourseInstructor.

Chiave composta

Dato che le chiavi esterne non sono nullable e insieme identificano in modo univoco ogni riga della tabella, non è necessario avere una chiave primaria separata. Le InstructorID proprietà e CourseID devono funzionare come chiave primaria composita. L'unico modo per identificare le chiavi primarie composte in Entity Framework è l'uso di API Fluent (l'operazione non può essere eseguita con gli attributi). Nella sezione successiva si vedrà come configurare la chiave primaria composta.

La chiave composta garantisce che anche se è possibile avere più righe per un corso e più righe per un insegnante, non è possibile avere più righe per lo stesso insegnante e lo stesso corso. L'entità di join Enrollment definisce la propria chiave primaria, pertanto sono possibili i duplicati di questo tipo. Per evitare tali duplicati è possibile aggiungere un indice univoco ai campi chiave esterna o configurare Enrollment con una chiave primaria composta simile a CourseAssignment. Per altre informazioni, vedere Indexes (Indici).

Aggiornare il contesto di database

Aggiungere il codice evidenziato seguente al Data/SchoolContext.cs file:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Questo codice aggiunge le nuove entità e configura la chiave primaria composta dell'entità CourseAssignment.

Informazioni su un'alternativa API Fluent

Il codice nel metodo OnModelCreating della classe DbContext usa l'API Fluent per configurare il comportamento di Entity Framework. L'API è denominata "fluent" perché viene spesso usata tramite la stringa di una serie di chiamate di metodo in un'unica istruzione, come in questo esempio della EF Core documentazione:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

In questa esercitazione si usa l'API Fluent solo per operazioni di mapping del database non eseguibili con gli attributi. È tuttavia possibile usare l'API Fluent anche per specificare la maggior parte delle regole di formattazione, convalida e mapping specificabili tramite gli attributi. Alcuni attributi quali MinimumLength non possono essere applicati con l'API Fluent. Come accennato in precedenza, MinimumLength non modifica lo schema, ma si limita ad applicare una regola di convalida lato client e lato server.

Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere "pulite" le classi di entità. È possibile combinare attributi e API Fluent se si vuole e sono disponibili alcune personalizzazioni che possono essere eseguite solo usando l'API Fluent, ma in generale la procedura consigliata consiste nel scegliere uno di questi due approcci e usarli in modo coerente il più possibile. Se si usano entrambi gli approcci, tenere presente che ogni volta che si verifica un conflitto l'API Fluent esegue l'override degli attributi.

Per altre informazioni sul confronto tra attributi e API Fluent, vedere Metodi di configurazione.

Diagramma dell'entità che visualizza le relazioni

La figura seguente visualizza il diagramma creato da Entity Framework Power Tools per il modello School completato.

Entity diagram

Oltre alle linee di relazione uno-a-molti (da 1 a *), è possibile vedere qui la linea di relazione uno-a-zero-o-uno (da 1 a 0,1) tra le Instructor entità e OfficeAssignment e la riga di relazione zero-o-uno-a-molti (da 0...1 a *) tra le entità Instructor e Department.

Eseguire il seeding del database con dati di test

Sostituire il codice nel Data/DbInitializer.cs file con il codice seguente per fornire i dati di inizializzazione per le nuove entità create.

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

Come si è visto nella prima esercitazione, la maggior parte di questo codice crea semplicemente nuovi oggetti entità e carica i dati di esempio nelle proprietà in base alle esigenze di test. Osservare la modalità di gestione delle relazioni molti-a-molti: il codice crea relazioni tramite la creazione di entità nei set di entità di join Enrollments e CourseAssignment.

Aggiungere una migrazione

Salvare le modifiche e compilare il progetto. Quindi aprire la finestra di comando nella cartella del progetto e immettere il comando migrations add (non eseguire ancora il comando update-database):

dotnet ef migrations add ComplexDataModel

Viene visualizzato un avviso sulla possibile perdita di dati.

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

Se si prova a eseguire il comando database update in questa fase (evitare di farlo), si ottiene il seguente errore:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database "ContosoUniversity", table "dbo.Department", column 'DepartmentID' (L'istruzione ALTER TABLE è in conflitto con il vincolo FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". Il conflitto si è verificato nella colonna 'DepartmentID' della tabella "dbo.Department"del database "ContosoUniversity").

In determinati casi, quando si eseguono migrazioni con dati esistenti è necessario inserire dati stub nel database per soddisfare i vincoli di chiave esterna. Il codice generato nel Up metodo aggiunge una chiave esterna non nullable DepartmentID alla Course tabella. Se sono già presenti righe nella tabella Course quando viene eseguito il codice, l'operazione AddColumn non riesce perché SQL Server non determina il valore da inserire nella colonna, che non può essere null. In questa esercitazione si esegue la migrazione in un nuovo database, ma in un'applicazione di produzione è necessario che la migrazione gestisca i dati esistenti. Le istruzioni che seguono visualizzano un esempio di esecuzione di questa operazione.

Per fare in modo che questa migrazione funzioni con i dati esistenti, è necessario modificare il codice per dare alla nuova colonna un valore predefinito e creare un reparto stub denominato "Temp" che svolga la funzione di reparto predefinito. Di conseguenza, dopo l'esecuzione del metodo Up tutte le righe Course esistenti saranno correlate al reparto "Temp".

  • Apri il file {timestamp}_ComplexDataModel.cs.

  • Impostare come commento la riga di codice che aggiunge la colonna DepartmentID alla tabella Course.

    migrationBuilder.AlterColumn<string>(
        name: "Title",
        table: "Course",
        maxLength: 50,
        nullable: true,
        oldClrType: typeof(string),
        oldNullable: true);
                
    //migrationBuilder.AddColumn<int>(
    //    name: "DepartmentID",
    //    table: "Course",
    //    nullable: false,
    //    defaultValue: 0);
    
  • Aggiungere il codice evidenziato seguente dopo il codice che crea la tabella Department:

    migrationBuilder.CreateTable(
        name: "Department",
        columns: table => new
        {
            DepartmentID = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Budget = table.Column<decimal>(type: "money", nullable: false),
            InstructorID = table.Column<int>(nullable: true),
            Name = table.Column<string>(maxLength: 50, nullable: true),
            StartDate = table.Column<DateTime>(nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Department", x => x.DepartmentID);
            table.ForeignKey(
                name: "FK_Department_Instructor_InstructorID",
                column: x => x.InstructorID,
                principalTable: "Instructor",
                principalColumn: "ID",
                onDelete: ReferentialAction.Restrict);
        });
    
    migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    // Default value for FK points to department created above, with
    // defaultValue changed to 1 in following AddColumn statement.
    
    migrationBuilder.AddColumn<int>(
        name: "DepartmentID",
        table: "Course",
        nullable: false,
        defaultValue: 1);
    

In un'applicazione di produzione è necessario creare codice o script per aggiungere righe Department e associare le righe Course alle nuove righe Department. Non sarà più necessario il reparto "Temp" o il valore predefinito nella Course.DepartmentID colonna.

Salvare le modifiche e compilare il progetto.

Modificare la stringa di connessione

Ora la classe DbInitializer include nuovo codice che aggiunge dati di inizializzazione per le nuove entità a un database vuoto. Per creare un nuovo database vuoto, modificare il nome del database nel stringa di connessione in appsettings.json ContosoUniversity3 o un altro nome non usato nel computer in uso.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

Salvare la modifica in appsettings.json.

Nota

In alternativa alla modifica del nome del database, è possibile eliminare il database. Usare Esplora oggetti di SQL Server o il comando della CLI database drop:

dotnet ef database drop

Aggiornare il database

Dopo aver modificato il nome del database o eliminato il database, eseguire il comando database update nella finestra di comando per eseguire le migrazioni.

dotnet ef database update

Eseguire l'app per far sì che il metodo DbInitializer.Initialize venga eseguito e popoli il nuovo database.

Aprire il database in SSOX come in precedenza, quindi espandere il nodo Tabelle per visualizzare tutte le tabelle che sono state create. Se SSOX è ancora aperto dall'operazione precedente, fare clic sul pulsante Aggiorna.

Tables in SSOX

Eseguire l'app per attivare il codice inizializzatore che esegue l'inizializzazione del database.

Fare clic con il pulsante destro del mouse sulla tabella CourseAssignment e selezionare Visualizza dati per verificare che la tabella contenga dati.

CourseAssignment data in SSOX

Ottenere il codice

Scaricare o visualizzare l'applicazione completata.

Passaggi successivi

In questa esercitazione:

  • Personalizzazione del modello di dati
  • Modifica dell'entità Student
  • Creazione dell'entità Instructor
  • Creazione dell'entità OfficeAssignment
  • Modifica dell'entità Course
  • Creazione dell'entità Department
  • Modifica dell'entità Enrollment
  • Aggiornamento del contesto di database
  • Seeding del database con dati di test
  • Aggiunta di una migrazione
  • Modifica della stringa di connessione
  • Aggiornamento del database

Passare all'esercitazione successiva per altre informazioni su come accedere ai dati correlati.