第 6 部分:ASP.NET Core 中具有 EF Core 的 Razor Pages - 讀取相關資料

作者:Tom DykstraJon P SmithRick Anderson

Contoso 大學 Web 應用程式將示範如何使用 EF Core 和 Visual Studio 來建立 Razor Pages Web 應用程式。 如需教學課程系列的資訊,請參閱第一個教學課程

如果您遇到無法解決的問題,請下載已完成的應用程式,並遵循本教學課程以將程式碼與您所建立的內容進行比較。

本教學課程說明如何讀取及顯示相關資料。 相關資料是 EF Core 載入到導覽屬性的資料。

下圖顯示本教學課程的已完成頁面:

Courses Index page

Instructors Index page

積極式、明確式和消極式載入

EF Core 有幾種方式可以將相關資料載入到實體的導覽屬性:

  • 積極式載入。 積極式載入是指某一類型實體的查詢同時也會載入相關實體。 讀取實體時,將會擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 EF Core 將針對某些類型的積極式載入發出多個查詢。 發出多個查詢可能比大型單一查詢更有效率。 積極式載入是使用 IncludeThenInclude 方法加以指定。

    Eager loading example

    當集合導覽包含在內時,積極式載入會傳送多個查詢:

    • 針對主查詢傳送一個查詢
    • 針對負載樹狀結構中的每個集合「邊緣」傳送一個查詢。
  • 使用 Load 的個別查詢:資料可以在個別查詢中擷取,而 EF Core 會「修正」導覽屬性。 「修正」表示 EF Core 會自動填入導覽屬性。 使用 Load 的個別查詢更像是明確式載入,而不是積極式載入。

    Separate queries example

    注意:EF Core 會將導覽屬性自動修正為先前已載入至內容執行個體的任何其他實體。 即使「未」明確包含導覽屬性的資料,如果先前已載入某些或所有相關實體,仍然可能會填入該屬性。

  • 明確式載入。 第一次讀取實體時,不會擷取相關資料。 必須撰寫程式碼,才能在需要時擷取相關資料。 使用個別查詢的明確式載入會導致多個查詢傳送至資料庫。 透過明確式載入,程式碼會指定要載入的導覽屬性。 請使用 Load 方法來執行明確式載入。 例如:

    Explicit loading example

  • 消極式載入。 第一次讀取實體時,不會擷取相關資料。 第一次存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 每當第一次存取導覽屬性時,查詢會傳送至資料庫。 延遲載入可能會損害效能,例如當開發人員使用 N+1 查詢時。 N+1 查詢會載入父系並列舉子系。

建立 Course 頁面

Course 實體包含導覽屬性,其中包含相關的 Department 實體。

Course.Department

若要顯示針對課程指派的部門名稱:

  • 將相關的 Department 實體載入 Course.Department 導覽屬性。
  • Department 實體的 Name 屬性取得名稱。

Scaffold Course 頁面

  • 遵循 Scaffold Student 頁面中的指示,下列部分除外:

    • 建立 Pages/Courses 資料夾。
    • 使用 Course 作為模型類別。
    • 使用現有內容類別,而非建立新的類別。
  • 開啟 Pages/Courses/Index.cshtml.cs 並檢查 OnGetAsync 方法。 Scaffolding 引擎已針對 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>

已對包含 Scaffold 的程式碼進行下列變更:

  • 已將 Course 屬性名稱變更為 Courses

  • 新增顯示 CourseID 屬性值的 [編號] 資料行。 主索引鍵預設不會進行 Scaffold,因為它們對終端使用者通常沒有任何意義。 不過,在此情況下,主索引鍵有意義。

  • 變更 [部門] 資料行來顯示部門名稱。 此程式碼會顯示已載入到 Department 導覽屬性之 Department 實體的 Name 屬性:

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

執行應用程式,並選取 [Courses] 索引標籤來查看含有部門名稱的清單。

Courses Index page

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

建立 Instructor 頁面

本節會 scaffold Instructor 頁面,並將相關的課程和註冊新增至 Instructors 索引頁面。

Instructors Index page

此頁面將以下列方式讀取和顯示相關資料:

  • 講師清單會顯示來自 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; }
    }
}

Scaffold Instructor 頁面

  • 遵循 Scaffold Students 頁面中的指示,下列部分除外:

    • 建立 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 方法會針對所選取講師的識別碼接受選擇性的路由資料。

檢查 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 屬性會使用 Course 實體從所選講師的 Courses 導覽屬性載入。

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

更新 Instructors [索引] 頁面

以下列程式碼來更新 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 中的整數查詢字串變更為路由資料。 例如,只有在 @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 語法,請參閱明確的行轉換

  • 新增程式碼,將 class="table-success" 動態新增至所選講師和課程的 tr 項目。 這會使用啟動程序類別設定所選取資料列的背景色彩。

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • 新增標示為選取的超連結。 此連結會將所選取的講師識別碼傳送至 Index 方法,並設定背景色彩。

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • 新增所選講師的課程資料表。

  • 新增所選課程的學生註冊資料表。

執行應用程式並選取 [講師] 索引標籤。此頁面會顯示來自相關 OfficeAssignment 實體的 Location (辦公室)。 如果 OfficeAssignment 為 Null,則會顯示空的資料表資料格。

按一下講師的 [選取] 連結。 資料列樣式會變更,並會顯示指派給該講師的課程。

選取課程,以查看已註冊學生和其年級的清單。

Instructors Index page instructor and course selected

下一步

下一個教學課程會示範如何更新相關資料。

本教學課程說明如何讀取及顯示相關資料。 相關資料是 EF Core 載入到導覽屬性的資料。

下圖顯示本教學課程的已完成頁面:

Courses Index page

Instructors Index page

積極式、明確式和消極式載入

EF Core 有幾種方式可以將相關資料載入到實體的導覽屬性:

  • 積極式載入。 積極式載入是指某一類型實體的查詢同時也會載入相關實體。 讀取實體時,將會擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 EF Core 將針對某些類型的積極式載入發出多個查詢。 發出多個查詢可能比大型單一查詢更有效率。 積極式載入是使用 IncludeThenInclude 方法加以指定。

    Eager loading example

    當集合導覽包含在內時,積極式載入會傳送多個查詢:

    • 針對主查詢傳送一個查詢
    • 針對負載樹狀結構中的每個集合「邊緣」傳送一個查詢。
  • 使用 Load 的個別查詢:資料可以在個別查詢中擷取,而 EF Core 會「修正」導覽屬性。 「修正」表示 EF Core 會自動填入導覽屬性。 使用 Load 的個別查詢更像是明確式載入,而不是積極式載入。

    Separate queries example

    注意:EF Core 會將導覽屬性自動修正為先前已載入至內容執行個體的任何其他實體。 即使「未」明確包含導覽屬性的資料,如果先前已載入某些或所有相關實體,仍然可能會填入該屬性。

  • 明確式載入。 第一次讀取實體時,不會擷取相關資料。 必須撰寫程式碼,才能在需要時擷取相關資料。 使用個別查詢的明確式載入會導致多個查詢傳送至資料庫。 透過明確式載入,程式碼會指定要載入的導覽屬性。 請使用 Load 方法來執行明確式載入。 例如:

    Explicit loading example

  • 消極式載入。 第一次讀取實體時,不會擷取相關資料。 第一次存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 每當第一次存取導覽屬性時,查詢會傳送至資料庫。 延遲載入可能會損害效能,例如當開發人員使用 N+1 模式來載入父系並列舉子系時。

建立 Course 頁面

Course 實體包含導覽屬性,其中包含相關的 Department 實體。

Course.Department

若要顯示針對課程指派的部門名稱:

  • 將相關的 Department 實體載入 Course.Department 導覽屬性。
  • Department 實體的 Name 屬性取得名稱。

Scaffold Course 頁面

  • 遵循 Scaffold Student 頁面中的指示,下列部分除外:

    • 建立 Pages/Courses 資料夾。
    • 使用 Course 作為模型類別。
    • 使用現有內容類別,而非建立新的類別。
  • 開啟 Pages/Courses/Index.cshtml.cs 並檢查 OnGetAsync 方法。 Scaffolding 引擎已針對 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>

已對包含 Scaffold 的程式碼進行下列變更:

  • 已將 Course 屬性名稱變更為 Courses

  • 新增顯示 CourseID 屬性值的 [編號] 資料行。 主索引鍵預設不會進行 Scaffold,因為它們對終端使用者通常沒有任何意義。 不過,在此情況下,主索引鍵有意義。

  • 變更 [部門] 資料行來顯示部門名稱。 此程式碼會顯示已載入到 Department 導覽屬性之 Department 實體的 Name 屬性:

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

執行應用程式,並選取 [Courses] 索引標籤來查看含有部門名稱的清單。

Courses Index page

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

建立 Instructor 頁面

本節會 scaffold Instructor 頁面,並將相關的課程和註冊新增至 Instructors 索引頁面。

Instructors Index page

此頁面將以下列方式讀取和顯示相關資料:

  • 講師清單會顯示來自 OfficeAssignment 實體 (上述映像中的 Office) 的相關資料。 InstructorOfficeAssignment 實體具有一對零或一關聯性。 積極式載入用於 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; }
    }
}

Scaffold Instructor 頁面

  • 遵循 Scaffold Students 頁面中的指示,下列部分除外:

    • 建立 Pages/Instructors 資料夾。
    • 使用 Instructor 作為模型類別。
    • 使用現有內容類別,而非建立新的類別。

若要在更新之前查看 Scaffold 頁面的外觀,請執行應用程式並巡覽至 Instructors 頁面。

以下列程式碼來更新 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 方法會針對所選取講師的識別碼接受選擇性的路由資料。

檢查 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

請注意 CourseAssignmentsCourseIncludeThenInclude 方法的重複。 若要針對 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 屬性則使用 Course 實體從該講師的 CourseAssignments 導覽屬性載入。

Where 方法會傳回集合。 但是在此情況下,篩選條件會選取單一實體,因此會呼叫 Single 方法,將集合轉換成單一 Instructor 實體。 Instructor 實體提供對 CourseAssignments 屬性的存取。 CourseAssignments 提供對相關 Course 實體的存取。

Instructor-to-Courses 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;
}

更新 Instructors [索引] 頁面

以下列程式碼來更新 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 語法,請參閱明確的行轉換

  • 新增程式碼,將 class="table-success" 動態新增至所選講師和課程的 tr 項目。 這會使用啟動程序類別設定所選取資料列的背景色彩。

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • 新增標示為選取的超連結。 此連結會將所選取的講師識別碼傳送至 Index 方法,並設定背景色彩。

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • 新增所選講師的課程資料表。

  • 新增所選課程的學生註冊資料表。

執行應用程式並選取 [講師] 索引標籤。此頁面會顯示來自相關 OfficeAssignment 實體的 Location (辦公室)。 如果 OfficeAssignment 為 Null,則會顯示空的資料表資料格。

按一下講師的 [選取] 連結。 資料列樣式會變更,並會顯示指派給該講師的課程。

選取課程,以查看已註冊學生和其年級的清單。

Instructors Index page instructor and course selected

使用 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 載入到導覽屬性的資料。

若您遇到無法解決的問題,請下載或檢視完整應用程式。下載指示

下圖顯示本教學課程的已完成頁面:

Courses Index page

Instructors Index page

EF Core 有幾種方式可以將相關資料載入到實體的導覽屬性:

  • 積極式載入。 積極式載入是指某一類型實體的查詢同時也會載入相關實體。 讀取實體時,將會擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 EF Core 將針對某些類型的積極式載入發出多個查詢。 相較於 EF6 中的某些查詢只有單一查詢的情況,發出多個查詢可能更有效率。 積極式載入是使用 IncludeThenInclude 方法加以指定。

    Eager loading example

    當集合導覽包含在內時,積極式載入會傳送多個查詢:

    • 針對主查詢傳送一個查詢
    • 針對負載樹狀結構中的每個集合「邊緣」傳送一個查詢。
  • 使用 Load 的個別查詢:資料可以在個別查詢中擷取,而 EF Core 會「修正」導覽屬性。 「修正」表示 EF Core 會自動填入導覽屬性。 使用 Load 的個別查詢更像是明確式載入,而不是積極式載入。

    Separate queries example

    注意:EF Core 會將導覽屬性自動修正為先前已載入至內容執行個體的任何其他實體。 即使「未」明確包含導覽屬性的資料,如果先前已載入某些或所有相關實體,仍然可能會填入該屬性。

  • 明確式載入。 第一次讀取實體時,不會擷取相關資料。 必須撰寫程式碼,才能在需要時擷取相關資料。 使用個別查詢的明確式載入會導致多個查詢傳送至資料庫。 透過明確式載入,程式碼會指定要載入的導覽屬性。 請使用 Load 方法來執行明確式載入。 例如:

    Explicit loading example

  • 消極式載入EF Core 已在 2.1 版中新增消極式載入。 第一次讀取實體時,不會擷取相關資料。 第一次存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 每當第一次存取導覽屬性時,查詢會傳送至資料庫。

  • Select 運算子只會載入所需的相關資料。

建立顯示部門名稱的 Course 頁面

Course 實體包含導覽屬性,其中包含 Department 實體。 Department 實體包含已指派課程的部門。

若要在課程清單中顯示所指派部門的名稱:

  • Department 實體取得 Name 屬性。
  • Department 實體來自 Course.Department 導覽屬性。

Course.Department

Scaffold Course 模型

請遵循建立學生結構模型中的指示,並為模型類別使用 Course

上述命令會 Scaffold Course 模型。 在 Visual Studio 中開啟專案。

開啟 Pages/Courses/Index.cshtml.cs 並檢查 OnGetAsync 方法。 Scaffolding 引擎已針對 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>

已對包含 Scaffold 的程式碼進行下列變更:

  • 已將標題從 Index 變更為 Courses。

  • 新增顯示 CourseID 屬性值的 [編號] 資料行。 主索引鍵預設不會進行 Scaffold,因為它們對終端使用者通常沒有任何意義。 不過,在此情況下,主索引鍵有意義。

  • 變更 [部門] 資料行來顯示部門名稱。 此程式碼會顯示已載入到 Department 導覽屬性之 Department 實體的 Name 屬性:

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

執行應用程式,並選取 [Courses] 索引標籤來查看含有部門名稱的清單。

Courses Index page

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

建立顯示「課程」和「註冊」的 Instructors 頁面

在本節中,將會建立 Instructors 頁面。

Instructors Index page

此頁面將以下列方式讀取和顯示相關資料:

  • 講師清單會顯示來自 OfficeAssignment 實體 (上述映像中的 Office) 的相關資料。 InstructorOfficeAssignment 實體具有一對零或一關聯性。 積極式載入用於 OfficeAssignment 實體。 若需要顯示相關資料,積極式載入通常更有效率。 在此情況下,將會顯示講師的辦公室指派。
  • 當使用者選取講師 (上述映像中的 Harui) 時,便會顯示相關的 Course 實體。 InstructorCourse 實體具有多對多關聯性。 將會針對 Course 實體和其相關 Department 實體使用積極式載入。 在此情況下,個別查詢可能更有效率,因為只需要所選取講師的課程。 這個範例示範如何使用導覽屬性中實體之導覽屬性的積極式載入。
  • 當使用者選取課程 (上述映像中的 Chemistry),隨即顯示 Enrollments 中的相關資料。 在上述映像中,將會顯示學生姓名和年級。 CourseEnrollment 實體具有一對多關聯性。

建立 Instructor [索引] 檢視的檢視模型

講師頁面會顯示下列三個不同資料表的資料。 建立的檢視模型會包含三個實體代表三個資料表。

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 模型

請遵循建立學生結構模型中的指示,並為模型類別使用 Instructor

上述命令會 Scaffold 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 方法會針對所選取講師的識別碼接受選擇性的路由資料。

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

查詢具有兩個 Include:

  • OfficeAssignment:顯示在 Instructors 檢視
  • CourseAssignments:它顯示所教授的課程。

更新 Instructors [索引] 頁面

使用下列標記建立 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

  • 頁面標題是 Instructors

  • 新增 [辦公室] 資料行,該資料行只有在 item.OfficeAssignment 不是 Null 時才會顯示 item.OfficeAssignment.Location。 因為這是一對零或一關聯性,所有可能沒有相關的 OfficeAssignment 實體。

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 新增 [課程 資料行,以顯示每位講師所教授課程。 若要深入了解此 Razor 語法,請參閱明確的行轉換

  • 新增程式碼,將 class="success" 動態新增至所選取講師的 tr 項目。 這會使用啟動程序類別設定所選取資料列的背景色彩。

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • 新增標示為選取的超連結。 此連結會將所選取的講師識別碼傳送至 Index 方法,並設定背景色彩。

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

執行應用程式並選取 [講師] 索引標籤。此頁面會顯示來自相關 OfficeAssignment 實體的 Location (辦公室)。 如果 OfficeAssignment 是 Null,就會顯示空的資料表資料格。

按一下選取連結。 資料列樣式變更。

新增選取的講師所教授的課程

以下列程式碼來更新 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 屬性則使用 Course 實體從該講師的 CourseAssignments 導覽屬性載入。

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 實體的存取。

Instructor-to-Courses m:M

當集合只有一個項目時,將會在集合上使用 Single 方法。 如果集合是空的或是有多個項目,Single 方法會擲回例外狀況。 替代方式是 SingleOrDefault,它會在集合是空的時傳回預設值 (在此情況下為 Null)。 在空集合上使用 SingleOrDefault

  • 造成例外狀況 (由於嘗試在 Null 參考上尋找 Courses 屬性)。
  • 例外狀況訊息會不太清楚地指出問題的原因。

選取課程時,下列程式碼會填入檢視模型的 Enrollments 屬性:

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

將下列標記新增至 Pages/Instructors/Index.cshtmlRazor Page 結尾:

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

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

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

    </table>
}

當選取講師時,上述標記會顯示與講師相關的課程。

測試應用程式。 按一下講師頁面上的選取連結。

顯示學生資料

在本節中,應用程式會更新以顯示所選取課程的學生資料。

使用下列程式碼在 Pages/Instructors/Index.cshtml.csOnGetAsync 方法中更新查詢:

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

上述標記會顯示註冊所選取課程的學生清單。

重新整理頁面,然後選取講師。 選取課程,以查看已註冊學生和其年級的清單。

Instructors Index page instructor and course selected

使用 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 方法樣式。

明確式載入

目前程式碼針對 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() 註解化。 針對所追蹤的實體,導覽屬性只能進行明確式載入。

測試應用程式。 就使用者的觀點而言,應用程式行為與之前的版本相同。

下一個教學課程會示範如何更新相關資料。

其他資源