第 6 部分,ASP.NET Core 中的 Razor Pages 和 EF Core - 读取相关数据

作者:Tom DykstraJon P SmithRick Anderson

Contoso University Web 应用演示了如何使用 Razor 和 Visual Studio 创建 EF Core Pages Web 应用。 若要了解系列教程,请参阅第一个教程

如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。

本教程介绍如何读取和显示相关数据。 相关数据是 EF Core 加载到导航属性中的数据。

下图显示了本教程中已完成的页面:

“课程索引”页

“讲师索引”页

急切加载、显式加载和延迟加载

EF Core 可以通过多种方式将相关数据加载到实体的导航属性中:

  • 急切加载。 预先加载是指在查询某一类实体时,同时也会加载相关实体。 读取实体时, EF Core 检索其相关数据。 此方法通常生成一个联接查询,用于检索所需的所有数据。 EF Core 会针对某些类型的预加载执行多个查询。 发布多个查询可能比发布大型的单个查询更为有效。 使用 IncludeThenInclude 方法指定预先加载。

    预先加载示例

    当包含集合导航时,预加载会发出多个查询:

    • 一个查询用于主查询
    • 一个查询用于加载树中每个集合“边缘”。
  • 使用 Load 分隔查询:您可以在单独的查询中检索数据,并由 EF Core 自动修正导航属性。 “修复”是指 EF Core 自动填充导航属性。 使用 Load 单独查询比预先加载更像是显式加载。

    单独查询示例

    注意:EF Core会自动修正与它之前加载到上下文实例中的任何其他实体之间的导航属性。 即使你没有显式包含某个导航属性的数据,如果之前已加载了部分或所有相关实体,该属性仍可能被填充。

  • 显式加载。 首次读取实体时, EF Core 不会检索相关数据。 必须编写代码才能在需要时检索相关数据。 使用单独查询进行显式加载时,会向数据库发送多个查询。 通过显式加载,可以指定要加载的导航属性。 使用 Load 方法进行显式加载。 例如:

    显式加载示例

  • 延迟加载。 首次读取实体时, EF Core 不会检索相关数据。 首次访问导航属性时, EF Core 会自动检索该导航属性所需的数据。 EF Core 每次首次访问导航属性时,都会向数据库发送查询。 延迟加载会损害性能,例如当开发人员使用 N+1 查询时。 N+1 查询会加载父项并遍历其子项。

创建课程页面

Course 实体包括一个带相关 Department 实体的导航属性。

Course.Department

若要显示课程的已分配院系的名称,请执行以下操作:

  • 将相关的 Department 实体加载到 Course.Department 导航属性。
  • 获取 Department 实体的 Name 属性中的名称。

搭建“课程”页的基架

  • 遵循搭建“学生”页的基架中的说明,但以下情况除外:

    • 创建“Pages/Courses”文件夹。
    • Course 用于模型类。
    • 使用现有的上下文类,而不是新建上下文类。
  • 打开 Pages/Courses/Index.cshtml.cs 并检查 OnGetAsync 方法。 脚手架引擎指定对 Department 导航属性进行预先加载。 Include 方法指定预先加载。

  • 运行应用并选择“课程”链接。 部门列显示 DepartmentID,这并无用处。

显示院系名称

使用以下代码更新 Pages/Courses/Index.cshtml.cs:

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,然后添加 AsNoTracking

当查询结果用于只读场景时,无跟踪查询非常有用。 它们执行起来通常更快,因为无需设置更改跟踪信息。 如果不需要更新从数据库检索到的实体,则非跟踪查询的性能可能优于跟踪查询。

在某些情况下,跟踪查询比非跟踪查询更高效。 有关详细信息,请参阅跟踪查询与非跟踪查询。 在前面的代码中,由于实体未在当前上下文中更新,因此调用 AsNoTracking

使用以下代码更新 Pages/Courses/Index.cshtml

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

对基架代码进行了以下更改:

  • Course 属性名称更改为了 Courses

  • 新增了一个 Number 列,用于显示 属性值。 默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。 但是,在这种情况下,这个主键是有意义的。

  • 将部门列更改为显示部门名称。 该代码显示加载到 Department 导航属性中的 Department 实体的 Name 属性:

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

运行应用并选择“课程”选项卡,查看包含系名称的列表。

“课程索引”页

OnGetAsync 方法使用 Include 方法加载相关数据。 Select 方法是只加载所需相关数据的替代方法。 对于单个项(如 Department.Name),它使用 SQL INNER JOIN。 对于集合,它会使用另一种数据库访问方式,而集合上的 Include 运算符也是如此。

以下代码使用 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();
}

上述代码不会返回任何实体类型,因此不进行任何跟踪。 有关 EF 跟踪的详细信息,请参阅 跟踪查询与非跟踪查询

CourseViewModel

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

有关完整的 Razor 页面,请参阅 IndexSelectModel

创建“讲师”页

本节为讲师页面搭建基础结构,并在讲师索引页中添加相关课程和选课记录。

讲师“索引”页

该页面通过以下方式读取和显示相关数据:

  • 讲师列表显示来自 OfficeAssignment 实体(上图中所示的 Office)的相关数据。 InstructorOfficeAssignment 实体之间存在一对零或一的关系。 预先加载适用于 OfficeAssignment 实体。 需要显示相关数据时,预先加载通常更高效。 在这种情况下,会显示讲师的办公室分配情况。
  • 用户选择一名讲师时,显示相关 Course 实体。 InstructorCourse 实体之间存在多对多关系。 对 Course 实体及其相关的 Department 实体使用急切加载。 这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。 此示例演示如何对导航属性中的实体的导航属性使用预先加载。
  • 用户选择一门课程时,会显示 Enrollments 实体的相关数据。 上图中显示了学生姓名和成绩。 CourseEnrollment 实体之间存在一对多的关系。

创建视图模型

“讲师”页显示来自三个不同表格的数据。 需要一个视图模型,该模型中包含表示三个表格的三个属性。

使用以下代码创建 Models/SchoolViewModels/InstructorIndexData.cs

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

搭建“讲师”页的基架

  • 按照搭建学生页面框架中的说明操作,但以下例外情况除外:

    • 创建“Pages/Instructors”文件夹。
    • Instructor 用于模型类。
    • 使用现有的上下文类,而不是新建上下文类。

运行应用并导航到“讲师”页。

使用以下代码更新 Pages/Instructors/Index.cshtml.cs

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

OnGetAsync 方法接受用于所选讲师 ID 的可选路由数据。

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

代码指定对以下导航属性进行预先加载:

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

选择讲师时(即 id != null),将执行以下代码。

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

从视图模型中的讲师列表检索所选讲师。 视图模型的 Courses 属性会加载来自所选讲师的 Courses 导航属性中的 Course 实体。

Where 方法返回一个集合。 在这种情况下,筛选器会选择单个实体,因此 Single 调用该方法将集合转换为单个 Instructor 实体。 Instructor 实体提供对 Course 导航属性的访问。

当集合中仅有一个元素时,可对该集合使用 Single 方法。 如果集合为空或包含多个项,Single 方法会引发异常。 还可使用 SingleOrDefault,该方式在集合为空时返回默认值。 对于此查询,默认返回 null

以下代码会在选中课程时为视图模型的 Enrollments 属性赋值:

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

更新“讲师索引”页

使用以下代码更新 Pages/Instructors/Index.cshtml

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

上面的代码执行以下更改:

  • page 指令更新为 @page "{id:int?}""{id:int?}" 是一个路由模板。 路由模板将 URL 中的整数查询字符串更改为路由数据。 例如,单击仅带有 指令的讲师的 选择 链接,会生成如下所示的 URL:

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

    如果页面指令为 @page "{id:int?}",则 URL 为:https://localhost:5001/Instructors/2

  • 添加一个“Office”列,该列仅在 不为 null 时显示 。 由于这是一对零或一的关系,因此可能没有相关的 OfficeAssignment 实体。

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 添加显示每位讲师所授课程的“课程”列。 有关此 razor 语法的详细信息,请参阅显式行转换

  • 添加代码,将 class="table-success" 动态添加到所选讲师和课程的 tr 元素中。 此时会使用 Bootstrap 类为所选行设置背景色。

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • 添加标记为“选择”的新的超链接。 该链接将所选讲师的 ID 发送给 Index 方法并设置背景色。

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • 添加所选讲师的课程表。

  • 添加所选课程的学生注册表。

运行应用并选择讲师选项卡。该页面显示来自相关实体的(办公室)。 如果 OfficeAssignment 为 NULL,则显示空白表格单元格。

单击“选择”链接,选择讲师。 该行的样式会发生变化,并显示分配给该讲师的课程。

选择一门课程,查看已注册的学生及其成绩列表。

已在“讲师索引”页面选择讲师和课程

后续步骤

下一个教程将介绍如何更新相关数据。

本教程介绍如何读取和显示相关数据。 相关数据是 EF Core 加载到导航属性中的数据。

下图显示了本教程中已完成的页面:

“课程索引”页

“讲师索引”页

急切加载、显式加载和延迟加载

EF Core 可以通过多种方式将相关数据加载到实体的导航属性中:

  • 急切加载。 预先加载是指在查询某一类实体时,同时也会加载相关实体。 读取实体时,会检索其相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 EF Core 对于某些类型的预加载,将执行多个查询。 发布多个查询可能比发布大型的单个查询更为有效。 预先加载可使用 IncludeThenInclude 方法指定。

    预先加载示例

    当包含集合导航时,预加载会发出多个查询:

    • 一个查询用于主查询
    • 一个查询用于加载树中每个集合“边缘”。
  • 使用 Load 分隔查询:您可以在单独的查询中检索数据,并由 EF Core 自动修正导航属性。 “修复”是指 EF Core 自动填充导航属性。 使用 Load 单独查询比预先加载更像是显式加载。

    单独查询示例

    注意:EF Core会自动修正与它之前加载到上下文实例中的任何其他实体之间的导航属性。 即使你没有显式包含某个导航属性的数据,如果之前已加载了部分或所有相关实体,该属性仍可能被填充。

  • 显式加载。 首次读取实体时, EF Core 不会检索相关数据。 必须编写代码才能在需要时检索相关数据。 使用单独查询进行显式加载时,会向数据库发送多个查询。 通过显式加载,可以指定要加载的导航属性。 使用 Load 方法进行显式加载。 例如:

    显式加载示例

  • 延迟加载。 首次读取实体时,不检索相关数据。 首次访问导航属性时,会自动检索该导航属性所需的数据。 首次访问导航属性时,都会向数据库发送一个查询。 延迟加载可能会损害性能,例如,在开发人员使用 N+1 模式时,先加载父对象,再遍历其子对象。

创建课程页面

Course 实体包括一个带相关 Department 实体的导航属性。

Course.Department

若要显示课程的已分配院系的名称,请执行以下操作:

  • 将相关的 Department 实体加载到 Course.Department 导航属性。
  • 获取 Department 实体的 Name 属性中的名称。

搭建“课程”页的基架

  • 按照Scaffold 学生页面中的说明操作,但以下情况除外:

    • 创建“Pages/Courses”文件夹。
    • Course 用于模型类。
    • 使用现有的上下文类,而不是新建上下文类。
  • 打开 Pages/Courses/Index.cshtml.cs 并检查 OnGetAsync 方法。 脚手架引擎为 Department 导航属性指定了预先加载。 Include 方法指定预先加载。

  • 运行应用并选择“课程”链接。 部门列显示 DepartmentID,这并无用处。

显示院系名称

使用以下代码更新 Pages/Courses/Index.cshtml.cs:

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,然后添加 AsNoTrackingAsNoTracking 可提高性能,因为返回的实体不会被跟踪。 无需跟踪这些实体,因为它们不会在当前上下文中更新。

使用以下代码更新 Pages/Courses/Index.cshtml

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

对基架代码进行了以下更改:

  • Course 属性名称更改为了 Courses

  • 添加了一个 Number 列,用于显示 属性值。 默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。 但在这种情况下,主键是有意义的。

  • 已将 部门 列更改为显示部门名称。 该代码显示已加载到 Department 导航属性中的 Department 实体的 Name 属性:

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

运行应用并选择“课程”选项卡,查看包含系名称的列表。

“课程索引”页

OnGetAsync 方法使用 Include 方法加载相关数据。 Select 方法是只加载所需相关数据的替代方法。 对于单个项(如 Department.Name),它使用 SQL INNER JOIN。 对于集合,它使用另一种数据库访问方式,而用于集合的 Include 运算符也是如此。

以下代码使用 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();
}

上述代码不会返回任何实体类型,因此不进行任何跟踪。 有关 EF 跟踪的详细信息,请参阅 跟踪查询与非跟踪查询

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

创建“讲师”页

本节为讲师页面生成脚手架,并在讲师索引页中添加相关课程和选课记录。

讲师“索引”页

该页面通过以下方式读取和显示相关数据:

  • 讲师列表显示来自 OfficeAssignment 实体(上图中的办公室)的相关数据。 InstructorOfficeAssignment 实体之间存在 1 对 0 或 1 的关系。 预先加载适用于 OfficeAssignment 实体。 需要显示相关数据时,预先加载通常更高效。 在这种情况下,会显示讲师的办公室分配情况。
  • 用户选择一名讲师时,显示相关 Course 实体。 InstructorCourse 实体之间存在多对多关系。 对 Course 实体及其相关的 Department 实体使用急切加载。 这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。 此示例演示如何对导航属性中的实体的导航属性使用预先加载。
  • 用户选择一门课程时,会显示 Enrollments 实体的相关数据。 上图中显示了学生姓名和成绩。 CourseEnrollment 实体之间存在一对多的关系。

创建视图模型

“讲师”页显示来自三个不同表格的数据。 需要一个视图模型,该模型中包含表示三个表格的三个属性。

使用以下代码创建 SchoolViewModels/InstructorIndexData.cs

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

搭建“讲师”页的基架

  • 按照搭建学生页面框架中的说明操作,但以下例外情况除外:

    • 创建“Pages/Instructors”文件夹。
    • Instructor 用于模型类。
    • 使用现有的上下文类,而不是新建上下文类。

若要在更新之前查看已搭建基架的页面的外观,则运行应用并导航到“讲师”页。

使用以下代码更新 Pages/Instructors/Index.cshtml.cs

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 的可选路由数据。

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

代码指定对以下导航属性进行预先加载:

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

注意 IncludeThenIncludeCourseAssignmentsCourse 方法的重复使用。 为了为 Course 实体的两个导航属性指定预先加载,这种重复是必要的。

选择讲师时 (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);
}

从视图模型中的讲师列表检索所选讲师。 视图模型的 Courses 属性会加载该讲师的 CourseAssignments 导航属性中的 Course 实体。

Where 方法返回一个集合。 但在这种情况下,筛选器将选择单个实体,因此会调用 Single 方法将集合转换为单个 Instructor 实体。 Instructor 实体提供对 CourseAssignments 属性的访问。 CourseAssignments 提供对相关 Course 实体的访问。

讲师与课程 m:M

当集合中仅有一个元素时,可对该集合使用 Single 方法。 如果集合为空或包含多个项,Single 方法会引发异常。 还可使用 SingleOrDefault,该方式在集合为空时返回默认值(本例中为 null)。

以下代码会在选中课程时为视图模型的 Enrollments 属性赋值:

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

更新“讲师索引”页

使用以下代码更新 Pages/Instructors/Index.cshtml

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

上面的代码执行以下更改:

  • page 指令从 @page 更新为 @page "{id:int?}""{id:int?}" 是一个路由模板。 路由模板将 URL 中的整数查询字符串更改为路由数据。 例如,单击仅带有 指令的讲师的 选择 链接,会生成如下所示的 URL:

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

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

    https://localhost:5001/Instructors/2

  • 添加一个办公室列,仅在 不为 null 时显示 。 由于这是一个 1 对 0 或 1 的关系,因此可能不存在相关的 OfficeAssignment 实体。

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 添加显示每位讲师所授课程的“课程”列。 有关此 razor 语法的详细信息,请参阅显式行转换

  • 添加代码,将 class="table-success" 动态添加到所选讲师和课程的 tr 元素中。 此时会使用 Bootstrap 类为所选行设置背景色。

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • 添加标记为“选择”的新的超链接。 该链接将所选讲师的 ID 发送给 Index 方法并设置背景色。

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • 添加所选讲师的课程表。

  • 添加所选课程的学生注册表。

运行应用并选择讲师选项卡。该页面显示来自相关实体的(办公室)。 如果 OfficeAssignment 为 NULL,则显示空白表格单元格。

单击“选择”链接,选择讲师。 该行的样式会发生变化,并显示分配给该讲师的课程。

选择一门课程,查看已注册的学生及其成绩列表。

已在“讲师索引”页面选择讲师和课程

使用 Single 方法

Single 方法可在 Where 条件中进行传递,无需分别调用 Where 方法:

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 条件的配合使用与个人偏好相关。 相较于使用 Where 方法,它没有提供任何优势。

显式加载

当前代码为 EnrollmentsStudents 指定了预加载:

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

假设用户几乎不希望课程中显示注册情况。 在这种情况下,一个优化方案是仅在请求时才加载注册数据。 在本部分中,会更新 OnGetAsync 以使用 EnrollmentsStudents 的显式加载。

使用以下代码更新 Pages/Instructors/Index.cshtml.cs

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 方法调用。 如果已选择某门课程,则显式加载代码会检索以下内容:

  • 所选课程的 Enrollment 实体。
  • 每个 EnrollmentStudent 实体。

注意,上述代码注释掉了 .AsNoTracking()。 对于跟踪的实体,仅可显式加载导航属性。

测试应用。 对用户而言,该应用的行为与上一版本相同。

后续步骤

下一个教程将介绍如何更新相关数据。

在本教程中,将读取和显示相关数据。 相关数据是指由 EF Core 加载到导航属性中的数据。

如果遇到无法解决的问题,请下载或查看已完成的应用下载说明

下图显示了本教程中已完成的页面:

“课程索引”页

“讲师索引”页

EF Core 可以通过多种方式将相关数据加载到实体的导航属性中:

  • 预加载。 预先加载是指在查询某一类型的实体时,一并加载相关实体。 读取实体时,会检索其相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 EF Core 对于某些类型的预加载,将执行多个查询。 与存在单一查询的 EF6 中的某些查询相比,发出多个查询可能更有效。 预加载使用 IncludeThenInclude 方法指定。

    预先加载示例

    当包含集合导航时,预加载会发出多个查询:

    • 一个查询用于主查询
    • 一个查询用于加载树中每个集合“边缘”。
  • 使用 Load 的单独查询:可在单独的查询中检索数据,EF Core 会“修复”导航属性。 “修复”是指 EF Core 自动填充导航属性。 使用 Load 单独查询比预先加载更像是显式加载。

    单独查询示例

    注意:EF Core 会自动将导航属性设置为与之前已加载到该上下文实例中的任何其他实体相关联。 即使导航属性的数据非显式包含在内,但如果先前加载了部分或所有相关实体,则仍可能填充该属性。

  • 显式加载。 首次读取实体时,不检索相关数据。 必须编写代码才能在需要时检索相关数据。 使用单独查询进行显式加载时,会向数据库发送多个查询。 该代码通过显式加载指定要加载的导航属性。 使用 Load 方法进行显式加载。 例如:

    显式加载示例

  • 延迟加载 EF Core 在 2.1 版本中新增了延迟加载功能。 首次读取实体时,不检索相关数据。 首次访问导航属性时,会自动检索该导航属性所需的数据。 首次访问导航属性时,都会向数据库发送一个查询。

  • Select 运算符仅加载所需的相关数据。

创建显示院系名称的“课程”页

课程实体包括一个带 Department 实体的导航属性。 Department 实体包含要分配课程的院系。

要在课程列表中显示已分配院系的名称:

  • Name 实体中获取 Department 属性。
  • Department 实体来自于 Course.Department 导航属性。

Course.Department

为课程模型创建基架

按照为“学生”模型搭建基架中的说明操作,并对模型类使用 Course

上述命令为 Course 模型创建基架。 在 Visual Studio 中打开项目。

打开 Pages/Courses/Index.cshtml.cs 并检查 OnGetAsync 方法。 脚手架引擎指定对 Department 导航属性进行预先载入。 Include 方法指定预先加载。

运行应用并选择“课程”链接。 部门列显示的是 DepartmentID,这并无用处。

使用以下代码更新 OnGetAsync 方法:

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

上述代码添加了 AsNoTrackingAsNoTracking 可提高性能,因为返回的实体不会被跟踪。 这些实体未被跟踪,因为它们未在当前上下文中更新。

使用以下高亮标记更新 Pages/Courses/Index.cshtml

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

对基架代码进行了以下更改:

  • 将标题从“索引”更改为“课程”。

  • 添加了一个编号列,用于显示属性值。 默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。 但在这种情况下,主键是有意义的。

  • 将部门列更改为显示部门名称。 该代码显示加载到 Department 导航属性中的 Department 实体的 Name 属性:

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

运行应用并选择“课程”选项卡,查看包含系名称的列表。

“课程索引”页

OnGetAsync 方法使用 Include 方法加载相关数据:

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

Select 运算符仅加载所需的相关数据。 对于单个项(如 Department.Name),它使用 SQL INNER JOIN。 对于集合,它会使用另一种数据库访问方式,而集合上的 Include 运算符也是如此。

以下代码使用 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; }
}

有关完整示例的信息,请参阅 IndexSelect.cshtmlIndexSelect.cshtml.cs

创建显示“课程”和“注册”的“讲师”页

在本部分中,将创建“讲师”页。

讲师“索引”页

该页面通过以下方式读取和显示相关数据:

  • 讲师列表显示来自 OfficeAssignment 实体(上图中所示的 Office)的相关数据。 InstructorOfficeAssignment 实体之间存在一对零或一的关系。 预先加载适用于 OfficeAssignment 实体。 需要显示相关数据时,预先加载通常更高效。 在此情况下,会显示讲师的办公室分配。
  • 当用户选择一名讲师(上图中的 Harui)时,显示相关的 Course 实体。 InstructorCourse 实体之间存在多对多关系。 对 Course 实体及其相关的 Department 实体采用急切加载。 这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。 此示例演示如何对导航属性中的实体所具有的导航属性使用预先加载。
  • 当用户选择一门课程(上图中的化学)时,显示 Enrollments 实体的相关数据。 上图中显示了学生姓名和成绩。 CourseEnrollment 实体之间是“一对多”关系。

创建“讲师索引”视图的视图模型

“讲师”页显示来自三个不同表格的数据。 创建一个视图模型,该模型中包含表示三个表格的三个实体。

SchoolViewModels 文件夹中,创建 ,代码如下:

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

为讲师模型创建基架

按照为“学生”模型搭建基架中的说明操作,并对模型类使用 Instructor

上述命令为 Instructor 模型创建基架。 运行应用并导航到“讲师”页。

Pages/Instructors/Index.cshtml.cs 替换为以下代码:

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 的可选路由数据。

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

查询包括两项内容:

  • OfficeAssignment:在讲师视图中显示。
  • CourseAssignments:课程的教学内容。

更新“讲师索引”页

使用以下标记更新 Pages/Instructors/Index.cshtml

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

前面的标记代码会带来以下更改:

  • page 指令从 @page 更新为 @page "{id:int?}""{id:int?}" 是一个路由模板。 路由模板将 URL 中的整数查询字符串更改为路由数据。 例如,单击仅带有 指令的讲师的 选择 链接,会生成如下所示的 URL:

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

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

    http://localhost:1234/Instructors/2

  • 页面标题为 讲师。

  • 新增了一个办公地点列,仅在 不为 null 时显示 。 由于这是一个 1 对 0 或 1 的关系,因此可能不存在相关的 OfficeAssignment 实体。

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 添加了显示每位讲师所授课程的“课程”列。 有关此 razor 语法的详细信息,请参阅显式行转换

  • 添加了向所选讲师的 tr 元素中动态添加 class="table-success" 的代码。 此时会使用 Bootstrap 类为所选行设置背景色。

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • 添加了标记为“选择”的新的超链接。 该链接将所选讲师的 ID 发送给 Index 方法并设置背景色。

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

运行应用并选择讲师选项卡。该页面显示来自相关实体的(办公室)。 如果 OfficeAssignment` 为 NULL,则显示空白表格单元格。

单击“选择”链接。 行样式会发生变化。

添加由所选讲师教授的课程

使用以下代码更新 OnGetAsync 中的 Pages/Instructors/Index.cshtml.cs 方法:

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

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

检查更新后的查询:

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 实体。

选择讲师时 (id != null),将执行以下代码。 从视图模型中的讲师列表检索所选讲师。 视图模型的 Courses 属性会加载该讲师的 CourseAssignments 导航属性中的 Course 实体。

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 方法返回一个集合。 在前面的 Where 方法中,仅返回单个 Instructor 实体。 Single 方法将集合转换为单个 Instructor 实体。 Instructor 实体提供对 CourseAssignments 属性的访问。 CourseAssignments 提供对相关 Course 实体的访问。

讲师与课程 m:M

当集合中仅有一个元素时,可对该集合使用 Single 方法。 如果集合为空或包含多个项,Single 方法会引发异常。 还可使用 SingleOrDefault,该方式在集合为空时返回默认值(本例中为 null)。 在空集合上使用 SingleOrDefault

  • 引发异常(因为尝试在空引用上找到 Courses 属性)。
  • 异常信息不太能清楚指出问题原因。

以下代码会在选中课程时为视图模型的 Enrollments 属性赋值:

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

Pages/Instructors/Index.cshtmlRazor 页面末尾添加以下标记:

                    <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 = "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>
}

上述标记显示选中某讲师时与该讲师相关的课程列表。

测试应用。 单击讲师页面上的“选择”链接。

显示学生数据

在本部分中,更新应用以显示所选课程的学生数据。

使用以下代码更新 OnGetAsyncPages/Instructors/Index.cshtml.cs 方法中的查询:

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。 在文件末尾添加以下标记:


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

上述标记显示已注册所选课程的学生列表。

刷新页面并选择讲师。 选择一门课程,查看已注册的学生及其成绩列表。

已在“讲师索引”页面选择讲师和课程

使用 Single 方法

Single 方法可在 Where 条件中进行传递,无需分别调用 Where 方法:

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

前述的 Single 方法相比使用 Where 并没有任何优势。 一些开发人员更喜欢 Single 方法样式。

显式加载

当前代码为 EnrollmentsStudents 指定了预加载:

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

假设用户几乎不希望课程中显示注册情况。 在这种情况下,一个优化方案是仅在请求时才加载注册数据。 在本部分中,会更新 OnGetAsync 以使用 EnrollmentsStudents 的显式加载。

使用以下代码更新 OnGetAsync

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 方法调用。 如果已选中课程,则突出显示的代码会检索:

  • 所选课程的 Enrollment 实体。
  • 每个 EnrollmentStudent 实体。

请注意,前面的代码将 .AsNoTracking() 注释掉了。 对于跟踪的实体,仅可显式加载导航属性。

测试应用。 对用户而言,该应用的行为与上一版本相同。

下一个教程将介绍如何更新相关数据。

其他资源