Partie 6, Razor Pages avec EF Core dans ASP.NET Core - Lire les données associées

Par Tom Dykstra, Jon P Smith et Rick Anderson

L’application web Contoso University montre comment créer des applications web Razor Pages 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.

Ce tutoriel montre comment lire et afficher des données associées. Les données associées sont des données qu’EF Core charge dans des propriétés de navigation.

Les illustrations suivantes montrent les pages terminées pour ce didacticiel :

Courses Index page

Instructors Index page

Chargement hâtif, explicite et différé

EF Core peut charger des données associées dans les propriétés de navigation d’une entité de plusieurs manières :

  • Chargement hâtif. Le chargement hâtif a lieu quand une requête pour un type d’entité charge également des entités associées. Quand une entité est lue, ses données associées sont récupérées. Cela génère en général une requête de jointure unique qui récupère toutes les données nécessaires. EF Core émet plusieurs requêtes pour certains types de chargement hâtif. Il peut s’avérer plus efficace d’émettre plusieurs requêtes plutôt qu’une seule grande. Le chargement hâtif est spécifié avec les méthodes Include et ThenInclude.

    Eager loading example

    Le chargement hâtif envoie plusieurs requêtes quand une navigation dans la collection est incluse :

    • Une requête pour la requête principale
    • Une requête pour chaque « périmètre » de collection dans l’arborescence de la charge.
  • Requêtes distinctes avec Load : les données peuvent être récupérées dans des requêtes distinctes, et EF Core « corrige » les propriétés de navigation. Quand EF Core « corrige », cela signifie que les propriétés de navigation sont renseignées automatiquement. Les requêtes distinctes avec Load s’apparentent plus au chargement explicite qu’au chargement hâtif.

    Separate queries example

    Remarque :EF Core corrige automatiquement les propriétés de navigation vers d’autres entités qui étaient précédemment chargées dans l’instance de contexte. Même si les données pour une propriété de navigation ne sont pas explicitement incluses, la propriété peut toujours être renseignée si toutes ou une partie des entités associées ont été précédemment chargées.

  • Chargement explicite. Quand l’entité est lue pour la première fois, les données associées ne sont pas récupérées. Vous devez écrire du code pour récupérer les données associées en cas de besoin. En cas de chargement explicite avec des requêtes distinctes, plusieurs requêtes sont envoyées à la base de données. Avec le chargement explicite, le code spécifie les propriétés de navigation à charger. Utilisez la méthode Load pour effectuer le chargement explicite. Par exemple :

    Explicit loading example

  • Chargement différé. Quand l’entité est lue pour la première fois, les données associées ne sont pas récupérées. Lors du premier accès à une propriété de navigation, les données requises pour cette propriété de navigation sont récupérées automatiquement. Une requête est envoyée à la base de données chaque fois qu’une propriété de navigation fait pour la première fois l’objet d’un accès. Le chargement différé peut nuire aux performances, par exemple lorsque les développeurs utilisent des requêtes N+1. Les requêtes N+1 chargent un parent et énumèrent via des enfants.

Créer des pages Course

L’entité Course comprend une propriété de navigation qui contient l’entité Department associée.

Course.Department

Pour afficher le nom du service (« department ») affecté pour un cours (« course ») :

  • Chargez l’entité Department associée dans la propriété de navigation Course.Department.
  • Obtenez le nom à partir de la propriété Name de l’entité Department.

Générer automatiquement des modèles de pages Course

  • Ouvrez Pages/Courses/Index.cshtml.cs et examinez la méthode OnGetAsync. Le moteur de génération de modèles automatique a spécifié le chargement hâtif pour la propriété de navigation Department. La méthode Include spécifie le chargement hâtif.

  • Exécutez l’application et sélectionnez le lien Courses. La colonne Department affiche le DepartmentID, ce qui n’est pas utile.

Afficher le nom du service (« department »)

Mettez à jour Pages/Courses/Index.cshtml.cs avec le code suivant :

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

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

        public async Task OnGetAsync()
        {
            Courses = await _context.Courses
                .Include(c => c.Department)
                .AsNoTracking()
                .ToListAsync();
        }
    }
}

Le code précédent remplace la propriété Course par Courses et ajoute AsNoTracking.

Les requêtes sans suivi sont utiles lorsque les résultats sont utilisés dans un scénario en lecture seule. Leur exécution est généralement plus rapide, car il n’est pas nécessaire de configurer les informations de suivi des modifications. Si les entités récupérées à partir de la base de données n’ont pas besoin d’être mises à jour, une requête sans suivi est susceptible de fonctionner mieux qu’une requête avec suivi.

Dans certains cas, une requête avec suivi est plus efficace qu’une requête sans suivi. Pour plus d’informations, consultez Requêtes avec suivi et non-suivi. Dans le code précédent, AsNoTracking est appelé, car les entités ne sont pas mises à jour dans le contexte actuel.

Mettez à jour Pages/Courses/Index.cshtml à l’aide du code suivant.

@page
@model ContosoUniversity.Pages.Courses.IndexModel

@{
    ViewData["Title"] = "Courses";
}

<h1>Courses</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Courses)
{
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.CourseID)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Credits)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Department.Name)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les modifications suivantes ont été apportées au code généré automatiquement :

  • Le nom de propriété Course est changé en Courses.

  • Ajout d’une colonne Number qui affiche la valeur de la propriété CourseID. Par défaut, les clés primaires ne sont pas générées automatiquement, car elles ne sont normalement pas significatives pour les utilisateurs finaux. Toutefois, dans le cas présent la clé primaire est significative.

  • Modification de la colonne Department afin d’afficher le nom du département. Le code affiche la propriété Name de l’entité Department qui est chargée dans la propriété de navigation Department :

    @Html.DisplayFor(modelItem => item.Department.Name)
    

Exécutez l’application et sélectionnez l’onglet Courses pour afficher la liste avec les noms des départements.

Courses Index page

La méthode OnGetAsync charge les données associées avec la méthode Include. La méthode Select est autre solution qui charge uniquement les données associées nécessaires. Pour les éléments uniques, comme Department.Name, il utilise SQL INNER JOIN. Pour les collections, il utilise un autre accès à la base de données, mais c’est aussi le cas de l’opérateur Include sur les collections.

Le code suivant charge les données associées avec la méthode Select :

public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
    .Select(p => new CourseViewModel
    {
        CourseID = p.CourseID,
        Title = p.Title,
        Credits = p.Credits,
        DepartmentName = p.Department.Name
    }).ToListAsync();
}

Le code précédent ne retourne aucun type d’entité. Par conséquent, aucun suivi n’est effectué. Pour plus d’informations sur le suivi EF, consultez Requêtes avec suivi et sans suivi.

CourseViewModel :

public class CourseViewModel
{
    public int CourseID { get; set; }
    public string Title { get; set; }
    public int Credits { get; set; }
    public string DepartmentName { get; set; }
}

Consultez IndexSelectModel pour des instructions Razor Pages complètes.

Créer des pages Instructor

Cette section génère automatiquement des modèles de pages Instructor et ajoute les cours et les inscriptions associés à la page d’index des formateurs.

Instructors Index page

Cette page lit et affiche les données associées comme suit :

  • La liste des formateurs affiche des données associées de l’entité OfficeAssignment (Office dans l’image précédente). Il existe une relation un-à-zéro-ou-un entre les entités Instructor et OfficeAssignment. Le chargement hâtif est utilisé pour les entités OfficeAssignment. Le chargement hâtif est généralement plus efficace quand les données associées doivent être affichées. Ici, les affectations de bureau pour les formateurs sont affichées.
  • Quand l’utilisateur sélectionne un formateur, les entités Course associées sont affichées. Il existe une relation plusieurs-à-plusieurs entre les entités Instructor et Course. Le chargement hâtif est utilisé pour les entités Course et leurs entités Department associées. Dans le cas présent, des requêtes distinctes peuvent être plus efficaces, car seuls les cours du formateur sélectionné sont nécessaires. Cet exemple montre comment utiliser le chargement hâtif pour des propriétés de navigation dans des entités qui se trouvent dans des propriétés de navigation.
  • Quand l’utilisateur sélectionne un cours, les données associées de l’entité Enrollments s’affichent. Dans l’image précédente, le nom et la note de l’étudiant sont affichés. Il existe une relation un-à-plusieurs entre les entités Course et Enrollment.

Création d'un modèle de vue

La page sur les formateurs affiche les données de trois tables différentes. Un modèle de vue comprenant trois propriétés représentant les trois tables est nécessaire.

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

Générer automatiquement des modèles de pages Instructor

  • Suivez les instructions dans Générer automatiquement des modèles de pages Student avec les exceptions suivantes :

    • Créez un dossier Pages/Instructors.
    • Utilisez Instructor pour la classe de modèle.
    • Utilisez la classe de contexte existante au lieu d’en créer une nouvelle.

Exécutez l’application et accédez à la page des formateurs.

Mettez à jour Pages/Instructors/Index.cshtml.cs à l’aide du code suivant :

using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData InstructorData { get; set; }
        public int InstructorID { get; set; }
        public int CourseID { get; set; }

        public async Task OnGetAsync(int? id, int? courseID)
        {
            InstructorData = new InstructorIndexData();
            InstructorData.Instructors = await _context.Instructors
                .Include(i => i.OfficeAssignment)                 
                .Include(i => i.Courses)
                    .ThenInclude(c => c.Department)
                .OrderBy(i => i.LastName)
                .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
                Instructor instructor = InstructorData.Instructors
                    .Where(i => i.ID == id.Value).Single();
                InstructorData.Courses = instructor.Courses;
            }

            if (courseID != null)
            {
                CourseID = courseID.Value;
                IEnumerable<Enrollment> Enrollments = await _context.Enrollments
                    .Where(x => x.CourseID == CourseID)                    
                    .Include(i=>i.Student)
                    .ToListAsync();                 
                InstructorData.Enrollments = Enrollments;
            }
        }
    }
}

La méthode OnGetAsync accepte des données de route facultatives pour l’ID du formateur sélectionné.

Examinez la requête dans le fichier Pages/Instructors/Index.cshtml.cs :

InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.Courses)
        .ThenInclude(c => c.Department)
    .OrderBy(i => i.LastName)
    .ToListAsync();

Le code spécifie un chargement hâtif pour les propriétés de navigation suivantes :

  • Instructor.OfficeAssignment
  • Instructor.Courses
    • Course.Department

Le code suivant s’exécute quand un formateur est sélectionné, à savoir id != null.

if (id != null)
{
    InstructorID = id.Value;
    Instructor instructor = InstructorData.Instructors
        .Where(i => i.ID == id.Value).Single();
    InstructorData.Courses = instructor.Courses;
}

Le formateur sélectionné est récupéré à partir de la liste des formateurs dans le modèle d’affichage. La propriété Courses du modèle d’affichage est chargée avec les entités Course de la propriété de navigation Courses sélectionnée de ce formateur.

La méthode Where retourne une collection. Dans ce cas, le filtre sélectionne une entité unique, de sorte que la méthode Single est appelée pour convertir la collection en une seule entité Instructor. L’entité Instructor fournit l’accès à la propriété de navigation Course.

La méthode Single est utilisée sur une collection quand la collection ne compte qu’un seul élément. La méthode Single lève une exception si la collection est vide ou s’il y a plusieurs éléments. Une alternative est SingleOrDefault, qui renvoie une valeur par défaut si la collection est vide. Pour cette requête, null dans la valeur par défaut retournée.

Le code suivant renseigne la propriété Enrollments du modèle d’affichage quand un cours est sélectionné :

if (courseID != null)
{
    CourseID = courseID.Value;
    IEnumerable<Enrollment> Enrollments = await _context.Enrollments
        .Where(x => x.CourseID == CourseID)                    
        .Include(i=>i.Student)
        .ToListAsync();                 
    InstructorData.Enrollments = Enrollments;
}

Mettre à jour la page d’index des formateurs

Mettez à jour Pages/Instructors/Index.cshtml à l’aide du code suivant.

@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.InstructorData.Instructors)
        {
            string selectedRow = "";
            if (item.ID == Model.InstructorID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.Courses)
                        {
                            @course.CourseID @:  @course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.InstructorData.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.InstructorData.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == Model.CourseID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    <a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

@if (Model.InstructorData.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.InstructorData.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}

Le code précédent apporte les modifications suivantes :

  • Met à jour la directive page en @page "{id:int?}". "{id:int?}" est un modèle de route. Le modèle de route change les chaînes de requête entières dans l’URL en données de route. Par exemple, si vous cliquez sur le lien Select pour un formateur avec seulement la directive @page, une URL comme celle-ci est générée :

    https://localhost:5001/Instructors?id=2

    Quand la directive de page est @page "{id:int?}", l’URL est : https://localhost:5001/Instructors/2

  • Ajoute une colonne Office qui affiche item.OfficeAssignment.Location seulement si item.OfficeAssignment n’a pas la valeur Null. Comme il s’agit d’une relation un-à-zéro-ou-un, il se peut qu’il n’y ait pas d’entité OfficeAssignment associée.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Ajoute une colonne Courses qui affiche les cours animés par chaque formateur. Consultez Conversion de ligne explicite pour en savoir plus sur cette syntaxe razor.

  • Ajoute du code qui ajoute dynamiquement class="table-success" à l’élément tr du formateur et du cours sélectionnés. Cela définit une couleur d’arrière-plan pour la ligne sélectionnée à l’aide d’une classe d’amorçage.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Ajoute un nouveau lien hypertexte libellé Select. Ce lien envoie l’ID du formateur sélectionné à la méthode Index, et définit une couleur d’arrière-plan.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Ajoute un tableau de cours pour l’instructeur sélectionné.

  • Ajoute un tableau d’inscriptions d’étudiants pour le cours sélectionné.

Exécutez l’application et sélectionnez l’onglet Instructeurs. La page affiche le Location (bureau) à partir de l’entité OfficeAssignment associée. Si OfficeAssignment a la valeur Null, une cellule de tableau vide est affichée.

Cliquez sur le lien Select pour un formateur. Le style de ligne change et les cours attribués à ce formateur s’affichent.

Sélectionnez un cours pour afficher la liste des étudiants inscrits et leurs notes.

Instructors Index page instructor and course selected

Étapes suivantes

Le didacticiel suivant montre comment mettre à jour les données associées.

Ce tutoriel montre comment lire et afficher des données associées. Les données associées sont des données qu’EF Core charge dans des propriétés de navigation.

Les illustrations suivantes montrent les pages terminées pour ce didacticiel :

Courses Index page

Instructors Index page

Chargement hâtif, explicite et différé

EF Core peut charger des données associées dans les propriétés de navigation d’une entité de plusieurs manières :

  • Chargement hâtif. Le chargement hâtif a lieu quand une requête pour un type d’entité charge également des entités associées. Quand une entité est lue, ses données associées sont récupérées. Cela génère en général une requête de jointure unique qui récupère toutes les données nécessaires. EF Core émet plusieurs requêtes pour certains types de chargement hâtif. Il peut s’avérer plus efficace d’émettre plusieurs requêtes plutôt qu’une seule très grande. Le chargement hâtif est spécifié avec les méthodes Include et ThenInclude.

    Eager loading example

    Le chargement hâtif envoie plusieurs requêtes quand une navigation dans la collection est incluse :

    • Une requête pour la requête principale
    • Une requête pour chaque « périmètre » de collection dans l’arborescence de la charge.
  • Requêtes distinctes avec Load : les données peuvent être récupérées dans des requêtes distinctes, et EF Core « corrige » les propriétés de navigation. Quand EF Core « corrige », cela signifie que les propriétés de navigation sont renseignées automatiquement. Les requêtes distinctes avec Load s’apparentent plus au chargement explicite qu’au chargement hâtif.

    Separate queries example

    Remarque :EF Core corrige automatiquement les propriétés de navigation vers d’autres entités qui étaient précédemment chargées dans l’instance de contexte. Même si les données pour une propriété de navigation ne sont pas explicitement incluses, la propriété peut toujours être renseignée si toutes ou une partie des entités associées ont été précédemment chargées.

  • Chargement explicite. Quand l’entité est lue pour la première fois, les données associées ne sont pas récupérées. Vous devez écrire du code pour récupérer les données associées en cas de besoin. En cas de chargement explicite avec des requêtes distinctes, plusieurs requêtes sont envoyées à la base de données. Avec le chargement explicite, le code spécifie les propriétés de navigation à charger. Utilisez la méthode Load pour effectuer le chargement explicite. Par exemple :

    Explicit loading example

  • Chargement différé. Quand l’entité est lue pour la première fois, les données associées ne sont pas récupérées. Lors du premier accès à une propriété de navigation, les données requises pour cette propriété de navigation sont récupérées automatiquement. Une requête est envoyée à la base de données chaque fois qu’une propriété de navigation fait pour la première fois l’objet d’un accès. Le chargement différé peut nuire aux performances, par exemple lorsque les développeurs utilisent des modèles N+1, chargent un parent et énumèrent les enfants.

Créer des pages Course

L’entité Course comprend une propriété de navigation qui contient l’entité Department associée.

Course.Department

Pour afficher le nom du service (« department ») affecté pour un cours (« course ») :

  • Chargez l’entité Department associée dans la propriété de navigation Course.Department.
  • Obtenez le nom à partir de la propriété Name de l’entité Department.

Générer automatiquement des modèles de pages Course

  • Ouvrez Pages/Courses/Index.cshtml.cs et examinez la méthode OnGetAsync. Le moteur de génération de modèles automatique a spécifié le chargement hâtif pour la propriété de navigation Department. La méthode Include spécifie le chargement hâtif.

  • Exécutez l’application et sélectionnez le lien Courses. La colonne Department affiche le DepartmentID, ce qui n’est pas utile.

Afficher le nom du service (« department »)

Mettez à jour Pages/Courses/Index.cshtml.cs avec le code suivant :

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

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

        public async Task OnGetAsync()
        {
            Courses = await _context.Courses
                .Include(c => c.Department)
                .AsNoTracking()
                .ToListAsync();
        }
    }
}

Le code précédent remplace la propriété Course par Courses et ajoute AsNoTracking. AsNoTracking améliore les performances, car les entités retournées ne sont pas suivies. Les entités n’ont pas besoin d’être suivies, car elles ne sont pas mises à jour dans le contexte actuel.

Mettez à jour Pages/Courses/Index.cshtml à l’aide du code suivant.

@page
@model ContosoUniversity.Pages.Courses.IndexModel

@{
    ViewData["Title"] = "Courses";
}

<h1>Courses</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Courses)
{
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.CourseID)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Credits)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Department.Name)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Les modifications suivantes ont été apportées au code généré automatiquement :

  • Le nom de propriété Course est changé en Courses.

  • Ajout d’une colonne Number qui affiche la valeur de la propriété CourseID. Par défaut, les clés primaires ne sont pas générées automatiquement, car elles ne sont normalement pas significatives pour les utilisateurs finaux. Toutefois, dans le cas présent la clé primaire est significative.

  • Modification de la colonne Department afin d’afficher le nom du département. Le code affiche la propriété Name de l’entité Department qui est chargée dans la propriété de navigation Department :

    @Html.DisplayFor(modelItem => item.Department.Name)
    

Exécutez l’application et sélectionnez l’onglet Courses pour afficher la liste avec les noms des départements.

Courses Index page

La méthode OnGetAsync charge les données associées avec la méthode Include. La méthode Select est autre solution qui charge uniquement les données associées nécessaires. Pour les éléments uniques, comme Department.Name, il utilise une jointure interne SQL. Pour les collections, il utilise un autre accès à la base de données, mais c’est aussi le cas de l’opérateur Include sur les collections.

Le code suivant charge les données associées avec la méthode Select :

public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
            .Select(p => new CourseViewModel
            {
                CourseID = p.CourseID,
                Title = p.Title,
                Credits = p.Credits,
                DepartmentName = p.Department.Name
            }).ToListAsync();
}

Le code précédent ne retourne aucun type d’entité. Par conséquent, aucun suivi n’est effectué. Pour plus d’informations sur le suivi EF, consultez Requêtes avec suivi et sans suivi.

CourseViewModel :

public class CourseViewModel
{
    public int CourseID { get; set; }
    public string Title { get; set; }
    public int Credits { get; set; }
    public string DepartmentName { get; set; }
}

Pour obtenir un exemple complet, consultez IndexSelect.cshtml et IndexSelect.cshtml.cs.

Créer des pages Instructor

Cette section génère automatiquement des modèles de pages Instructor et ajoute les cours et les inscriptions associés à la page d’index des formateurs.

Instructors Index page

Cette page lit et affiche les données associées comme suit :

  • La liste des formateurs affiche des données associées de l’entité OfficeAssignment (Office dans l’image précédente). Il existe une relation un-à-zéro-ou-un entre les entités Instructor et OfficeAssignment. Le chargement hâtif est utilisé pour les entités OfficeAssignment. Le chargement hâtif est généralement plus efficace quand les données associées doivent être affichées. Ici, les affectations de bureau pour les formateurs sont affichées.
  • Quand l’utilisateur sélectionne un formateur, les entités Course associées sont affichées. Il existe une relation plusieurs-à-plusieurs entre les entités Instructor et Course. Le chargement hâtif est utilisé pour les entités Course et leurs entités Department associées. Dans le cas présent, des requêtes distinctes peuvent être plus efficaces, car seuls les cours du formateur sélectionné sont nécessaires. Cet exemple montre comment utiliser le chargement hâtif pour des propriétés de navigation dans des entités qui se trouvent dans des propriétés de navigation.
  • Quand l’utilisateur sélectionne un cours, les données associées de l’entité Enrollments s’affichent. Dans l’image précédente, le nom et la note de l’étudiant sont affichés. Il existe une relation un-à-plusieurs entre les entités Course et Enrollment.

Création d'un modèle de vue

La page sur les formateurs affiche les données de trois tables différentes. Un modèle de vue comprenant trois propriétés représentant les trois tables est nécessaire.

Créez SchoolViewModels/InstructorIndexData.cs avec le code suivant :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

Générer automatiquement des modèles de pages Instructor

  • Suivez les instructions dans Générer automatiquement des modèles de pages Student avec les exceptions suivantes :

    • Créez un dossier Pages/Instructors.
    • Utilisez Instructor pour la classe de modèle.
    • Utilisez la classe de contexte existante au lieu d’en créer une nouvelle.

Pour voir à quoi ressemble la page générée automatiquement avant de la mettre à jour, exécutez l’application et accédez à la page Instructors.

Mettez à jour Pages/Instructors/Index.cshtml.cs à l’aide du code suivant :

using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData InstructorData { get; set; }
        public int InstructorID { get; set; }
        public int CourseID { get; set; }

        public async Task OnGetAsync(int? id, int? courseID)
        {
            InstructorData = new InstructorIndexData();
            InstructorData.Instructors = await _context.Instructors
                .Include(i => i.OfficeAssignment)                 
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                        .ThenInclude(i => i.Department)
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                        .ThenInclude(i => i.Enrollments)
                            .ThenInclude(i => i.Student)
                .AsNoTracking()
                .OrderBy(i => i.LastName)
                .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
                Instructor instructor = InstructorData.Instructors
                    .Where(i => i.ID == id.Value).Single();
                InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
            }

            if (courseID != null)
            {
                CourseID = courseID.Value;
                var selectedCourse = InstructorData.Courses
                    .Where(x => x.CourseID == courseID).Single();
                InstructorData.Enrollments = selectedCourse.Enrollments;
            }
        }
    }
}

La méthode OnGetAsync accepte des données de route facultatives pour l’ID du formateur sélectionné.

Examinez la requête dans le fichier Pages/Instructors/Index.cshtml.cs :

InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
    .AsNoTracking()
    .OrderBy(i => i.LastName)
    .ToListAsync();

Le code spécifie un chargement hâtif pour les propriétés de navigation suivantes :

  • Instructor.OfficeAssignment
  • Instructor.CourseAssignments
    • CourseAssignments.Course
      • Course.Department
      • Course.Enrollments
        • Enrollment.Student

Notez la répétition des méthodes Include et ThenInclude pour CourseAssignments et Course. Cette répétition est nécessaire pour spécifier un chargement hâtif pour deux propriétés de navigation de l’entité Course.

Le code suivant s’exécute quand un formateur est sélectionné (id != null).

if (id != null)
{
    InstructorID = id.Value;
    Instructor instructor = InstructorData.Instructors
        .Where(i => i.ID == id.Value).Single();
    InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

Le formateur sélectionné est récupéré à partir de la liste des formateurs dans le modèle d’affichage. La propriété Courses du modèle d’affichage est chargée avec les entités Course de la propriété de navigation CourseAssignments de ce formateur.

La méthode Where retourne une collection. Toutefois, dans ce cas, le filtre sélectionne une seule entité, de sorte que la méthode Single est appelée pour convertir la collection en une seule entité Instructor. L’entité Instructor fournit l’accès à la propriété CourseAssignments. CourseAssignments fournit l’accès aux entités Course associées.

Instructor-to-Courses m:M

La méthode Single est utilisée sur une collection quand la collection ne compte qu’un seul élément. La méthode Single lève une exception si la collection est vide ou s’il y a plusieurs éléments. Une alternative est SingleOrDefault, qui renvoie une valeur par défaut (Null dans ce cas) si la collection est vide.

Le code suivant renseigne la propriété Enrollments du modèle d’affichage quand un cours est sélectionné :

if (courseID != null)
{
    CourseID = courseID.Value;
    var selectedCourse = InstructorData.Courses
        .Where(x => x.CourseID == courseID).Single();
    InstructorData.Enrollments = selectedCourse.Enrollments;
}

Mettre à jour la page d’index des formateurs

Mettez à jour Pages/Instructors/Index.cshtml à l’aide du code suivant.

@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.InstructorData.Instructors)
        {
            string selectedRow = "";
            if (item.ID == Model.InstructorID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.CourseAssignments)
                        {
                            @course.Course.CourseID @:  @course.Course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.InstructorData.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.InstructorData.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == Model.CourseID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    <a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

@if (Model.InstructorData.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.InstructorData.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}

Le code précédent apporte les modifications suivantes :

  • Il met à jour la directive page en remplaçant @page par @page "{id:int?}". "{id:int?}" est un modèle de route. Le modèle de route change les chaînes de requête entières dans l’URL en données de route. Par exemple, si vous cliquez sur le lien Select pour un formateur avec seulement la directive @page, une URL comme celle-ci est générée :

    https://localhost:5001/Instructors?id=2

    Quand la directive de page est @page "{id:int?}", l’URL est :

    https://localhost:5001/Instructors/2

  • Ajoute une colonne Office qui affiche item.OfficeAssignment.Location seulement si item.OfficeAssignment n’a pas la valeur Null. Comme il s’agit d’une relation un-à-zéro-ou-un, il se peut qu’il n’y ait pas d’entité OfficeAssignment associée.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Ajoute une colonne Courses qui affiche les cours animés par chaque formateur. Consultez Conversion de ligne explicite pour en savoir plus sur cette syntaxe razor.

  • Ajoute du code qui ajoute dynamiquement class="table-success" à l’élément tr du formateur et du cours sélectionnés. Cela définit une couleur d’arrière-plan pour la ligne sélectionnée à l’aide d’une classe d’amorçage.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Ajoute un nouveau lien hypertexte libellé Select. Ce lien envoie l’ID du formateur sélectionné à la méthode Index, et définit une couleur d’arrière-plan.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Ajoute un tableau de cours pour l’instructeur sélectionné.

  • Ajoute un tableau d’inscriptions d’étudiants pour le cours sélectionné.

Exécutez l’application et sélectionnez l’onglet Instructeurs. La page affiche le Location (bureau) à partir de l’entité OfficeAssignment associée. Si OfficeAssignment a la valeur Null, une cellule de tableau vide est affichée.

Cliquez sur le lien Select pour un formateur. Le style de ligne change et les cours attribués à ce formateur s’affichent.

Sélectionnez un cours pour afficher la liste des étudiants inscrits et leurs notes.

Instructors Index page instructor and course selected

Utilisation de Single

La méthode Single peut passer la condition Where au lieu d’appeler la méthode Where séparément :

public async Task OnGetAsync(int? id, int? courseID)
{
    InstructorData = new InstructorIndexData();

    InstructorData.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
            .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Enrollments)
                        .ThenInclude(i => i.Student)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = InstructorData.Instructors.Single(
            i => i.ID == id.Value);
        InstructorData.Courses = instructor.CourseAssignments.Select(
            s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        InstructorData.Enrollments = InstructorData.Courses.Single(
            x => x.CourseID == courseID).Enrollments;
    }
}

L’utilisation de Single avec une condition Where est une question de préférence personnelle. Elle n’offre pas d’avantages par rapport à la méthode Where.

Chargement explicite

Le code actuel spécifie le chargement hâtif pour Enrollments et Students :

InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
    .AsNoTracking()
    .OrderBy(i => i.LastName)
    .ToListAsync();

Supposez que les utilisateurs souhaitent rarement voir les inscriptions à un cours. Dans ce cas, une optimisation consisterait à charger les données d’inscription uniquement si elles sont demandées. Dans cette section, la méthode OnGetAsync est mise à jour de façon à utiliser le chargement explicite de Enrollments et Students.

Mettez à jour Pages/Instructors/Index.cshtml.cs à l’aide du code suivant.

using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData InstructorData { get; set; }
        public int InstructorID { get; set; }
        public int CourseID { get; set; }

        public async Task OnGetAsync(int? id, int? courseID)
        {
            InstructorData = new InstructorIndexData();
            InstructorData.Instructors = await _context.Instructors
                .Include(i => i.OfficeAssignment)                 
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                        .ThenInclude(i => i.Department)
                //.Include(i => i.CourseAssignments)
                //    .ThenInclude(i => i.Course)
                //        .ThenInclude(i => i.Enrollments)
                //            .ThenInclude(i => i.Student)
                //.AsNoTracking()
                .OrderBy(i => i.LastName)
                .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
                Instructor instructor = InstructorData.Instructors
                    .Where(i => i.ID == id.Value).Single();
                InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
            }

            if (courseID != null)
            {
                CourseID = courseID.Value;
                var selectedCourse = InstructorData.Courses
                    .Where(x => x.CourseID == courseID).Single();
                await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
                foreach (Enrollment enrollment in selectedCourse.Enrollments)
                {
                    await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
                }
                InstructorData.Enrollments = selectedCourse.Enrollments;
            }
        }
    }
}

Le code précédent supprime les appels de méthode ThenInclude pour les données sur les inscriptions et les étudiants. Si un cours est sélectionné, le code de chargement explicite récupère :

  • Les entités Enrollment pour le cours sélectionné.
  • Les entités Student pour chaque Enrollment.

Notez que le code précédent commente .AsNoTracking(). Les propriétés de navigation peuvent être chargées explicitement uniquement pour les entités suivies.

Tester l'application. Du point de vue d’un utilisateur, l’application se comporte de façon identique à la version précédente.

Étapes suivantes

Le didacticiel suivant montre comment mettre à jour les données associées.

Dans ce didacticiel, nous allons lire et afficher des données associées. Les données associées sont des données qu’EF Core charge dans des propriétés de navigation.

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez ou affichez l’application terminéeInstructions de téléchargement.

Les illustrations suivantes montrent les pages terminées pour ce didacticiel :

Courses Index page

Instructors Index page

EF Core peut charger des données associées dans les propriétés de navigation d’une entité de plusieurs manières :

  • Chargement hâtif. Le chargement hâtif a lieu quand une requête pour un type d’entité charge également des entités associées. Quand l’entité est lue, ses données associées sont récupérées. Cela génère en général une requête de jointure unique qui récupère toutes les données nécessaires. EF Core émet plusieurs requêtes pour certains types de chargement hâtif. L’émission de requêtes multiples peut être plus efficace que ce n’était le cas pour certaines requêtes dans EF6 où une seule requête était émise. Le chargement hâtif est spécifié avec les méthodes Include et ThenInclude.

    Eager loading example

    Le chargement hâtif envoie plusieurs requêtes quand une navigation dans la collection est incluse :

    • Une requête pour la requête principale
    • Une requête pour chaque « périmètre » de collection dans l’arborescence de la charge.
  • Requêtes distinctes avec Load : les données peuvent être récupérées dans des requêtes distinctes, et EF Core « corrige » les propriétés de navigation. Quand EF Core « corrige », cela signifie que les propriétés de navigation sont renseignées automatiquement. Les requêtes distinctes avec Load s’apparentent plus au chargement explicite qu’au chargement hâtif.

    Separate queries example

    Remarque : EF Core corrige automatiquement les propriétés de navigation vers d’autres entités qui étaient précédemment chargées dans l’instance de contexte. Même si les données pour une propriété de navigation ne sont pas explicitement incluses, la propriété peut toujours être renseignée si toutes ou une partie des entités associées ont été précédemment chargées.

  • Chargement explicite. Quand l’entité est lue pour la première fois, les données associées ne sont pas récupérées. Vous devez écrire du code pour récupérer les données associées en cas de besoin. En cas de chargement explicite avec des requêtes distinctes, plusieurs requêtes sont envoyées à la base de données. Avec le chargement explicite, le code spécifie les propriétés de navigation à charger. Utilisez la méthode Load pour effectuer le chargement explicite. Par exemple :

    Explicit loading example

  • Chargement différé. Le chargement différé a été ajouté à EF Core dans la version 2.1. Quand l’entité est lue pour la première fois, les données associées ne sont pas récupérées. Lors du premier accès à une propriété de navigation, les données requises pour cette propriété de navigation sont récupérées automatiquement. Une requête est envoyée à la base de données chaque fois qu’une propriété de navigation est sollicitée pour la première fois.

  • L’opérateur Select charge uniquement les données associées nécessaires.

Créer une page Course qui affiche le nom du département

L’entité Course comprend une propriété de navigation qui contient l’entité Department. L’entité Department contient le département auquel le cours est affecté.

Pour afficher le nom du département affecté dans une liste de cours

  • Obtenez la propriété Name à partir de l’entité Department.
  • L’entité Department provient de la propriété de navigation Course.Department.

Course.Department

Génération automatique du modèle Course

Suivez les instructions fournies dans Générer automatiquement le modèle d’étudiant et utilisez Course pour la classe de modèle.

La commande précédente génère automatiquement le modèle Course. Ouvrez le projet dans Visual Studio.

Ouvrez Pages/Courses/Index.cshtml.cs et examinez la méthode OnGetAsync. Le moteur de génération de modèles automatique a spécifié le chargement hâtif pour la propriété de navigation Department. La méthode Include spécifie le chargement hâtif.

Exécutez l’application et sélectionnez le lien Courses. La colonne Department affiche le DepartmentID, ce qui n’est pas utile.

Mettez à jour la méthode OnGetAsync avec le code suivant :

public async Task OnGetAsync()
{
    Course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .ToListAsync();
}

Le code précédent ajoute AsNoTracking. AsNoTracking améliore les performances, car les entités retournées ne sont pas suivies. Les entités ne sont pas suivies, car elles ne sont pas mises à jour dans le contexte actuel.

Mettez à jour Pages/Courses/Index.cshtml avec le balisage mis en évidence suivant :

@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
    ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Course)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.CourseID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Credits)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Department.Name)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Les modifications suivantes ont été apportées au code généré automatiquement :

  • Changement de l’en-tête : Index a été remplacé par Courses.

  • Ajout d’une colonne Number qui affiche la valeur de la propriété CourseID. Par défaut, les clés primaires ne sont pas générées automatiquement, car elles ne sont normalement pas significatives pour les utilisateurs finaux. Toutefois, dans le cas présent la clé primaire est significative.

  • Modification de la colonne Department afin d’afficher le nom du département. Le code affiche la propriété Name de l’entité Department qui est chargée dans la propriété de navigation Department :

    @Html.DisplayFor(modelItem => item.Department.Name)
    

Exécutez l’application et sélectionnez l’onglet Courses pour afficher la liste avec les noms des départements.

Courses Index page

La méthode OnGetAsync charge les données associées avec la méthode Include :

public async Task OnGetAsync()
{
    Course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .ToListAsync();
}

L’opérateur Select charge uniquement les données associées nécessaires. Pour les éléments uniques, comme Department.Name, il utilise une jointure interne SQL. Pour les collections, il utilise un autre accès à la base de données, mais c’est aussi le cas de l’opérateur Include sur les collections.

Le code suivant charge les données associées avec la méthode Select :

public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
            .Select(p => new CourseViewModel
            {
                CourseID = p.CourseID,
                Title = p.Title,
                Credits = p.Credits,
                DepartmentName = p.Department.Name
            }).ToListAsync();
}

CourseViewModel :

public class CourseViewModel
{
    public int CourseID { get; set; }
    public string Title { get; set; }
    public int Credits { get; set; }
    public string DepartmentName { get; set; }
}

Pour obtenir un exemple complet, consultez IndexSelect.cshtml et IndexSelect.cshtml.cs.

Créer une page Instructors qui affiche les cours et les inscriptions

Dans cette section, nous allons créer la page Instructors.

Instructors Index page

Cette page lit et affiche les données associées comme suit :

  • La liste des formateurs affiche des données associées de l’entité OfficeAssignment (Office dans l’image précédente). Il existe une relation un-à-zéro-ou-un entre les entités Instructor et OfficeAssignment. Le chargement hâtif est utilisé pour les entités OfficeAssignment. Le chargement hâtif est généralement plus efficace quand les données associées doivent être affichées. Ici, les affectations de bureau pour les formateurs sont affichées.
  • Quand l’utilisateur sélectionne un formateur (Harui dans l’image précédente), les entités Course associées sont affichées. Il existe une relation plusieurs-à-plusieurs entre les entités Instructor et Course. Le chargement hâtif est utilisé pour les entités Course et leurs entités Department associées. Dans le cas présent, des requêtes distinctes peuvent être plus efficaces, car seuls les cours du formateur sélectionné sont nécessaires. Cet exemple montre comment utiliser le chargement hâtif pour des propriétés de navigation dans des entités qui se trouvent dans des propriétés de navigation.
  • Quand l’utilisateur sélectionne un cours (Chemistry dans l’image précédente), les données associées de l’entité Enrollments sont affichées. Dans l’image précédente, le nom et la note de l’étudiant sont affichés. Il existe une relation un-à-plusieurs entre les entités Course et Enrollment.

Créer un modèle de vue pour la vue d’index des formateurs

La page sur les formateurs affiche les données de trois tables différentes. Nous allons créer un modèle d’affichage qui comprend les trois entités représentant les trois tables.

Dans le dossier SchoolViewModels, créez InstructorIndexData.cs avec le code suivant :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

Générer automatiquement le modèle Instructor

Suivez les instructions fournies dans Générer automatiquement le modèle d’étudiant et utilisez Instructor pour la classe de modèle.

La commande précédente génère automatiquement le modèle Instructor. Exécutez l’application et accédez à la page des formateurs.

Remplacez Pages/Instructors/Index.cshtml.cs par le code suivant :

using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData Instructor { get; set; }
        public int InstructorID { get; set; }

        public async Task OnGetAsync(int? id)
        {
            Instructor = new InstructorIndexData();
            Instructor.Instructors = await _context.Instructors
                  .Include(i => i.OfficeAssignment)
                  .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                  .AsNoTracking()
                  .OrderBy(i => i.LastName)
                  .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
            }           
        }
    }
}

La méthode OnGetAsync accepte des données de route facultatives pour l’ID du formateur sélectionné.

Examinez la requête dans le fichier Pages/Instructors/Index.cshtml.cs :

Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

La requête comporte deux Include :

  • OfficeAssignment : affiché dans l’affichage Instructors.
  • CourseAssignments : qui affiche les cours dispensés.

Mettre à jour la page d’index des formateurs

Mettez à jour Pages/Instructors/Index.cshtml avec le balisage suivant :

@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructor.Instructors)
        {
            string selectedRow = "";
            if (item.ID == Model.InstructorID)
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.CourseAssignments)
                        {
                            @course.Course.CourseID @:  @course.Course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Le balisage précédent apporte les modifications suivantes :

  • Il met à jour la directive page en remplaçant @page par @page "{id:int?}". "{id:int?}" est un modèle de route. Le modèle de route change les chaînes de requête entières dans l’URL en données de route. Par exemple, si vous cliquez sur le lien Select pour un formateur avec seulement la directive @page, une URL comme celle-ci est générée :

    http://localhost:1234/Instructors?id=2

    Quand la directive de page est @page "{id:int?}", l’URL précédente est :

    http://localhost:1234/Instructors/2

  • Le titre de la page est Instructors.

  • Il ajoute une colonne Office qui affiche item.OfficeAssignment.Location uniquement si item.OfficeAssignment n’est pas Null. Comme il s’agit d’une relation un-à-zéro-ou-un, il se peut qu’il n’y ait pas d’entité OfficeAssignment associée.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Vous avez ajouté une colonne Courses qui affiche les cours animés par chaque formateur. Consultez Conversion de ligne explicite pour en savoir plus sur cette syntaxe razor.

  • Vous avez ajouté un code qui ajoute dynamiquement class="success" à l’élément tr du formateur sélectionné. Cela définit une couleur d’arrière-plan pour la ligne sélectionnée à l’aide d’une classe d’amorçage.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • Ajout d’un nouveau lien hypertexte libellé Select. Ce lien envoie l’ID du formateur sélectionné à la méthode Index, et définit une couleur d’arrière-plan.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    

Exécutez l’application et sélectionnez l’onglet Instructeurs. La page affiche le Location (bureau) à partir de l’entité OfficeAssignment associée. Si OfficeAssignment` est Null, une cellule de table vide est affichée.

Cliquez sur le lien Select. Le style de ligne change.

Ajouter les cours dispensés par le formateur sélectionné

Mettez à jour la méthode OnGetAsync dans Pages/Instructors/Index.cshtml.cs avec le code suivant :

public async Task OnGetAsync(int? id, int? courseID)
{
    Instructor = new InstructorIndexData();
    Instructor.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = Instructor.Instructors.Where(
            i => i.ID == id.Value).Single();
        Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        Instructor.Enrollments = Instructor.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }
}

Ajouter public int CourseID { get; set; }

public class IndexModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public IndexModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    public InstructorIndexData Instructor { get; set; }
    public int InstructorID { get; set; }
    public int CourseID { get; set; }

    public async Task OnGetAsync(int? id, int? courseID)
    {
        Instructor = new InstructorIndexData();
        Instructor.Instructors = await _context.Instructors
              .Include(i => i.OfficeAssignment)
              .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Department)
              .AsNoTracking()
              .OrderBy(i => i.LastName)
              .ToListAsync();

        if (id != null)
        {
            InstructorID = id.Value;
            Instructor instructor = Instructor.Instructors.Where(
                i => i.ID == id.Value).Single();
            Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
        }

        if (courseID != null)
        {
            CourseID = courseID.Value;
            Instructor.Enrollments = Instructor.Courses.Where(
                x => x.CourseID == courseID).Single().Enrollments;
        }
    }

Examinez la requête mise à jour :

Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

La requête précédente ajoute les entités Department.

Le code suivant s’exécute quand un formateur est sélectionné (id != null). Le formateur sélectionné est récupéré à partir de la liste des formateurs dans le modèle d’affichage. La propriété Courses du modèle d’affichage est chargée avec les entités Course de la propriété de navigation CourseAssignments de ce formateur.

if (id != null)
{
    InstructorID = id.Value;
    Instructor instructor = Instructor.Instructors.Where(
        i => i.ID == id.Value).Single();
    Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

La méthode Where retourne une collection. Dans la méthode Where précédente, une seule entité Instructor est retournée. La méthode Single convertit la collection en une seule entité Instructor. L’entité Instructor fournit l’accès à la propriété CourseAssignments. CourseAssignments fournit l’accès aux entités Course associées.

Instructor-to-Courses m:M

La méthode Single est utilisée sur une collection quand la collection ne compte qu’un seul élément. La méthode Single lève une exception si la collection est vide ou s’il y a plusieurs éléments. Une alternative est SingleOrDefault, qui renvoie une valeur par défaut (Null dans ce cas) si la collection est vide. L’utilisation de SingleOrDefault sur une collection vide :

  • Génère une exception (à cause de la tentative de trouver une propriété Courses sur une référence Null).
  • Le message d’exception indique moins clairement la cause du problème.

Le code suivant renseigne la propriété Enrollments du modèle d’affichage quand un cours est sélectionné :

if (courseID != null)
{
    CourseID = courseID.Value;
    Instructor.Enrollments = Instructor.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

Ajoutez le balisage suivant à la fin de la Pages/Instructors/Index.cshtmlRazor Page :

                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.Instructor.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Instructor.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == Model.CourseID)
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    <a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

Le balisage précédent affiche une liste de cours associés à un formateur quand un formateur est sélectionné.

Tester l'application. Cliquez sur un lien Select dans la page des formateurs.

Afficher les données sur les étudiants

Dans cette section, nous allons mettre à jour l’application afin d’afficher les données sur les étudiants pour le cours sélectionné.

Mettez à jour la requête dans la méthode OnGetAsync dans Pages/Instructors/Index.cshtml.cs avec le code suivant :

Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)                 
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

Mettez à jour Pages/Instructors/Index.cshtml. Ajoutez le balisage suivant à la fin du fichier :


@if (Model.Instructor.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Instructor.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}

Le balisage précédent affiche une liste des étudiants qui sont inscrits au cours sélectionné.

Actualisez la page et sélectionnez un formateur. Sélectionnez un cours pour afficher la liste des étudiants inscrits et leurs notes.

Instructors Index page instructor and course selected

Utilisation de Single

La méthode Single peut passer la condition Where au lieu d’appeler la méthode Where séparément :

public async Task OnGetAsync(int? id, int? courseID)
{
    Instructor = new InstructorIndexData();

    Instructor.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
            .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Enrollments)
                        .ThenInclude(i => i.Student)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = Instructor.Instructors.Single(
            i => i.ID == id.Value);
        Instructor.Courses = instructor.CourseAssignments.Select(
            s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        Instructor.Enrollments = Instructor.Courses.Single(
            x => x.CourseID == courseID).Enrollments;
    }
}

L’approche précédente faisant appel à Single ne présente aucun avantage par rapport à l’utilisation de Where. Certains développeurs préfèrent le style d’approche Single.

Chargement explicite

Le code actuel spécifie le chargement hâtif pour Enrollments et Students :

Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)                 
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

Supposez que les utilisateurs souhaitent rarement voir les inscriptions à un cours. Dans ce cas, une optimisation consisterait à charger les données d’inscription uniquement si elles sont demandées. Dans cette section, la méthode OnGetAsync est mise à jour de façon à utiliser le chargement explicite de Enrollments et Students.

Mettez à jour la méthode OnGetAsync avec le code suivant :

public async Task OnGetAsync(int? id, int? courseID)
{
    Instructor = new InstructorIndexData();
    Instructor.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)                 
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
            //.Include(i => i.CourseAssignments)
            //    .ThenInclude(i => i.Course)
            //        .ThenInclude(i => i.Enrollments)
            //            .ThenInclude(i => i.Student)
         // .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();


    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = Instructor.Instructors.Where(
            i => i.ID == id.Value).Single();
        Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        var selectedCourse = Instructor.Courses.Where(x => x.CourseID == courseID).Single();
        await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
        }
        Instructor.Enrollments = selectedCourse.Enrollments;
    }
}

Le code précédent supprime les appels de méthode ThenInclude pour les données sur les inscriptions et les étudiants. Si un cours est sélectionné, le code en surbrillance récupère :

  • Les entités Enrollment pour le cours sélectionné.
  • Les entités Student pour chaque Enrollment.

Notez que le code précédent commente .AsNoTracking(). Les propriétés de navigation peuvent être chargées explicitement uniquement pour les entités suivies.

Tester l'application. Du point de vue des utilisateurs, l’application se comporte comme la version précédente.

Le didacticiel suivant montre comment mettre à jour les données associées.

Ressources supplémentaires