Parte 5, Razor Pagine con EF Core in ASP.NET Core - Data Model

Di Tom Dykstra, Jeremy Likness e Jon P Smith

L'app Web Contoso University illustra come creare Razor app Web Pages usando EF Core e Visual Studio. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione.

Se si verificano problemi che non è possibile risolvere, scaricare l'app completata e confrontare tale codice con quello creato seguendo questa esercitazione.

Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. Contenuto dell'esercitazione:

  • Vengono aggiunte altre entità e relazioni.
  • Il modello di dati viene personalizzato specificando regole di formattazione, convalida e mapping del database.

Il modello di dati completato è illustrato nella figura seguente:

Diagramma dell'entità

Il diagramma di database seguente è stato creato con Dataedo:

Diagramma di Dataedo

Per creare un diagramma di database con Dataedo:

Nel diagramma Dataedo precedente, CourseInstructor è una tabella join creata da Entity Framework. Per altre informazioni, vedere Molti-a-molti

Entità Student (Studente)

Sostituire il codice in Models/Student.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 Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [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; }
    }
}

Il codice precedente aggiunge una proprietà FullName e aggiunge gli attributi seguenti alle proprietà esistenti:

Proprietà calcolata FullName

FullName è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. FullName non può essere impostata, quindi include solo una funzione di accesso get. Nel database non viene creata una colonna FullName.

Attributo DataType

[DataType(DataType.Date)]

Per le date di iscrizione degli studenti, tutte le pagine visualizzano attualmente l'ora del giorno insieme alla data, anche se è pertinente solo la data. Mediante gli attributi di annotazione dei dati è possibile modificare il codice per correggere il formato di visualizzazione in tutte le pagine che visualizzano i dati.

L'attributo DataType indica un tipo di dati più specifico rispetto al tipo intrinseco del database. In questo caso deve essere visualizzata solo la data e non la data e l'ora. L'enumerazione DataType fornisce molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e così via. L'attributo DataType può anche consentire all'app di fornire automaticamente funzionalità specifiche del tipo. Ad esempio:

  • Il collegamento mailto: viene creato automaticamente per DataType.EmailAddress.
  • Il selettore data viene incluso per DataType.Date nella maggior parte dei browser.

L'attributo DataType genera attributi HTML 5 data-. Gli attributi DataType non garantiscono la convalida.

Attributo DisplayFormat

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

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

L'attributo DisplayFormat viene usato per specificare in modo esplicito il formato della data. L'impostazione ApplyFormatInEditMode specifica che la formattazione deve essere applicata anche all'interfaccia utente di modifica. Alcuni campi non devono usare ApplyFormatInEditMode. Ad esempio il simbolo di valuta in genere non deve essere visualizzato in una casella di testo di modifica.

L'attributo DisplayFormat può essere usato da solo. In genere l'uso dell'attributo DataType con l'attributo DisplayFormat è consigliato. L'attributo DataType offre la semantica dei dati anziché specificarne il rendering in una schermata. L'attributo DataType offre i vantaggi seguenti che non sono disponibili in DisplayFormat:

  • Il browser può abilitare le funzionalità HTML5. Ad esempio può visualizzare un controllo di calendario, il simbolo della valuta appropriato per le impostazioni locali, i collegamenti alla posta elettronica e la convalida dell'input sul lato client.
  • Per impostazione predefinita, il browser esegue il rendering dei dati usando il formato corretto in base alle impostazioni locali.

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

Attributo StringLength

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

È possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando gli attributi. L'attributo StringLength specifica il numero minimo e massimo di caratteri consentiti in un campo dati. Il codice precedente limita i nomi a un massimo di 50 caratteri. Un esempio che imposta la lunghezza minima della stringa è disponibile di seguito.

L'attributo StringLength offre anche la convalida lato client e lato server. Il valore minimo non ha alcun effetto sullo schema del database.

L'attributo StringLength non impedisce a un utente di immettere spazi vuoti per un nome. L'attributo RegularExpression può essere usato 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]*$")]

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

Tabella Student (Studenti) in SSOX prima delle migrazioni

L'immagine precedente visualizza lo schema per la tabella Student. I campi del nome sono di tipo nvarchar(MAX). Quando una migrazione viene creata e applicata più avanti in questa esercitazione, i campi del nome diventano nvarchar(50) come risultato degli attributi di lunghezza della stringa.

Attributo Column

[Column("FirstName")]
public string FirstMidName { get; set; }

Gli attributi possono controllare il mapping delle classi e delle proprietà nel database. Nel modello Student l'attributo Column viene usato per il mapping del nome della proprietà FirstMidName su "FirstName" nel database.

Quando viene creato il database, i nomi delle proprietà nel modello vengono usati per i nomi di colonna (tranne quando viene usato l'attributo Column). Il modello Student usa il nome FirstMidName per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome.

Con l'attributo [Column], per Student.FirstMidName nel modello di dati viene eseguito il mapping alla colonna FirstName della tabella Student. L'aggiunta dell'attributo Column modifica il modello che supporta SchoolContext. Il modello che supporta SchoolContext non corrisponde più al database. Questa discrepanza verrà risolta aggiungendo una migrazione più avanti in questa esercitazione.

Attributo Required

[Required]

L'attributo Required rende obbligatori i campi delle proprietà del nome. L'attributo Required non è necessario per i tipi che non ammettono valori Null come i tipi valore (ad esempio, DateTime, int e double). 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; }

MinimumLength e Required consentono spazi vuoti per soddisfare la convalida. Usare l'attributo RegularExpression per il controllo completo sulla stringa.

Attributo Display

[Display(Name = "Last Name")]

L'attributo Display specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date". Le didascalie predefinite non hanno spazio che divide le parole, ad esempio "Lastname".

Creare una migrazione

Eseguire l'app e passare alla pagina Students (Studenti). Viene generata un'eccezione. Con l'attributo [Column] EF si aspetta di trovare una colonna denominata FirstName, ma il nome della colonna nel database è ancora FirstMidName.

Il messaggio di errore è simile al seguente:

SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:

SchoolContext
  • Nella console di Gestione pacchetti immettere i comandi seguenti per creare una nuova migrazione e aggiornare il database:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    Il primo di questi comandi genera il messaggio di avviso seguente:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    L'avviso viene generato perché i campi nome ora sono limitati a 50 caratteri. Se un nome nel database ha più di 50 caratteri, i caratteri dal 51 all'ultimo andranno perduti.

  • Aprire la tabella Student (Studente) in SSOX:

    Tabella Students (Studenti) in SSOX dopo le migrazioni

    Prima dell'applicazione della migrazione, le colonne del nome erano di tipo nvarchar(MAX). Ora le colonne del nome sono di tipo nvarchar(50). Il nome della colonna è cambiato da FirstMidName a FirstName.

  • Eseguire l'app e passare alla pagina Students (Studenti).
  • Si noti che l'ora non viene inclusa nell'input o visualizzata insieme alla data.
  • Selezionare Create New (Crea nuovo) e provare immettere un nome di lunghezza superiore a 50 caratteri.

Nota

Nelle sezioni seguenti la compilazione dell'app genera errori del compilatore in alcune fasi. Le istruzioni specificano quando compilare l'applicazione.

Entità Instructor

Creare Models/Instructor.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 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<Course> Courses { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Un'unica riga può ospitare più attributi. Gli attributi HireDate possono essere scritti come segue:

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

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

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

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

Un insegnante può avere al massimo un ufficio, quindi la proprietà OfficeAssignment contiene una singola entità OfficeAssignment. OfficeAssignment è null se non è assegnato nessun ufficio.

public OfficeAssignment OfficeAssignment { get; set; }

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 Key

L'attributo [Key] viene usato per identificare una proprietà come chiave primaria (PK) quando il nome della proprietà è diverso da classnameID o ID.

È una relazione uno-a-zero-o-uno tra le entità Instructor e OfficeAssignment. L'assegnazione di un ufficio esiste solo in relazione all'insegnante al quale viene assegnato l'ufficio. La chiave primaria OfficeAssignment è anche la chiave esterna (FK, Foreign Key) per l'entità Instructor. Una relazione uno-a-zero-o-uno si verifica quando un'infrastruttura a chiave pubblica in una tabella è sia un pk che un FK in un'altra tabella.

EF Core non può riconoscere InstructorID automaticamente come pk of OfficeAssignment perché InstructorID non segue la convenzione di denominazione ID o classnameID. Di conseguenza l'attributo Key viene usato per identificare l'entità InstructorID come chiave primaria:

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

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

Proprietà di navigazione Instructor

La proprietà di navigazione Instructor.OfficeAssignment può essere Null perché potrebbe non essere presente una riga OfficeAssignment per un determinato insegnante. Un insegnante potrebbe non avere un ufficio assegnato.

La proprietà di navigazione OfficeAssignment.Instructor avrà sempre un'entità Instructor perché il tipo InstructorID della chiave esterna è int, ovvero un tipo valore che non ammette valori Null. Un'assegnazione di ufficio non può esistere senza un insegnante.

Quando un'entità Instructor dispone di un'entità OfficeAssignment correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.

Entità Course

Aggiornare Models/Course.cs con il codice seguente:

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<Instructor> Instructors { get; set; }
    }
}

L'entità Course dispone di una proprietà chiave esterna (FK) DepartmentID. DepartmentID fa riferimento all'entità Department correlata. L'entità Course dispone di una proprietà di navigazione Department.

EF Core non richiede una proprietà di chiave esterna per un modello di dati quando il modello ha una proprietà di navigazione per un'entità correlata. EF Core crea automaticamente IK nel database ovunque siano necessari. EF Core crea proprietà shadow per gli FK creati automaticamente. Includere la chiave esterna in modo esplicito nel modello di dati, tuttavia, può rendere più semplici ed efficienti gli aggiornamenti. Si consideri ad esempio un modello in cui la proprietà chiave esterna DepartmentIDnon è inclusa. Quando un'entità Course viene recuperata per la modifica:

  • La Department proprietà è null se non viene caricata in modo esplicito.
  • Per aggiornare l'entità Course, è in primo luogo necessario recuperare l'entità Department.

Quando la proprietà chiave esterna DepartmentID è inclusa nel modello di dati, non è necessario recuperare l'entità Department prima di un aggiornamento.

Attributo DatabaseGenerated

L'attributo [DatabaseGenerated(DatabaseGeneratedOption.None)] indica che la chiave primaria viene resa disponibile dall'applicazione anziché essere generata dal database.

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

Per impostazione predefinita, EF Core si presuppone che i valori PK vengano generati dal database. La generazione nel database è in genere l'approccio migliore. Per le entità Course la chiave primaria viene specificata dall'utente. Un esempio può essere un numero di corso, quale la serie 1000 per il reparto di matematica o la serie 2000 per il reparto di lingua inglese.

L'attributo DatabaseGenerated può essere usato anche per generare valori predefiniti. Ad esempio, il database può generare automaticamente un campo data 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à chiave esterna (FK) e le proprietà di navigazione nell'entità Course riflettono le relazioni seguenti:

Un corso viene assegnato a un solo reparto, pertanto è presente una chiave esterna DepartmentID e una proprietà di navigazione Department.

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 Instructors è una raccolta:

public ICollection<Instructor> Instructors { get; set; }

Entità Department

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 dell'entità Department l'attributo Column viene usato per modificare il mapping dei tipi di dati SQL. La colonna Budget viene definita usando il tipo SQL Server money nel database:

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

In genere il mapping di colonne non è necessario. EF Core sceglie il tipo di dati SQL Server appropriato in base al tipo CLR per la proprietà . Il tipo CLR decimal esegue il mapping a un tipo SQL Server decimal. Budget è associato alla valuta e il tipo di dati money è più adatto per la valuta.

Proprietà chiave esterna e di navigazione

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

  • Un reparto può avere o non avere un amministratore.
  • Un amministratore è sempre un insegnante. Di conseguenza la proprietà InstructorID è inclusa come chiave esterna per l'entità Instructor.

La proprietà di navigazione è denominata Administrator ma contiene un'entità Instructor:

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

Nel ? codice precedente specifica che la proprietà è nullable.

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

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

Per convenzione, EF Core abilita l'eliminazione a catena per gli FK non nullable e per le relazioni molti-a-molti. Questo comportamento predefinito può generare regole di eliminazione a catena circolari. Quando viene aggiunta una migrazione, le regole di eliminazione a catena circolari causano un'eccezione.

Ad esempio, se la Department.InstructorID proprietà è stata definita come non nullable, EF Core configurare una regola di eliminazione a catena. In tal caso, il dipartimento verrebbe eliminato in seguito all'eliminazione dell'insegnante assegnato come amministratore. In questo scenario, una regola Restrict potrebbe essere più sensata. L'API Fluent seguente imposta una regola di limitazione e disabilita l'eliminazione a catena.

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

Proprietà di registrazione chiave esterna e navigazione

Un record di iscrizione è relativo a un solo corso frequentato da un solo studente.

Entità Enrollment

Aggiornare Models/Enrollment.cs con il codice seguente:

using System.ComponentModel.DataAnnotations;

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

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

Un record di iscrizione è relativo a un solo corso, pertanto sono presenti una proprietà 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 solo studente, pertanto sono presenti una proprietà 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. L'entità Enrollment funziona come una tabella di join molti-a-molti con payload nel database. Con payload si intende che la Enrollment tabella contiene dati aggiuntivi oltre agli FK per le tabelle unite in join. Nell'entità Enrollment , i dati aggiuntivi oltre agli FK sono pk e Grade.

La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato tramite EF Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione.

Relazione molti-a-molti Student-Course (Studente-Corso)

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 sul grado, sarebbe necessario contenere solo i due FK CourseID e StudentID. Una tabella di join molti-a-molti senza payload è anche detta tabella di join pura (PJT, Pure Join Table).

Le Instructor entità e Course hanno una relazione molti-a-molti usando un PJT.

Aggiornare il contesto di database

Aggiornare Data/SchoolContext.cs con il codice seguente:

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

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable(nameof(Course))
                .HasMany(c => c.Instructors)
                .WithMany(i => i.Courses);
            modelBuilder.Entity<Student>().ToTable(nameof(Student));
            modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
        }
    }
}

Il codice precedente aggiunge le nuove entità e configura la relazione molti-a-molti tra le Instructor entità e Course .

Alternativa API Fluent agli attributi

Il OnModelCreating metodo nel codice precedente usa l'API Fluent per configurare EF Core il comportamento. L'API è denominata "API Fluent" perché viene spesso usata unendo una serie di chiamate di metodi in un'unica istruzione. Il codice seguente è un esempio di API Fluent:

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

In questa esercitazione l'API Fluent viene usata solo per le operazioni di mapping del database che non possono essere eseguite con gli attributi. Tuttavia l'API Fluent può 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. MinimumLength non modifica lo schema, ma si limita ad applicare una regola di convalida per la lunghezza minima.

Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere pulite le classi di entità. È possibile combinare gli attributi e l'API Fluent. Esistono alcune configurazioni che possono essere eseguite solo con l'API Fluent, ad esempio specificando un'infrastruttura PK composita. Altre configurazioni possono essere eseguite solo con gli attributi (MinimumLength). La procedura consigliata per l'uso dell'API Fluent o degli attributi è la seguente:

  • Scegliere uno dei due approcci.
  • Usare l'approccio scelto con la massima coerenza possibile.

Alcuni degli attributi usati in questa esercitazione vengono usati per:

  • Solo convalida (ad esempio MinimumLength).
  • EF Core solo configurazione (ad esempio, HasKey).
  • Convalida e EF Core configurazione (ad esempio, [StringLength(50)]).

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

Specificare il valore di inizializzazione del database

Aggiornare il codice in Data/DbInitializer.cs:

using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var alexander = new Student
            {
                FirstMidName = "Carson",
                LastName = "Alexander",
                EnrollmentDate = DateTime.Parse("2016-09-01")
            };

            var alonso = new Student
            {
                FirstMidName = "Meredith",
                LastName = "Alonso",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var anand = new Student
            {
                FirstMidName = "Arturo",
                LastName = "Anand",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var barzdukas = new Student
            {
                FirstMidName = "Gytis",
                LastName = "Barzdukas",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var li = new Student
            {
                FirstMidName = "Yan",
                LastName = "Li",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var justice = new Student
            {
                FirstMidName = "Peggy",
                LastName = "Justice",
                EnrollmentDate = DateTime.Parse("2017-09-01")
            };

            var norman = new Student
            {
                FirstMidName = "Laura",
                LastName = "Norman",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var olivetto = new Student
            {
                FirstMidName = "Nino",
                LastName = "Olivetto",
                EnrollmentDate = DateTime.Parse("2011-09-01")
            };

            var students = new Student[]
            {
                alexander,
                alonso,
                anand,
                barzdukas,
                li,
                justice,
                norman,
                olivetto
            };

            context.AddRange(students);

            var abercrombie = new Instructor
            {
                FirstMidName = "Kim",
                LastName = "Abercrombie",
                HireDate = DateTime.Parse("1995-03-11")
            };

            var fakhouri = new Instructor
            {
                FirstMidName = "Fadi",
                LastName = "Fakhouri",
                HireDate = DateTime.Parse("2002-07-06")
            };

            var harui = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Harui",
                HireDate = DateTime.Parse("1998-07-01")
            };

            var kapoor = new Instructor
            {
                FirstMidName = "Candace",
                LastName = "Kapoor",
                HireDate = DateTime.Parse("2001-01-15")
            };

            var zheng = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Zheng",
                HireDate = DateTime.Parse("2004-02-12")
            };

            var instructors = new Instructor[]
            {
                abercrombie,
                fakhouri,
                harui,
                kapoor,
                zheng
            };

            context.AddRange(instructors);

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    Instructor = fakhouri,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    Instructor = harui,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    Instructor = kapoor,
                    Location = "Thompson 304" }
            };

            context.AddRange(officeAssignments);

            var english = new Department
            {
                Name = "English",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = abercrombie
            };

            var mathematics = new Department
            {
                Name = "Mathematics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = fakhouri
            };

            var engineering = new Department
            {
                Name = "Engineering",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = harui
            };

            var economics = new Department
            {
                Name = "Economics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = kapoor
            };

            var departments = new Department[]
            {
                english,
                mathematics,
                engineering,
                economics
            };

            context.AddRange(departments);

            var chemistry = new Course
            {
                CourseID = 1050,
                Title = "Chemistry",
                Credits = 3,
                Department = engineering,
                Instructors = new List<Instructor> { kapoor, harui }
            };

            var microeconomics = new Course
            {
                CourseID = 4022,
                Title = "Microeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var macroeconmics = new Course
            {
                CourseID = 4041,
                Title = "Macroeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var calculus = new Course
            {
                CourseID = 1045,
                Title = "Calculus",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { fakhouri }
            };

            var trigonometry = new Course
            {
                CourseID = 3141,
                Title = "Trigonometry",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { harui }
            };

            var composition = new Course
            {
                CourseID = 2021,
                Title = "Composition",
                Credits = 3,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var literature = new Course
            {
                CourseID = 2042,
                Title = "Literature",
                Credits = 4,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var courses = new Course[]
            {
                chemistry,
                microeconomics,
                macroeconmics,
                calculus,
                trigonometry,
                composition,
                literature
            };

            context.AddRange(courses);

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    Student = alexander,
                    Course = chemistry,
                    Grade = Grade.A
                },
                new Enrollment {
                    Student = alexander,
                    Course = microeconomics,
                    Grade = Grade.C
                },
                new Enrollment {
                    Student = alexander,
                    Course = macroeconmics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = calculus,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = trigonometry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = anand,
                    Course = chemistry
                },
                new Enrollment {
                    Student = anand,
                    Course = microeconomics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = barzdukas,
                    Course = chemistry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = li,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = justice,
                    Course = literature,
                    Grade = Grade.B
                }
            };

            context.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

Il codice precedente offre i dati di inizializzazione per le nuove entità. La maggior parte di questo codice crea nuovi oggetti entità e carica dati di esempio. I dati di esempio vengono usati per i test.

Applicare la migrazione o eliminare e ricreare

Con il database esistente, esistono due approcci per modificare il database:

Entrambe le scelte funzionano per SQL Server. Anche se il metodo che prevede l'applicazione della migrazione è più complesso e richiede più tempo, si tratta dell'approccio consigliato per gli ambienti di produzione reali.

Eliminare e ricreare il database

Per forzare EF Core la creazione di un nuovo database, eliminare e aggiornare il database:

  • Eliminare la cartella Migrations.
  • Nella console Gestione pacchetti eseguire i comandi seguenti:
Drop-Database
Add-Migration InitialCreate
Update-Database

Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize. DbInitializer.Initialize popola il nuovo database.

Aprire il database in SSOX:

  • Se SSOX è stato aperto in precedenza, fare clic sul pulsante Aggiorna.
  • Espandere il nodo Tabelle. Vengono visualizzate le tabelle create.

Passaggi successivi

Nelle due esercitazioni successive viene illustrato come leggere e aggiornare i dati correlati.

Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. Contenuto dell'esercitazione:

  • Vengono aggiunte altre entità e relazioni.
  • Il modello di dati viene personalizzato specificando regole di formattazione, convalida e mapping del database.

Il modello di dati completato è illustrato nella figura seguente:

Diagramma dell'entità

Entità Student (Studente)

Entità Student

Sostituire il codice in Models/Student.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 Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [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; }
    }
}

Il codice precedente aggiunge una proprietà FullName e aggiunge gli attributi seguenti alle proprietà esistenti:

  • [DataType]
  • [DisplayFormat]
  • [StringLength]
  • [Column]
  • [Required]
  • [Display]

Proprietà calcolata FullName

FullName è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. FullName non può essere impostata, quindi include solo una funzione di accesso get. Nel database non viene creata una colonna FullName.

Attributo DataType

[DataType(DataType.Date)]

Per le date di iscrizione degli studenti, tutte le pagine visualizzano attualmente l'ora del giorno insieme alla data, anche se è pertinente solo la data. Mediante gli attributi di annotazione dei dati è possibile modificare il codice per correggere il formato di visualizzazione in tutte le pagine che visualizzano i dati.

L'attributo DataType indica un tipo di dati più specifico rispetto al tipo intrinseco del database. In questo caso deve essere visualizzata solo la data e non la data e l'ora. L'enumerazione DataType fornisce molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e così via. L'attributo DataType può anche consentire all'app di fornire automaticamente funzionalità specifiche del tipo. Ad esempio:

  • Il collegamento mailto: viene creato automaticamente per DataType.EmailAddress.
  • Il selettore data viene incluso per DataType.Date nella maggior parte dei browser.

L'attributo DataType genera attributi HTML 5 data-. Gli attributi DataType non garantiscono la convalida.

Attributo DisplayFormat

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

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

L'attributo DisplayFormat viene usato per specificare in modo esplicito il formato della data. L'impostazione ApplyFormatInEditMode specifica che la formattazione deve essere applicata anche all'interfaccia utente di modifica. Alcuni campi non devono usare ApplyFormatInEditMode. Ad esempio il simbolo di valuta in genere non deve essere visualizzato in una casella di testo di modifica.

L'attributo DisplayFormat può essere usato da solo. In genere l'uso dell'attributo DataType con l'attributo DisplayFormat è consigliato. L'attributo DataType offre la semantica dei dati anziché specificarne il rendering in una schermata. L'attributo DataType offre i vantaggi seguenti che non sono disponibili in DisplayFormat:

  • Il browser può abilitare le funzionalità HTML5. Ad esempio può visualizzare un controllo di calendario, il simbolo della valuta appropriato per le impostazioni locali, i collegamenti alla posta elettronica e la convalida dell'input sul lato client.
  • Per impostazione predefinita, il browser esegue il rendering dei dati usando il formato corretto in base alle impostazioni locali.

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

Attributo StringLength

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

È possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando gli attributi. L'attributo StringLength specifica il numero minimo e massimo di caratteri consentiti in un campo dati. Il codice precedente limita i nomi a un massimo di 50 caratteri. Un esempio che imposta la lunghezza minima della stringa è disponibile di seguito.

L'attributo StringLength offre anche la convalida lato client e lato server. Il valore minimo non ha alcun effetto sullo schema del database.

L'attributo StringLength non impedisce a un utente di immettere spazi vuoti per un nome. L'attributo RegularExpression può essere usato 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]*$")]

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

Tabella Student (Studenti) in SSOX prima delle migrazioni

L'immagine precedente visualizza lo schema per la tabella Student. I campi del nome sono di tipo nvarchar(MAX). Quando una migrazione viene creata e applicata più avanti in questa esercitazione, i campi del nome diventano nvarchar(50) come risultato degli attributi di lunghezza della stringa.

Attributo Column

[Column("FirstName")]
public string FirstMidName { get; set; }

Gli attributi possono controllare il mapping delle classi e delle proprietà nel database. Nel modello Student l'attributo Column viene usato per il mapping del nome della proprietà FirstMidName su "FirstName" nel database.

Quando viene creato il database, i nomi delle proprietà nel modello vengono usati per i nomi di colonna (tranne quando viene usato l'attributo Column). Il modello Student usa il nome FirstMidName per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome.

Con l'attributo [Column], per Student.FirstMidName nel modello di dati viene eseguito il mapping alla colonna FirstName della tabella Student. L'aggiunta dell'attributo Column modifica il modello che supporta SchoolContext. Il modello che supporta SchoolContext non corrisponde più al database. Questa discrepanza verrà risolta aggiungendo una migrazione più avanti in questa esercitazione.

Attributo Required

[Required]

L'attributo Required rende obbligatori i campi delle proprietà del nome. L'attributo Required non è necessario per i tipi che non ammettono valori Null come i tipi valore (ad esempio, DateTime, int e double). 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; }

MinimumLength e Required consentono spazi vuoti per soddisfare la convalida. Usare l'attributo RegularExpression per il controllo completo sulla stringa.

Attributo Display

[Display(Name = "Last Name")]

L'attributo Display specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date". Le didascalie predefinite non hanno spazio che divide le parole, ad esempio "Lastname".

Creare una migrazione

Eseguire l'app e passare alla pagina Students (Studenti). Viene generata un'eccezione. Con l'attributo [Column] EF si aspetta di trovare una colonna denominata FirstName, ma il nome della colonna nel database è ancora FirstMidName.

Il messaggio di errore è simile al seguente:

SqlException: Invalid column name 'FirstName'.
  • Nella console di Gestione pacchetti immettere i comandi seguenti per creare una nuova migrazione e aggiornare il database:

    Add-Migration ColumnFirstName
    Update-Database
    

    Il primo di questi comandi genera il messaggio di avviso seguente:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    L'avviso viene generato perché i campi nome ora sono limitati a 50 caratteri. Se un nome nel database ha più di 50 caratteri, i caratteri dal 51 all'ultimo andranno perduti.

  • Aprire la tabella Student (Studente) in SSOX:

    Tabella Students (Studenti) in SSOX dopo le migrazioni

    Prima dell'applicazione della migrazione, le colonne del nome erano di tipo nvarchar(MAX). Ora le colonne del nome sono di tipo nvarchar(50). Il nome della colonna è cambiato da FirstMidName a FirstName.

  • Eseguire l'app e passare alla pagina Students (Studenti).
  • Si noti che l'ora non viene inclusa nell'input o visualizzata insieme alla data.
  • Selezionare Create New (Crea nuovo) e provare immettere un nome di lunghezza superiore a 50 caratteri.

Nota

Nelle sezioni seguenti la compilazione dell'app genera errori del compilatore in alcune fasi. Le istruzioni specificano quando compilare l'applicazione.

Entità Instructor

Entità Instructor

Creare Models/Instructor.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 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; }
    }
}

Un'unica riga può ospitare più attributi. Gli attributi HireDate possono essere scritti come segue:

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

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

Un insegnante può avere al massimo un ufficio, quindi la proprietà OfficeAssignment contiene una singola entità OfficeAssignment. OfficeAssignment è null se non è assegnato nessun ufficio.

public OfficeAssignment OfficeAssignment { get; set; }

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 Key

L'attributo [Key] viene usato per identificare una proprietà come chiave primaria (PK, Primary Key) quando il nome della proprietà è diverso da classnameID o ID.

È una relazione uno-a-zero-o-uno tra le entità Instructor e OfficeAssignment. L'assegnazione di un ufficio esiste solo in relazione all'insegnante al quale viene assegnato l'ufficio. La chiave primaria OfficeAssignment è anche la chiave esterna (FK, Foreign Key) per l'entità Instructor.

EF Core non può riconoscere InstructorID automaticamente come pk of OfficeAssignment perché InstructorID non segue la convenzione di denominazione ID o classnameID. Di conseguenza l'attributo Key viene usato per identificare l'entità InstructorID come chiave primaria:

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

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

Proprietà di navigazione Instructor

La proprietà di navigazione Instructor.OfficeAssignment può essere Null perché potrebbe non essere presente una riga OfficeAssignment per un determinato insegnante. Un insegnante potrebbe non avere un ufficio assegnato.

La proprietà di navigazione OfficeAssignment.Instructor avrà sempre un'entità Instructor perché il tipo InstructorID della chiave esterna è int, ovvero un tipo valore che non ammette valori Null. Un'assegnazione di ufficio non può esistere senza un insegnante.

Quando un'entità Instructor dispone di un'entità OfficeAssignment correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.

Entità Course

Entità Course

Aggiornare Models/Course.cs con il codice seguente:

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 dispone di una proprietà chiave esterna (FK) DepartmentID. DepartmentID fa riferimento all'entità Department correlata. L'entità Course dispone di una proprietà di navigazione Department.

EF Core non richiede una proprietà di chiave esterna per un modello di dati quando il modello ha una proprietà di navigazione per un'entità correlata. EF Core crea automaticamente IK nel database ovunque siano necessari. EF Core crea proprietà shadow per gli FK creati automaticamente. Includere la chiave esterna in modo esplicito nel modello di dati, tuttavia, può rendere più semplici ed efficienti gli aggiornamenti. Si consideri ad esempio un modello in cui la proprietà chiave esterna DepartmentIDnon è inclusa. Quando un'entità Course viene recuperata per la modifica:

  • La proprietà Department è Null se non viene caricata in modo esplicito.
  • Per aggiornare l'entità Course, è in primo luogo necessario recuperare l'entità Department.

Quando la proprietà chiave esterna DepartmentID è inclusa nel modello di dati, non è necessario recuperare l'entità Department prima di un aggiornamento.

Attributo DatabaseGenerated

L'attributo [DatabaseGenerated(DatabaseGeneratedOption.None)] indica che la chiave primaria viene resa disponibile dall'applicazione anziché essere generata dal database.

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

Per impostazione predefinita, EF Core si presuppone che i valori PK vengano generati dal database. La generazione nel database è in genere l'approccio migliore. Per le entità Course la chiave primaria viene specificata dall'utente. Un esempio può essere un numero di corso, quale la serie 1000 per il reparto di matematica o la serie 2000 per il reparto di lingua inglese.

L'attributo DatabaseGenerated può essere usato anche per generare valori predefiniti. Ad esempio, il database può generare automaticamente un campo data 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à chiave esterna (FK) e le proprietà di navigazione nell'entità Course riflettono le relazioni seguenti:

Un corso viene assegnato a un solo reparto, pertanto è presente una chiave esterna DepartmentID e una proprietà di navigazione Department.

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:

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

CourseAssignment viene illustrato più avanti.

Entità Department

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

Attributo Column

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

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

In genere il mapping di colonne non è necessario. EF Core sceglie il tipo di dati SQL Server appropriato in base al tipo CLR per la proprietà . Il tipo CLR decimal esegue il mapping a un tipo SQL Server decimal. Budget è associato alla valuta e il tipo di dati money è più adatto per la valuta.

Proprietà chiave esterna e di navigazione

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

  • Un reparto può avere o non avere un amministratore.
  • Un amministratore è sempre un insegnante. Di conseguenza la proprietà InstructorID è inclusa come chiave esterna per l'entità Instructor.

La proprietà di navigazione è denominata Administrator ma contiene un'entità Instructor:

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

Il punto interrogativo (?) nel codice precedente specifica che la proprietà è nullable.

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

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

Per convenzione, EF Core abilita l'eliminazione a catena per gli FK non nullable e per le relazioni molti-a-molti. Questo comportamento predefinito può generare regole di eliminazione a catena circolari. Quando viene aggiunta una migrazione, le regole di eliminazione a catena circolari causano un'eccezione.

Ad esempio, se la Department.InstructorID proprietà è stata definita come non nullable, EF Core configurare una regola di eliminazione a catena. In tal caso, il dipartimento verrebbe eliminato in seguito all'eliminazione dell'insegnante assegnato come amministratore. In questo scenario, una regola Restrict potrebbe essere più sensata. L'API Fluent seguente imposta una regola di limitazione e disabilita l'eliminazione a catena.

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

Entità Enrollment (Iscrizione)

Un record di iscrizione è relativo a un solo corso frequentato da un solo studente.

Entità Enrollment

Aggiornare Models/Enrollment.cs 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à chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:

Un record di iscrizione è relativo a un solo corso, pertanto sono presenti una proprietà 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 solo studente, pertanto sono presenti una proprietà 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. L'entità Enrollment funziona come una tabella di join molti-a-molti con payload nel database. "Con payload" significa che la tabella Enrollment contiene dati aggiuntivi oltre alle chiavi esterne delle tabelle di join (in questo caso la chiave primaria e Grade).

La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato tramite EF Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione.

Relazione molti-a-molti Student-Course (Studente-Corso)

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

Se la tabella Enrollment non include informazioni sul livello, è sufficiente che contenga le due chiavi esterne CourseID e StudentID. Una tabella di join molti-a-molti senza payload è anche detta tabella di join pura (PJT, Pure Join Table).

Le entità Instructor e Course hanno una relazione molti-a-molti con una tabella di join pura.

Nota: EF 6.x supporta tabelle di join implicite per relazioni molti-a-molti, ma EF Core non. Per altre informazioni, vedere Relazioni molti-a-molti nella EF Core versione 2.0.

Entità CourseAssignment

Entità CourseAssignment

Creare Models/CourseAssignment.cs con il codice seguente:

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

La relazione molti-a-molti tra insegnanti e corsi richiede una tabella di join e l'entità per tale tabella di join è CourseAssignment.

Relazione m:M tra insegnante e corsi

È pratica comune assegnare a un'entità di join un nome EntityName1EntityName2. Ad esempio la tabella di join istruttori-corsi che usa questa convenzione sarebbe CourseInstructor. È tuttavia consigliabile usare un nome che descrive la relazione.

I modelli di dati iniziano come strutture semplici, quindi le loro dimensioni aumentano. Le tabelle di join senza payload (PJT) si evolvono spesso per includere il payload. Se si assegna inizialmente un nome di entità descrittivo, non sarà necessario modificarlo quando la tabella di join cambia. 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 un'entità di join Ratings (Valutazioni). Per la relazione molti-a-molti Instructor-to-Courses CourseAssignment è preferibile a CourseInstructor.

Chiave composta

Le due chiavi esterne in CourseAssignment (InstructorID e CourseID) identificano insieme in modo univoco ogni riga della tabella CourseAssignment. CourseAssignment non richiede una chiave primaria dedicata. Le proprietà InstructorID e CourseID funzionano come una chiave primaria composta. L'unico modo per specificare i PK compositi da EF Core usare è l'API Fluent. La sezione successiva illustra come configurare la chiave primaria composta.

La chiave composta garantisce che:

  • Sono consentite più righe per un corso.
  • Sono consentite più righe per un insegnante.
  • Non sono consentite 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:

  • Aggiungere un indice univoco ai campi chiave esterna oppure
  • Configurare Enrollment con una chiave primaria composta simile a CourseAssignment. Per altre informazioni, vedere Indexes (Indici).

Aggiornare il contesto di database

Aggiornare Data/SchoolContext.cs con il codice seguente:

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.

Alternativa API Fluent agli attributi

Il OnModelCreating metodo nel codice precedente usa l'API Fluent per configurare EF Core il comportamento. L'API è denominata "API Fluent" perché viene spesso usata unendo una serie di chiamate di metodi in un'unica istruzione. Il codice seguente è un esempio di API Fluent:

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

In questa esercitazione l'API Fluent viene usata solo per le operazioni di mapping del database che non possono essere eseguite con gli attributi. Tuttavia l'API Fluent può 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. MinimumLength non modifica lo schema, ma si limita ad applicare una regola di convalida per la lunghezza minima.

Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere "pulite" le classi di entità. Gli attributi e l'API Fluent possono essere misti. Alcune configurazioni possono essere eseguite solo con l'API Fluent (specificando una chiave primaria composta). Altre configurazioni possono essere eseguite solo con gli attributi (MinimumLength). La procedura consigliata per l'uso dell'API Fluent o degli attributi è la seguente:

  • Scegliere uno dei due approcci.
  • Usare l'approccio scelto con la massima coerenza possibile.

Alcuni degli attributi usati in questa esercitazione vengono usati per:

  • Solo convalida (ad esempio MinimumLength).
  • EF Core solo configurazione (ad esempio, HasKey).
  • Convalida e EF Core configurazione (ad esempio, [StringLength(50)]).

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

Diagramma dell'entità

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

Diagramma dell'entità

Il diagramma precedente mostra quanto segue:

  • Diverse linee di relazione uno-a-molti (da 1 a *).
  • La linea di relazione uno-a-zero-o-uno (da 1 a 0..1) tra le entità Instructor e OfficeAssignment.
  • La linea di relazione zero-o-uno-a-molti (da 0..1 a *) tra le entità Instructor e Department.

Specificare il valore di inizializzazione del database

Aggiornare il codice in Data/DbInitializer.cs:

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("2016-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2017-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2011-09-01") }
            };

            context.Students.AddRange(students);
            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") }
            };

            context.Instructors.AddRange(instructors);
            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 }
            };

            context.Departments.AddRange(departments);
            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
                },
            };

            context.Courses.AddRange(courses);
            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" },
            };

            context.OfficeAssignments.AddRange(officeAssignments);
            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
                    },
            };

            context.CourseAssignments.AddRange(courseInstructors);
            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();
        }
    }
}

Il codice precedente offre i dati di inizializzazione per le nuove entità. La maggior parte di questo codice crea nuovi oggetti entità e carica dati di esempio. I dati di esempio vengono usati per i test. Visualizzare Enrollments e CourseAssignments per alcuni esempi del modo in cui può essere impostato il valore di inizializzazione per le tabelle join molti-a-molti.

Aggiungere una migrazione

Compilare il progetto.

Nella console di Gestione pacchetti eseguire il comando seguente.

Add-Migration ComplexDataModel

Il comando precedente visualizza 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.
To undo this action, use 'ef migrations remove'

Se viene eseguito il comando database update, viene generato l'errore seguente:

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'.

Nella prossima sezione viene descritto come procedere per questo errore.

Applicare la migrazione o eliminare e ricreare

Ora che è disponibile un database esistente, è necessario preoccuparsi di come applicare eventuali modifiche. Questa esercitazione illustra due alternative:

Entrambe le scelte funzionano per SQL Server. Anche se il metodo che prevede l'applicazione della migrazione è più complesso e richiede più tempo, si tratta dell'approccio consigliato per gli ambienti di produzione reali.

Eliminare e ricreare il database

Ignorare questa sezione se si usa SQL Server e si vuole adottare l'approccio con applicazione della migrazione nella sezione seguente.

Per forzare EF Core la creazione di un nuovo database, eliminare e aggiornare il database:

  • Nella console di Gestione pacchetti eseguire il comando seguente:

    Drop-Database
    
  • Eliminare la cartella Migrations e quindi eseguire il comando seguente:

    Add-Migration InitialCreate
    Update-Database
    

Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize. DbInitializer.Initialize popola il nuovo database.

Aprire il database in SSOX:

  • Se SSOX è stato aperto in precedenza, fare clic sul pulsante Aggiorna.

  • Espandere il nodo Tabelle. Vengono visualizzate le tabelle create.

    Tabelle in SSOX

  • Esaminare la tabella CourseAssignment:

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

    Dati CourseAssignment in SSOX

Applicare la migrazione

Questa sezione è facoltativa. Questa procedura funziona solo per SQL Server Local DB e solo se è stata ignorata la sezione Eliminare e ricreare il database precedente.

Quando le migrazioni vengono eseguite con dati esistenti, possono essere presenti vincoli di chiave esterna che non vengono soddisfatti con i dati esistenti. Con i dati di produzione, è necessario eseguire passaggi per la migrazione dei dati esistenti. Questa sezione visualizza un esempio di correzione delle violazioni dei vincoli di chiave esterna. Non apportare queste modifiche al codice senza un backup. Non apportare queste modifiche al codice se è stata completata la sezione precedente Eliminare e ricreare il database.

Il {timestamp}_ComplexDataModel.cs file contiene il codice seguente:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

Il codice precedente aggiunge una chiave esterna non nullable DepartmentID alla tabella Course. Il database dell'esercitazione precedente contiene righe in Course, pertanto la tabella non può essere aggiornata mediante le migrazioni.

Per far sì che la migrazione ComplexDataModel funzioni con i dati esistenti:

  • Modificare il codice per assegnare un valore predefinito alla nuova colonna (DepartmentID).
  • Creare un reparto fittizio denominato "Temp" che assume il ruolo di reparto predefinito.

Risolvere i vincoli della chiave esterna

Nella classe della migrazione ComplexDataModel aggiornare il metodo Up:

  • 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. Il nuovo codice viene inserito dopo il blocco .CreateTable( name: "Department":

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", 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);

Con le modifiche precedenti, le righe Course esistenti saranno correlate al dipartimento "Temp" dopo l'esecuzione del metodo ComplexDataModel.Up.

La modalità di gestione della situazione illustrata di seguito è semplificata per questa esercitazione. Un'app di produzione:

  • Includerà codice o script per l'aggiunta di righe Department e righe Course correlate alle nuove righe Department.
  • Non userà il reparto "Temp" o il valore predefinito per Course.DepartmentID.
  • Nella console di Gestione pacchetti eseguire il comando seguente:

    Update-Database
    

Dato che il metodo DbInitializer.Initialize è progettato per funzionare solo con un database vuoto, usare SSOX per eliminare tutte le righe nelle tabelle Student e Course. (L'eliminazione a catena si occuperà della tabella Enrollment.)

Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize. DbInitializer.Initialize popola il nuovo database.

Passaggi successivi

Nelle due esercitazioni successive viene illustrato come leggere e aggiornare i dati correlati.

Nelle esercitazioni precedenti è stato usato un modello di dati semplice costituito da tre entità. Contenuto dell'esercitazione:

  • Vengono aggiunte altre entità e relazioni.
  • Il modello di dati viene personalizzato specificando regole di formattazione, convalida e mapping del database.

Le classi di entità per il modello di dati completato sono visualizzate nella figura seguente:

Diagramma dell'entità

Se si verificano problemi che non si è in grado di risolvere, scaricare l'app completa.

Personalizzare il modello di dati usando gli attributi

In questa sezione il modello di dati viene personalizzato usando gli attributi.

Attributo DataType

Attualmente le pagine Student (Studente) visualizzano l'ora associata alla data di iscrizione. In genere i campi data visualizzano solo la data e non l'ora.

Eseguire l'aggiornamento Models/Student.cs con il codice evidenziato 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 indica un tipo di dati più specifico rispetto al tipo intrinseco del database. In questo caso deve essere visualizzata solo la data e non la data e l'ora. L'enumerazione DataType fornisce molti tipi di dati, ad esempio Data, Ora, PhoneNumber, Valuta, EmailAddress e così via. L'attributo DataType può anche consentire all'app di fornire automaticamente funzionalità specifiche del tipo. Ad esempio:

  • Il collegamento mailto: viene creato automaticamente per DataType.EmailAddress.
  • Il selettore data viene incluso per DataType.Date nella maggior parte dei browser.

L'attributo DataType genera attributi HTML 5 data- supportati dai browser HTML 5. Gli attributi DataType non garantiscono la convalida.

DataType.Date non specifica il formato della data visualizzata. Per impostazione predefinita il campo data viene visualizzato in base ai formati predefiniti per il 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 all'interfaccia utente di modifica. Alcuni campi non devono usare ApplyFormatInEditMode. Ad esempio il simbolo di valuta in genere non deve essere visualizzato in una casella di testo di modifica.

L'attributo DisplayFormat può essere usato da solo. In genere l'uso dell'attributo DataType con l'attributo DisplayFormat è consigliato. L'attributo DataType offre la semantica dei dati anziché specificarne il rendering in una schermata. L'attributo DataType offre i vantaggi seguenti che non sono disponibili in DisplayFormat:

  • Il browser può abilitare le funzionalità HTML5. Ad esempio può 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 esegue il rendering dei dati usando il formato corretto in base alle impostazioni locali.

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

Eseguire l'app. Passare alla pagina Students Index (Indice studenti). L'ora non viene più visualizzata. Ogni visualizzazione che usa il modello Student visualizza la data senza l'ora.

Pagina Students Index (Indice studenti) con date e senza ore

Attributo StringLength

È possibile specificare regole di convalida dei dati e messaggi di errore di convalida usando gli attributi. L'attributo StringLength specifica il numero minimo e massimo di caratteri consentiti in un campo dati. L'attributo StringLength offre anche la convalida lato client e lato server. Il valore minimo non ha alcun effetto sullo schema del database.

Aggiornare il modello Student con il codice 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, ErrorMessage = "First name cannot be longer than 50 characters.")]
        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; }
    }
}

Il codice precedente limita i nomi a un massimo di 50 caratteri. L'attributo StringLength non impedisce a un utente di immettere spazi vuoti per un nome. L'attributo RegularExpression viene usato 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]*$")]

Eseguire l'app:

  • Passare alla pagina Student (Studente).
  • Selezionare Crea nuovo e immettere un nome di lunghezza superiore a 50 caratteri.
  • Quando si fa clic su Crea la convalida lato client visualizza un messaggio di errore.

Pagina Students Index (Indice studenti) con errori di lunghezza stringa

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

Tabella Student (Studenti) in SSOX prima delle migrazioni

L'immagine precedente visualizza lo schema per la tabella Student. I campi nome hanno il tipo nvarchar(MAX) perché migrations non è stato eseguito nel database. Quando le istruzioni migrations verranno eseguite, più avanti in questa esercitazione, i campi nome diventeranno nvarchar(50).

Attributo Column

Gli attributi possono controllare il mapping delle classi e delle proprietà nel database. In questa sezione l'attributo Column viene usato per il mapping del nome della proprietà FirstMidName su "FirstName" nel database.

Quando viene creato il database, i nomi delle proprietà nel modello vengono usati per i nomi di colonna (tranne quando viene usato l'attributo Column).

Il modello Student usa il nome FirstMidName per il campo first-name (Nome) perché il campo potrebbe contenere anche un secondo nome.

Aggiornare il Student.cs file con il 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, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [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; }
    }
}

Con la modifica precedente, Student.FirstMidName nell'app esegue il mapping alla colonna FirstName della tabella Student.

L'aggiunta dell'attributo Column modifica il modello che supporta SchoolContext. Il modello che supporta SchoolContext non corrisponde più al database. Se l'app viene eseguita prima di applicare migrations, viene generata l'eccezione seguente:

SqlException: Invalid column name 'FirstName'.

Per aggiornare il database:

  • Compilare il progetto.
  • Aprire una finestra di comando nella cartella di progetto. Immettere i comandi seguenti per creare una nuova migrazione e aggiornare il database:
Add-Migration ColumnFirstName
Update-Database

Il comando migrations add ColumnFirstName genera il messaggio di avviso seguente:

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.

L'avviso viene generato perché i campi nome ora sono limitati a 50 caratteri. Se un nome nel database ha più di 50 caratteri, i caratteri dal 51 all'ultimo andranno perduti.

  • Test dell'app.

Aprire la tabella Student (Studente) in SSOX:

Tabella Students (Studenti) in SSOX dopo le migrazioni

Prima dell'applicazione della migrazione, le colonne del nome erano di tipo nvarchar(MAX). Ora le colonne del nome sono di tipo nvarchar(50). Il nome della colonna è cambiato da FirstMidName a FirstName.

Nota

Nella sezione seguente la compilazione dell'applicazione genera errori del compilatore in alcune fasi. Le istruzioni specificano quando compilare l'applicazione.

Aggiornamento dell'entità Student

Entità Student

Aggiornare Models/Student.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 Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [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, ad esempio per i tipi valore (DateTime, int, double e così via). I tipi che non possono essere null vengono considerati automaticamente come campi obbligatori.

L'attributo Required può essere sostituito con un parametro di lunghezza minima nell'attributo StringLength:

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

Attributo Display

L'attributo Display specifica che la didascalia delle caselle di testo deve essere "First Name", "Last Name", "Full Name" e "Enrollment Date". Le didascalie predefinite non hanno spazio che divide le parole, ad esempio "Lastname".

Proprietà calcolata FullName

FullName è una proprietà calcolata che restituisce un valore creato concatenando altre due proprietà. FullName non è impostabile e include solo una funzione di accesso get. Nel database non viene creata una colonna FullName.

Creare l'entità Instructor

Entità Instructor

Creare Models/Instructor.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 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; }
    }
}

Un'unica riga può ospitare più attributi. Gli attributi HireDate possono essere scritti come segue:

[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 contiene più entità:

  • Deve essere un tipo di elenco in cui le voci possono essere aggiunte, eliminate e aggiornate.

I tipi di proprietà di navigazione includono:

  • ICollection<T>
  • List<T>
  • HashSet<T>

Se ICollection<T> viene specificato, EF Core crea una HashSet<T> raccolta per impostazione predefinita.

L'entità CourseAssignment è illustrata nella sezione sulle relazioni molti-a-molti.

Le regole business di Contoso University specificano che un insegnante non può avere più di un ufficio. La proprietà OfficeAssignment contiene un'unica entità OfficeAssignment. OfficeAssignment è null se non è assegnato nessun 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 Key

L'attributo [Key] viene usato per identificare una proprietà come chiave primaria (PK, Primary Key) quando il nome della proprietà è diverso da classnameID o ID.

È una relazione uno-a-zero-o-uno tra le entità Instructor e OfficeAssignment. L'assegnazione di un ufficio esiste solo in relazione all'insegnante al quale viene assegnato l'ufficio. La chiave primaria OfficeAssignment è anche la chiave esterna (FK, Foreign Key) per l'entità Instructor. EF Core non è in grado di riconoscere InstructorID automaticamente l'infrastruttura a chiave pubblica di OfficeAssignment perché:

  • InstructorID non segue la convenzione di denominazione ID o classnameID.

Di conseguenza l'attributo Key viene usato per identificare l'entità InstructorID come chiave primaria:

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

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

Proprietà di navigazione Instructor

La proprietà di navigazione OfficeAssignment per l'entità Instructor è nullable perché:

  • I tipi di riferimento (ad esempio le classi) sono nullable.
  • Un insegnante potrebbe non avere un ufficio assegnato.

L'entità OfficeAssignment ha una proprietà di navigazione Instructor non nullable perché:

  • InstructorID è non nullable.
  • Un'assegnazione di ufficio non può esistere senza un insegnante.

Quando un'entità Instructor dispone di un'entità OfficeAssignment correlata, ogni entità include un riferimento all'altra entità nella relativa proprietà di navigazione.

L'attributo [Required] può essere applicato alla proprietà di navigazione Instructor:

[Required]
public Instructor Instructor { get; set; }

Il codice precedente specifica che deve essere presente un insegnante correlato. Il codice precedente non è necessario perché la chiave esterna InstructorID (che è anche la chiave primaria) è non nullable.

Modificare l'entità Course

Entità Course

Aggiornare Models/Course.cs con il codice seguente:

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 dispone di una proprietà chiave esterna (FK) DepartmentID. DepartmentID fa riferimento all'entità Department correlata. L'entità Course dispone di una proprietà di navigazione Department.

EF Core non richiede una proprietà FK per un modello di dati quando il modello ha una proprietà di navigazione per un'entità correlata.

EF Core crea automaticamente IK nel database ovunque siano necessari. EF Core crea proprietà shadow per gli FK creati automaticamente. Il fatto di avere la chiave esterna nel modello di dati può rendere più semplici ed efficienti gli aggiornamenti. Si consideri ad esempio un modello in cui la proprietà chiave esterna DepartmentIDnon è inclusa. Quando un'entità Course viene recuperata per la modifica:

  • L'entità Department è null se non viene caricata in modo esplicito.
  • Per aggiornare l'entità Course, è in primo luogo necessario recuperare l'entità Department.

Quando la proprietà chiave esterna DepartmentID è inclusa nel modello di dati, non è necessario recuperare l'entità Department prima di un aggiornamento.

Attributo DatabaseGenerated

L'attributo [DatabaseGenerated(DatabaseGeneratedOption.None)] indica che la chiave primaria viene resa disponibile dall'applicazione anziché essere generata dal database.

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

Per impostazione predefinita, EF Core si presuppone che i valori PK vengano generati dal database. La generazione dei valori di chiave primaria nel database è in genere l'approccio migliore. Per le entità Course la chiave primaria viene specificata dall'utente. Un esempio può essere un numero di corso, quale la serie 1000 per il reparto di matematica o la serie 2000 per il reparto di lingua inglese.

L'attributo DatabaseGenerated può essere usato anche per generare valori predefiniti. Ad esempio il database può generare automaticamente un campo data 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à chiave esterna (FK) e le proprietà di navigazione nell'entità Course riflettono le relazioni seguenti:

Un corso viene assegnato a un solo reparto, pertanto è presente una chiave esterna DepartmentID e una proprietà di navigazione Department.

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:

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

CourseAssignment viene illustrato più avanti.

Creare l'entità Department

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

Attributo Column

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

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

In genere il mapping di colonne non è necessario. EF Core in genere sceglie il tipo di dati DI SQL Server appropriato in base al tipo CLR per la proprietà . Il tipo CLR decimal esegue il mapping a un tipo SQL Server decimal. Budget è associato alla valuta e il tipo di dati money è più adatto per la valuta.

Proprietà chiave esterna e di navigazione

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

  • Un reparto può avere o non avere un amministratore.
  • Un amministratore è sempre un insegnante. Di conseguenza la proprietà InstructorID è inclusa come chiave esterna per l'entità Instructor.

La proprietà di navigazione è denominata Administrator ma contiene un'entità Instructor:

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

Il punto interrogativo (?) nel codice precedente specifica che la proprietà è nullable.

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

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

Nota: per convenzione, EF Core abilita l'eliminazione a catena per gli FK non nullable e per le relazioni molti-a-molti. L'eliminazione a catena può generare regole di eliminazione a catena circolari. Quando viene aggiunta una migrazione, le regole di eliminazione a catena circolari determinano un'eccezione.

Ad esempio, se la proprietà Department.InstructorID è stata definita come nullable:

  • EF Core configura una regola di eliminazione a catena per eliminare il reparto quando l'insegnante viene eliminato.

  • L'eliminazione del reparto quando viene eliminato l'insegnante non è il comportamento previsto.

  • L'API Fluent seguente imposta una regola di limitazione anziché a catena.

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

Il codice precedente disabilita l'eliminazione a catena per la relazione reparto-insegnante.

Aggiornare l'entità Enrollment

Un record di iscrizione è relativo a un solo corso frequentato da un solo studente.

Entità Enrollment

Aggiornare Models/Enrollment.cs 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à chiave esterna e le proprietà di navigazione riflettono le relazioni seguenti:

Un record di iscrizione è relativo a un solo corso, pertanto sono presenti una proprietà 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 solo studente, pertanto sono presenti una proprietà 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. L'entità Enrollment funziona come una tabella di join molti-a-molti con payload nel database. "Con payload" significa che la tabella Enrollment contiene dati aggiuntivi oltre alle chiavi esterne delle tabelle di join (in questo caso la chiave primaria e Grade).

La figura seguente illustra l'aspetto di queste relazioni in un diagramma di entità. Questo diagramma è stato generato tramite EF Power Tools per EF 6.x. La creazione del diagramma non fa parte dell'esercitazione.

Relazione molti-a-molti Student-Course (Studente-Corso)

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

Se la tabella Enrollment non include informazioni sul livello, è sufficiente che contenga le due chiavi esterne CourseID e StudentID. Una tabella di join molti-a-molti senza payload è anche detta tabella di join pura (PJT, Pure Join Table).

Le entità Instructor e Course hanno una relazione molti-a-molti con una tabella di join pura.

Nota: EF 6.x supporta tabelle di join implicite per relazioni molti-a-molti, ma EF Core non. Per altre informazioni, vedere Relazioni molti-a-molti nella EF Core versione 2.0.

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

Instructor-to-Courses

Relazione m:M tra insegnante e corsi

La relazione molti-a-molti Instructor-to-Courses (Insegnante-Corsi):

  • Richiede una tabella di join che deve essere rappresentata da un set di entità.
  • È una tabella di join pura (tabella senza payload).

È pratica comune assegnare a un'entità di join un nome EntityName1EntityName2. Ad esempio la tabella di join Instructor-to-Courses che usa questa convenzione sarà CourseInstructor. È tuttavia consigliabile usare un nome che descrive la relazione.

I modelli di dati iniziano come strutture semplici, quindi le loro dimensioni aumentano. In molti casi ai join senza payload vengono assegnati payload in un secondo momento. Se si assegna inizialmente un nome di entità descrittivo, non sarà necessario modificarlo quando la tabella di join cambia. 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 un'entità di join Ratings (Valutazioni). Per la relazione molti-a-molti Instructor-to-Courses CourseAssignment è preferibile a CourseInstructor.

Chiave composta

Le chiavi esterne non sono nullable. Le due chiavi esterne in CourseAssignment (InstructorID e CourseID) identificano insieme in modo univoco ogni riga della tabella CourseAssignment. CourseAssignment non richiede una chiave primaria dedicata. Le proprietà InstructorID e CourseID funzionano come una chiave primaria composta. L'unico modo per specificare i PK compositi da EF Core usare è l'API Fluent. La sezione successiva illustra come configurare la chiave primaria composta.

La chiave composta garantisce quanto segue:

  • Sono consentite più righe per un corso.
  • Sono consentite più righe per un insegnante.
  • Non sono consentite 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:

  • Aggiungere un indice univoco ai campi chiave esterna oppure
  • Configurare Enrollment con una chiave primaria composta simile a CourseAssignment. Per altre informazioni, vedere Indexes (Indici).

Aggiornare il contesto del database

Aggiungere il codice evidenziato seguente a Data/SchoolContext.cs:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

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

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { 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.

Alternativa API Fluent agli attributi

Il OnModelCreating metodo nel codice precedente usa l'API Fluent per configurare EF Core il comportamento. L'API è denominata "API Fluent" perché viene spesso usata unendo una serie di chiamate di metodi in un'unica istruzione. Il codice seguente è un esempio di API Fluent:

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

In questa esercitazione l'API Fluent viene usata solo per le operazioni di mapping del database che non possono essere eseguite con gli attributi. Tuttavia l'API Fluent può 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. MinimumLength non modifica lo schema, ma si limita ad applicare una regola di convalida per la lunghezza minima.

Alcuni sviluppatori preferiscono usare esclusivamente l'API Fluent in modo che possano mantenere "pulite" le classi di entità. Gli attributi e l'API Fluent possono essere misti. Alcune configurazioni possono essere eseguite solo con l'API Fluent (specificando una chiave primaria composta). Altre configurazioni possono essere eseguite solo con gli attributi (MinimumLength). La procedura consigliata per l'uso dell'API Fluent o degli attributi è la seguente:

  • Scegliere uno dei due approcci.
  • Usare l'approccio scelto con la massima coerenza possibile.

Alcuni attributi di questa esercitazione vengono usati per:

  • Solo convalida (ad esempio MinimumLength).
  • EF Core solo configurazione (ad esempio, HasKey).
  • Convalida e EF Core configurazione (ad esempio, [StringLength(50)]).

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 EF Power Tools per il modello School completato.

Diagramma dell'entità

Il diagramma precedente mostra quanto segue:

  • Diverse linee di relazione uno-a-molti (da 1 a *).
  • La linea di relazione uno-a-zero-o-uno (da 1 a 0..1) tra le entità Instructor e OfficeAssignment.
  • La linea di relazione zero-o-uno-a-molti (da 0..1 a *) tra le entità Instructor e Department.

Inizializzare il database con dati di test

Aggiornare il codice in Data/DbInitializer.cs:

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.Student.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.Student.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.Enrollment.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollment.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

Il codice precedente offre i dati di inizializzazione per le nuove entità. La maggior parte di questo codice crea nuovi oggetti entità e carica dati di esempio. I dati di esempio vengono usati per i test. Visualizzare Enrollments e CourseAssignments per alcuni esempi del modo in cui può essere impostato il valore di inizializzazione per le tabelle join molti-a-molti.

Aggiungere una migrazione

Compilare il progetto.

Add-Migration ComplexDataModel

Il comando precedente visualizza 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 viene eseguito il comando database update, viene generato l'errore seguente:

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'.

Applicare la migrazione

Ora che è disponibile un database esistente, è necessario preoccuparsi di come applicare eventuali modifiche future. Questa esercitazione illustra due approcci:

  • Eliminare e ricreare il database
  • Applicare la migrazione al database esistente. Anche se questo metodo è più complesso e richiede più tempo, è l'approccio consigliato per gli ambienti di produzione reali. Nota: questa è una sezione facoltativa dell'esercitazione. È possibile eseguire i passaggi di eliminazione e ricreazione e ignorare questa sezione. Se si vuole seguire la procedura descritta in questa sezione, non eseguire i passaggi di eliminazione e ricreazione.

Eliminare e ricreare il database

Il codice aggiornato in DbInitializer aggiunge dati di inizializzazione per le nuove entità. Per forzare EF Core la creazione di un nuovo database, eliminare e aggiornare il database:

Nella console di Gestione pacchetti eseguire il comando seguente:

Drop-Database
Update-Database

Eseguire Get-Help about_EntityFrameworkCore dalla console di Gestione pacchetti per ottenere informazioni.

Eseguire l'app. Quando si esegue l'app viene eseguito il metodo DbInitializer.Initialize. DbInitializer.Initialize popola il nuovo database.

Aprire il database in SSOX:

  • Se SSOX è stato aperto in precedenza, fare clic sul pulsante Aggiorna.
  • Espandere il nodo Tabelle. Vengono visualizzate le tabelle create.

Tabelle in SSOX

Esaminare la tabella CourseAssignment:

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

Dati CourseAssignment in SSOX

Applicare la migrazione al database esistente

Questa sezione è facoltativa. Questa procedura funziona solo se è stata ignorata la sezione Eliminare e ricreare il database precedente.

Quando le migrazioni vengono eseguite con dati esistenti, possono essere presenti vincoli di chiave esterna che non vengono soddisfatti con i dati esistenti. Con i dati di produzione, è necessario eseguire passaggi per la migrazione dei dati esistenti. Questa sezione visualizza un esempio di correzione delle violazioni dei vincoli di chiave esterna. Non apportare queste modifiche al codice senza un backup. Non apportare queste modifiche al codice se è stata completata la sezione precedente e il database è stato aggiornato.

Il {timestamp}_ComplexDataModel.cs file contiene il codice seguente:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

Il codice precedente aggiunge una chiave esterna non nullable DepartmentID alla tabella Course. Il database dell'esercitazione precedente contiene righe in Course, pertanto la tabella non può essere aggiornata mediante le migrazioni.

Per far sì che la migrazione ComplexDataModel funzioni con i dati esistenti:

  • Modificare il codice per assegnare un valore predefinito alla nuova colonna (DepartmentID).
  • Creare un reparto fittizio denominato "Temp" che assume il ruolo di reparto predefinito.

Risolvere i vincoli della chiave esterna

Aggiornare il metodo Up delle classi ComplexDataModel:

  • 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. Il nuovo codice viene inserito dopo il blocco .CreateTable( name: "Department":

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", 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);

Con le modifiche precedenti, le righe esistenti Course saranno correlate al reparto "Temp" dopo l'esecuzione del ComplexDataModel Up metodo.

Un'app di produzione:

  • Includerà codice o script per l'aggiunta di righe Department e righe Course correlate alle nuove righe Department.
  • Non userà il reparto "Temp" o il valore predefinito per Course.DepartmentID.

L'esercitazione successiva illustra i dati correlati.

Risorse aggiuntive