Partager via


Partie 5, Razor Pages avec EF Core dans ASP.NET Core - Modèle de données

Par Tom Dykstra, Jeremy Likness et Jon P Smith

L’application web Contoso University montre comment créer des applications web Pages Razor avec EF Core et Visual Studio. Pour obtenir des informations sur la série de didacticiels, consultez le premier didacticiel.

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez l’application finale et comparez ce code à celui que vous avez créé en suivant le tutoriel.

Dans les didacticiels précédents, nous avons travaillé avec un modèle de données de base composé de trois entités. Dans ce tutoriel, vous allez :

  • Nous allons ajouter d’autres entités et relations
  • Nous allons personnaliser le modèle de données en spécifiant des règles de mise en forme, de validation et de mappage de base de données.

Le modèle de données final est présenté dans l’illustration suivante :

Diagramme des entités

Le diagramme de base de données suivant a été créé avec Dataedo :

Diagramme Dataedo

Pour créer un diagramme de base de données avec Dataedo :

Dans le diagramme Dataedo précédent, le CourseInstructor est une table de jointure créée par Entity Framework. Pour plus d’informations, consultez Plusieurs-à-plusieurs.

L’entité Student

Remplacez le code de Models/Student.cs par le code suivant :

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

Le code précédent ajoute une propriété FullName et les attributs suivants aux propriétés existantes :

Propriété calculée FullName

FullName est une propriété calculée qui retourne une valeur créée par concaténation de deux autres propriétés. FullName ne pouvant pas être définie, elle n’a qu’un seul accesseur get. Aucune colonne FullName n’est créée dans la base de données.

Attribut DataType

[DataType(DataType.Date)]

Pour les dates d’inscription des étudiants, toutes les pages affichent actuellement l’heure du jour avec la date, alors que seule la date présente un intérêt. Vous pouvez avoir recours aux attributs d’annotation de données pour apporter une modification au code, permettant de corriger le format d’affichage dans chaque page qui affiche ces données.

L’attribut DataType spécifie un type de données qui est plus spécifique que le type intrinsèque de la base de données. Ici, seule la date doit être affichée (pas la date et l’heure). L’énumération DataType fournit de nombreux types de données, tels que Date, Heure, Numéro de téléphone, Devise, Adresse e-mail, etc. L’attribut DataType peut également permettre à l’application de fournir automatiquement des fonctionnalités spécifiques au type. Par exemple :

  • Le lien mailto: est créé automatiquement pour DataType.EmailAddress.
  • Le sélecteur de date est fourni pour DataType.Date dans la plupart des navigateurs.

L’attribut DataType émet des attributs HTML 5 data- (prononcé data dash en anglais). Les attributs DataType ne fournissent aucune validation.

Attribut DisplayFormat

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

DataType.Date ne spécifie pas le format de la date qui s’affiche. Par défaut, le champ de date est affiché conformément aux formats par défaut basés sur l’objet CultureInfo du serveur.

L’attribut DisplayFormat sert à spécifier explicitement le format de date. Le paramètre ApplyFormatInEditMode spécifie que la mise en forme doit également être appliquée à l’interface utilisateur de modification. Certains champs ne doivent pas utiliser ApplyFormatInEditMode. Par exemple, le symbole monétaire ne doit généralement pas être affiché dans une zone de texte d’édition.

L’attribut DisplayFormat peut être utilisé seul. Il est généralement préférable d’utiliser l’attribut DataType avec l’attribut DisplayFormat. L’attribut DataType transmet la sémantique des données, plutôt que la manière de l’afficher à l’écran. L’attribut DataType offre les avantages suivants qui ne sont pas disponibles dans DisplayFormat :

  • Le navigateur peut activer des fonctionnalités HTML5 (par exemple, pour afficher un contrôle de calendrier, le symbole monétaire correspondant aux paramètres régionaux, des liens de messagerie, une validation d’entrées côté client).
  • Par défaut, le navigateur affiche les données à l’aide du format correspondant aux paramètres régionaux.

Pour plus d’informations, consultez la documentation relative au Tag Helper <input>.

Attribut StringLength

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

Vous pouvez également spécifier des règles de validation de données et des messages d’erreur de validation à l’aide d’attributs. L’attribut StringLength spécifie les longueurs minimale et maximale de caractères autorisées dans un champ de données. Le code présenté limite la longueur des noms à 50 caractères. Un exemple qui définit la longueur de chaîne minimale est présenté plus loin.

L’attribut StringLength fournit également la validation côté client et côté serveur. La valeur minimale n’a aucun impact sur le schéma de base de données.

L’attribut StringLength n’empêche pas un utilisateur d’entrer un espace blanc comme nom. L’attribut RegularExpression peut être utilisé pour appliquer des restrictions à l’entrée. Par exemple, le code suivant exige que le premier caractère soit en majuscule et que les autres caractères soient alphabétiques :

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

Dans l’Explorateur d’objets SQL Server (SSOX), ouvrez le concepteur de tables Student en double-cliquant sur la table Student.

Table Students dans SSOX avant les migrations

L’image précédente montre le schéma pour la table Student. Les champs de nom sont de type nvarchar(MAX). Quand une migration est créée et appliquée plus loin dans ce tutoriel, les champs de nom deviennent nvarchar(50) en raison des attributs de longueur de chaîne.

Attribut Column

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

Les attributs peuvent contrôler comment les classes et les propriétés sont mappées à la base de données. Dans le modèle Student, l’attribut Column sert à mapper le nom de la propriété FirstMidName à « FirstName » dans la base de données.

Pendant la création de la base de données, les noms de propriétés du modèle sont utilisés comme noms de colonnes (sauf quand l’attribut Column est utilisé). Le modèle Student utilise FirstMidName pour le champ de prénom, car le champ peut également contenir un deuxième prénom.

Avec l’attribut [Column], dans le modèle de données, Student.FirstMidName est mappé à la colonne FirstName de la table Student. L’ajout de l’attribut Column change le modèle sur lequel repose SchoolContext. Le modèle sur lequel repose le SchoolContext ne correspond plus à la base de données. Cette différence sera résolue en ajoutant une migration plus loin dans ce tutoriel.

Attribut Required

[Required]

L’attribut Required fait des propriétés de nom des champs obligatoires. L’attribut Required n’est pas nécessaire pour les types qui n’autorisent pas les valeurs Null comme les types valeur (par exemple, DateTime, int et double). Les types qui n’acceptent pas les valeurs Null sont traités automatiquement comme des champs obligatoires.

L'attribut Required doit être utilisé avec MinimumLength pour appliquer MinimumLength.

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

MinimumLength et Required autorisent un espace blanc pour satisfaire la validation. Utilisez l'attribut RegularExpression pour un contrôle total sur la chaîne.

Attribut Display

[Display(Name = "Last Name")]

L’attribut Display spécifie que la légende pour les zones de texte doit être « First Name », « Last Name », « Full Name » et « Enrollment Date ». Par défaut, les légendes n’avaient pas d’espace pour séparer les mots, par exemple « Lastname ».

Créer une migration

Exécutez l’application et accédez à la page des étudiants. Une exception est levée. En raison de l’attribut [Column], EF s’attend à trouver une colonne nommée FirstName, mais le nom de la colonne dans la base de données est toujours FirstMidName.

Le message d’erreur est semblable à l’exemple suivant :

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

SchoolContext
  • Dans PMC, entrez les commandes suivantes pour créer une migration et mettre à jour la base de données :

    Add-Migration ColumnFirstName
    Update-Database
    
    

    La première de ces commandes génère le message d’avertissement suivant :

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

    Cet avertissement est généré, car les champs de nom sont désormais limités à 50 caractères. Si la base de données comporte un nom de plus 50 caractères, tous les caractères au-delà du cinquantième sont perdus.

  • Ouvrez la table Student dans SSOX :

    Table Students dans SSOX après les migrations

    Avant l’application de la migration, les colonnes de noms étaient de type nvarchar(MAX). Les colonnes de nom sont maintenant nvarchar(50). Le nom de la colonne est passé de FirstMidName à FirstName.

  • Exécutez l’application et accédez à la page des étudiants.
  • Notez que les heures ne sont pas entrées ou affichées avec les dates.
  • Sélectionnez Create New et essayez d’entrer un nom de plus de 50 caractères.

Notes

Dans les sections suivantes, la génération de l’application à certaines étapes génère des erreurs du compilateur. Les instructions indiquent quand générer l’application.

Entité Instructor

Créez Models/Instructor.cs avec le code suivant :

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

Plusieurs attributs peuvent être sur une seule ligne. Les attributs HireDate peuvent être écrits comme suit :

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

Les propriétés Courses et OfficeAssignment sont des propriétés de navigation.

Un formateur pouvant animer un nombre quelconque de cours, Courses est défini comme une collection.

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

Un formateur ne pouvant avoir au plus un bureau, la propriété OfficeAssignment contient une seule entité OfficeAssignment. OfficeAssignment a la valeur Null si aucun bureau n’est affecté.

public OfficeAssignment OfficeAssignment { get; set; }

Entité OfficeAssignment

Entité OfficeAssignment

Créez Models/OfficeAssignment.cs avec le code suivant :

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

Attribut Key

L’attribut [Key] est utilisé pour identifier une propriété comme clé primaire quand le nom de la propriété est autre que classnameID ou ID.

Il existe une relation un-à-zéro-ou-un entre les entités Instructor et OfficeAssignment. Une affectation de bureau existe uniquement relativement au formateur auquel elle est assignée. La clé primaire OfficeAssignment est également sa clé étrangère pour l’entité Instructor. Une relation un-à-zéro-ou-un se produit lorsqu’une clé primaire dans une table est à la fois une clé primaire et une clé étrangère dans une autre table.

EF Core ne peut pas reconnaître automatiquement InstructorID comme clé primaire de OfficeAssignment, car InstructorID ne respecte pas la convention d’affectation de noms d’ID ou de classnameID. Ainsi, l’attribut Key est utilisé pour identifier InstructorID comme clé primaire :

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

Par défaut, EF Core traite la clé comme n’étant pas générée par la base de données, car la colonne est utilisée pour une relation d’identification. Pour plus d’informations, consultez Clés EF.

Propriété de navigation Instructor

La propriété de navigation Instructor.OfficeAssignment peut avoir la valeur null, car il n’est pas certain qu’il existe une ligne OfficeAssignment pour un formateur donné. Un formateur peut ne pas avoir d’affectation de bureau.

La propriété de navigation OfficeAssignment.Instructor aura toujours une entité Instructor, car le type InstructorID de clé étrangère est int, type valeur qui n’autorise pas les valeurs Null. Une affectation de bureau ne peut pas exister sans un formateur.

Quand une entité Instructor a une entité OfficeAssignment associée, chaque entité a une référence à l’autre dans sa propriété de navigation.

Entité Course

Mettez à jour Models/Course.cs à l’aide du code suivant :

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 a une propriété de clé étrangère DepartmentID. DepartmentID pointe vers l’entité Department associée. L’entité Course a une propriété de navigation Department.

EF Core n’exige pas de propriété de clé étrangère pour un modèle de données quand le modèle a une propriété de navigation pour une entité associée. EF Core crée automatiquement des clés étrangères dans la base de données partout où elles sont nécessaires. EF Core crée des propriétés cachées pour les clés étrangères créées automatiquement. Cependant, le fait d’inclure la clé étrangère dans le modèle de données peut rendre les mises à jour plus simples et plus efficaces. Par exemple, considérez un modèle où la propriété de clé étrangère DepartmentID n’est pas incluse. Quand une entité Course est récupérée en vue d’une modification :

  • La propriété Department a la valeur null si elle n’est pas chargée explicitement.
  • Pour mettre à jour l’entité Course, vous devez d’abord récupérer l’entité Department.

Quand la propriété de clé étrangère DepartmentID est incluse dans le modèle de données, il n’est pas nécessaire de récupérer l’entité Department avant une mise à jour.

Attribut DatabaseGenerated

L’attribut [DatabaseGenerated(DatabaseGeneratedOption.None)] indique que la clé primaire est fournie par l’application plutôt que générée par la base de données.

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

Par défaut, EF Core part du principe que les valeurs de clé primaire sont générées par la base de données. La génération par la base de données est généralement la meilleure approche. Pour les entités Course, l’utilisateur spécifie la clé primaire. Par exemple, un numéro de cours comme une série 1000 pour le département Mathématiques et une série 2000 pour le département Anglais.

Vous pouvez aussi utiliser l’attribut DatabaseGenerated pour générer des valeurs par défaut. Par exemple, la base de données peut générer automatiquement un champ de date pour enregistrer la date de création ou de mise à jour d’une ligne. Pour plus d’informations, consultez Propriétés générées.

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et les propriétés de navigation dans l’entité Course reflètent les relations suivantes :

Un cours étant affecté à un seul département, il y a une clé étrangère DepartmentID et une propriété de navigation Department.

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

Un cours pouvant avoir un nombre quelconque d’étudiants inscrits, la propriété de navigation Enrollments est une collection :

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

Un cours pouvant être animé par plusieurs formateurs, la propriété de navigation Instructors est une collection :

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

Entité Department

Créez Models/Department.cs avec le code suivant :

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

Attribut Column

Précédemment, vous avez utilisé l’attribut Column pour changer le mappage des noms de colonne. Dans le code de l’entité Department, l’attribut Column est utilisé pour changer le mappage des types de données SQL. La colonne Budget est définie à l’aide du type monétaire SQL Server dans la base de données :

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

Le mappage de colonnes n’est généralement pas nécessaire. EF Core choisit le type de données SQL Server approprié en fonction du type CLR de la propriété. Le type CLR decimal est mappé à un type SQL Server decimal. Budget étant une valeur monétaire, le type de données money est plus approprié.

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et de navigation reflètent les relations suivantes :

  • Un département peut avoir ou ne pas avoir un administrateur.
  • Un administrateur est toujours un formateur. Ainsi, la propriété InstructorID est incluse en tant que clé étrangère de l’entité Instructor.

La propriété de navigation se nomme Administrator, mais elle contient une entité Instructor :

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

Le ? dans le code précédent indique que la propriété peut accepter la valeur Null.

Un département pouvant avoir de nombreux cours, il existe une propriété de navigation Courses :

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

Par convention, EF Core autorise la suppression en cascade pour les clés étrangères non nullables et pour les relations plusieurs à plusieurs. Ce comportement par défaut peut engendrer des règles de suppression en cascade circulaires. Les règles de suppression en cascade circulaires provoquent une exception quand une migration est ajoutée.

Par exemple, si la propriété Department.InstructorID a été définie comme n’acceptant pas les valeurs Null, EF Core configure une règle de suppression en cascade. Dans ce cas, le service est supprimé quand le formateur désigné comme étant son administrateur est supprimé. Dans ce scénario, une règle de restriction est plus logique. L’API Fluent suivante définit une règle de restriction et désactive la suppression en cascade.

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

Propriétés de clé étrangère Enrollment et de navigation

Un enregistrement d’inscription correspond à un cours suivi par un étudiant.

Entité Enrollment

Mettez à jour Models/Enrollment.cs à l’aide du code suivant :

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

Les propriétés de clé étrangère et de navigation reflètent les relations suivantes :

Un enregistrement d’inscription ne concernant qu’un seul cours, il y a une propriété de clé étrangère CourseID et une propriété de navigation Course :

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

Un enregistrement d’inscription ne concernant qu’un seul étudiant, il y a une propriété de clé étrangère StudentID et une propriété de navigation Student :

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

Relations plusieurs-à-plusieurs

Il existe une relation plusieurs-à-plusieurs entre les entités Student et Course. L’entité Enrollment joue le rôle de table de jointure plusieurs-à-plusieurs avec charge utile dans la base de données. Avec charge utile signifie que la table Enrollment contient des données supplémentaires en plus des clés étrangères pour les tables jointes. Dans l’entité Enrollment, les données supplémentaires en plus des clés étrangères sont la clé primaire et Grade.

L’illustration suivante montre à quoi ressemblent ces relations dans un diagramme d’entité. (Ce diagramme a été généré à l’aide d’EF Power Tools pour EF 6.x. La création du diagramme ne fait pas partie du tutoriel.)

Relation plusieurs-à-plusieurs Student-Course

Chaque ligne de relation comporte un 1 à une extrémité et un astérisque (*) à l’autre, ce qui indique une relation un-à-plusieurs.

Si la table Enrollment n’incluait pas d’informations de notes, elle aurait uniquement besoin de contenir les deux clés étrangères, CourseID et StudentID. Une table de jointure plusieurs-à-plusieurs sans charge utile est parfois appelée « table de jointure pure ».

Les entités Instructor et Course ont une relation plusieurs-à-plusieurs à l’aide d’une table de jointure pure (PJT).

Mettre à jour le contexte de base de données

Mettez à jour Data/SchoolContext.cs à l’aide du code suivant :

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

Le code précédent ajoute les nouvelles entités et configure la relation plusieurs-à-plusieurs entre les entités Instructor et Course.

Alternative d’API Fluent aux attributs

La méthode OnModelCreating du code précédent utilise l’API Fluent pour configurer le comportement de EF Core. L’API est appelée « Fluent », car elle est souvent utilisée en enchaînant une série d’appels de méthode en une seule instruction. Le code suivant est un exemple de l’API Fluent :

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

Dans ce tutoriel, l’API Fluent est utilisée uniquement pour le mappage de base de données qui ne peut pas être effectué avec des attributs. Toutefois, l’API Fluent peut spécifier la plupart des règles de mise en forme, de validation et de mappage pouvant être spécifiées à l’aide d’attributs.

Certains attributs, tels que MinimumLength, ne peuvent pas être appliqués avec l’API Fluent. MinimumLength ne change pas le schéma. Il applique uniquement une règle de validation de longueur minimale.

Certains développeurs préfèrent utiliser exclusivement l’API Fluent afin de conserver des classes d’entité propres. Vous pouvez combiner des attributs et l’API Fluent. Certaines configurations peuvent être effectuées uniquement avec l’API Fluent, par exemple spécification d’une clé primaire composite. Certaines autres peuvent être effectuées uniquement avec des attributs (MinimumLength). Voici ce que nous recommandons pour l’utilisation des API Fluent ou des attributs :

  • Choisissez l’une de ces deux approches.
  • Dans la mesure du possible, utilisez l’approche choisie de manière cohérente.

Certains des attributs utilisés dans ce tutoriel sont utilisés pour :

  • La validation uniquement (par exemple, MinimumLength).
  • La configuration de EF Core uniquement (par exemple, HasKey).
  • La validation et la configuration de EF Core (par exemple, [StringLength(50)]).

Pour plus d’informations sur les attributs et l’API Fluent, consultez Méthodes de configuration.

Amorcer la base de données

Mettez à jour le code dans 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();
        }
    }
}

Le code précédent fournit des données de valeur initiale pour les nouvelles entités. La majeure partie de ce code crée des objets d’entités et charge des exemples de données. Les exemples de données sont utilisés à des fins de test.

Appliquer la migration ou supprimer et recréer

Avec la base de données existante, il existe deux approches pour modifier la base de données :

Les deux options fonctionnent pour SQL Server. Bien que la méthode d’application de la migration soit plus longue et complexe, il s’agit de l’approche privilégiée pour les environnements de production réels.

Supprimer et recréer la base de données

Pour forcer EF Core à créer une base de données, annulez et mettez à jour la base de données :

  • Supprimez le dossier Migrations.
  • Dans la console du Gestionnaire de package, exécutez les commandes suivantes :
Drop-Database
Add-Migration InitialCreate
Update-Database

Exécutez l'application. L’exécution de l’application entraîne l’exécution de la méthode DbInitializer.Initialize. La méthode DbInitializer.Initialize remplit la nouvelle base de données.

Ouvrez la base de données dans SSOX :

  • Si SSOX était déjà ouvert, cliquez sur le bouton Actualiser.
  • Développez le nœud Tables. Les tables créées sont affichées.

Étapes suivantes

Les deux tutoriels suivants montrent comment lire et mettre à jour des données associées.

Dans les didacticiels précédents, nous avons travaillé avec un modèle de données de base composé de trois entités. Dans ce tutoriel, vous allez :

  • Nous allons ajouter d’autres entités et relations
  • Nous allons personnaliser le modèle de données en spécifiant des règles de mise en forme, de validation et de mappage de base de données.

Le modèle de données final est présenté dans l’illustration suivante :

Diagramme des entités

L’entité Student

Entité Student

Remplacez le code de Models/Student.cs par le code suivant :

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

Le code précédent ajoute une propriété FullName et les attributs suivants aux propriétés existantes :

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

Propriété calculée FullName

FullName est une propriété calculée qui retourne une valeur créée par concaténation de deux autres propriétés. FullName ne pouvant pas être définie, elle n’a qu’un seul accesseur get. Aucune colonne FullName n’est créée dans la base de données.

Attribut DataType

[DataType(DataType.Date)]

Pour les dates d’inscription des étudiants, toutes les pages affichent actuellement l’heure du jour avec la date, alors que seule la date présente un intérêt. Vous pouvez avoir recours aux attributs d’annotation de données pour apporter une modification au code, permettant de corriger le format d’affichage dans chaque page qui affiche ces données.

L’attribut DataType spécifie un type de données qui est plus spécifique que le type intrinsèque de la base de données. Ici, seule la date doit être affichée (pas la date et l’heure). L’énumération DataType fournit de nombreux types de données, tels que Date, Heure, Numéro de téléphone, Devise, Adresse e-mail, etc. L’attribut DataType peut également permettre à l’application de fournir automatiquement des fonctionnalités spécifiques au type. Par exemple :

  • Le lien mailto: est créé automatiquement pour DataType.EmailAddress.
  • Le sélecteur de date est fourni pour DataType.Date dans la plupart des navigateurs.

L’attribut DataType émet des attributs HTML 5 data- (prononcé data dash en anglais). Les attributs DataType ne fournissent aucune validation.

Attribut DisplayFormat

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

DataType.Date ne spécifie pas le format de la date qui s’affiche. Par défaut, le champ de date est affiché conformément aux formats par défaut basés sur l’objet CultureInfo du serveur.

L’attribut DisplayFormat sert à spécifier explicitement le format de date. Le paramètre ApplyFormatInEditMode spécifie que la mise en forme doit également être appliquée à l’interface utilisateur de modification. Certains champs ne doivent pas utiliser ApplyFormatInEditMode. Par exemple, le symbole monétaire ne doit généralement pas être affiché dans une zone de texte d’édition.

L’attribut DisplayFormat peut être utilisé seul. Il est généralement préférable d’utiliser l’attribut DataType avec l’attribut DisplayFormat. L’attribut DataType transmet la sémantique des données, plutôt que la manière de l’afficher à l’écran. L’attribut DataType offre les avantages suivants qui ne sont pas disponibles dans DisplayFormat :

  • Le navigateur peut activer des fonctionnalités HTML5 (par exemple, pour afficher un contrôle de calendrier, le symbole monétaire correspondant aux paramètres régionaux, des liens de messagerie, une validation d’entrées côté client).
  • Par défaut, le navigateur affiche les données à l’aide du format correspondant aux paramètres régionaux.

Pour plus d’informations, consultez la documentation relative au Tag Helper <input>.

Attribut StringLength

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

Vous pouvez également spécifier des règles de validation de données et des messages d’erreur de validation à l’aide d’attributs. L’attribut StringLength spécifie les longueurs minimale et maximale de caractères autorisées dans un champ de données. Le code présenté limite la longueur des noms à 50 caractères. Un exemple qui définit la longueur de chaîne minimale est présenté plus loin.

L’attribut StringLength fournit également la validation côté client et côté serveur. La valeur minimale n’a aucun impact sur le schéma de base de données.

L’attribut StringLength n’empêche pas un utilisateur d’entrer un espace blanc comme nom. L’attribut RegularExpression peut être utilisé pour appliquer des restrictions à l’entrée. Par exemple, le code suivant exige que le premier caractère soit en majuscule et que les autres caractères soient alphabétiques :

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

Dans l’Explorateur d’objets SQL Server (SSOX), ouvrez le concepteur de tables Student en double-cliquant sur la table Student.

Table Students dans SSOX avant les migrations

L’image précédente montre le schéma pour la table Student. Les champs de nom sont de type nvarchar(MAX). Quand une migration est créée et appliquée plus loin dans ce tutoriel, les champs de nom deviennent nvarchar(50) en raison des attributs de longueur de chaîne.

Attribut Column

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

Les attributs peuvent contrôler comment les classes et les propriétés sont mappées à la base de données. Dans le modèle Student, l’attribut Column sert à mapper le nom de la propriété FirstMidName à « FirstName » dans la base de données.

Pendant la création de la base de données, les noms de propriétés du modèle sont utilisés comme noms de colonnes (sauf quand l’attribut Column est utilisé). Le modèle Student utilise FirstMidName pour le champ de prénom, car le champ peut également contenir un deuxième prénom.

Avec l’attribut [Column], dans le modèle de données, Student.FirstMidName est mappé à la colonne FirstName de la table Student. L’ajout de l’attribut Column change le modèle sur lequel repose SchoolContext. Le modèle sur lequel repose le SchoolContext ne correspond plus à la base de données. Cette différence sera résolue en ajoutant une migration plus loin dans ce tutoriel.

Attribut Required

[Required]

L’attribut Required fait des propriétés de nom des champs obligatoires. L’attribut Required n’est pas nécessaire pour les types qui n’autorisent pas les valeurs Null comme les types valeur (par exemple, DateTime, int et double). Les types qui n’acceptent pas les valeurs Null sont traités automatiquement comme des champs obligatoires.

L'attribut Required doit être utilisé avec MinimumLength pour appliquer MinimumLength.

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

MinimumLength et Required autorisent un espace blanc pour satisfaire la validation. Utilisez l'attribut RegularExpression pour un contrôle total sur la chaîne.

Attribut Display

[Display(Name = "Last Name")]

L’attribut Display spécifie que la légende pour les zones de texte doit être « First Name », « Last Name », « Full Name » et « Enrollment Date ». Par défaut, les légendes n’avaient pas d’espace pour séparer les mots, par exemple « Lastname ».

Créer une migration

Exécutez l’application et accédez à la page des étudiants. Une exception est levée. En raison de l’attribut [Column], EF s’attend à trouver une colonne nommée FirstName, mais le nom de la colonne dans la base de données est toujours FirstMidName.

Le message d’erreur est semblable à l’exemple suivant :

SqlException: Invalid column name 'FirstName'.
  • Dans PMC, entrez les commandes suivantes pour créer une migration et mettre à jour la base de données :

    Add-Migration ColumnFirstName
    Update-Database
    

    La première de ces commandes génère le message d’avertissement suivant :

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

    Cet avertissement est généré, car les champs de nom sont désormais limités à 50 caractères. Si la base de données comporte un nom de plus 50 caractères, tous les caractères au-delà du cinquantième sont perdus.

  • Ouvrez la table Student dans SSOX :

    Table Students dans SSOX après les migrations

    Avant l’application de la migration, les colonnes de noms étaient de type nvarchar(MAX). Les colonnes de nom sont maintenant nvarchar(50). Le nom de la colonne est passé de FirstMidName à FirstName.

  • Exécutez l’application et accédez à la page des étudiants.
  • Notez que les heures ne sont pas entrées ou affichées avec les dates.
  • Sélectionnez Create New et essayez d’entrer un nom de plus de 50 caractères.

Notes

Dans les sections suivantes, la génération de l’application à certaines étapes génère des erreurs du compilateur. Les instructions indiquent quand générer l’application.

Entité Instructor

Entité Instructor

Créez Models/Instructor.cs avec le code suivant :

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

Plusieurs attributs peuvent être sur une seule ligne. Les attributs HireDate peuvent être écrits comme suit :

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

Les propriétés CourseAssignments et OfficeAssignment sont des propriétés de navigation.

Un formateur pouvant animer un nombre quelconque de cours, CourseAssignments est défini comme une collection.

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

Un formateur ne pouvant avoir au plus un bureau, la propriété OfficeAssignment contient une seule entité OfficeAssignment. OfficeAssignment a la valeur Null si aucun bureau n’est affecté.

public OfficeAssignment OfficeAssignment { get; set; }

Entité OfficeAssignment

Entité OfficeAssignment

Créez Models/OfficeAssignment.cs avec le code suivant :

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

Attribut Key

L’attribut [Key] est utilisé pour identifier une propriété comme clé primaire quand le nom de la propriété est autre que classnameID ou ID.

Il existe une relation un-à-zéro-ou-un entre les entités Instructor et OfficeAssignment. Une affectation de bureau existe uniquement relativement au formateur auquel elle est assignée. La clé primaire OfficeAssignment est également sa clé étrangère pour l’entité Instructor.

EF Core ne peut pas reconnaître automatiquement InstructorID comme clé primaire de OfficeAssignment, car InstructorID ne respecte pas la convention d’affectation de noms d’ID ou de classnameID. Ainsi, l’attribut Key est utilisé pour identifier InstructorID comme clé primaire :

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

Par défaut, EF Core traite la clé comme n’étant pas générée par la base de données, car la colonne est utilisée pour une relation d’identification.

Propriété de navigation Instructor

La propriété de navigation Instructor.OfficeAssignment peut avoir la valeur null, car il n’est pas certain qu’il existe une ligne OfficeAssignment pour un formateur donné. Un formateur peut ne pas avoir d’affectation de bureau.

La propriété de navigation OfficeAssignment.Instructor aura toujours une entité Instructor, car le type InstructorID de clé étrangère est int, type valeur qui n’autorise pas les valeurs Null. Une affectation de bureau ne peut pas exister sans un formateur.

Quand une entité Instructor a une entité OfficeAssignment associée, chaque entité a une référence à l’autre dans sa propriété de navigation.

Entité Course

Entité Course

Mettez à jour Models/Course.cs à l’aide du code suivant :

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 a une propriété de clé étrangère DepartmentID. DepartmentID pointe vers l’entité Department associée. L’entité Course a une propriété de navigation Department.

EF Core n’exige pas de propriété de clé étrangère pour un modèle de données quand le modèle a une propriété de navigation pour une entité associée. EF Core crée automatiquement des clés étrangères dans la base de données partout où elles sont nécessaires. EF Core crée des propriétés cachées pour les clés étrangères créées automatiquement. Cependant, le fait d’inclure la clé étrangère dans le modèle de données peut rendre les mises à jour plus simples et plus efficaces. Par exemple, considérez un modèle où la propriété de clé étrangère DepartmentID n’est pas incluse. Quand une entité Course est récupérée en vue d’une modification :

  • La propriété Department a la valeur Null si elle n’est pas chargée explicitement.
  • Pour mettre à jour l’entité Course, vous devez d’abord récupérer l’entité Department.

Quand la propriété de clé étrangère DepartmentID est incluse dans le modèle de données, il n’est pas nécessaire de récupérer l’entité Department avant une mise à jour.

Attribut DatabaseGenerated

L’attribut [DatabaseGenerated(DatabaseGeneratedOption.None)] indique que la clé primaire est fournie par l’application plutôt que générée par la base de données.

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

Par défaut, EF Core part du principe que les valeurs de clé primaire sont générées par la base de données. La génération par la base de données est généralement la meilleure approche. Pour les entités Course, l’utilisateur spécifie la clé primaire. Par exemple, un numéro de cours comme une série 1000 pour le département Mathématiques et une série 2000 pour le département Anglais.

Vous pouvez aussi utiliser l’attribut DatabaseGenerated pour générer des valeurs par défaut. Par exemple, la base de données peut générer automatiquement un champ de date pour enregistrer la date de création ou de mise à jour d’une ligne. Pour plus d’informations, consultez Propriétés générées.

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et les propriétés de navigation dans l’entité Course reflètent les relations suivantes :

Un cours étant affecté à un seul département, il y a une clé étrangère DepartmentID et une propriété de navigation Department.

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

Un cours pouvant avoir un nombre quelconque d’étudiants inscrits, la propriété de navigation Enrollments est une collection :

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

Un cours pouvant être animé par plusieurs formateurs, la propriété de navigation CourseAssignments est une collection :

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

CourseAssignment est expliquée plus loin.

Entité Department

Entité de service

Créez Models/Department.cs avec le code suivant :

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

Attribut Column

Précédemment, vous avez utilisé l’attribut Column pour changer le mappage des noms de colonne. Dans le code de l’entité Department, l’attribut Column est utilisé pour changer le mappage des types de données SQL. La colonne Budget est définie à l’aide du type monétaire SQL Server dans la base de données :

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

Le mappage de colonnes n’est généralement pas nécessaire. EF Core choisit le type de données SQL Server approprié en fonction du type CLR de la propriété. Le type CLR decimal est mappé à un type SQL Server decimal. Budget étant une valeur monétaire, le type de données money est plus approprié.

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et de navigation reflètent les relations suivantes :

  • Un département peut avoir ou ne pas avoir un administrateur.
  • Un administrateur est toujours un formateur. Ainsi, la propriété InstructorID est incluse en tant que clé étrangère de l’entité Instructor.

La propriété de navigation se nomme Administrator, mais elle contient une entité Instructor :

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

Le point d’interrogation (?) dans le code précédent indique que la propriété est nullable.

Un département pouvant avoir de nombreux cours, il existe une propriété de navigation Courses :

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

Par convention, EF Core autorise la suppression en cascade pour les clés étrangères non nullables et pour les relations plusieurs à plusieurs. Ce comportement par défaut peut engendrer des règles de suppression en cascade circulaires. Les règles de suppression en cascade circulaires provoquent une exception quand une migration est ajoutée.

Par exemple, si la propriété Department.InstructorID a été définie comme n’acceptant pas les valeurs Null, EF Core configure une règle de suppression en cascade. Dans ce cas, le service est supprimé quand le formateur désigné comme étant son administrateur est supprimé. Dans ce scénario, une règle de restriction est plus logique. L’API Fluent suivante définit une règle de restriction et désactive la suppression en cascade.

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

L’entité Enrollment

Un enregistrement d’inscription correspond à un cours suivi par un étudiant.

Entité Enrollment

Mettez à jour Models/Enrollment.cs à l’aide du code suivant :

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

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et de navigation reflètent les relations suivantes :

Un enregistrement d’inscription ne concernant qu’un seul cours, il y a une propriété de clé étrangère CourseID et une propriété de navigation Course :

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

Un enregistrement d’inscription ne concernant qu’un seul étudiant, il y a une propriété de clé étrangère StudentID et une propriété de navigation Student :

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

Relations plusieurs-à-plusieurs

Il existe une relation plusieurs-à-plusieurs entre les entités Student et Course. L’entité Enrollment joue le rôle de table de jointure plusieurs-à-plusieurs avec charge utile dans la base de données. « Avec charge utile » signifie que la table Enrollment contient des données supplémentaires en plus des clés étrangères pour les tables jointes (dans le cas présent, la clé primaire et Grade).

L’illustration suivante montre à quoi ressemblent ces relations dans un diagramme d’entité. (Ce diagramme a été généré à l’aide d’EF Power Tools pour EF 6.x. La création du diagramme ne fait pas partie du tutoriel.)

Relation plusieurs-à-plusieurs Student-Course

Chaque ligne de relation comporte un 1 à une extrémité et un astérisque (*) à l’autre, ce qui indique une relation un-à-plusieurs.

Si la table Enrollment n’incluait pas d’informations de notes, elle aurait uniquement besoin de contenir les deux clés étrangères (CourseID et StudentID). Une table de jointure plusieurs-à-plusieurs sans charge utile est parfois appelée « table de jointure pure ».

Les entités Instructor et Course ont une relation plusieurs-à-plusieurs à l’aide d’une table de jointure pure.

Remarque : EF 6.x prend en charge les tables de jointure implicites pour les relations plusieurs-à-plusieurs, mais pas EF Core. Pour plus d’informations, consultez Relations plusieurs-à-plusieurs dans EF Core 2.0.

Entité CourseAssignment

Entité CourseAssignment

Créez Models/CourseAssignment.cs avec le code suivant :

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 relation « plusieurs-à-plusieurs » entre le formateur et les cours nécessite une table de jointure, et l’entité pour cette table de jointure est CourseAssignment.

Instructor-to-Courses m:M

Il est courant de nommer une entité de jointure EntityName1EntityName2. Par exemple, la table de jointure Instructor-to-Courses utilisant ce modèle est CourseInstructor. Toutefois, nous vous recommandons d’utiliser un nom qui décrit la relation.

Les modèles de données sont simples au début, puis ils augmentent en complexité. Les tables de jointure sans charge utile évoluent fréquemment pour inclure une charge utile. En commençant par un nom d’entité descriptif, vous n’aurez pas besoin de modifier le nom quand la table de jointure changera. Dans l’idéal, l’entité de jointure aura son propre nom (éventuellement un mot unique) naturel dans le domaine d’entreprise. Par exemple, les livres et les clients pourraient être liés avec une entité de jointure nommée Évaluations. Pour la relation plusieurs-à-plusieurs Instructor-Courses, il vaut mieux utiliser CourseAssignment que CourseInstructor.

Clé composite

Ensemble, le deux clés étrangères dans CourseAssignment (InstructorID et CourseID) identifient de façon unique chaque ligne de la table CourseAssignment. CourseAssignment ne nécessite pas de clé primaire dédiée. Les propriétés InstructorID et CourseID fonctionnent comme une clé primaire composite. Le seul moyen de spécifier des clés primaires composites dans EF Core consiste à faire appel à l’API Fluent. La section suivante montre comment configurer la clé primaire composite.

La clé composite garantit que :

  • Plusieurs lignes sont autorisées pour un cours.
  • Plusieurs lignes sont autorisées pour un formateur.
  • Plusieurs lignes ne sont pas autorisées pour la même paire formateur-cours.

Comme l’entité de jointure Enrollment définit sa propre clé primaire, des doublons de ce type sont possibles. Pour éviter ces doublons :

  • Ajoutez un index unique sur les champs de clé primaire, ou
  • Configurez Enrollment avec une clé primaire composite similaire à CourseAssignment. Pour plus d’informations, consultez Index.

Mettre à jour le contexte de base de données

Mettez à jour Data/SchoolContext.cs à l’aide du code suivant :

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

Le code précédent ajoute les nouvelles entités et configure la clé primaire composite de l’entité CourseAssignment.

Alternative d’API Fluent aux attributs

La méthode OnModelCreating du code précédent utilise l’API Fluent pour configurer le comportement de EF Core. L’API est appelée « Fluent », car elle est souvent utilisée en enchaînant une série d’appels de méthode en une seule instruction. Le code suivant est un exemple de l’API Fluent :

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

Dans ce tutoriel, l’API Fluent est utilisée uniquement pour le mappage de base de données qui ne peut pas être effectué avec des attributs. Toutefois, l’API Fluent peut spécifier la plupart des règles de mise en forme, de validation et de mappage pouvant être spécifiées à l’aide d’attributs.

Certains attributs, tels que MinimumLength, ne peuvent pas être appliqués avec l’API Fluent. MinimumLength ne change pas le schéma. Il applique uniquement une règle de validation de longueur minimale.

Certains développeurs préfèrent utiliser exclusivement l’API Fluent afin de conserver des classes d’entité « propres ». Il est possible de combiner des attributs et l’API Fluent. Certaines configurations peuvent être effectuées uniquement avec l’API Fluent (spécification d’une clé primaire composite). Certaines autres peuvent être effectuées uniquement avec des attributs (MinimumLength). Voici ce que nous recommandons pour l’utilisation des API Fluent ou des attributs :

  • Choisissez l’une de ces deux approches.
  • Dans la mesure du possible, utilisez l’approche choisie de manière cohérente.

Certains des attributs utilisés dans ce tutoriel sont utilisés pour :

  • La validation uniquement (par exemple, MinimumLength).
  • La configuration de EF Core uniquement (par exemple, HasKey).
  • La validation et la configuration de EF Core (par exemple, [StringLength(50)]).

Pour plus d’informations sur les attributs et l’API Fluent, consultez Méthodes de configuration.

Diagramme des entités

L’illustration suivante montre le diagramme que les outils EF Power créent pour le modèle School complet.

Diagramme des entités

Le schéma précédent illustre :

  • Plusieurs lignes de relations un-à-plusieurs (1 à *).
  • La ligne de relation un-à-zéro-ou-un (1 à 0..1) entre les entités Instructor et OfficeAssignment.
  • La ligne de relation zéro-ou-un-à-plusieurs (0..1 à *) entre les entités Instructor et Department.

Amorcer la base de données

Mettez à jour le code dans 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();
        }
    }
}

Le code précédent fournit des données de valeur initiale pour les nouvelles entités. La majeure partie de ce code crée des objets d’entités et charge des exemples de données. Les exemples de données sont utilisés à des fins de test. Consultez Enrollments et CourseAssignments pour obtenir des exemples de la façon dont des tables de jointure plusieurs-à-plusieurs peuvent être amorcées.

Ajouter une migration

Créez le projet.

Dans PMC, exécutez la commande suivante.

Add-Migration ComplexDataModel

La commande précédente affiche un avertissement concernant les pertes de données possibles.

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'

Si la commande database update est exécutée, l’erreur suivante se produit :

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

Dans la section suivante, vous allez voir comment traiter cette erreur.

Appliquer la migration ou supprimer et recréer

Maintenant que vous disposez d’une base de données, vous devez réfléchir à la façon dont vous y apporterez des modifications. Ce tutoriel présente deux autres solutions :

Les deux options fonctionnent pour SQL Server. Bien que la méthode d’application de la migration soit plus longue et complexe, il s’agit de l’approche privilégiée pour les environnements de production réels.

Supprimer et recréer la base de données

Ignorez cette section si vous utilisez SQL Server et que vous souhaitez effectuer l’approche d’application de la migration dans la section suivante.

Pour forcer EF Core à créer une base de données, annulez et mettez à jour la base de données :

  • Dans la console du Gestionnaire de package, exécutez la commande suivante :

    Drop-Database
    
  • Supprimez le dossier Migrations, puis exécutez la commande suivante :

    Add-Migration InitialCreate
    Update-Database
    

Exécutez l'application. L’exécution de l’application entraîne l’exécution de la méthode DbInitializer.Initialize. La méthode DbInitializer.Initialize remplit la nouvelle base de données.

Ouvrez la base de données dans SSOX :

  • Si SSOX était déjà ouvert, cliquez sur le bouton Actualiser.

  • Développez le nœud Tables. Les tables créées sont affichées.

    Tables dans SSOX

  • Examinez la table CourseAssignment :

    • Cliquez avec le bouton droit sur la table CourseAssignment et sélectionnez Afficher les données.
    • Vérifiez que la table CourseAssignment contient des données.

    Données CourseAssignment dans SSOX

Appliquer la migration

Cette section est facultative. Ces étapes fonctionnent uniquement pour la Base de données locale SQL Server et seulement si vous avez ignoré la section précédente Supprimer et recréer la base de données.

Quand des migrations sont exécutées avec des données existantes, il peut y avoir des contraintes de clé étrangère qui ne sont pas satisfaites avec les données existantes. Avec des données de production, vous devez effectuer certaines actions pour migrer les données existantes. Cette section fournit un exemple de correction des violations de contraintes de clé étrangère. N’effectuez pas ces modifications de code sans une sauvegarde. N’apportez pas ces modifications au code si vous avez terminé la section précédente Supprimer et recréer la base de données.

Le fichier {timestamp}_ComplexDataModel.cs contient le code suivant :

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

Le code précédent ajoute une clé étrangère DepartmentID non-nullable à la table Course. La base de données du tutoriel précédent contient des lignes dans Course ; cette table ne peut donc pas être mise à jour par des migrations.

Pour faire en sorte que la migration ComplexDataModel fonctionne avec des données existantes

  • Modifiez le code pour affecter une valeur par défaut à la nouvelle colonne (DepartmentID).
  • Créez un département fictif nommé « Temp » assumant la fonction de département par défaut.

Corriger les contraintes de clé étrangère

Dans la classe de migration ComplexDataModel, mettez à jour la méthode Up :

  • Ouvrez le fichier {timestamp}_ComplexDataModel.cs .
  • Commentez la ligne de code qui ajoute la colonne DepartmentID à la table 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);

Ajoutez le code en surbrillance suivant. Le nouveau code va après le bloc .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);

Avec les modifications précédentes, les lignes Course existantes seront liées au service « Temp » après l’exécution de la méthode ComplexDataModel.Up.

La façon de gérer la situation présentée ici est simplifiée pour ce tutoriel. Une application de production :

  • Comprendrait du code ou des scripts pour ajouter des lignes Department et des lignes Course associées aux nouvelles lignes Department.
  • N’utiliserait pas le département « Temp » ou la valeur par défaut pour Course.DepartmentID.
  • Dans la console du Gestionnaire de package, exécutez la commande suivante :

    Update-Database
    

La méthode DbInitializer.Initialize étant conçue pour fonctionner uniquement avec une base de données vide, utilisez SSOX pour supprimer toutes les lignes des tables Student et Course. (La suppression en cascade s’occupe de la table Enrollment)

Exécutez l'application. L’exécution de l’application entraîne l’exécution de la méthode DbInitializer.Initialize. La méthode DbInitializer.Initialize remplit la nouvelle base de données.

Étapes suivantes

Les deux tutoriels suivants montrent comment lire et mettre à jour des données associées.

Dans les didacticiels précédents, nous avons travaillé avec un modèle de données de base composé de trois entités. Dans ce tutoriel, vous allez :

  • Nous allons ajouter d’autres entités et relations
  • Nous allons personnaliser le modèle de données en spécifiant des règles de mise en forme, de validation et de mappage de base de données.

Les classes d’entité pour le modèle de données final sont présentées dans l’illustration suivante :

Diagramme des entités

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez l’application terminée.

Personnaliser le modèle de données avec des attributs

Dans cette section, nous allons personnaliser le modèle de données à l’aide d’attributs.

Attribut DataType

Actuellement, les pages sur les étudiants affichent l’heure et la date d’inscription. En règle générale, les champs de date ne montrent que la date, et non l’heure.

Mettez à jour Models/Student.cs avec le code mis en évidence suivant :

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’attribut DataType spécifie un type de données qui est plus spécifique que le type intrinsèque de la base de données. Ici, seule la date doit être affichée (pas la date et l’heure). L’énumération DataType fournit de nombreux types de données, tels que Date, Heure, Numéro de téléphone, Devise, Adresse e-mail, etc. L’attribut DataType peut également permettre à l’application de fournir automatiquement des fonctionnalités spécifiques au type. Par exemple :

  • Le lien mailto: est créé automatiquement pour DataType.EmailAddress.
  • Le sélecteur de date est fourni pour DataType.Date dans la plupart des navigateurs.

L’attribut DataType émet des attributs HTML 5 data- utilisés par les navigateurs HTML 5. Les attributs DataType ne fournissent aucune validation.

DataType.Date ne spécifie pas le format de la date qui s’affiche. Par défaut, le champ de date est affiché conformément aux formats par défaut basés sur l’objet CultureInfo du serveur.

L’attribut DisplayFormat est utilisé pour spécifier explicitement le format de date :

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

Le paramètre ApplyFormatInEditMode spécifie que la mise en forme doit également être appliquée à l’interface utilisateur de modification. Certains champs ne doivent pas utiliser ApplyFormatInEditMode. Par exemple, le symbole monétaire ne doit généralement pas être affiché dans une zone de texte d’édition.

L’attribut DisplayFormat peut être utilisé seul. Il est généralement préférable d’utiliser l’attribut DataType avec l’attribut DisplayFormat. L’attribut DataType transmet la sémantique des données, plutôt que la manière de l’afficher à l’écran. L’attribut DataType offre les avantages suivants qui ne sont pas disponibles dans DisplayFormat :

  • Le navigateur peut activer des fonctionnalités HTML5 (par exemple pour afficher un contrôle de calendrier, le symbole monétaire correspondant aux paramètres régionaux, des liens de messagerie, une validation des entrées côté client, et ainsi de suite).
  • Par défaut, le navigateur affiche les données à l’aide du format correspondant aux paramètres régionaux.

Pour plus d’informations, consultez la documentation relative au Tag Helper <input>.

Exécutez l'application. Accédez à la page d’index des étudiants. Les heures ne sont plus affichées. Tous les affichages qui utilisent le modèle Student affichent la date sans heure.

Page d’index des étudiants affichant les dates sans les heures

Attribut StringLength

Vous pouvez également spécifier des règles de validation de données et des messages d’erreur de validation à l’aide d’attributs. L’attribut StringLength spécifie les longueurs minimale et maximale de caractères autorisées dans un champ de données. L’attribut StringLength fournit également la validation côté client et côté serveur. La valeur minimale n’a aucun impact sur le schéma de base de données.

Mettez à jour le modèle Student avec le code suivant :

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

Le code précédent limite la longueur des noms à 50 caractères. L’attribut StringLength n’empêche pas un utilisateur d’entrer un espace blanc comme nom. L’attribut RegularExpression est utilisé pour appliquer des restrictions à l’entrée. Par exemple, le code suivant exige que le premier caractère soit en majuscule et que les autres caractères soient alphabétiques :

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

Exécutez l’application :

  • Accédez à la page Students.
  • Sélectionnez Create New et entrez un nom de plus de 50 caractères.
  • Sélectionnez Create. La validation côté client affiche un message d’erreur.

Page d’index des étudiants affichant des erreurs de longueur de chaîne

Dans l’Explorateur d’objets SQL Server (SSOX), ouvrez le concepteur de tables Student en double-cliquant sur la table Student.

Table Students dans SSOX avant les migrations

L’image précédente montre le schéma pour la table Student. Les champs de nom sont de type nvarchar(MAX), car les migrations n’ont pas été exécutées sur la base de données. Quand nous exécuterons les migrations plus loin dans ce didacticiel, les champs de nom deviendront nvarchar(50).

Attribut Column

Les attributs peuvent contrôler comment les classes et les propriétés sont mappées à la base de données. Dans cette section, nous utilisons l’attribut Column pour mapper le nom de la propriété FirstMidName « FirstName » dans la base de données.

Lors de la création de la base de données, les noms des propriétés sur le modèle sont utilisés comme noms de colonne (sauf quand l’attribut Column est utilisé).

Le modèle Student utilise FirstMidName pour le champ de prénom, car le champ peut également contenir un deuxième prénom.

Mettez à jour le fichier Student.cs avec le code en surbrillance suivant :

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

Avec la modification précédente, Student.FirstMidName dans l’application est mappé à la colonne FirstName de la table Student.

L’ajout de l’attribut Column change le modèle sur lequel repose SchoolContext. Le modèle sur lequel repose le SchoolContext ne correspond plus à la base de données. Si vous exécutez l’application avant d’appliquer les migrations, l’exception suivante est générée :

SqlException: Invalid column name 'FirstName'.

Pour mettre à jour la base de données

  • Créez le projet.
  • Ouvrez une fenêtre de commande dans le dossier du projet. Entrez les commandes suivantes pour créer une migration et mettre à jour la base de données :
Add-Migration ColumnFirstName
Update-Database

La commande migrations add ColumnFirstName génère le message d’avertissement suivant :

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

Cet avertissement est généré, car les champs de nom sont désormais limités à 50 caractères. Si un nom dans la base de données a plus de 50 caractères, tous les caractères au-delà du cinquantième sont perdus.

  • Tester l'application.

Ouvrez la table Student dans SSOX :

Table Students dans SSOX après les migrations

Avant l’application de la migration, les colonnes de nom étaient de type nvarchar(MAX). Les colonnes de nom sont maintenant nvarchar(50). Le nom de la colonne est passé de FirstMidName à FirstName.

Notes

Dans la section suivante, la génération de l’application à certaines étapes génère des erreurs du compilateur. Les instructions indiquent quand générer l’application.

Mise à jour de l’entité Student

Entité Student

Mettez à jour Models/Student.cs à l’aide du code suivant :

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

Attribut Required

L’attribut Required fait des propriétés de nom des champs obligatoires. L’attribut Required n’est pas nécessaire pour les types non nullables tels que les types valeur (DateTime, int, double et ainsi de suite). Les types qui n’acceptent pas les valeurs Null sont traités automatiquement comme des champs obligatoires.

L’attribut Required peut être remplacé par un paramètre de longueur minimale dans l’attribut StringLength :

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

Attribut Display

L’attribut Display spécifie que la légende pour les zones de texte doit être « First Name », « Last Name », « Full Name » et « Enrollment Date ». Par défaut, les légendes n’avaient pas d’espace pour séparer les mots, par exemple « Lastname ».

Propriété calculée FullName

FullName est une propriété calculée qui retourne une valeur créée par concaténation de deux autres propriétés. FullName ne peut pas être définie. Elle a uniquement un accesseur get. Aucune colonne FullName n’est créée dans la base de données.

Créer l’entité Instructor

Entité Instructor

Créez Models/Instructor.cs avec le code suivant :

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

Plusieurs attributs peuvent être sur une seule ligne. Les attributs HireDate peuvent être écrits comme suit :

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

Propriétés de navigation CourseAssignments et OfficeAssignment

Les propriétés CourseAssignments et OfficeAssignment sont des propriétés de navigation.

Un formateur pouvant animer un nombre quelconque de cours, CourseAssignments est défini comme une collection.

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

Si une propriété de navigation contient plusieurs entités :

  • Elle doit être un type de liste où les entrées peuvent être ajoutées, supprimées et mises à jour.

Les types de propriétés de navigation sont :

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

Si ICollection<T> est spécifié, EF Core crée une collection HashSet<T> par défaut.

L’entité CourseAssignment est expliquée dans la section sur les relations plusieurs-à-plusieurs.

Les règles professionnelles de Contoso University stipulent qu’un formateur peut avoir au plus un bureau. La propriété OfficeAssignment contient une seule entité OfficeAssignment. OfficeAssignment a la valeur Null si aucun bureau n’est affecté.

public OfficeAssignment OfficeAssignment { get; set; }

Créer l’entité OfficeAssignment

Entité OfficeAssignment

Créez Models/OfficeAssignment.cs avec le code suivant :

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

Attribut Key

L’attribut [Key] est utilisé pour identifier une propriété comme clé primaire quand le nom de la propriété est autre que classnameID ou ID.

Il existe une relation un-à-zéro-ou-un entre les entités Instructor et OfficeAssignment. Une affectation de bureau existe uniquement relativement au formateur auquel elle est assignée. La clé primaire OfficeAssignment est également sa clé étrangère pour l’entité Instructor. EF Core ne peut pas reconnaître automatiquement InstructorID comme clé primaire de OfficeAssignment, car :

  • InstructorID ne suit pas la convention de nommage ID ou classnameID.

Ainsi, l’attribut Key est utilisé pour identifier InstructorID comme clé primaire :

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

Par défaut, EF Core traite la clé comme n’étant pas générée par la base de données, car la colonne est utilisée pour une relation d’identification.

Propriété de navigation Instructor

La propriété de navigation OfficeAssignment pour l’entité Instructor est nullable car :

  • Les types référence (tels que les classes) sont nullables.
  • Un formateur peut ne pas avoir d’affectation de bureau.

L’entité OfficeAssignment a une propriété de navigation Instructor non-nullable car :

  • InstructorID n'autorise pas les valeurs Null.
  • Une affectation de bureau ne peut pas exister sans un formateur.

Quand une entité Instructor a une entité OfficeAssignment associée, chaque entité a une référence à l’autre dans sa propriété de navigation.

L’attribut [Required] peut être appliqué à la propriété de navigation Instructor :

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

Le code précédent spécifie qu’il doit y avoir un formateur associé. Le code précédent n’est pas nécessaire, car la clé étrangère InstructorID (qui est également la clé primaire) n'autorise pas les valeurs Null.

Modifier l’entité Course

Entité Course

Mettez à jour Models/Course.cs à l’aide du code suivant :

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 a une propriété de clé étrangère DepartmentID. DepartmentID pointe vers l’entité Department associée. L’entité Course a une propriété de navigation Department.

EF Core n’exige pas de propriété de clé étrangère pour un modèle de données quand le modèle a une propriété de navigation pour une entité associée.

EF Core crée automatiquement des clés étrangères dans la base de données partout où elles sont nécessaires. EF Core crée des propriétés cachées pour les clés étrangères créées automatiquement. Le fait d’avoir la clé étrangère dans le modèle de données peut rendre les mises à jour plus simples et plus efficaces. Par exemple, considérez un modèle où la propriété de clé étrangère DepartmentID n’est pas incluse. Quand une entité Course est récupérée en vue d’une modification :

  • L’entité Department a la valeur Null si elle n’est pas chargée explicitement.
  • Pour mettre à jour l’entité Course, vous devez d’abord récupérer l’entité Department.

Quand la propriété de clé étrangère DepartmentID est incluse dans le modèle de données, il n’est pas nécessaire de récupérer l’entité Department avant une mise à jour.

Attribut DatabaseGenerated

L’attribut [DatabaseGenerated(DatabaseGeneratedOption.None)] indique que la clé primaire est fournie par l’application plutôt que générée par la base de données.

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

Par défaut, EF Core part du principe que les valeurs de clé primaire sont générées par la base de données. Les valeurs de clé primaire générées par la base de données constituent en général la meilleure approche. Pour les entités Course, l’utilisateur spécifie la clé primaire. Par exemple, un numéro de cours comme une série 1000 pour le département Mathématiques et une série 2000 pour le département Anglais.

Vous pouvez aussi utiliser l’attribut DatabaseGenerated pour générer des valeurs par défaut. Par exemple, la base de données peut générer automatiquement un champ de date pour enregistrer la date de création ou de mise à jour d’une ligne. Pour plus d’informations, consultez Propriétés générées.

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et les propriétés de navigation dans l’entité Course reflètent les relations suivantes :

Un cours étant affecté à un seul département, il y a une clé étrangère DepartmentID et une propriété de navigation Department.

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

Un cours pouvant avoir un nombre quelconque d’étudiants inscrits, la propriété de navigation Enrollments est une collection :

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

Un cours pouvant être animé par plusieurs formateurs, la propriété de navigation CourseAssignments est une collection :

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

CourseAssignment est expliquée plus loin.

Créer l’entité Department

Entité de service

Créez Models/Department.cs avec le code suivant :

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

Attribut Column

Précédemment, vous avez utilisé l’attribut Column pour changer le mappage des noms de colonne. Dans le code de l’entité Department, l’attribut Column est utilisé pour changer le mappage des types de données SQL. La colonne Budget est définie à l’aide du type monétaire de SQL Server dans la base de données :

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

Le mappage de colonnes n’est généralement pas nécessaire. EF Core choisit en général le type de données SQL Server approprié en fonction du type CLR de la propriété. Le type CLR decimal est mappé à un type SQL Server decimal. Budget étant une valeur monétaire, le type de données money est plus approprié.

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et de navigation reflètent les relations suivantes :

  • Un département peut avoir ou ne pas avoir un administrateur.
  • Un administrateur est toujours un formateur. Ainsi, la propriété InstructorID est incluse en tant que clé étrangère de l’entité Instructor.

La propriété de navigation se nomme Administrator, mais elle contient une entité Instructor :

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

Le point d’interrogation (?) dans le code précédent indique que la propriété est nullable.

Un département pouvant avoir de nombreux cours, il existe une propriété de navigation Courses :

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

Remarque : par convention, EF Core autorise la suppression en cascade pour les clés étrangères non nullables et pour les relations plusieurs à plusieurs. Les suppressions en cascade peuvent engendrer des règles de suppression en cascade circulaires. Les règles de suppression en cascade circulaires provoquent une exception quand une migration est ajoutée.

Par exemple, si la propriété Department.InstructorID ne doit pas accepter les valeurs Null :

  • EF Core configure une règle de suppression en cascade pour supprimer le département lorsque l’instructeur est supprimé.

  • La suppression du service lorsque l’instructeur est supprimé n’est pas le comportement souhaité.

  • L’API Fluent suivante définit une règle de restriction au lieu d’une cascade.

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

Le code précédent désactive la suppression en cascade sur la relation formateur-département.

Mettre à jour l’entité Enrollment

Un enregistrement d’inscription correspond à un cours suivi par un étudiant.

Entité Enrollment

Mettez à jour Models/Enrollment.cs à l’aide du code suivant :

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

Propriétés de clé étrangère et de navigation

Les propriétés de clé étrangère et de navigation reflètent les relations suivantes :

Un enregistrement d’inscription ne concernant qu’un seul cours, il y a une propriété de clé étrangère CourseID et une propriété de navigation Course :

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

Un enregistrement d’inscription ne concernant qu’un seul étudiant, il y a une propriété de clé étrangère StudentID et une propriété de navigation Student :

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

Relations plusieurs-à-plusieurs

Il existe une relation plusieurs-à-plusieurs entre les entités Student et Course. L’entité Enrollment joue le rôle de table de jointure plusieurs-à-plusieurs avec charge utile dans la base de données. « Avec charge utile » signifie que la table Enrollment contient des données supplémentaires en plus des clés étrangères pour les tables jointes (dans le cas présent, la clé primaire et Grade).

L’illustration suivante montre à quoi ressemblent ces relations dans un diagramme d’entité. (Ce diagramme a été généré à l’aide d’EF Power Tools pour EF 6.x. La création du diagramme ne fait pas partie du tutoriel.)

Relation plusieurs-à-plusieurs Student-Course

Chaque ligne de relation comporte un 1 à une extrémité et un astérisque (*) à l’autre, ce qui indique une relation un-à-plusieurs.

Si la table Enrollment n’incluait pas d’informations de notes, elle aurait uniquement besoin de contenir les deux clés étrangères (CourseID et StudentID). Une table de jointure plusieurs-à-plusieurs sans charge utile est parfois appelée « table de jointure pure ».

Les entités Instructor et Course ont une relation plusieurs-à-plusieurs à l’aide d’une table de jointure pure.

Remarque : EF 6.x prend en charge les tables de jointure implicites pour les relations plusieurs-à-plusieurs, mais pas EF Core. Pour plus d’informations, consultez Relations plusieurs-à-plusieurs dans EF Core 2.0.

Entité CourseAssignment

Entité CourseAssignment

Créez Models/CourseAssignment.cs avec le code suivant :

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-Courses

Instructor-to-Courses m:M

La relation plusieurs-à-plusieurs Instructor-Courses :

  • Nécessite une table de jointure qui doit être représentée par un jeu d’entités.
  • Est une table de jointure pure (table sans charge utile).

Il est courant de nommer une entité de jointure EntityName1EntityName2. Par exemple, la table de jointure Instructor-to-Courses utilisant ce modèle est CourseInstructor. Toutefois, nous vous recommandons d’utiliser un nom qui décrit la relation.

Les modèles de données sont simples au début, puis ils augmentent en complexité. Les jointures sans charge utile (tables de jointure pures) évoluent fréquemment pour inclure une charge utile. En commençant par un nom d’entité descriptif, vous n’aurez pas besoin de modifier le nom quand la table de jointure changera. Dans l’idéal, l’entité de jointure aura son propre nom (éventuellement un mot unique) naturel dans le domaine d’entreprise. Par exemple, les livres et les clients pourraient être liés avec une entité de jointure nommée Évaluations. Pour la relation plusieurs-à-plusieurs Instructor-Courses, il vaut mieux utiliser CourseAssignment que CourseInstructor.

Clé composite

Les clés étrangères ne sont pas nullables. Ensemble, le deux clés étrangères dans CourseAssignment (InstructorID et CourseID) identifient de façon unique chaque ligne de la table CourseAssignment. CourseAssignment ne nécessite pas de clé primaire dédiée. Les propriétés InstructorID et CourseID fonctionnent comme une clé primaire composite. Le seul moyen de spécifier des clés primaires composites dans EF Core consiste à faire appel à l’API Fluent. La section suivante montre comment configurer la clé primaire composite.

La clé composite garantit que :

  • Plusieurs lignes sont autorisées pour un cours.
  • Plusieurs lignes sont autorisées pour un formateur.
  • Plusieurs lignes pour la même paire formateur-cours ne sont pas autorisées.

Comme l’entité de jointure Enrollment définit sa propre clé primaire, des doublons de ce type sont possibles. Pour éviter ces doublons :

  • Ajoutez un index unique sur les champs de clé primaire, ou
  • Configurez Enrollment avec une clé primaire composite similaire à CourseAssignment. Pour plus d’informations, consultez Index.

Mettre à jour le contexte de base de données

Ajoutez le code en surbrillance suivant à 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 });
        }
    }
}

Le code précédent ajoute les nouvelles entités et configure la clé primaire composite de l’entité CourseAssignment.

Alternative d’API Fluent aux attributs

La méthode OnModelCreating du code précédent utilise l’API Fluent pour configurer le comportement de EF Core. L’API est appelée « Fluent », car elle est souvent utilisée en enchaînant une série d’appels de méthode en une seule instruction. Le code suivant est un exemple de l’API Fluent :

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

Dans ce didacticiel, l’API Fluent est utilisée uniquement pour le mappage de base de données qui ne peut pas être effectué avec des attributs. Toutefois, l’API Fluent peut spécifier la plupart des règles de mise en forme, de validation et de mappage pouvant être spécifiées à l’aide d’attributs.

Certains attributs, tels que MinimumLength, ne peuvent pas être appliqués avec l’API Fluent. MinimumLength ne change pas le schéma. Il applique uniquement une règle de validation de longueur minimale.

Certains développeurs préfèrent utiliser exclusivement l’API Fluent afin de conserver des classes d’entité « propres ». Il est possible de combiner des attributs et l’API Fluent. Certaines configurations peuvent être effectuées uniquement avec l’API Fluent (spécification d’une clé primaire composite). Certaines autres peuvent être effectuées uniquement avec des attributs (MinimumLength). Voici ce que nous recommandons pour l’utilisation des API Fluent ou des attributs :

  • Choisissez l’une de ces deux approches.
  • Dans la mesure du possible, utilisez l’approche choisie de manière cohérente.

Certains des attributs utilisés dans ce didacticiel sont utilisés pour :

  • La validation uniquement (par exemple, MinimumLength).
  • La configuration de EF Core uniquement (par exemple, HasKey).
  • La validation et la configuration de EF Core (par exemple, [StringLength(50)]).

Pour plus d’informations sur les attributs et l’API Fluent, consultez Méthodes de configuration.

Diagramme des entités montrant les relations

L’illustration suivante montre le diagramme que les outils EF Power créent pour le modèle School complet.

Diagramme des entités

Le schéma précédent illustre :

  • Plusieurs lignes de relations un-à-plusieurs (1 à *).
  • La ligne de relation un-à-zéro-ou-un (1 à 0..1) entre les entités Instructor et OfficeAssignment.
  • La ligne de relation zéro-ou-un-à-plusieurs (0..1 à *) entre les entités Instructor et Department.

Amorcer la base de données avec des données de test

Mettez à jour le code dans 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();
        }
    }
}

Le code précédent fournit des données de valeur initiale pour les nouvelles entités. La majeure partie de ce code crée des objets d’entités et charge des exemples de données. Les exemples de données sont utilisés à des fins de test. Consultez Enrollments et CourseAssignments pour obtenir des exemples de la façon dont des tables de jointure plusieurs-à-plusieurs peuvent être amorcées.

Ajouter une migration

Créez le projet.

Add-Migration ComplexDataModel

La commande précédente affiche un avertissement concernant les pertes de données possibles.

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'

Si la commande database update est exécutée, l’erreur suivante se produit :

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

Appliquer la migration

Disposant à présent d’une base de données, vous devez réfléchir à la façon dont vous y apporterez des modifications. Ce tutoriel montre deux approches :

  • Supprimer et recréer la base de données
  • Appliquer la migration à la base de données Bien que cette méthode soit plus longue et complexe, elle constitue l’approche privilégiée pour les environnements de production réels. Remarque : Cette section du tutoriel est facultative. Vous pouvez effectuer les étapes de suppression et de recréation et ignorer cette section. Si vous souhaitez suivre les étapes décrites dans cette section, n’effectuez pas les étapes de suppression et de recréation.

Supprimer et recréer la base de données

Le code dans le DbInitializer mis à jour ajoute des données de valeur initiale pour les nouvelles entités. Pour forcer EF Core à créer une autre base de données, supprimez et mettez à jour la base de données :

Dans la console du Gestionnaire de package, exécutez la commande suivante :

Drop-Database
Update-Database

Exécutez Get-Help about_EntityFrameworkCore à partir de la console du Gestionnaire de package pour obtenir des informations d’aide.

Exécutez l'application. L’exécution de l’application entraîne l’exécution de la méthode DbInitializer.Initialize. La méthode DbInitializer.Initialize remplit la nouvelle base de données.

Ouvrez la base de données dans SSOX :

  • Si SSOX était déjà ouvert, cliquez sur le bouton Actualiser.
  • Développez le nœud Tables. Les tables créées sont affichées.

Tables dans SSOX

Examinez la table CourseAssignment :

  • Cliquez avec le bouton droit sur la table CourseAssignment et sélectionnez Afficher les données.
  • Vérifiez que la table CourseAssignment contient des données.

Données CourseAssignment dans SSOX

Appliquer la migration à la base de données

Cette section est facultative. Ces étapes fonctionnent seulement si vous avez ignoré la section Supprimer et recréer la base de données précédente.

Quand des migrations sont exécutées avec des données existantes, il peut y avoir des contraintes de clé étrangère qui ne sont pas satisfaites avec les données existantes. Avec des données de production, vous devez effectuer certaines actions pour migrer les données existantes. Cette section fournit un exemple de correction des violations de contraintes de clé étrangère. N’effectuez pas ces modifications de code sans une sauvegarde. N’effectuez pas ces modifications de code si vous avez terminé la section précédente et mis à jour la base de données.

Le fichier {timestamp}_ComplexDataModel.cs contient le code suivant :

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

Le code précédent ajoute une clé étrangère DepartmentID non-nullable à la table Course. La base de données du didacticiel précédent contient des lignes dans Course ; cette table ne peut donc pas être mise à jour par des migrations.

Pour faire en sorte que la migration ComplexDataModel fonctionne avec des données existantes

  • Modifiez le code pour affecter une valeur par défaut à la nouvelle colonne (DepartmentID).
  • Créez un département fictif nommé « Temp » assumant la fonction de département par défaut.

Corriger les contraintes de clé étrangère

Mettez à jour la méthode Up de la classe ComplexDataModel :

  • Ouvrez le fichier {timestamp}_ComplexDataModel.cs .
  • Commentez la ligne de code qui ajoute la colonne DepartmentID à la table 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);

Ajoutez le code en surbrillance suivant. Le nouveau code va après le bloc .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);

Avec les modifications précédentes, les lignes Course existantes seront liées au service « Temp » après l’exécution de la méthode ComplexDataModel Up.

Une application de production :

  • Comprendrait du code ou des scripts pour ajouter des lignes Department et des lignes Course associées aux nouvelles lignes Department.
  • N’utiliserait pas le département « Temp » ou la valeur par défaut pour Course.DepartmentID.

Le didacticiel suivant traite des données associées.

Ressources supplémentaires