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

Nelle esercitazioni precedenti si è lavorato con un modello di dati semplice composto da tre entità. In questa esercitazione verranno aggiunte altre entità e relazioni e si personalizza 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:

Diagramma entità

In questa esercitazione, imparerai a:

  • Personalizzare il modello di dati
  • Apportare modifiche all'entità Student
  • Crea l'entità Instructor
  • Creare l'entità OfficeAssignment
  • Modifica entità Course
  • Crea entità reparto
  • Modifica entità Enrollment
  • Aggiornare il contesto del database
  • Inizializzare il database con dati di test
  • Aggiungere una migrazione
  • Modifica la stringa di connessione
  • Aggiornare il database

Prerequisites

Personalizza il modello 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, in diverse sezioni seguenti si creerà il modello di dati School completo aggiungendo attributi alle classi già create e creando nuove classi per i tipi di entità rimanenti nel modello.

L'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.cs, aggiungere un'istruzione using per il namespace System.ComponentModel.DataAnnotations e aggiungere gli attributi DataType e DisplayFormat alla proprietà EnrollmentDate, 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 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 genera attributi HTML 5 data- (pronunciato data dash) che i browser HTML 5 possono comprendere. Gli DataType attributi non forniscono alcuna convalida.

DataType.Date non specifica il formato della data visualizzata. Per impostazione predefinita, il campo dati viene visualizzato in base ai formati predefiniti basati su 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. È possibile che per alcuni campi, ad esempio per i valori di valuta, non si voglia visualizzare il simbolo di valuta nella casella di testo durante la modifica.

È possibile usare l'attributo DisplayFormat da solo, ma in genere è consigliabile usare anche l'attributo DataType . L'attributo DataType fornisce la semantica dei dati anziché al modo in cui vengono visualizzati sullo schermo e offre i vantaggi seguenti che non si ottengono con DisplayFormat:

  • Il browser può abilitare le funzionalità HTML5, ad esempio per visualizzare un controllo calendario, il simbolo di valuta appropriato per le impostazioni locali, i collegamenti di posta elettronica, la convalida dell'input sul 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 <input>.

Eseguire l'app, passare alla pagina Elenco studenti e notare che gli orari non vengono più visualizzati per le date di iscrizione. Lo stesso vale per qualsiasi visualizzazione che usa il modello Student.

Pagina Students Index (Indice studenti) che mostra le date senza orari

L'attributo StringLength

È anche possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando attributi. L'attributo StringLength imposta la lunghezza massima nel database e fornisce 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 StringLength attributi 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 bianchi 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 fornisce funzionalità simili all'attributo StringLength , ma non fornisce la convalida lato client.

Il modello di database è stato modificato in modo da richiedere una modifica dello schema del database. Si useranno le migrazioni per aggiornare lo schema senza perdere dati che potrebbero essere stati aggiunti al database usando l'interfaccia utente dell'applicazione.

Salvare le modifiche e compilare il progetto. Aprire quindi la finestra di comando nella cartella del progetto e immettere i comandi seguenti:

dotnet ef migrations add MaxLengthOnNames
dotnet ef database update

Il migrations add comando avvisa che la perdita di dati può verificarsi, perché la modifica rende la lunghezza massima più breve per due colonne. Migrations crea un file denominato <timeStamp>_MaxLengthOnNames.cs. Questo file contiene codice nel Up metodo che aggiornerà il database in modo che corrisponda al modello di dati corrente. Il comando database update ha eseguito quel codice.

Il timestamp anteposto al nome del file di migrazione viene usato da Entity Framework per ordinare le migrazioni. È possibile creare più migrazioni prima di eseguire il comando update-database e quindi tutte le migrazioni vengono applicate nell'ordine in cui sono state create.

Eseguire l'app, selezionare la scheda Students, fare clic su Create New e provare a immettere uno dei due nomi con una lunghezza superiore a 50 caratteri. L'applicazione deve impedire l'esecuzione di questa operazione.

L'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, vengono assegnati gli stessi nomi delle 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 sottostante a SchoolContext, in modo che non corrisponda al database.

Salvare le modifiche e compilare il progetto. Aprire quindi la finestra di comando nella cartella del progetto e immettere i comandi seguenti per creare un'altra migrazione:

dotnet ef migrations add ColumnFirstName
dotnet ef database update

In SQL Server Esplora oggetti aprire la finestra di progettazione tabelle Student facendo doppio clic sulla tabella Student.

Tabella Students (Studenti) in SSOX dopo le migrazioni

Prima di applicare le prime due migrazioni, le colonne name erano di tipo nvarchar(MAX). Sono ora nvarchar(50) e il nome della colonna è cambiato da FirstMidName a FirstName.

Note

Se si tenta di compilare prima di completare la creazione di tutte le classi di entità nelle sezioni seguenti, è possibile che vengano visualizzati errori del compilatore.

Modifiche all'entità Student

Entità studente

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 obbligatorio

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

L'attributo Required deve essere usato con MinimumLength affinché MinimumLength venga applicato.

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

L'attributo Display

L'attributo Display specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date" anziché il nome della proprietà in ogni istanza (che non ha spazio che divide le parole).

Proprietà calcolata FullName

FullName è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. Pertanto, dispone solo di una funzione di accesso get e non verrà generata alcuna FullName colonna nel database.

Crea un'entità Instructor

Entità istruttore

Crea 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 diverse proprietà sono le stesse nelle entità Student e Instructor. Nell'esercitazione Implementazione dell'ereditarietà più avanti in questa serie si eseguirà il refactoring di questo codice per eliminare la ridondanza.

È possibile inserire più attributi su una riga, quindi è anche possibile scrivere gli HireDate attributi 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 relativo tipo deve essere un elenco in cui è possibile aggiungere, eliminare e aggiornare le voci. È 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 si tratta di CourseAssignment entità è illustrato di seguito nella sezione relativa alle relazioni molti-a-molti.

Le regole business di Contoso University dichiarano che un insegnante può avere al massimo un ufficio, quindi la OfficeAssignment proprietà contiene una singola entità OfficeAssignment (che può essere Null se non viene assegnato alcun ufficio).

public OfficeAssignment OfficeAssignment { get; set; }

Creare l'entità OfficeAssignment

Entità OfficeAssignment

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 chiave

Esiste una relazione uno-a-zero-o-uno tra le entità Instructor 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 automaticamente InstructorID come chiave primaria di questa entità perché il nome non segue la convenzione di denominazione ID 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 propria chiave primaria, ma si vuole assegnare alla proprietà un nome diverso da classnameID o ID.

Per impostazione predefinita, Entity Framework considera la chiave come non generata dal database perché la colonna è per una relazione di identificazione.

Proprietà di navigazione Instructor

L'entità Instructor ha una proprietà di navigazione nullable OfficeAssignment (perché un insegnante potrebbe non avere un'assegnazione di ufficio) e l'entità OfficeAssignment ha una proprietà di navigazione non nullable Instructor (perché un'assegnazione di ufficio non può esistere senza un insegnante- InstructorID non è nullable). Quando un'entità Instructor ha un'entità OfficeAssignment correlata, ogni entità avrà 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 non è necessario farlo perché la chiave esterna InstructorID (che è anche la chiave di questa tabella) non ammette valori null.

Modifica entità Course

Entità corso

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à DepartmentID di chiave esterna che punta all'entità Department correlata e ha una Department proprietà di navigazione.

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 ovunque siano necessarie e crea le proprietà shadow. 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 .

L'attributo DatabaseGenerated

L'attributo DatabaseGenerated con il None parametro nella CourseID proprietà specifica che i valori di chiave primaria vengono forniti dall'utente anziché 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. Tuttavia, per le entità Course, si utilizzerà un numero di corso specificato dall'utente, come un numero della serie 1000 per un dipartimento, un numero della serie 2000 per un altro dipartimento e così via.

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 insegnato da più istruttori, quindi la proprietà di CourseAssignments navigazione è una raccolta (il tipo CourseAssignment viene spiegato più avanti):

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

Crea entità Dipartimento

Entità del reparto

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

L'attributo Column

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

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

Il mapping delle colonne in genere non è obbligatorio, perché Entity Framework sceglie il tipo di dati SQL Server appropriato in base al tipo CLR definito per la proprietà. Il tipo CLR decimal mappa a un tipo SQL Server decimal. In questo caso, tuttavia, si sa che la colonna conterrà gli importi in valuta e il tipo di dati money è più appropriato.

Proprietà chiave esterna e di navigazione

Le relazioni sono riflesse dalle seguenti proprietà di chiave esterna e di navigazione:

Un reparto può avere o meno un amministratore e un amministratore è sempre un insegnante. Di conseguenza, la InstructorID proprietà viene inclusa come chiave esterna per l'entità Instructor e viene aggiunto un punto interrogativo dopo la int designazione del tipo 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 dipartimento può avere molti corsi, pertanto è disponibile una proprietà di navigazione Courses:

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

Note

Per convenzione, Entity Framework consente l'eliminazione a catena per chiavi esterne non annullabili e relazioni molti-a-molti. Ciò può comportare regole di eliminazione a catena circolare, che causeranno un'eccezione quando si tenta di aggiungere una migrazione. Ad esempio, se non avessi definito la proprietà Department.InstructorID come nullable, EF configurerebbe una regola di eliminazione a catena per eliminare il reparto quando elimini l'insegnante, che non è quello che si vuole avere. Se le regole business richiedono che la proprietà InstructorID non sia nullable, è necessario usare l'istruzione API Fluent seguente per disabilitare l'eliminazione a catena nella relazione:

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

Modifica entità Enrollment

Entità di registrazione

In Models/Enrollment.cs, sostituire il codice che hai 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; }
    }
}

Chiave esterna e proprietà 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 entità Student e Course, e l'entità Enrollment funziona come una tabella di connessione 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à. Questo diagramma è stato generato usando Entity Framework Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione, ma il diagramma è usato qui solo a scopo illustrativo.

Relazione molti-a-molti tra Student-Course

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 includesse informazioni di valutazione, dovrebbe contenere solo le due chiavi esterne CourseID e StudentID. In tal caso, si tratta di una tabella join molti-a-molti senza payload (o una tabella di join pura) nel 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 questa esercitazione non è stata aggiornata per usare una tabella di join implicita. Vedere Relazioni molti-a-molti, la Razor versione Pages di questa esercitazione che è stata aggiornata.

L'entità CourseAssignment

Entità CourseAssignment

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

Unire nomi di entità

Nel database è necessaria una tabella join per la relazione molti-a-molti Instructor-to-Courses e deve essere rappresentata da un insieme di entità. È comune denominare un'entità join EntityName1EntityName2, che in questo caso sarebbe CourseInstructor. È tuttavia consigliabile scegliere un nome che descriva la relazione. I modelli di dati iniziano in modo semplice e crescono, con join senza payload che spesso ottengono payload in un secondo momento. Se si inizia con un nome di entità descrittivo, non sarà necessario modificare il nome in un secondo momento. Idealmente l'entità di join dovrebbe avere il proprio nome naturale (eventualmente anche di una sola parola) nel dominio di business. Ad esempio, libri e clienti possono essere collegati tramite valutazioni. Per questa relazione, CourseAssignment è una scelta migliore rispetto a CourseInstructor.

Chiave composta

Poiché le chiavi esterne non sono nullable e insieme identificano in modo univoco ogni riga della tabella, non è necessaria una chiave primaria separata. Le proprietà InstructorID e CourseID devono funzionare come chiave primaria composita. L'unico modo per identificare le chiavi primarie composite in Entity Framework consiste nell'usare l'API Fluent (non può essere eseguita usando gli attributi). Nella sezione successiva verrà illustrato come configurare la chiave primaria composita.

La chiave composita garantisce che, sebbene sia 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à Enrollment join definisce la propria chiave primaria, quindi sono possibili duplicati di questo tipo. Per evitare tali duplicati, è possibile aggiungere un indice univoco sui campi della chiave esterna o configurare Enrollment con una chiave composita primaria simile a CourseAssignment. Per altre informazioni, vedere Indexes (Indici).

Aggiornare il contesto del 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 composita dell'entità CourseAssignment.

Informazioni su un'alternativa all'API fluent

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

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

In questa esercitazione si usa l'API Fluent solo per il mapping di database che non è possibile eseguire 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 applica solo una regola di convalida lato client e 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 usi entrambi, tieni presente che ovunque ci sia 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.

Diagramma entità

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.

Inizializza il 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à necessarie per il test. Si noti come vengono gestite le relazioni molti-a-molti: il codice crea relazioni creando entità nei set di entità di join Enrollments e CourseAssignment.

Aggiungere una migrazione

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

dotnet ef migrations add ComplexDataModel

Viene visualizzato un avviso relativo alla 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 tentasse di eseguire il comando database update a questo punto (non farlo ancora), si otterrebbe l'errore seguente:

L'istruzione ALTER TABLE è in conflitto con il vincolo FOREIGN KEY "FK_dbo.Course_dbo.Department_DepartmentID". Il conflitto si è verificato nel database "ContosoUniversity", tabella "dbo.Department", colonna 'DepartmentID'.

In alcuni 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 durante l'esecuzione del codice, lAddColumn'operazione ha esito negativo perché SQL Server non conosce il valore da inserire nella colonna che non può essere Null. Per questa esercitazione si eseguirà la migrazione in un nuovo database, ma in un'applicazione di produzione è necessario fare in modo che la migrazione gestisca i dati esistenti, quindi le indicazioni seguenti illustrano un esempio di come eseguire questa operazione.

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

  • Aprire il {timestamp}_ComplexDataModel.cs file.

  • 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 scrivere codice o script per aggiungere righe Department e correlare le righe Course alle nuove righe Department. Non sarà più necessario il dipartimento "Temp" o il valore predefinito nella colonna Course.DepartmentID.

Salvare le modifiche e compilare il progetto.

Modifica la stringa di connessione

È ora disponibile un nuovo codice nella DbInitializer classe che aggiunge i dati di inizializzazione per le nuove entità a un database vuoto. Per fare in modo che EF crei un nuovo database vuoto, modificare il nome del database nella stringa di connessione in appsettings.json in ContosoUniversity3 o in un altro nome non usato nel computer in uso.

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

Salva la modifica in appsettings.json.

Note

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

dotnet ef database drop

Aggiornare il database

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

dotnet ef database update

Eseguire l'app per fare in modo che il metodo DbInitializer.Initialize venga eseguito e popoli il nuovo database.

Aprire il database in SSOX come in precedenza ed espandere il nodo Tabelle per verificare che tutte le tabelle siano state create. (Se hai ancora SSOX aperto da prima, fare clic sul pulsante Aggiorna.)

Tabelle in SSOX

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

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

Dati CourseAssignment in SSOX

Ottenere il codice

Scaricare o visualizzare l'applicazione completata.

Passaggi successivi

In questa esercitazione, imparerai a:

  • Personalizzato il modello di dati
  • Apportate modifiche all'entità Student
  • Creata l'entità Instructor
  • Entità OfficeAssignment creata
  • Entità del corso modificata
  • Entità reparto creata
  • Entità di iscrizione modificata
  • Contesto del database aggiornato
  • Database popolato con dati di test
  • Migrazione aggiunta
  • Stringa di connessione modificata
  • Database aggiornato

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