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 :
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.
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 avecLoad
s’apparentent plus au chargement explicite qu’au chargement hâtif.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 :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.
Pour afficher le nom du service (« department ») affecté pour un cours (« course ») :
- Chargez l’entité
Department
associée dans la propriété de navigationCourse.Department
. - Obtenez le nom à partir de la propriété
Name
de l’entitéDepartment
.
Générer automatiquement des modèles de pages Course
Suivez les instructions dans Générer automatiquement des modèles de pages Student avec les exceptions suivantes :
- Créez un dossier Pages/Courses.
- Utilisez
Course
pour la classe de modèle. - Utilisez la classe de contexte existante au lieu d’en créer une nouvelle.
Ouvrez
Pages/Courses/Index.cshtml.cs
et examinez la méthodeOnGetAsync
. Le moteur de génération de modèles automatique a spécifié le chargement hâtif pour la propriété de navigationDepartment
. La méthodeInclude
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é enCourses
.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 navigationDepartment
:@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.
Chargement de données associées avec Select
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.
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ésInstructor
etOfficeAssignment
. Le chargement hâtif est utilisé pour les entitésOfficeAssignment
. 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ésInstructor
etCourse
. Le chargement hâtif est utilisé pour les entitésCourse
et leurs entitésDepartment
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ésCourse
etEnrollment
.
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 siitem.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émenttr
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.
É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 :
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
etThenInclude
.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 avecLoad
s’apparentent plus au chargement explicite qu’au chargement hâtif.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 :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.
Pour afficher le nom du service (« department ») affecté pour un cours (« course ») :
- Chargez l’entité
Department
associée dans la propriété de navigationCourse.Department
. - Obtenez le nom à partir de la propriété
Name
de l’entitéDepartment
.
Générer automatiquement des modèles de pages Course
Suivez les instructions dans Générer automatiquement des modèles de pages Student avec les exceptions suivantes :
- Créez un dossier Pages/Courses.
- Utilisez
Course
pour la classe de modèle. - Utilisez la classe de contexte existante au lieu d’en créer une nouvelle.
Ouvrez
Pages/Courses/Index.cshtml.cs
et examinez la méthodeOnGetAsync
. Le moteur de génération de modèles automatique a spécifié le chargement hâtif pour la propriété de navigationDepartment
. La méthodeInclude
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é enCourses
.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 navigationDepartment
:@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.
Chargement de données associées avec Select
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.
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ésInstructor
etOfficeAssignment
. Le chargement hâtif est utilisé pour les entitésOfficeAssignment
. 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ésInstructor
etCourse
. Le chargement hâtif est utilisé pour les entitésCourse
et leurs entitésDepartment
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ésCourse
etEnrollment
.
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.
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 siitem.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émenttr
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.
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 chaqueEnrollment
.
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ée. Télécharger les instructions.
Les illustrations suivantes montrent les pages terminées pour ce didacticiel :
Chargement hâtif, explicite et différé des données associées
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
etThenInclude
.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 avecLoad
s’apparentent plus au chargement explicite qu’au chargement hâtif.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 :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 navigationCourse.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 navigationDepartment
:@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.
Chargement de données associées avec Select
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.
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ésInstructor
etOfficeAssignment
. Le chargement hâtif est utilisé pour les entitésOfficeAssignment
. 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ésInstructor
etCourse
. Le chargement hâtif est utilisé pour les entitésCourse
et leurs entitésDepartment
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ésCourse
etEnrollment
.
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 siitem.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émenttr
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.
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.cshtml
Razor 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.
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 chaqueEnrollment
.
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.