- ASP.NET Core 中的 Razor 页面和 EF Core - 读取相关数据 - 第 6 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - Read Related Data - 6 of 8
- 预先加载、显式加载和延迟加载Eager, explicit, and lazy loading
- 创建“课程”页Create Course pages
- 创建“讲师”页Create Instructor pages
- 使用 Single 方法Using Single
- 显式加载Explicit loading
- 后续步骤Next steps
- 相关数据的预先加载、显式加载和延迟加载Eager, explicit, and lazy Loading of related data
- 创建显示院系名称的“课程”页Create a Course page that displays department name
- 创建显示“课程”和“注册”的“讲师”页Create an Instructors page that shows Courses and Enrollments
- 使用 Single 方法Using Single
- 显式加载Explicit loading
- 其他资源Additional resources
ASP.NET Core 中的 Razor 页面和 EF Core - 读取相关数据 - 第 6 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - Read Related Data - 6 of 8
本文内容
作者:Tom Dykstra、Jon P Smith 和 Rick Anderson
Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor 页面 Web 应用。若要了解系列教程,请参阅第一个教程。
如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。
本教程介绍如何读取和显示相关数据。相关数据为 EF Core 加载到导航属性中的数据。
下图显示了本教程中已完成的页面:
预先加载、显式加载和延迟加载Eager, explicit, and lazy loading
EF Core 可采用多种方式将相关数据加载到实体的导航属性中:
- 预先加载。预先加载是指对查询某类型的实体时一并加载相关实体。读取实体时,会检索其相关数据。此时通常会出现单一联接查询,检索所有必需数据。EF Core 将针对预先加载的某些类型发出多个查询。发布多个查询可能比发布大型的单个查询更为有效。预先加载通过
Include
和ThenInclude
方法进行指定。
当包含集合导航时,预先加载会发送多个查询:
- 一个查询用于主查询
- 一个查询用于加载树中每个集合“边缘”。
- 使用
Load
的单独查询:可在单独的查询中检索数据,EF Core 会“修复”导航属性。“修复”是指 EF Core 自动填充导航属性。使用Load
单独查询比预先加载更像是显式加载。
- 使用
注意:EF Core 会将导航属性自动“修复”为之前加载到上下文实例中的任何其他实体。即使导航属性的数据非显式包含在内 ,但如果先前加载了部分或所有相关实体,则仍可能填充该属性。
- 显式加载。首次读取实体时,不检索相关数据。必须编写代码才能在需要时检索相关数据。使用单独查询进行显式加载时,会向数据库发送多个查询。该代码通过显式加载指定要加载的导航属性。使用
Load
方法进行显式加载。例如:
- 延迟加载。延迟加载已添加到版本 2.1 中的 EF Core。首次读取实体时,不检索相关数据。首次访问导航属性时,会自动检索该导航属性所需的数据。首次访问导航属性时,都会向数据库发送一个查询。
创建“课程”页Create Course pages
Course
实体包括一个带相关 Department
实体的导航属性。
若要显示课程的已分配院系的名称,请执行以下操作:
- 将相关的
Department
实体加载到Course.Department
导航属性。 - 获取
Department
实体的Name
属性中的名称。
搭建“课程”页的基架Scaffold Course pages
遵循搭建“学生”页的基架中的说明,但以下情况除外:
- 创建“Pages/Courses”文件夹 。
- 将
Course
用于模型类。 - 使用现有的上下文类,而不是新建上下文类。
创建“Pages/Courses”文件夹 。
运行以下命令,搭建“课程”页的基架。
在 Windows 上:
dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --referenceScriptLibraries
在 Linux 或 macOS 上:
dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages/Courses --referenceScriptLibraries
打开 Pages/Courses/Index.cshtml.cs 并检查
OnGetAsync
方法。基架引擎为Department
导航属性指定了预先加载。Include
方法指定预先加载。运行应用并选择“课程”链接 。院系列显示
DepartmentID
(该项无用)。
显示院系名称Display the department name
使用以下代码更新 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
。添加了显示
CourseID
属性值的“数字”列 。默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。但在此情况下主键是有意义的。更改“院系”列,显示院系名称 。该代码显示已加载到
Department
导航属性中的Department
实体的Name
属性:
@Html.DisplayFor(modelItem => item.Department.Name)
运行应用并选择“课程”选项卡,查看包含系名称的列表 。
使用 Select 加载相关数据Loading related data with Select
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();
}
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.cshtml 和 IndexSelect.cshtml.cs。
创建“讲师”页Create Instructor pages
本节搭建“讲师”页的基架,并向讲师“索引”页添加相关“课程”和“注册”。
该页面通过以下方式读取和显示相关数据:
- 讲师列表显示
OfficeAssignment
实体(上图中的办公室)的相关数据。Instructor
和OfficeAssignment
实体之间存在一对零或一的关系。预先加载适用于OfficeAssignment
实体。需要显示相关数据时,预先加载通常更高效。在此情况下,会显示讲师的办公室分配。 - 用户选择一名讲师时,显示相关
Course
实体。Instructor
和Course
实体之间存在多对多关系。对Course
实体及其相关的Department
实体使用预先加载。这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。此示例演示如何在位于导航实体内的实体中预先加载这些导航实体。 - 用户选择一门课程时,会显示
Enrollments
实体的相关数据。上图中显示了学生姓名和成绩。Course
和Enrollment
实体之间存在一对多的关系。
创建视图模型Create a view model
“讲师”页显示来自三个不同表格的数据。需要一个视图模型,该模型中包含表示三个表格的三个属性。
使用以下代码创建 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; }
}
}
搭建“讲师”页的基架Scaffold Instructor pages
遵循搭建“学生”页的基架中的说明,但以下情况除外:
- 创建“Pages/Instructors”文件夹 。
- 将
Instructor
用于模型类。 - 使用现有的上下文类,而不是新建上下文类。
创建“Pages/Instructors”文件夹 。
运行以下命令,搭建“讲师”页的基架。
在 Windows 上:
dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --referenceScriptLibraries
在 Linux 或 macOS 上:
dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages/Instructors --referenceScriptLibraries
若要在更新之前查看已搭建基架的页面的外观,则运行应用并导航到“讲师”页。
使用以下代码更新 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
注意 CourseAssignments
和 Course
对 Include
和 ThenInclude
方法的重复使用。若要指定 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
实体的访问。
当集合仅包含一个项时,集合使用 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;
}
更新“讲师索引”页Update the instructors Index page
使用以下代码更新 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 中的整数查询字符串更改为路由数据。例如,单击仅具有@page
指令的讲师的“选择”链接将生成如下 URL :
https://localhost:5001/Instructors?id=2
如果页面指令为 @page "{id:int?}"
时,则 URL 为:
https://localhost:5001/Instructors/2
- 添加仅在
item.OfficeAssignment
不为 null 时才显示item.OfficeAssignment.Location
的“办公室”列 。由于这是一对零或一的关系,因此可能没有相关的 OfficeAssignment 实体。
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
添加显示每位讲师所授课程的“课程”列 。有关此 razor 语法的详细信息,请参阅显式行转换。
添加向所选讲师和课程的
tr
元素中动态添加class="success"
的代码。此时会使用 Bootstrap 类为所选行设置背景色。
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
- 添加标记为“选择”的新的超链接 。该链接将所选讲师的 ID 发送给
Index
方法并设置背景色。
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
添加所选讲师的课程表。
添加所选课程的学生注册表。
运行应用并选择“讲师”选项卡 。该页显示来自相关 OfficeAssignment
实体的 Location
(办公室)。如果 OfficeAssignment
为 NULL,则显示空白表格单元格。
单击“选择”链接,选择讲师 。显示行样式更改和分配给该讲师的课程。
选择一门课程,查看已注册的学生及其成绩列表。
使用 Single 方法Using 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
方法,它没有提供任何优势。
显式加载Explicit loading
当前代码为 Enrollments
和 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();
假设用户几乎不希望课程中显示注册情况。在此情况下,可仅在请求时加载注册数据进行优化。在本部分中,会更新 OnGetAsync
以使用 Enrollments
和 Students
的显式加载。
使用以下代码更新 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
实体。 - 每个
Enrollment
的Student
实体。
注意,上述代码注释掉了 .AsNoTracking()
。对于跟踪的实体,仅可显式加载导航属性。
测试应用。对用户而言,该应用的行为与上一版本相同。
后续步骤Next steps
下一个教程将介绍如何更新相关数据。
在本教程中,将读取和显示相关数据。相关数据为 EF Core 加载到导航属性中的数据。
如果遇到无法解决的问题,请下载或查看已完成的应用。下载说明。
下图显示了本教程中已完成的页面:
相关数据的预先加载、显式加载和延迟加载Eager, explicit, and lazy Loading of related data
EF Core 可采用多种方式将相关数据加载到实体的导航属性中:
- 预先加载。预先加载是指对查询某类型的实体时一并加载相关实体。读取实体时,会检索其相关数据。此时通常会出现单一联接查询,检索所有必需数据。EF Core 将针对预先加载的某些类型发出多个查询。与存在单一查询的 EF6 中的某些查询相比,发出多个查询可能更有效。预先加载通过
Include
和ThenInclude
方法进行指定。
当包含集合导航时,预先加载会发送多个查询:
- 一个查询用于主查询
- 一个查询用于加载树中每个集合“边缘”。
- 使用
Load
的单独查询:可在单独的查询中检索数据,EF Core 会“修复”导航属性。“修复”是指 EF Core 自动填充导航属性。使用Load
单独查询比预先加载更像是显式加载。
- 使用
注意:EF Core 会将导航属性自动“修复”为之前加载到上下文实例中的任何其他实体。即使导航属性的数据非显式包含在内 ,但如果先前加载了部分或所有相关实体,则仍可能填充该属性。
- 显式加载。首次读取实体时,不检索相关数据。必须编写代码才能在需要时检索相关数据。使用单独查询进行显式加载时,会向数据库发送多个查询。该代码通过显式加载指定要加载的导航属性。使用
Load
方法进行显式加载。例如:
延迟加载。延迟加载已添加到版本 2.1 中的 EF Core。首次读取实体时,不检索相关数据。首次访问导航属性时,会自动检索该导航属性所需的数据。首次访问导航属性时,都会向数据库发送一个查询。
Select
运算符仅加载所需的相关数据。
创建显示院系名称的“课程”页Create a Course page that displays department name
课程实体包括一个带 Department
实体的导航属性。Department
实体包含要分配课程的院系。
要在课程列表中显示已分配院系的名称:
- 从
Department
实体中获取Name
属性。 Department
实体来自于Course.Department
导航属性。
为课程模型创建基架Scaffold the Course model
按照为“学生”模型搭建基架中的说明操作,并对模型类使用 Course
。
运行下面的命令:
dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --referenceScriptLibraries
上述命令为 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();
}
上述代码添加了 AsNoTracking
。由于未跟踪返回的实体,因此 AsNoTracking
提升了性能。未跟踪实体,因为未在当前上下文中更新这些实体。
使用以下突出显示的标记更新 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>
对基架代码进行了以下更改:
将标题从“索引”更改为“课程”。
添加了显示
CourseID
属性值的“数字”列 。默认情况下,不针对主键进行架构,因为对最终用户而言,它们通常没有意义。但在此情况下主键是有意义的。更改“院系”列,显示院系名称 。该代码显示已加载到
Department
导航属性中的Department
实体的Name
属性:
@Html.DisplayFor(modelItem => item.Department.Name)
运行应用并选择“课程”选项卡,查看包含系名称的列表 。
使用 Select 加载相关数据Loading related data with Select
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.cshtml 和 IndexSelect.cshtml.cs。
创建显示“课程”和“注册”的“讲师”页Create an Instructors page that shows Courses and Enrollments
在本部分中,将创建“讲师”页。
该页面通过以下方式读取和显示相关数据:
- 讲师列表显示
OfficeAssignment
实体(上图中的办公室)的相关数据。Instructor
和OfficeAssignment
实体之间存在一对零或一的关系。预先加载适用于OfficeAssignment
实体。需要显示相关数据时,预先加载通常更高效。在此情况下,会显示讲师的办公室分配。 - 当用户选择一名讲师(上图中的 Harui)时,显示相关的
Course
实体。Instructor
和Course
实体之间存在多对多关系。对Course
实体及其相关的Department
实体使用预先加载。这种情况下,单独查询可能更有效,因为仅需显示所选讲师的课程。此示例演示如何在位于导航实体内的实体中预先加载这些导航实体。 - 当用户选择一门课程(上图中的化学)时,显示
Enrollments
实体的相关数据。上图中显示了学生姓名和成绩。Course
和Enrollment
实体之间存在一对多的关系。
创建“讲师索引”视图的视图模型Create a view model for the Instructor Index view
“讲师”页显示来自三个不同表格的数据。创建一个视图模型,该模型中包含表示三个表格的三个实体。
在 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; }
}
}
为讲师模型创建基架Scaffold the Instructor model
按照为“学生”模型搭建基架中的说明操作,并对模型类使用 Instructor
。
运行下面的命令:
dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --referenceScriptLibraries
上述命令为 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
:课程的教学内容。
更新“讲师索引”页Update the instructors Index page
使用以下标记更新 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 = "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 中的整数查询字符串更改为路由数据。例如,单击仅具有@page
指令的讲师的“选择”链接将生成如下 URL :
http://localhost:1234/Instructors?id=2
当页面指令是 @page "{id:int?}"
时,之前的 URL 为:
http://localhost:1234/Instructors/2
页标题为“讲师” 。
添加了仅在
item.OfficeAssignment
不为 null 时才显示item.OfficeAssignment.Location
的“办公室”列 。由于这是一对零或一的关系,因此可能没有相关的 OfficeAssignment 实体。
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
添加了显示每位讲师所授课程的“课程”列 。有关此 razor 语法的详细信息,请参阅显式行转换。
添加了向所选讲师的
tr
元素中动态添加class="success"
的代码。此时会使用 Bootstrap 类为所选行设置背景色。
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
- 添加了标记为“选择”的新的超链接 。该链接将所选讲师的 ID 发送给
Index
方法并设置背景色。
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
运行应用并选择“讲师”选项卡 。该页显示来自相关 OfficeAssignment
实体的 Location
(办公室)。如果 OfficeAssignment` 为 NULL,则显示空白表格单元格。
单击“选择” 链接。随即更改行样式。
添加由所选讲师教授的课程Add courses taught by selected instructor
将 Pages/Instructors/Index.cshtml.cs 中的 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)
.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
实体的访问。
当集合仅包含一个项时,集合使用 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.cshtml Razor 页面末尾添加以下标记 :
<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>
}
上述标记显示选中某讲师时与该讲师相关的课程列表。
测试应用。单击讲师页面上的“选择” 链接。
显示学生数据Show student data
在本部分中,更新应用以显示所选课程的学生数据。
使用以下代码在 Pages/Instructors/Index.cshtml.cs 中更新 OnGetAsync
方法中的查询:
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 方法Using 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;
}
}
使用 Where
时,前面的 Single
方法不适用。一些开发人员更喜欢 Single
方法样式。
显式加载Explicit loading
当前代码为 Enrollments
和 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();
假设用户几乎不希望课程中显示注册情况。在此情况下,可仅在请求时加载注册数据进行优化。在本部分中,会更新 OnGetAsync
以使用 Enrollments
和 Students
的显式加载。
使用以下代码更新 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
实体。 - 每个
Enrollment
的Student
实体。
请注意,上述代码为 .AsNoTracking()
加上注释。对于跟踪的实体,仅可显式加载导航属性。
测试应用。对用户而言,该应用的行为与上一版本相同。
下一个教程将介绍如何更新相关数据。