ASP.NET Core 中的 Razor 页面和 EF Core - 读取相关数据 - 第 6 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - Read Related Data - 6 of 8

作者:Tom DykstraJon P SmithRick AndersonBy Tom Dykstra, Jon P Smith, and Rick Anderson

Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor 页面 Web 应用。The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. 若要了解系列教程,请参阅第一个教程For information about the tutorial series, see the first tutorial.

如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。If you run into problems you can't solve, download the completed app and compare that code to what you created by following the tutorial.

本教程介绍如何读取和显示相关数据。This tutorial shows how to read and display related data. 相关数据为 EF Core 加载到导航属性中的数据。Related data is data that EF Core loads into navigation properties.

下图显示了本教程中已完成的页面:The following illustrations show the completed pages for this tutorial:

“课程索引”页

“讲师索引”页

预先加载、显式加载和延迟加载Eager, explicit, and lazy loading

EF Core 可采用多种方式将相关数据加载到实体的导航属性中:There are several ways that EF Core can load related data into the navigation properties of an entity:

  • 预先加载Eager loading. 预先加载是指对查询某类型的实体时一并加载相关实体。Eager loading is when a query for one type of entity also loads related entities. 读取实体时,会检索其相关数据。When an entity is read, its related data is retrieved. 此时通常会出现单一联接查询,检索所有必需数据。This typically results in a single join query that retrieves all of the data that's needed. EF Core 将针对预先加载的某些类型发出多个查询。EF Core will issue multiple queries for some types of eager loading. 发布多个查询可能比发布大型的单个查询更为有效。Issuing multiple queries can be more efficient than a giant single query. 预先加载通过 IncludeThenInclude 方法进行指定。Eager loading is specified with the Include and ThenInclude methods.

    预先加载示例

    当包含集合导航时,预先加载会发送多个查询:Eager loading sends multiple queries when a collection navigation is included:

    • 一个查询用于主查询One query for the main query
    • 一个查询用于加载树中每个集合“边缘”。One query for each collection "edge" in the load tree.
  • 使用 Load 的单独查询:可在单独的查询中检索数据,EF Core 会“修复”导航属性。Separate queries with Load: The data can be retrieved in separate queries, and EF Core "fixes up" the navigation properties. “修复”是指 EF Core 自动填充导航属性。"Fixes up" means that EF Core automatically populates the navigation properties. 使用 Load 单独查询比预先加载更像是显式加载。Separate queries with Load is more like explicit loading than eager loading.

    单独查询示例

    注意:EF Core 会将导航属性自动“修复”为之前加载到上下文实例中的任何其他实体。Note: EF Core automatically fixes up navigation properties to any other entities that were previously loaded into the context instance. 即使导航属性的数据非显式包含在内 ,但如果先前加载了部分或所有相关实体,则仍可能填充该属性。Even if the data for a navigation property is not explicitly included, the property may still be populated if some or all of the related entities were previously loaded.

  • 显式加载Explicit loading. 首次读取实体时,不检索相关数据。When the entity is first read, related data isn't retrieved. 必须编写代码才能在需要时检索相关数据。Code must be written to retrieve the related data when it's needed. 使用单独查询进行显式加载时,会向数据库发送多个查询。Explicit loading with separate queries results in multiple queries sent to the database. 该代码通过显式加载指定要加载的导航属性。With explicit loading, the code specifies the navigation properties to be loaded. 使用 Load 方法进行显式加载。Use the Load method to do explicit loading. 例如:For example:

    显式加载示例

  • 延迟加载Lazy loading. 延迟加载已添加到版本 2.1 中的 EF CoreLazy loading was added to EF Core in version 2.1. 首次读取实体时,不检索相关数据。When the entity is first read, related data isn't retrieved. 首次访问导航属性时,会自动检索该导航属性所需的数据。The first time a navigation property is accessed, the data required for that navigation property is automatically retrieved. 首次访问导航属性时,都会向数据库发送一个查询。A query is sent to the database each time a navigation property is accessed for the first time.

创建“课程”页Create Course pages

Course 实体包括一个带相关 Department 实体的导航属性。The Course entity includes a navigation property that contains the related Department entity.

Course.Department

若要显示课程的已分配院系的名称,请执行以下操作:To display the name of the assigned department for a course:

  • 将相关的 Department 实体加载到 Course.Department 导航属性。Load the related Department entity into the Course.Department navigation property.
  • 获取 Department 实体的 Name 属性中的名称。Get the name from the Department entity's Name property.

搭建“课程”页的基架Scaffold Course pages

  • 遵循搭建“学生”页的基架中的说明,但以下情况除外:Follow the instructions in Scaffold Student pages with the following exceptions:

    • 创建“Pages/Courses”文件夹 。Create a Pages/Courses folder.
    • Course 用于模型类。Use Course for the model class.
    • 使用现有的上下文类,而不是新建上下文类。Use the existing context class instead of creating a new one.
  • 打开 Pages/Courses/Index.cshtml.cs 并检查 OnGetAsync 方法。Open Pages/Courses/Index.cshtml.cs and examine the OnGetAsync method. 基架引擎为 Department 导航属性指定了预先加载。The scaffolding engine specified eager loading for the Department navigation property. Include 方法指定预先加载。The Include method specifies eager loading.

  • 运行应用并选择“课程”链接 。Run the app and select the Courses link. 院系列显示 DepartmentID(该项无用)。The department column displays the DepartmentID, which isn't useful.

显示院系名称Display the department name

使用以下代码更新 Pages/Courses/Index.cshtml.cs:Update Pages/Courses/Index.cshtml.cs with the following code:

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

上述代码将 Course 属性更改为 Courses,然后添加 AsNoTrackingThe preceding code changes the Course property to Courses and adds AsNoTracking. 由于未跟踪返回的实体,因此 AsNoTracking 提升了性能。AsNoTracking improves performance because the entities returned are not tracked. 无需跟踪实体,因为未在当前的上下文中更新这些实体。The entities don't need to be tracked because they're not updated in the current context.

使用以下代码更新 Pages/Courses/Index.cshtml 。Update Pages/Courses/Index.cshtml with the following code.

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

对基架代码进行了以下更改:The following changes have been made to the scaffolded code:

  • Course 属性名称更改为了 CoursesChanged the Course property name to Courses.

  • 添加了显示 CourseID 属性值的“数字”列 。Added a Number column that shows the CourseID property value. 默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。By default, primary keys aren't scaffolded because normally they're meaningless to end users. 但在此情况下主键是有意义的。However, in this case the primary key is meaningful.

  • 更改“院系”列,显示院系名称 。Changed the Department column to display the department name. 该代码显示已加载到 Department 导航属性中的 Department 实体的 Name 属性:The code displays the Name property of the Department entity that's loaded into the Department navigation property:

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

运行应用并选择“课程”选项卡,查看包含系名称的列表 。Run the app and select the Courses tab to see the list with department names.

“课程索引”页

OnGetAsync 方法使用 Include 方法加载相关数据。The OnGetAsync method loads related data with the Include method. Select 方法是只加载所需相关数据的替代方法。The Select method is an alternative that loads only the related data needed. 对于单个项(如 Department.Name),它使用 SQL INNER JOIN。For single items, like the Department.Name it uses a SQL INNER JOIN. 对于集合,它使用另一个数据库访问,但集合上的 Include 运算符也是如此。For collections, it uses another database access, but so does the Include operator on collections.

以下代码使用 Select 方法加载相关数据:The following code loads related data with the Select method:

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

CourseViewModelThe CourseViewModel:

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

有关完整示例的信息,请参阅 IndexSelect.cshtmlIndexSelect.cshtml.csSee IndexSelect.cshtml and IndexSelect.cshtml.cs for a complete example.

创建“讲师”页Create Instructor pages

本节搭建“讲师”页的基架,并向讲师“索引”页添加相关“课程”和“注册”。This section scaffolds Instructor pages and adds related Courses and Enrollments to the Instructors Index page.

“讲师索引”页Instructors Index page

该页面通过以下方式读取和显示相关数据:This page reads and displays related data in the following ways:

  • 讲师列表显示 OfficeAssignment 实体(上图中的办公室)的相关数据。The list of instructors displays related data from the OfficeAssignment entity (Office in the preceding image). InstructorOfficeAssignment 实体之间存在一对零或一的关系。The Instructor and OfficeAssignment entities are in a one-to-zero-or-one relationship. 预先加载适用于 OfficeAssignment 实体。Eager loading is used for the OfficeAssignment entities. 需要显示相关数据时,预先加载通常更高效。Eager loading is typically more efficient when the related data needs to be displayed. 在此情况下,会显示讲师的办公室分配。In this case, office assignments for the instructors are displayed.
  • 用户选择一名讲师时,显示相关 Course 实体。When the user selects an instructor, related Course entities are displayed. InstructorCourse 实体之间存在多对多关系。The Instructor and Course entities are in a many-to-many relationship. Course 实体及其相关的 Department 实体使用预先加载。Eager loading is used for the Course entities and their related Department entities. 这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。In this case, separate queries might be more efficient because only courses for the selected instructor are needed. 此示例演示如何在位于导航实体内的实体中预先加载这些导航实体。This example shows how to use eager loading for navigation properties in entities that are in navigation properties.
  • 用户选择一门课程时,会显示 Enrollments 实体的相关数据。When the user selects a course, related data from the Enrollments entity is displayed. 上图中显示了学生姓名和成绩。In the preceding image, student name and grade are displayed. CourseEnrollment 实体之间存在一对多的关系。The Course and Enrollment entities are in a one-to-many relationship.

创建视图模型Create a view model

“讲师”页显示来自三个不同表格的数据。The instructors page shows data from three different tables. 需要一个视图模型,该模型中包含表示三个表格的三个属性。A view model is needed that includes three properties representing the three tables.

使用以下代码创建 SchoolViewModels/InstructorIndexData.cs :Create SchoolViewModels/InstructorIndexData.cs with the following code:

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

搭建“讲师”页的基架Scaffold Instructor pages

  • 遵循搭建“学生”页的基架中的说明,但以下情况除外:Follow the instructions in Scaffold the student pages with the following exceptions:

    • 创建“Pages/Instructors”文件夹 。Create a Pages/Instructors folder.
    • Instructor 用于模型类。Use Instructor for the model class.
    • 使用现有的上下文类,而不是新建上下文类。Use the existing context class instead of creating a new one.

若要在更新之前查看已搭建基架的页面的外观,则运行应用并导航到“讲师”页。To see what the scaffolded page looks like before you update it, run the app and navigate to the Instructors page.

使用以下代码更新 Pages/Instructors/Index.cshtml.cs :Update Pages/Instructors/Index.cshtml.cs with the following code:

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

OnGetAsync 方法接受所选讲师 ID 的可选路由数据。The OnGetAsync method accepts optional route data for the ID of the selected instructor.

检查 Pages/Instructors/Index.cshtml.cs 文件中的查询 :Examine the query in the Pages/Instructors/Index.cshtml.cs file:

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

代码指定以下导航属性的预先加载:The code specifies eager loading for the following navigation properties:

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

注意 CourseAssignmentsCourseIncludeThenInclude 方法的重复使用。Notice the repetition of Include and ThenInclude methods for CourseAssignments and Course. 若要指定 Course 实体的两个导航属性的预先加载,则这种重复使用是必要的。This repetition is necessary to specify eager loading for two navigation properties of the Course entity.

选择讲师时 (id != null),将执行以下代码。The following code executes when an instructor is selected (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);
}

从视图模型中的讲师列表检索所选讲师。The selected instructor is retrieved from the list of instructors in the view model. 向视图模型的 Courses 属性加载来自讲师 CourseAssignments 导航属性的 Course 实体。The view model's Courses property is loaded with the Course entities from that instructor's CourseAssignments navigation property.

Where 方法返回一个集合。The Where method returns a collection. 但在本例中,筛选器将选择单个实体。But in this case, the filter will select a single entity. 因此,调用 Single 方法将集合转换为单个 Instructor 实体。so the Single method is called to convert the collection into a single Instructor entity. Instructor 实体提供对 CourseAssignments 属性的访问。The Instructor entity provides access to the CourseAssignments property. CourseAssignments 提供对相关 Course 实体的访问。CourseAssignments provides access to the related Course entities.

讲师-课程 m:M

当集合仅包含一个项时,集合使用 Single 方法。The Single method is used on a collection when the collection has only one item. 如果集合为空或包含多个项,Single 方法会引发异常。The Single method throws an exception if the collection is empty or if there's more than one item. 还可使用 SingleOrDefault,该方式在集合为空时返回默认值(本例中为 null)。An alternative is SingleOrDefault, which returns a default value (null in this case) if the collection is empty.

选中课程时,视图模型的 Enrollments 属性将填充以下代码:The following code populates the view model's Enrollments property when a course is selected:

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

更新“讲师索引”页Update the instructors Index page

使用以下代码更新 Pages/Instructors/Index.cshtml 。Update Pages/Instructors/Index.cshtml with the following code.

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

上面的代码执行以下更改:The preceding code makes the following changes:

  • page 指令从 @page 更新为 @page "{id:int?}"Updates the page directive from @page to @page "{id:int?}". "{id:int?}" 是一个路由模板。"{id:int?}" is a route template. 路由模板将 URL 中的整数查询字符串更改为路由数据。The route template changes integer query strings in the URL to route data. 例如,单击仅具有 @page 指令的讲师的“选择”链接将生成如下 URL :For example, clicking on the Select link for an instructor with only the @page directive produces a URL like the following:

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

    如果页面指令为 @page "{id:int?}" 时,则 URL 为:When the page directive is @page "{id:int?}", the URL is:

    https://localhost:5001/Instructors/2

  • 添加仅在 item.OfficeAssignment 不为 null 时才显示 item.OfficeAssignment.Location 的“办公室”列 。Adds an Office column that displays item.OfficeAssignment.Location only if item.OfficeAssignment isn't null. 由于这是一对零或一的关系,因此可能没有相关的 OfficeAssignment 实体。Because this is a one-to-zero-or-one relationship, there might not be a related OfficeAssignment entity.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 添加显示每位讲师所授课程的“课程”列 。Adds a Courses column that displays courses taught by each instructor. 有关此 razor 语法的详细信息,请参阅显式行转换See Explicit line transition for more about this razor syntax.

  • 添加向所选讲师和课程的 tr 元素中动态添加 class="success" 的代码。Adds code that dynamically adds class="success" to the tr element of the selected instructor and course. 此时会使用 Bootstrap 类为所选行设置背景色。This sets a background color for the selected row using a Bootstrap class.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • 添加标记为“选择”的新的超链接 。Adds a new hyperlink labeled Select. 该链接将所选讲师的 ID 发送给 Index 方法并设置背景色。This link sends the selected instructor's ID to the Index method and sets a background color.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • 添加所选讲师的课程表。Adds a table of courses for the selected Instructor.

  • 添加所选课程的学生注册表。Adds a table of student enrollments for the selected course.

运行应用并选择“讲师”选项卡 。该页显示来自相关 OfficeAssignment 实体的 Location(办公室)。Run the app and select the Instructors tab. The page displays the Location (office) from the related OfficeAssignment entity. 如果 OfficeAssignment 为 NULL,则显示空白表格单元格。If OfficeAssignment is null, an empty table cell is displayed.

单击“选择”链接,选择讲师 。Click on the Select link for an instructor. 显示行样式更改和分配给该讲师的课程。The row style changes and courses assigned to that instructor are displayed.

选择一门课程,查看已注册的学生及其成绩列表。Select a course to see the list of enrolled students and their grades.

已选择“讲师索引”页中的讲师和课程

使用 Single 方法Using Single

Single 方法可在 Where 条件中进行传递,无需分别调用 Where 方法:The Single method can pass in the Where condition instead of calling the Where method separately:

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

Single 与 Where 条件的配合使用与个人偏好相关。Use of Single with a Where condition is a matter of personal preference. 相较于使用 Where 方法,它没有提供任何优势。It provides no benefits over using the Where method.

显式加载Explicit loading

当前代码为 EnrollmentsStudents 指定预先加载:The current code specifies eager loading for Enrollments and 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();

假设用户几乎不希望课程中显示注册情况。Suppose users rarely want to see enrollments in a course. 在此情况下,可仅在请求时加载注册数据进行优化。In that case, an optimization would be to only load the enrollment data if it's requested. 在本部分中,会更新 OnGetAsync 以使用 EnrollmentsStudents 的显式加载。In this section, the OnGetAsync is updated to use explicit loading of Enrollments and Students.

使用以下代码更新 Pages/Instructors/Index.cshtml.cs 。Update Pages/Instructors/Index.cshtml.cs with the following code.

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

上述代码取消针对注册和学生数据的 ThenInclude 方法调用 。The preceding code drops the ThenInclude method calls for enrollment and student data. 如果已选中课程,则显式加载的代码会检索:If a course is selected, the explicit loading code retrieves:

  • 所选课程的 Enrollment 实体。The Enrollment entities for the selected course.
  • 每个 EnrollmentStudent 实体。The Student entities for each Enrollment.

注意,上述代码注释掉了 .AsNoTracking()Notice that the preceding code comments out .AsNoTracking(). 对于跟踪的实体,仅可显式加载导航属性。Navigation properties can only be explicitly loaded for tracked entities.

测试应用。Test the app. 对用户而言,该应用的行为与上一版本相同。From a user's perspective, the app behaves identically to the previous version.

后续步骤Next steps

下一个教程将介绍如何更新相关数据。The next tutorial shows how to update related data.

在本教程中,将读取和显示相关数据。In this tutorial, related data is read and displayed. 相关数据为 EF Core 加载到导航属性中的数据。Related data is data that EF Core loads into navigation properties.

如果遇到无法解决的问题,请下载或查看已完成的应用If you run into problems you can't solve, download or view the completed app. 下载说明Download instructions.

下图显示了本教程中已完成的页面:The following illustrations show the completed pages for this tutorial:

“课程索引”页

“讲师索引”页

EF Core 可采用多种方式将相关数据加载到实体的导航属性中:There are several ways that EF Core can load related data into the navigation properties of an entity:

  • 预先加载Eager loading. 预先加载是指对查询某类型的实体时一并加载相关实体。Eager loading is when a query for one type of entity also loads related entities. 读取实体时,会检索其相关数据。When the entity is read, its related data is retrieved. 此时通常会出现单一联接查询,检索所有必需数据。This typically results in a single join query that retrieves all of the data that's needed. EF Core 将针对预先加载的某些类型发出多个查询。EF Core will issue multiple queries for some types of eager loading. 与存在单一查询的 EF6 中的某些查询相比,发出多个查询可能更有效。Issuing multiple queries can be more efficient than was the case for some queries in EF6 where there was a single query. 预先加载通过 IncludeThenInclude 方法进行指定。Eager loading is specified with the Include and ThenInclude methods.

    预先加载示例

    当包含集合导航时,预先加载会发送多个查询:Eager loading sends multiple queries when a collection navigation is included:

    • 一个查询用于主查询One query for the main query
    • 一个查询用于加载树中每个集合“边缘”。One query for each collection "edge" in the load tree.
  • 使用 Load 的单独查询:可在单独的查询中检索数据,EF Core 会“修复”导航属性。Separate queries with Load: The data can be retrieved in separate queries, and EF Core "fixes up" the navigation properties. “修复”是指 EF Core 自动填充导航属性。"fixes up" means that EF Core automatically populates the navigation properties. 使用 Load 单独查询比预先加载更像是显式加载。Separate queries with Load is more like explicit loading than eager loading.

    单独查询示例

    注意:EF Core 会将导航属性自动“修复”为之前加载到上下文实例中的任何其他实体。Note: EF Core automatically fixes up navigation properties to any other entities that were previously loaded into the context instance. 即使导航属性的数据非显式包含在内 ,但如果先前加载了部分或所有相关实体,则仍可能填充该属性。Even if the data for a navigation property is not explicitly included, the property may still be populated if some or all of the related entities were previously loaded.

  • 显式加载Explicit loading. 首次读取实体时,不检索相关数据。When the entity is first read, related data isn't retrieved. 必须编写代码才能在需要时检索相关数据。Code must be written to retrieve the related data when it's needed. 使用单独查询进行显式加载时,会向数据库发送多个查询。Explicit loading with separate queries results in multiple queries sent to the DB. 该代码通过显式加载指定要加载的导航属性。With explicit loading, the code specifies the navigation properties to be loaded. 使用 Load 方法进行显式加载。Use the Load method to do explicit loading. 例如:For example:

    显式加载示例

  • 延迟加载Lazy loading. 延迟加载已添加到版本 2.1 中的 EF CoreLazy loading was added to EF Core in version 2.1. 首次读取实体时,不检索相关数据。When the entity is first read, related data isn't retrieved. 首次访问导航属性时,会自动检索该导航属性所需的数据。The first time a navigation property is accessed, the data required for that navigation property is automatically retrieved. 首次访问导航属性时,都会向数据库发送一个查询。A query is sent to the DB each time a navigation property is accessed for the first time.

  • Select 运算符仅加载所需的相关数据。The Select operator loads only the related data needed.

创建显示院系名称的“课程”页Create a Course page that displays department name

课程实体包括一个带 Department 实体的导航属性。The Course entity includes a navigation property that contains the Department entity. Department 实体包含要分配课程的院系。The Department entity contains the department that the course is assigned to.

要在课程列表中显示已分配院系的名称:To display the name of the assigned department in a list of courses:

  • Department 实体中获取 Name 属性。Get the Name property from the Department entity.
  • Department 实体来自于 Course.Department 导航属性。The Department entity comes from the Course.Department navigation property.

Course.Department

为课程模型创建基架Scaffold the Course model

按照为“学生”模型搭建基架中的说明操作,并对模型类使用 CourseFollow the instructions in Scaffold the student model and use Course for the model class.

上述命令为 Course 模型创建基架。The preceding command scaffolds the Course model. 在 Visual Studio 中打开项目。Open the project in Visual Studio.

打开 Pages/Courses/Index.cshtml.cs 并检查 OnGetAsync 方法。Open Pages/Courses/Index.cshtml.cs and examine the OnGetAsync method. 基架引擎为 Department 导航属性指定了预先加载。The scaffolding engine specified eager loading for the Department navigation property. Include 方法指定预先加载。The Include method specifies eager loading.

运行应用并选择“课程”链接 。Run the app and select the Courses link. 院系列显示 DepartmentID(该项无用)。The department column displays the DepartmentID, which isn't useful.

使用以下代码更新 OnGetAsync 方法:Update the OnGetAsync method with the following code:

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

上述代码添加了 AsNoTrackingThe preceding code adds AsNoTracking. 由于未跟踪返回的实体,因此 AsNoTracking 提升了性能。AsNoTracking improves performance because the entities returned are not tracked. 未跟踪实体,因为未在当前上下文中更新这些实体。The entities are not tracked because they're not updated in the current context.

使用以下突出显示的标记更新 Pages/Courses/Index.cshtml :Update Pages/Courses/Index.cshtml with the following highlighted markup:

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

对基架代码进行了以下更改:The following changes have been made to the scaffolded code:

  • 将标题从“索引”更改为“课程”。Changed the heading from Index to Courses.

  • 添加了显示 CourseID 属性值的“数字”列 。Added a Number column that shows the CourseID property value. 默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。By default, primary keys aren't scaffolded because normally they're meaningless to end users. 但在此情况下主键是有意义的。However, in this case the primary key is meaningful.

  • 更改“院系”列,显示院系名称 。Changed the Department column to display the department name. 该代码显示已加载到 Department 导航属性中的 Department 实体的 Name 属性:The code displays the Name property of the Department entity that's loaded into the Department navigation property:

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

运行应用并选择“课程”选项卡,查看包含系名称的列表 。Run the app and select the Courses tab to see the list with department names.

“课程索引”页

OnGetAsync 方法使用 Include 方法加载相关数据:The OnGetAsync method loads related data with the Include method:

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

Select 运算符仅加载所需的相关数据。The Select operator loads only the related data needed. 对于单个项(如 Department.Name),它使用 SQL INNER JOIN。For single items, like the Department.Name it uses a SQL INNER JOIN. 对于集合,它使用另一个数据库访问,但集合上的 Include 运算符也是如此。For collections, it uses another database access, but so does the Include operator on collections.

以下代码使用 Select 方法加载相关数据:The following code loads related data with the Select method:

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

CourseViewModelThe CourseViewModel:

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

有关完整示例的信息,请参阅 IndexSelect.cshtmlIndexSelect.cshtml.csSee IndexSelect.cshtml and IndexSelect.cshtml.cs for a complete example.

创建显示“课程”和“注册”的“讲师”页Create an Instructors page that shows Courses and Enrollments

在本部分中,将创建“讲师”页。In this section, the Instructors page is created.

“讲师索引”页Instructors Index page

该页面通过以下方式读取和显示相关数据:This page reads and displays related data in the following ways:

  • 讲师列表显示 OfficeAssignment 实体(上图中的办公室)的相关数据。The list of instructors displays related data from the OfficeAssignment entity (Office in the preceding image). InstructorOfficeAssignment 实体之间存在一对零或一的关系。The Instructor and OfficeAssignment entities are in a one-to-zero-or-one relationship. 预先加载适用于 OfficeAssignment 实体。Eager loading is used for the OfficeAssignment entities. 需要显示相关数据时,预先加载通常更高效。Eager loading is typically more efficient when the related data needs to be displayed. 在此情况下,会显示讲师的办公室分配。In this case, office assignments for the instructors are displayed.
  • 当用户选择一名讲师(上图中的 Harui)时,显示相关的 Course 实体。When the user selects an instructor (Harui in the preceding image), related Course entities are displayed. InstructorCourse 实体之间存在多对多关系。The Instructor and Course entities are in a many-to-many relationship. Course 实体及其相关的 Department 实体使用预先加载。Eager loading is used for the Course entities and their related Department entities. 这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。In this case, separate queries might be more efficient because only courses for the selected instructor are needed. 此示例演示如何在位于导航实体内的实体中预先加载这些导航实体。This example shows how to use eager loading for navigation properties in entities that are in navigation properties.
  • 当用户选择一门课程(上图中的化学)时,显示 Enrollments 实体的相关数据。When the user selects a course (Chemistry in the preceding image), related data from the Enrollments entity is displayed. 上图中显示了学生姓名和成绩。In the preceding image, student name and grade are displayed. CourseEnrollment 实体之间存在一对多的关系。The Course and Enrollment entities are in a one-to-many relationship.

创建“讲师索引”视图的视图模型Create a view model for the Instructor Index view

“讲师”页显示来自三个不同表格的数据。The instructors page shows data from three different tables. 创建一个视图模型,该模型中包含表示三个表格的三个实体。A view model is created that includes the three entities representing the three tables.

在 SchoolViewModels 文件夹中,使用以下代码创建 InstructorIndexData.cs :In the SchoolViewModels folder, create InstructorIndexData.cs with the following code:

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

为讲师模型创建基架Scaffold the Instructor model

按照为“学生”模型搭建基架中的说明操作,并对模型类使用 InstructorFollow the instructions in Scaffold the student model and use Instructor for the model class.

上述命令为 Instructor 模型创建基架。The preceding command scaffolds the Instructor model. 运行应用并导航到“讲师”页。Run the app and navigate to the instructors page.

将 Pages/Instructors/Index.cshtml.cs 替换为以下代码:Replace Pages/Instructors/Index.cshtml.cs with the following code:

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

OnGetAsync 方法接受所选讲师 ID 的可选路由数据。The OnGetAsync method accepts optional route data for the ID of the selected instructor.

检查 Pages/Instructors/Index.cshtml.cs 文件中的查询 :Examine the query in the Pages/Instructors/Index.cshtml.cs file:

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

查询包括两项内容:The query has two includes:

  • OfficeAssignment:在讲师视图中显示。OfficeAssignment: Displayed in the instructors view.
  • CourseAssignments:课程的教学内容。CourseAssignments: Which brings in the courses taught.

更新“讲师索引”页Update the instructors Index page

使用以下标记更新 Pages/Instructors/Index.cshtml :Update Pages/Instructors/Index.cshtml with the following markup:

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

上述标记进行以下更改:The preceding markup makes the following changes:

  • page 指令从 @page 更新为 @page "{id:int?}"Updates the page directive from @page to @page "{id:int?}". "{id:int?}" 是一个路由模板。"{id:int?}" is a route template. 路由模板将 URL 中的整数查询字符串更改为路由数据。The route template changes integer query strings in the URL to route data. 例如,单击仅具有 @page 指令的讲师的“选择”链接将生成如下 URL :For example, clicking on the Select link for an instructor with only the @page directive produces a URL like the following:

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

    当页面指令是 @page "{id:int?}" 时,之前的 URL 为:When the page directive is @page "{id:int?}", the previous URL is:

    http://localhost:1234/Instructors/2

  • 页标题为“讲师” 。Page title is Instructors.

  • 添加了仅在 item.OfficeAssignment 不为 null 时才显示 item.OfficeAssignment.Location 的“办公室”列 。Added an Office column that displays item.OfficeAssignment.Location only if item.OfficeAssignment isn't null. 由于这是一对零或一的关系,因此可能没有相关的 OfficeAssignment 实体。Because this is a one-to-zero-or-one relationship, there might not be a related OfficeAssignment entity.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 添加了显示每位讲师所授课程的“课程”列 。Added a Courses column that displays courses taught by each instructor. 有关此 razor 语法的详细信息,请参阅显式行转换See Explicit line transition for more about this razor syntax.

  • 添加了向所选讲师的 tr 元素中动态添加 class="success" 的代码。Added code that dynamically adds class="success" to the tr element of the selected instructor. 此时会使用 Bootstrap 类为所选行设置背景色。This sets a background color for the selected row using a Bootstrap class.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • 添加了标记为“选择”的新的超链接 。Added a new hyperlink labeled Select. 该链接将所选讲师的 ID 发送给 Index 方法并设置背景色。This link sends the selected instructor's ID to the Index method and sets a background color.

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

运行应用并选择“讲师”选项卡 。该页显示来自相关 OfficeAssignment 实体的 Location(办公室)。Run the app and select the Instructors tab. The page displays the Location (office) from the related OfficeAssignment entity. 如果 OfficeAssignment` 为 NULL,则显示空白表格单元格。If OfficeAssignment` is null, an empty table cell is displayed.

单击“选择” 链接。Click on the Select link. 随即更改行样式。The row style changes.

添加由所选讲师教授的课程Add courses taught by selected instructor

将 Pages/Instructors/Index.cshtml.cs 中的 OnGetAsync 方法替换为以下代码:Update the OnGetAsync method in Pages/Instructors/Index.cshtml.cs with the following code:

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

添加 public int CourseID { get; set; }Add 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;
        }
    }

检查更新后的查询:Examine the updated query:

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

先前查询添加了 Department 实体。The preceding query adds the Department entities.

选择讲师时 (id != null),将执行以下代码。The following code executes when an instructor is selected (id != null). 从视图模型中的讲师列表检索所选讲师。The selected instructor is retrieved from the list of instructors in the view model. 向视图模型的 Courses 属性加载来自讲师 CourseAssignments 导航属性的 Course 实体。The view model's Courses property is loaded with the Course entities from that instructor's CourseAssignments navigation property.

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

Where 方法返回一个集合。The Where method returns a collection. 在前面的 Where 方法中,仅返回单个 Instructor 实体。In the preceding Where method, only a single Instructor entity is returned. Single 方法将集合转换为单个 Instructor 实体。The Single method converts the collection into a single Instructor entity. Instructor 实体提供对 CourseAssignments 属性的访问。The Instructor entity provides access to the CourseAssignments property. CourseAssignments 提供对相关 Course 实体的访问。CourseAssignments provides access to the related Course entities.

讲师-课程 m:M

当集合仅包含一个项时,集合使用 Single 方法。The Single method is used on a collection when the collection has only one item. 如果集合为空或包含多个项,Single 方法会引发异常。The Single method throws an exception if the collection is empty or if there's more than one item. 还可使用 SingleOrDefault,该方式在集合为空时返回默认值(本例中为 null)。An alternative is SingleOrDefault, which returns a default value (null in this case) if the collection is empty. 在空集合上使用 SingleOrDefaultUsing SingleOrDefault on an empty collection:

  • 引发异常(因为尝试在空引用上找到 Courses 属性)。Results in an exception (from trying to find a Courses property on a null reference).
  • 异常信息不太能清楚指出问题原因。The exception message would less clearly indicate the cause of the problem.

选中课程时,视图模型的 Enrollments 属性将填充以下代码:The following code populates the view model's Enrollments property when a course is selected:

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

在 Pages/Instructors/Index.cshtml Razor 页面末尾添加以下标记 :Add the following markup to the end of the 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>
}

上述标记显示选中某讲师时与该讲师相关的课程列表。The preceding markup displays a list of courses related to an instructor when an instructor is selected.

测试应用。Test the app. 单击讲师页面上的“选择” 链接。Click on a Select link on the instructors page.

显示学生数据Show student data

在本部分中,更新应用以显示所选课程的学生数据。In this section, the app is updated to show the student data for a selected course.

使用以下代码在 Pages/Instructors/Index.cshtml.cs 中更新 OnGetAsync 方法中的查询:Update the query in the OnGetAsync method in Pages/Instructors/Index.cshtml.cs with the following code:

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

更新 Pages/Instructors/Index.cshtml 。Update Pages/Instructors/Index.cshtml. 在文件末尾添加以下标记:Add the following markup to the end of the file:


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

上述标记显示已注册所选课程的学生列表。The preceding markup displays a list of the students who are enrolled in the selected course.

刷新页面并选择讲师。Refresh the page and select an instructor. 选择一门课程,查看已注册的学生及其成绩列表。Select a course to see the list of enrolled students and their grades.

已选择“讲师索引”页中的讲师和课程

使用 Single 方法Using Single

Single 方法可在 Where 条件中进行传递,无需分别调用 Where 方法:The Single method can pass in the Where condition instead of calling the Where method separately:

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

使用 Where 时,前面的 Single 方法不适用。The preceding Single approach provides no benefits over using Where. 一些开发人员更喜欢 Single 方法样式。Some developers prefer the Single approach style.

显式加载Explicit loading

当前代码为 EnrollmentsStudents 指定预先加载:The current code specifies eager loading for Enrollments and 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();

假设用户几乎不希望课程中显示注册情况。Suppose users rarely want to see enrollments in a course. 在此情况下,可仅在请求时加载注册数据进行优化。In that case, an optimization would be to only load the enrollment data if it's requested. 在本部分中,会更新 OnGetAsync 以使用 EnrollmentsStudents 的显式加载。In this section, the OnGetAsync is updated to use explicit loading of Enrollments and Students.

使用以下代码更新 OnGetAsyncUpdate the OnGetAsync with the following code:

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

上述代码取消针对注册和学生数据的 ThenInclude 方法调用 。The preceding code drops the ThenInclude method calls for enrollment and student data. 如果已选中课程,则突出显示的代码会检索:If a course is selected, the highlighted code retrieves:

  • 所选课程的 Enrollment 实体。The Enrollment entities for the selected course.
  • 每个 EnrollmentStudent 实体。The Student entities for each Enrollment.

请注意,上述代码为 .AsNoTracking() 加上注释。Notice the preceding code comments out .AsNoTracking(). 对于跟踪的实体,仅可显式加载导航属性。Navigation properties can only be explicitly loaded for tracked entities.

测试应用。Test the app. 对用户而言,该应用的行为与上一版本相同。From a users perspective, the app behaves identically to the previous version.

下一个教程将介绍如何更新相关数据。The next tutorial shows how to update related data.

其他资源Additional resources