Часть 6. Razor Страницы с EF Core ASP.NET Core — чтение связанных данных

Авторы: Том Дайкстра (Tom Dykstra), Йон П. Смит (Jon P Smith) и Рик Андерсон (Rick Anderson)

Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.

При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.

Этот учебник посвящен чтению и отображению связанных данных. Связанные данные — это данные, которые EF Core загружаются в свойства навигации.

На следующих рисунках изображены готовые страницы для этого руководства:

Courses Index page

Instructors Index page

Безотложная, явная и отложенная загрузка

Существует несколько способов EF Core загрузки связанных данных в свойства навигации сущности:

  • Безотложная загрузка. Безотложной является загрузка, когда запрос для одного типа сущности также загружает связанные сущности. При чтении сущности извлекаются ее связанные данные. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. EF Core будет выдавать несколько запросов для некоторых типов охотной загрузки. Отправка нескольких запросов может оказаться эффективнее, чем выполнение одного большого запроса. Безотложная загрузка указывается с помощью методов Include и ThenInclude.

    Eager loading example

    Безотложная загрузка отправляет несколько запросов, когда включена навигация коллекции:

    • Один запрос для основного запроса
    • Один запрос для каждого "края" коллекции в дереве загрузки.
  • Отдельные запросы с Load: данные можно получить в отдельных запросах и EF Core "исправляет" свойства навигации. "Исправление" означает, что EF Core автоматически заполняет свойства навигации. Отдельные запросы с Load больше похожи на явную загрузку, чем на безотложную.

    Separate queries example

    Примечание.EF Core Автоматически исправляет свойства навигации для любых других сущностей, которые ранее были загружены в экземпляр контекста. Даже если данные для свойства навигации не включены явно, свойство все равно можно заполнить при условии, что ранее были загружены некоторые или все связанные сущности.

  • Явная загрузка. При первом чтении сущности связанные данные не извлекаются. Нужно написать код, извлекающий связанные данные, когда они необходимы. Явная загрузка с отдельными запросами приводит к отправке нескольких запросов к базе данных. При явной загрузке код указывает, какие свойства навигации нужно загрузить. Для выполнения явной загрузки используется метод Load. Например:

    Explicit loading example

  • Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. При первом обращении к свойству навигации необходимые для этого свойства данные извлекаются автоматически. Запрос к базе данных отправляется при каждом первом обращении к свойству навигации. Отложенная загрузка может снизить производительность, например когда разработчики используют запросы N+1, загружая родительский объект и перечисляя дочерние объекты.

Создание страниц курсов

Сущность Course включает в себя свойство навигации, содержащее связанную сущность Department.

Course.Department

Чтобы отобразить имя кафедры, которой назначен курс, выполните указанные ниже действия.

  • Загрузите связанную сущность Department в свойство навигации Course.Department.
  • Получите имя из свойства Name сущности Department.

Формирование шаблона для страниц курсов

  • Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:

    • Создайте папку Pages/Courses.
    • Используйте класс модели Course.
    • Используйте существующий класс контекста вместо создания нового.
  • Откройте Pages/Courses/Index.cshtml.cs и проверьте метод OnGetAsync. Подсистема формирования шаблонов указала безотложную загрузку для свойства навигации Department. Метод Include задает безотложную загрузку.

  • Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры отображается бесполезный 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 (Номер), отображающий значение свойства CourseID. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл.

  • Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство Name сущности Department, которая загружена в свойство навигации Department:

    @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 Pages см. здесь.

Создание страниц преподавателей

В этом разделе формируется шаблон для страниц преподавателей, а затем на страницу указателя преподавателей добавляются связанные курсы и регистрации.

Instructors Index page

Эта страница считывает и отображает связанные данные следующим образом:

  • Список преподавателей отображает связанные данные из сущности OfficeAssignment ("Office" (Кабинет) на предыдущем изображении). Между сущностями Instructor и OfficeAssignment действует связь один к нулю или к одному. Безотложная загрузка используется для сущностей OfficeAssignment. Безотложная загрузка обычно более эффективна, когда требуется отобразить связанные данные. В этом случае отображается принадлежность к кабинету для каждого преподавателя.
  • Когда пользователь выбирает преподавателя, отображаются связанные сущности Course. Между сущностями Instructor и Course действует связь многие ко многим. Используется безотложная загрузка для сущностей Course и связанных сущностей Department. В этом случае отдельные запросы могут оказаться эффективнее, так как требуются только курсы для выбранного преподавателя. Этот пример показывает, как использовать безотложную загрузку для свойств навигации в сущностях, находящихся в свойствах навигации.
  • Когда пользователь выбирает курс, отображаются связанные данные из сущности Enrollments. На предыдущем изображении отображается имя и оценка учащегося. Между сущностями Course и Enrollment действует связь один ко многим.

Создание модели представления

На странице "Instructors" (Преподаватели) отображаются данные из трех различных таблиц. Требуется модель представления, которая включает в себя три свойства, представляющие три таблицы.

Создайте 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; }
    }
}

Формирование шаблона для страниц преподавателей

  • Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:

    • Создайте папку Pages/Instructors.
    • Используйте класс модели Instructor.
    • Используйте существующий класс контекста вместо создания нового.

Запустите приложение и перейдите на страницу 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.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;
}

Изменение страницы индекса преподавателей

Обновите 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-адресе для маршрутизации данных. Например, при выборе ссылки Select для преподавателя только с директивой @page формируется URL-адрес следующего вида:

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

    Когда используется директива страницы @page "{id:int?}", URL-адрес имеет следующее значение: https://localhost:5001/Instructors/2.

  • Добавляет столбец Office, в котором отображается item.OfficeAssignment.Location только тогда, когда item.OfficeAssignment не равно NULL. Так как это связь один к нулю или одному, то связанная сущность OfficeAssignment может отсутствовать.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Добавляет столбец Courses, содержащий курсы, которые ведет конкретный преподаватель. Подробные сведения об использовании синтаксиса Razor см. в разделе Явный перенос строки.

  • Добавляет код, который динамически добавляет class="table-success" к элементу tr выбранного преподавателя и курса. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Добавляет новую гиперссылку с меткой Select (Выбрать). Она отправляет идентификатор выбранного преподавателя в метод Index и задает цвет фона.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Добавляет таблицу курсов для выбранного преподавателя.

  • Добавляет таблицу регистраций учащихся для выбранного курса.

Запустите приложение и выберите вкладку Instructors. На странице отображается Location (office) из связанной сущности OfficeAssignment. Если OfficeAssignment имеет значение NULL, отображается пустая ячейка таблицы.

Щелкните ссылку Select для преподавателя. Стиль строки изменится, и отобразятся курсы, назначенные этому преподавателю.

Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.

Instructors Index page instructor and course selected

Следующие шаги

Следующее руководство посвящено обновлению связанных данных.

Этот учебник посвящен чтению и отображению связанных данных. Связанные данные — это данные, которые EF Core загружаются в свойства навигации.

На следующих рисунках изображены готовые страницы для этого руководства:

Courses Index page

Instructors Index page

Безотложная, явная и отложенная загрузка

Существует несколько способов EF Core загрузки связанных данных в свойства навигации сущности:

  • Безотложная загрузка. Безотложной является загрузка, когда запрос для одного типа сущности также загружает связанные сущности. При чтении сущности извлекаются ее связанные данные. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. EF Core будет выдавать несколько запросов для некоторых типов охотной загрузки. Отправка нескольких запросов может оказаться эффективнее, чем выполнение одного большого запроса. Безотложная загрузка указывается с помощью методов Include и ThenInclude.

    Eager loading example

    Безотложная загрузка отправляет несколько запросов, когда включена навигация коллекции:

    • Один запрос для основного запроса
    • Один запрос для каждого "края" коллекции в дереве загрузки.
  • Отдельные запросы с Load: данные можно получить в отдельных запросах и EF Core "исправляет" свойства навигации. "Исправление" означает, что EF Core автоматически заполняет свойства навигации. Отдельные запросы с Load больше похожи на явную загрузку, чем на безотложную.

    Separate queries example

    Примечание.EF Core Автоматически исправляет свойства навигации для любых других сущностей, которые ранее были загружены в экземпляр контекста. Даже если данные для свойства навигации не включены явно, свойство все равно можно заполнить при условии, что ранее были загружены некоторые или все связанные сущности.

  • Явная загрузка. При первом чтении сущности связанные данные не извлекаются. Нужно написать код, извлекающий связанные данные, когда они необходимы. Явная загрузка с отдельными запросами приводит к отправке нескольких запросов к базе данных. При явной загрузке код указывает, какие свойства навигации нужно загрузить. Для выполнения явной загрузки используется метод Load. Например:

    Explicit loading example

  • Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. При первом обращении к свойству навигации необходимые для этого свойства данные извлекаются автоматически. Запрос к базе данных отправляется при каждом первом обращении к свойству навигации. Отложенная загрузка может снизить производительность, например когда разработчики используют шаблоны N+1, загружая родительский объект и перечисляя дочерние объекты.

Создание страниц курсов

Сущность Course включает в себя свойство навигации, содержащее связанную сущность Department.

Course.Department

Чтобы отобразить имя кафедры, которой назначен курс, выполните указанные ниже действия.

  • Загрузите связанную сущность Department в свойство навигации Course.Department.
  • Получите имя из свойства Name сущности Department.

Формирование шаблона для страниц курсов

  • Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:

    • Создайте папку Pages/Courses.
    • Используйте класс модели Course.
    • Используйте существующий класс контекста вместо создания нового.
  • Откройте Pages/Courses/Index.cshtml.cs и проверьте метод OnGetAsync. Подсистема формирования шаблонов указала безотложную загрузку для свойства навигации Department. Метод Include задает безотложную загрузку.

  • Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры отображается бесполезный 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 (Номер), отображающий значение свойства CourseID. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл.

  • Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство Name сущности Department, которая загружена в свойство навигации Department:

    @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.cshtml и IndexSelect.cshtml.cs.

Создание страниц преподавателей

В этом разделе формируется шаблон для страниц преподавателей, а затем на страницу указателя преподавателей добавляются связанные курсы и регистрации.

Instructors Index page

Эта страница считывает и отображает связанные данные следующим образом:

  • Список преподавателей отображает связанные данные из сущности OfficeAssignment ("Office" (Кабинет) на предыдущем изображении). Между сущностями Instructor и OfficeAssignment действует связь один к нулю или к одному. Безотложная загрузка используется для сущностей OfficeAssignment. Безотложная загрузка обычно более эффективна, когда требуется отобразить связанные данные. В этом случае отображается принадлежность к кабинету для каждого преподавателя.
  • Когда пользователь выбирает преподавателя, отображаются связанные сущности Course. Между сущностями Instructor и Course действует связь многие ко многим. Используется безотложная загрузка для сущностей Course и связанных сущностей Department. В этом случае отдельные запросы могут оказаться эффективнее, так как требуются только курсы для выбранного преподавателя. Этот пример показывает, как использовать безотложную загрузку для свойств навигации в сущностях, находящихся в свойствах навигации.
  • Когда пользователь выбирает курс, отображаются связанные данные из сущности Enrollments. На предыдущем изображении отображается имя и оценка учащегося. Между сущностями Course и Enrollment действует связь один ко многим.

Создание модели представления

На странице "Instructors" (Преподаватели) отображаются данные из трех различных таблиц. Требуется модель представления, которая включает в себя три свойства, представляющие три таблицы.

Создайте 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; }
    }
}

Формирование шаблона для страниц преподавателей

  • Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:

    • Создайте папку 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 принимает необязательные данные маршрутизации для идентификатора выбранного преподавателя.

Проверьте запрос в 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

Обратите внимание на то, что методы Include и ThenInclude повторяются для CourseAssignments и Course. Это необходимо для того, чтобы задать безотложную загрузку для двух свойств навигации сущности 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);
}

Выбранный преподаватель извлекается из списка преподавателей в модели представления. Из свойства навигации CourseAssignments этого преподавателя загружается свойство модели представления Courses вместе с сущностями Course.

Метод 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;
}

Изменение страницы индекса преподавателей

Обновите 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-адресе для маршрутизации данных. Например, при выборе ссылки Select для преподавателя только с директивой @page формируется URL-адрес следующего вида:

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

    Когда используется директива страницы @page "{id:int?}", URL-адрес имеет следующее значение:

    https://localhost:5001/Instructors/2

  • Добавляет столбец Office, в котором отображается item.OfficeAssignment.Location только тогда, когда item.OfficeAssignment не равно NULL. Так как это связь один к нулю или одному, то связанная сущность OfficeAssignment может отсутствовать.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Добавляет столбец Courses, содержащий курсы, которые ведет конкретный преподаватель. Подробные сведения об использовании синтаксиса Razor см. в разделе Явный перенос строки.

  • Добавляет код, который динамически добавляет class="table-success" к элементу tr выбранного преподавателя и курса. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Добавляет новую гиперссылку с меткой Select (Выбрать). Она отправляет идентификатор выбранного преподавателя в метод Index и задает цвет фона.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Добавляет таблицу курсов для выбранного преподавателя.

  • Добавляет таблицу регистраций учащихся для выбранного курса.

Запустите приложение и выберите вкладку Instructors. На странице отображается Location (office) из связанной сущности OfficeAssignment. Если OfficeAssignment имеет значение NULL, отображается пустая ячейка таблицы.

Щелкните ссылку Select для преподавателя. Стиль строки изменится, и отобразятся курсы, назначенные этому преподавателю.

Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.

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.

Явная загрузка

Текущий код указывает упреждающую загрузку для 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 для выбранного курса.
  • Сущности Student для каждого Enrollment.

Обратите внимание, что предыдущий код закомментирует .AsNoTracking(). Для отслеживаемых сущностей свойства навигации можно загрузить лишь явно.

Тестирование приложения. С точки зрения пользователя приложение работает аналогично предыдущей версии.

Следующие шаги

Следующее руководство посвящено обновлению связанных данных.

В этом руководстве выполняется чтение и отображение связанных данных. Связанные данные — это данные, которые EF Core загружаются в свойства навигации.

При возникновении проблем, которые вам не удается устранить, скачайте или просмотрите готовое приложение.Инструкции по скачиванию

На следующих рисунках изображены готовые страницы для этого руководства:

Courses Index page

Instructors Index page

Существует несколько способов EF Core загрузки связанных данных в свойства навигации сущности:

  • Безотложная загрузка. Безотложной является загрузка, когда запрос для одного типа сущности также загружает связанные сущности. При чтении сущности связанные данные извлекаются. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. EF Core будет выдавать несколько запросов для некоторых типов охотной загрузки. Выдача нескольких запросов может оказаться более эффективной по сравнению с отдельными ситуациями в EF6, когда выполнялся отдельный запрос. Безотложная загрузка указывается с помощью методов Include и ThenInclude.

    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 содержит кафедру, которой назначен курс.

Отображение имени назначенной кафедры в списке курсов:

  • Получите свойство Name из сущности Department.
  • Сущность Department получается из свойства навигации Course.Department.

Course.Department

Формирование шаблона для модели Course

Следуйте инструкциям в разделе Формирование шаблона для модели Student и используйте Course для класса модели.

Предыдущая команда формирует шаблон для модели Course. Откройте проект в Visual Studio.

Откройте Pages/Courses/Index.cshtml.cs и проверьте метод OnGetAsync. Подсистема формирования шаблонов указала безотложную загрузку для свойства навигации Department. Метод Include задает безотложную загрузку.

Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры отображается бесполезный 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>

В шаблонный код были внесены следующие изменения:

  • Изменен заголовок с Index (Индекс) на Courses (Курсы).

  • Добавлен столбец Number (Номер), отображающий значение свойства CourseID. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл.

  • Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство Name сущности Department, которая загружена в свойство навигации Department:

    @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.cshtml и IndexSelect.cshtml.cs.

Создание страницы "Instructors" (Преподаватели) с отображением курсов и дат зачисления

В этом разделе создается страница "Instructors" (Преподаватели).

Instructors Index page

Эта страница считывает и отображает связанные данные следующим образом:

  • Список преподавателей отображает связанные данные из сущности OfficeAssignment ("Office" (Кабинет) на предыдущем изображении). Между сущностями Instructor и OfficeAssignment действует связь один к нулю или к одному. Безотложная загрузка используется для сущностей OfficeAssignment. Безотложная загрузка обычно более эффективна, когда требуется отобразить связанные данные. В этом случае отображается принадлежность к кабинету для каждого преподавателя.
  • Когда пользователь выбирает преподавателя (Harui на предыдущем изображении), отображаются связанные сущности Course. Между сущностями Instructor и Course действует связь многие ко многим. Используется безотложная загрузка для сущностей Course и связанных сущностей Department. В этом случае отдельные запросы могут оказаться эффективнее, так как требуются только курсы для выбранного преподавателя. Этот пример показывает, как использовать безотложную загрузку для свойств навигации в сущностях, находящихся в свойствах навигации.
  • Когда пользователь выбирает курс ("Chemistry" (Химия) на предыдущем изображении), отображаются связанные данные из сущности Enrollments. На предыдущем изображении отображается имя и оценка учащегося. Между сущностями Course и Enrollment действует связь один ко многим.

Создание модели для представления индекса преподавателей

На странице "Instructors" (Преподаватели) отображаются данные из трех различных таблиц. Создается модель представления, которая включает три сущности, представляющие три таблицы.

В папке 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; }
    }
}

Формирование шаблона для модели "Instructor" (Преподаватель)

Следуйте инструкциям в разделе Формирование шаблона для модели Student и используйте 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 принимает необязательные данные маршрутизации для идентификатора выбранного преподавателя.

Проверьте запрос в 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();

Запрос имеет две операции включения:

Изменение страницы индекса преподавателей

Обновите 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-адресе для маршрутизации данных. Например, при выборе ссылки Select для преподавателя только с директивой @page формируется URL-адрес следующего вида:

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

    Когда используется директива страницы @page "{id:int?}", предыдущий URL-адрес имеет следующее значение:

    http://localhost:1234/Instructors/2

  • Заголовком страницы является Instructors.

  • Добавили столбец Office, отображающий item.OfficeAssignment.Location только тогда, когда item.OfficeAssignment не равно null. Так как это связь один к нулю или одному, то связанная сущность OfficeAssignment может отсутствовать.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Добавили столбец Courses, отображающий курсы, которые ведет конкретный преподаватель. Подробные сведения об использовании синтаксиса Razor см. в разделе Явный перенос строки.

  • Добавили код, который динамически добавляет class="success" к элементу tr выбранного преподавателя. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • Добавлена новая гиперссылка с меткой Select (Выбрать). Она отправляет идентификатор выбранного преподавателя в метод Index и задает цвет фона.

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

Запустите приложение и выберите вкладку Instructors. На странице отображается Location (office) из связанной сущности OfficeAssignment. Если OfficeAssignment имеет значение null, отображается пустая ячейка таблицы.

Щелкните ссылку Select (Выбрать). Изменяется стиль строки.

Добавление курсов, проводимых выбранным преподавателем

Обновите метод 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). Выбранный преподаватель извлекается из списка преподавателей в модели представления. Из свойства навигации CourseAssignments этого преподавателя загружается свойство модели представления Courses вместе с сущностями 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.

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

Предыдущая разметка отображает список связанных с преподавателем курсов при выборе преподавателя.

Тестирование приложения. Щелкните ссылку Select (Выбрать) на странице преподавателей.

Отображение данных об учащихся

В этом разделе приложение изменяется, чтобы отобразить данные об учащихся для выбранного курса.

Обновите запрос в методе OnGetAsync в файле Pages/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>
}

Предыдущая разметка отображает список учащихся, которые зачислены на курс.

Обновите страницу и выберите преподавателя. Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.

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

Приведенный выше подход на основе Single не дает преимуществ по сравнению с использованием Where. Некоторые разработчики предпочитают использовать подход Single.

Явная загрузка

Текущий код указывает упреждающую загрузку для 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 для выбранного курса.
  • Сущности Student для каждого Enrollment.

Обратите внимание, что предыдущий код закомментирует .AsNoTracking(). Для отслеживаемых сущностей свойства навигации можно загрузить лишь явно.

Тестирование приложения. С точки зрения пользователей приложение работает аналогично предыдущей версии.

Следующее руководство посвящено обновлению связанных данных.

Дополнительные ресурсы