Parte 6, Razor Pages com EF Core em ASP.NET Core - Atualizar dados relacionados

Por Tom Dykstra, Jon P Smith e Rick Anderson

O aplicativo Web Contoso University demonstra como criar aplicativos Web do Razor Pages usando o EF Core e o Visual Studio. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial.

Se você encontrar problemas que não possa resolver, baixe o aplicativo concluído e compare esse código com o que você criou seguindo o tutorial.

Este tutorial mostra como ler e exibir dados relacionados. Dados relacionados são dados que o EF Core carrega nas propriedades de navegação.

As seguintes ilustrações mostram as páginas concluídas para este tutorial:

Courses Index page

Instructors Index page

Carregamento adiantado, explícito e lento

Há várias maneiras pelas quais o EF Core pode carregar dados relacionados nas propriedades de navegação de uma entidade:

  • Carregamento adiantado. O carregamento adiantado é quando uma consulta para um tipo de entidade também carrega entidades relacionadas. Quando uma entidade é lida, seus dados relacionados são recuperados. Normalmente, isso resulta em uma única consulta de junção que recupera todos os dados necessários. O EF Core emitirá várias consultas para alguns tipos de carregamento adiantado. A emissão de várias consultas pode ser mais eficiente do que uma única consulta gigante. O carregamento adiantado é especificado com os métodos Include e ThenInclude.

    Eager loading example

    O carregamento adiantado envia várias consultas quando a navegação de coleção é incluída:

    • Uma consulta para a consulta principal
    • Uma consulta para cada "borda" de coleção na árvore de carregamento.
  • Separe consultas com Load: os dados podem ser recuperados em consultas separadas e o EF Core "corrige" as propriedades de navegação. "Correção" significa que o EF Core preenche automaticamente as propriedades de navegação. A separação de consultas com Load é mais parecida com o carregamento explícito do que com o carregamento adiantado.

    Separate queries example

    Observação: o EF Core corrige automaticamente as propriedades de navegação para outras entidades que foram carregadas anteriormente na instância do contexto. Mesmo se os dados de uma propriedade de navegação não foram incluídos de forma explícita, a propriedade ainda pode ser populada se algumas ou todas as entidades relacionadas foram carregadas anteriormente.

  • Carregamento explícito. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. Um código precisa ser escrito para recuperar os dados relacionados quando eles forem necessários. O carregamento explícito com consultas separadas resulta no envio de várias consultas ao banco de dados. Com o carregamento explícito, o código especifica as propriedades de navegação a serem carregadas. Use o método Load para fazer o carregamento explícito. Por exemplo:

    Explicit loading example

  • Carregamento lento. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. Na primeira vez que uma propriedade de navegação é acessada, os dados necessários para essa propriedade de navegação são recuperados automaticamente. Uma consulta é enviada para o banco de dados sempre que uma propriedade de navegação é acessada pela primeira vez. O carregamento lento pode prejudicar o desempenho, por exemplo, quando os desenvolvedores usam consultas N+1. As consultas N+1 carregam um pai e enumeram por meio de filhos.

Criar páginas do Curso

A entidade Course inclui uma propriedade de navegação que contém a entidade Department relacionada.

Course.Department

Para exibir o nome do departamento atribuído para um curso:

  • Carregue a entidade relacionada Department na propriedade de navegação Course.Department.
  • Obtenha o nome da propriedade Department da entidade Name.

Aplicar scaffold às páginas do curso

  • Siga as instruções em páginas do aluno do Scaffold com as seguintes exceções:

    • Crie uma pasta Pages/Courses.
    • Use Course para a classe de modelo.
    • Use a classe de contexto existente, em vez de criar uma nova.
  • Abra Pages/Courses/Index.cshtml.cs e examine o método OnGetAsync. O mecanismo de scaffolding especificou o carregamento adiantado para a propriedade de navegação Department. O método Include especifica o carregamento adiantado.

  • Execute o aplicativo e selecione o link Cursos. A coluna de departamento exibe a DepartmentID, que não é útil.

Exibir o nome do departamento

Atualize Pages/Courses/Index.cshtml.cs com o seguinte código:

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

O código anterior altera a propriedade Course para Courses e adiciona AsNoTracking.

As consultas sem acompanhamento são úteis quando os resultados são usados em um cenário de somente leitura. Geralmente, eles são mais rápidos para executar porque não há necessidade de configurar as informações de controle de alterações. Se as entidades recuperadas do banco de dados não precisarem ser atualizadas, é provável que uma consulta sem acompanhamento tenha um desempenho melhor do que uma consulta de acompanhamento.

Em alguns casos, uma consulta de acompanhamento é mais eficiente do que uma consulta sem acompanhamento. Para obter mais informações, consulte Comparação entre consultas com e sem acompanhamento. No código anterior, AsNoTracking é chamado porque as entidades não são atualizadas no contexto atual.

Atualize Pages/Courses/Index.cshtml com o seguinte código.

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

As seguintes alterações foram feitas na biblioteca gerada por código em scaffolding:

  • O nome da propriedade Course foi alterado para Courses.

  • Adicionou uma coluna Número que mostra o valor da propriedade CourseID. Por padrão, as chaves primárias não são geradas por scaffolding porque normalmente não têm sentido para os usuários finais. No entanto, nesse caso, a chave primária é significativa.

  • Alterou a coluna Departamento para que ela exiba o nome de departamento. O código exibe a propriedade Name da entidade Department que é carregada na propriedade de navegação Department:

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

Execute o aplicativo e selecione a guia Cursos para ver a lista com nomes de departamentos.

Courses Index page

O método OnGetAsync carrega dados relacionados com o método Include. O método Select é uma alternativa que carrega apenas os dados relacionados necessários. Para itens únicos, como o Department.Name, ele usa um SQL INNER JOIN. Para coleções, ele usa outro acesso ao banco de dados, assim como o operador Include em coleções.

O seguinte código carrega dados relacionados com o método 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();
}

O código anterior não retorna nenhum tipo de entidade, portanto, nenhum acompanhamento é feito. Para obter mais informações sobre o acompanhamento do EF, consulte Comparação entre Consultas Com e Sem Acompanhamento.

CourseViewModel:

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

Consulte IndexSelectModel para ver as Páginas completas do Razor .

Criar as páginas de Instrutor

Esta seção aplica scaffold a páginas do Instrutor e adiciona Cursos e Inscrições relacionados à página Índice de Instrutores.

Instructors Index page

Essa página lê e exibe dados relacionados das seguintes maneiras:

  • A lista de instrutores exibe dados relacionados da entidade OfficeAssignment (Office na imagem anterior). As entidades Instructor e OfficeAssignment estão em uma relação um para zero ou um. O carregamento adiantado é usado para as entidades OfficeAssignment. O carregamento adiantado costuma ser mais eficiente quando os dados relacionados precisam ser exibidos. Nesse caso, as atribuições de escritório para os instrutores são exibidas.
  • Quando o usuário seleciona um instrutor, as entidades Course relacionadas são exibidas. As entidades Instructor e Course estão em uma relação muitos para muitos. O carregamento adiantado é usado para entidades Course e suas entidades Department relacionadas. Nesse caso, consultas separadas podem ser mais eficientes porque somente os cursos para o instrutor selecionado são necessários. Este exemplo mostra como usar o carregamento adiantado para propriedades de navegação em entidades que estão nas propriedades de navegação.
  • Quando o usuário seleciona um curso, dados relacionados da entidade Enrollments são exibidos. Na imagem anterior, o nome do aluno e a nota são exibidos. As entidades Course e Enrollment estão em uma relação um-para-muitos.

Criar um modelo de exibição

A página Instrutores mostra dados de três tabelas diferentes. É necessário um modelo de exibição que inclui três propriedades que representam as três tabelas.

Crie Models/SchoolViewModels/InstructorIndexData.cs com o seguinte código:

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

Aplicar scaffold às páginas do Instrutor

  • Siga as instruções em Aplicar scaffold às páginas do aluno com as seguintes exceções:

    • Crie uma pasta Pages/Instructors.
    • Use Instructor para a classe de modelo.
    • Use a classe de contexto existente, em vez de criar uma nova.

Execute o aplicativo e navegue para a página Instrutores.

Atualize Pages/Instructors/Index.cshtml.cs com o seguinte código:

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

O método OnGetAsync aceita dados de rota opcionais para a ID do instrutor selecionado.

Examine a consulta no arquivo 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();

O código especifica o carregamento adiantado para as seguintes propriedades de navegação:

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

O código a seguir é executado quando o instrutor é selecionado, isto é, id != null.

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

O instrutor selecionado é recuperado da lista de instrutores no modelo de exibição. A propriedade Courses do modelo de exibição é carregada com as entidades Course da propriedade de navegação Courses desse instrutor selecionado.

O método Where retorna uma coleção. Nesse caso, o filtro seleciona uma única entidade, portanto, o método Single é chamado para converter a coleção em uma única entidade Instructor. A entidade Instructor fornece acesso à propriedade de navegação Course.

O método Single é usado em uma coleção quando a coleção tem apenas um item. O método Single gera uma exceção se a coleção está vazia ou se há mais de um item. Uma alternativa é SingleOrDefault, que retorna um valor padrão se a coleção estiver vazia. Para essa consulta, null no padrão retornado.

O seguinte código popula a propriedade Enrollments do modelo de exibição quando um curso é selecionado:

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

Atualizar a página Índice de instrutores

Atualize Pages/Instructors/Index.cshtml com o seguinte código.

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

O código anterior faz as seguintes alterações:

  • Atualiza a diretiva page para @page "{id:int?}". "{id:int?}" é um modelo de rota. O modelo de rota altera cadeias de consulta de números inteiros na URL para dados de rota. Por exemplo, clicar no link Selecionar de um o instrutor apenas com a diretiva @page produz uma URL semelhante à seguinte:

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

    Quando a diretiva de página é @page "{id:int?}", a URL é: https://localhost:5001/Instructors/2

  • Adiciona uma coluna Escritório que exibirá item.OfficeAssignment.Location somente se item.OfficeAssignment não for nulo. Como essa é uma relação um para zero ou um, pode não haver uma entidade OfficeAssignment relacionada.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Adiciona uma coluna Cursos que exibe os cursos ministrados por cada instrutor. Consulte Transição de linha explícita para saber mais sobre essa sintaxe do Razor.

  • Adiciona um código que adiciona dinamicamente class="table-success" ao elemento tr do instrutor e do curso selecionados. Isso define uma cor da tela de fundo para a linha selecionada usando uma classe Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Adiciona um novo hiperlink rotulado Selecionar. Este link envia a ID do instrutor selecionado para o método Index e define uma cor da tela de fundo.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Adiciona uma tabela de cursos para o Instrutor selecionado.

  • Adiciona uma tabela de inscrições de alunos para o curso selecionado.

Execute o aplicativo e selecione a guia Instrutores. A página exibe o Location (Office) da entidade relacionada OfficeAssignment . Se OfficeAssignment for nulo, uma célula de tabela vazia será exibida.

Clique no link Selecionar para um instrutor. As alterações de estilo de linha e os cursos atribuídos a esse instrutor são exibidos.

Selecione um curso para ver a lista de alunos registrados e suas notas.

Instructors Index page instructor and course selected

Próximas etapas

O próximo tutorial mostra como atualizar os dados relacionados.

Este tutorial mostra como ler e exibir dados relacionados. Dados relacionados são dados que o EF Core carrega nas propriedades de navegação.

As seguintes ilustrações mostram as páginas concluídas para este tutorial:

Courses Index page

Instructors Index page

Carregamento adiantado, explícito e lento

Há várias maneiras pelas quais o EF Core pode carregar dados relacionados nas propriedades de navegação de uma entidade:

  • Carregamento adiantado. O carregamento adiantado é quando uma consulta para um tipo de entidade também carrega entidades relacionadas. Quando uma entidade é lida, seus dados relacionados são recuperados. Normalmente, isso resulta em uma única consulta de junção que recupera todos os dados necessários. O EF Core emitirá várias consultas para alguns tipos de carregamento adiantado. A emissão de várias consultas pode ser mais eficiente do que uma única consulta gigante. O carregamento adiantado é especificado com os métodos Include e ThenInclude.

    Eager loading example

    O carregamento adiantado envia várias consultas quando a navegação de coleção é incluída:

    • Uma consulta para a consulta principal
    • Uma consulta para cada "borda" de coleção na árvore de carregamento.
  • Separe consultas com Load: os dados podem ser recuperados em consultas separadas e o EF Core "corrige" as propriedades de navegação. "Correção" significa que o EF Core preenche automaticamente as propriedades de navegação. A separação de consultas com Load é mais parecida com o carregamento explícito do que com o carregamento adiantado.

    Separate queries example

    Observação: o EF Core corrige automaticamente as propriedades de navegação para outras entidades que foram carregadas anteriormente na instância do contexto. Mesmo se os dados de uma propriedade de navegação não foram incluídos de forma explícita, a propriedade ainda pode ser populada se algumas ou todas as entidades relacionadas foram carregadas anteriormente.

  • Carregamento explícito. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. Um código precisa ser escrito para recuperar os dados relacionados quando eles forem necessários. O carregamento explícito com consultas separadas resulta no envio de várias consultas ao banco de dados. Com o carregamento explícito, o código especifica as propriedades de navegação a serem carregadas. Use o método Load para fazer o carregamento explícito. Por exemplo:

    Explicit loading example

  • Carregamento lento. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. Na primeira vez que uma propriedade de navegação é acessada, os dados necessários para essa propriedade de navegação são recuperados automaticamente. Uma consulta é enviada para o banco de dados sempre que uma propriedade de navegação é acessada pela primeira vez. O carregamento lento pode prejudicar o desempenho, por exemplo, quando os desenvolvedores usam padrões N+1, carregando um pai e enumerando por meio de filhos.

Criar páginas do Curso

A entidade Course inclui uma propriedade de navegação que contém a entidade Department relacionada.

Course.Department

Para exibir o nome do departamento atribuído para um curso:

  • Carregue a entidade relacionada Department na propriedade de navegação Course.Department.
  • Obtenha o nome da propriedade Department da entidade Name.

Aplicar scaffold às páginas do curso

  • Siga as instruções em páginas do aluno do Scaffold com as seguintes exceções:

    • Crie uma pasta Pages/Courses.
    • Use Course para a classe de modelo.
    • Use a classe de contexto existente, em vez de criar uma nova.
  • Abra Pages/Courses/Index.cshtml.cs e examine o método OnGetAsync. O mecanismo de scaffolding especificou o carregamento adiantado para a propriedade de navegação Department. O método Include especifica o carregamento adiantado.

  • Execute o aplicativo e selecione o link Cursos. A coluna de departamento exibe a DepartmentID, que não é útil.

Exibir o nome do departamento

Atualize Pages/Courses/Index.cshtml.cs com o seguinte código:

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

O código anterior altera a propriedade Course para Courses e adiciona AsNoTracking. AsNoTracking melhora o desempenho porque as entidades retornadas não são controladas. As entidades não precisam ser controladas porque não são atualizadas no contexto atual.

Atualize Pages/Courses/Index.cshtml com o seguinte código.

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

As seguintes alterações foram feitas na biblioteca gerada por código em scaffolding:

  • O nome da propriedade Course foi alterado para Courses.

  • Adicionou uma coluna Número que mostra o valor da propriedade CourseID. Por padrão, as chaves primárias não são geradas por scaffolding porque normalmente não têm sentido para os usuários finais. No entanto, nesse caso, a chave primária é significativa.

  • Alterou a coluna Departamento para que ela exiba o nome de departamento. O código exibe a propriedade Name da entidade Department que é carregada na propriedade de navegação Department:

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

Execute o aplicativo e selecione a guia Cursos para ver a lista com nomes de departamentos.

Courses Index page

O método OnGetAsync carrega dados relacionados com o método Include. O método Select é uma alternativa que carrega apenas os dados relacionados necessários. Para itens únicos, como o Department.Name, ele usa um SQL INNER JOIN. Para coleções, ele usa outro acesso ao banco de dados, assim como o operador Include em coleções.

O seguinte código carrega dados relacionados com o método 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();
}

O código anterior não retorna nenhum tipo de entidade, portanto, nenhum acompanhamento é feito. Para obter mais informações sobre o acompanhamento do EF, consulte Comparação entre Consultas Com e Sem Acompanhamento.

CourseViewModel:

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

Consulte IndexSelect.cshtml e IndexSelect.cshtml.cs para obter um exemplo completo.

Criar as páginas de Instrutor

Esta seção aplica scaffold a páginas do Instrutor e adiciona Cursos e Inscrições relacionados à página Índice de Instrutores.

Instructors Index page

Essa página lê e exibe dados relacionados das seguintes maneiras:

  • A lista de instrutores exibe dados relacionados da entidade OfficeAssignment (Office na imagem anterior). As entidades Instructor e OfficeAssignment estão em uma relação um para zero ou um. O carregamento adiantado é usado para as entidades OfficeAssignment. O carregamento adiantado costuma ser mais eficiente quando os dados relacionados precisam ser exibidos. Nesse caso, as atribuições de escritório para os instrutores são exibidas.
  • Quando o usuário seleciona um instrutor, as entidades Course relacionadas são exibidas. As entidades Instructor e Course estão em uma relação muitos para muitos. O carregamento adiantado é usado para entidades Course e suas entidades Department relacionadas. Nesse caso, consultas separadas podem ser mais eficientes porque somente os cursos para o instrutor selecionado são necessários. Este exemplo mostra como usar o carregamento adiantado para propriedades de navegação em entidades que estão nas propriedades de navegação.
  • Quando o usuário seleciona um curso, dados relacionados da entidade Enrollments são exibidos. Na imagem anterior, o nome do aluno e a nota são exibidos. As entidades Course e Enrollment estão em uma relação um-para-muitos.

Criar um modelo de exibição

A página Instrutores mostra dados de três tabelas diferentes. É necessário um modelo de exibição que inclui três propriedades que representam as três tabelas.

Crie SchoolViewModels/InstructorIndexData.cs com o seguinte código:

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

Aplicar scaffold às páginas do Instrutor

  • Siga as instruções em Aplicar scaffold às páginas do aluno com as seguintes exceções:

    • Crie uma pasta Pages/Instructors.
    • Use Instructor para a classe de modelo.
    • Use a classe de contexto existente, em vez de criar uma nova.

Para ver a aparência da página com scaffold antes de atualizá-la, execute o aplicativo e navegue até a página Instrutores.

Atualize Pages/Instructors/Index.cshtml.cs com o seguinte código:

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

O método OnGetAsync aceita dados de rota opcionais para a ID do instrutor selecionado.

Examine a consulta no arquivo 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();

O código especifica o carregamento adiantado para as seguintes propriedades de navegação:

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

Observe a repetição dos métodos Include e ThenInclude para CourseAssignments e Course. Essa repetição é necessária para especificar o carregamento adiantado para duas propriedades de navegação da entidade Course.

O código a seguir é executado quando o instrutor é selecionado (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);
}

O instrutor selecionado é recuperado da lista de instrutores no modelo de exibição. Em seguida, a propriedade Courses do modelo de exibição é carregada com as entidades Course da propriedade de navegação CourseAssignments desse instrutor.

O método Where retorna uma coleção. Nesse caso, o filtro seleciona uma única entidade, portanto, o método Single é chamado para converter a coleção em uma única entidade Instructor. A entidade Instructor fornece acesso à propriedade CourseAssignments. CourseAssignments fornece acesso às entidades Course relacionadas.

Instructor-to-Courses m:M

O método Single é usado em uma coleção quando a coleção tem apenas um item. O método Single gera uma exceção se a coleção está vazia ou se há mais de um item. Uma alternativa é SingleOrDefault, que retorna um valor padrão (nulo, nesse caso) se a coleção está vazia.

O seguinte código popula a propriedade Enrollments do modelo de exibição quando um curso é selecionado:

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

Atualizar a página Índice de instrutores

Atualize Pages/Instructors/Index.cshtml com o seguinte código.

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

O código anterior faz as seguintes alterações:

  • Atualiza a diretiva page de @page para @page "{id:int?}". "{id:int?}" é um modelo de rota. O modelo de rota altera cadeias de consulta de inteiro na URL para dados de rota. Por exemplo, clicar no link Selecionar de um o instrutor apenas com a diretiva @page produz uma URL semelhante à seguinte:

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

    Quando a diretiva de página é @page "{id:int?}", a URL é:

    https://localhost:5001/Instructors/2

  • Adiciona uma coluna Escritório que exibirá item.OfficeAssignment.Location somente se item.OfficeAssignment não for nulo. Como essa é uma relação um para zero ou um, pode não haver uma entidade OfficeAssignment relacionada.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Adiciona uma coluna Cursos que exibe os cursos ministrados por cada instrutor. Consulte Transição de linha explícita para saber mais sobre essa sintaxe do Razor.

  • Adiciona um código que adiciona dinamicamente class="table-success" ao elemento tr do instrutor e do curso selecionados. Isso define uma cor da tela de fundo para a linha selecionada usando uma classe Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Adiciona um novo hiperlink rotulado Selecionar. Este link envia a ID do instrutor selecionado para o método Index e define uma cor da tela de fundo.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Adiciona uma tabela de cursos para o Instrutor selecionado.

  • Adiciona uma tabela de inscrições de alunos para o curso selecionado.

Execute o aplicativo e selecione a guia Instrutores. A página exibe o Location (Office) da entidade relacionada OfficeAssignment . Se OfficeAssignment for nulo, uma célula de tabela vazia será exibida.

Clique no link Selecionar para um instrutor. As alterações de estilo de linha e os cursos atribuídos a esse instrutor são exibidos.

Selecione um curso para ver a lista de alunos registrados e suas notas.

Instructors Index page instructor and course selected

Usando Single

O método Single pode passar a condição Where em vez de chamar o método Where separadamente:

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

O uso de Single com uma condição Where é uma questão de preferência pessoal. Não oferece nenhum benefício sobre o uso do método Where.

Carregamento explícito

O código atual especifica o carregamento adiantado para Enrollments e 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();

Suponha que os usuários raramente desejem ver registros em um curso. Nesse caso, uma otimização será carregar apenas os dados de registro se eles forem solicitados. Nesta seção, o OnGetAsync é atualizado para usar o carregamento explícito de Enrollments e Students.

Atualize Pages/Instructors/Index.cshtml.cs com o seguinte código.

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

O código anterior remove as chamadas do método ThenInclude para dados de registro e de alunos. Se um curso for selecionado, o código de carregamento explícito recuperará:

  • As entidades Enrollment para o curso selecionado.
  • As entidades Student para cada Enrollment.

Observe que o código anterior comenta .AsNoTracking(). As propriedades de navegação apenas podem ser carregadas de forma explícita para entidades controladas.

Testar o aplicativo. De uma perspectiva dos usuários, o aplicativo se comporta de forma idêntica à versão anterior.

Próximas etapas

O próximo tutorial mostra como atualizar os dados relacionados.

Neste tutorial, os dados relacionados são lidos e exibidos. Dados relacionados são dados que o EF Core carrega nas propriedades de navegação.

Caso tenha problemas que não consiga resolver, baixe ou exiba o aplicativo concluído.Baixe as instruções.

As seguintes ilustrações mostram as páginas concluídas para este tutorial:

Courses Index page

Instructors Index page

Há várias maneiras pelas quais o EF Core pode carregar dados relacionados nas propriedades de navegação de uma entidade:

  • Carregamento adiantado. O carregamento adiantado é quando uma consulta para um tipo de entidade também carrega entidades relacionadas. Quando a entidade é lida, seus dados relacionados são recuperados. Normalmente, isso resulta em uma única consulta de junção que recupera todos os dados necessários. O EF Core emitirá várias consultas para alguns tipos de carregamento adiantado. A emissão de várias consultas pode ser mais eficiente do que era o caso para algumas consultas no EF6 quando havia uma única consulta. O carregamento adiantado é especificado com os métodos Include e ThenInclude.

    Eager loading example

    O carregamento adiantado envia várias consultas quando a navegação de coleção é incluída:

    • Uma consulta para a consulta principal
    • Uma consulta para cada "borda" de coleção na árvore de carregamento.
  • Separe consultas com Load: os dados podem ser recuperados em consultas separadas e o EF Core "corrige" as propriedades de navegação. "Correção" significa que o EF Core preenche automaticamente as propriedades de navegação. A separação de consultas com Load é mais parecida com o carregamento explícito do que com o carregamento adiantado.

    Separate queries example

    Observação: o EF Core corrige automaticamente as propriedades de navegação para outras entidades que foram carregadas anteriormente na instância do contexto. Mesmo se os dados de uma propriedade de navegação não foram incluídos de forma explícita, a propriedade ainda pode ser populada se algumas ou todas as entidades relacionadas foram carregadas anteriormente.

  • Carregamento explícito. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. Um código precisa ser escrito para recuperar os dados relacionados quando eles forem necessários. O carregamento explícito com consultas separadas resulta no envio de várias consultas ao BD. Com o carregamento explícito, o código especifica as propriedades de navegação a serem carregadas. Use o método Load para fazer o carregamento explícito. Por exemplo:

    Explicit loading example

  • Carregamento lento. O carregamento lento foi adicionado ao na versão 2.1EF Core. Quando a entidade é lida pela primeira vez, os dados relacionados não são recuperados. Na primeira vez que uma propriedade de navegação é acessada, os dados necessários para essa propriedade de navegação são recuperados automaticamente. Uma consulta é enviada para o BD sempre que uma propriedade de navegação é acessada pela primeira vez.

  • O operador Select carrega somente os dados relacionados necessários.

Criar uma página Course que exibe o nome do departamento

A entidade Course inclui uma propriedade de navegação que contém a entidade Department. A entidade Department contém o departamento ao qual o curso é atribuído.

Para exibir o nome do departamento atribuído em uma lista de cursos:

  • Obtenha a propriedade Name da entidade Department.
  • A entidade Department é obtida da propriedade de navegação Course.Department.

Course.Department

Gerar o modelo Curso por scaffolding

Siga as instruções em Gere um modelo de aluno por scaffold e use Course para a classe de modelo.

O comando anterior gera o modelo Course por scaffolding. Abra o projeto no Visual Studio.

Abra Pages/Courses/Index.cshtml.cs e examine o método OnGetAsync. O mecanismo de scaffolding especificou o carregamento adiantado para a propriedade de navegação Department. O método Include especifica o carregamento adiantado.

Execute o aplicativo e selecione o link Cursos. A coluna de departamento exibe a DepartmentID, que não é útil.

Atualize o método OnGetAsync pelo seguinte código:

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

O código anterior adiciona AsNoTracking. AsNoTracking melhora o desempenho porque as entidades retornadas não são controladas. As entidades não são controladas porque elas não são atualizadas no contexto atual.

Atualize o Pages/Courses/Index.cshtml com o seguinte código realçado:

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

As seguintes alterações foram feitas na biblioteca gerada por código em scaffolding:

  • Alterou o cabeçalho de Índice para Cursos.

  • Adicionou uma coluna Número que mostra o valor da propriedade CourseID. Por padrão, as chaves primárias não são geradas por scaffolding porque normalmente não têm sentido para os usuários finais. No entanto, nesse caso, a chave primária é significativa.

  • Alterou a coluna Departamento para que ela exiba o nome de departamento. O código exibe a propriedade Name da entidade Department que é carregada na propriedade de navegação Department:

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

Execute o aplicativo e selecione a guia Cursos para ver a lista com nomes de departamentos.

Courses Index page

O método OnGetAsync carrega dados relacionados com o método Include:

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

O operador Select carrega somente os dados relacionados necessários. Para itens únicos, como o Department.Name, ele usa um SQL INNER JOIN. Para coleções, ele usa outro acesso ao banco de dados, assim como o operador Include em coleções.

O seguinte código carrega dados relacionados com o método 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; }
}

Consulte IndexSelect.cshtml e IndexSelect.cshtml.cs para obter um exemplo completo.

Criar uma página Instrutores que mostra Cursos e Registros

Nesta seção, a página Instrutores é criada.

Instructors Index page

Essa página lê e exibe dados relacionados das seguintes maneiras:

  • A lista de instrutores exibe dados relacionados da entidade OfficeAssignment (Office na imagem anterior). As entidades Instructor e OfficeAssignment estão em uma relação um para zero ou um. O carregamento adiantado é usado para as entidades OfficeAssignment. O carregamento adiantado costuma ser mais eficiente quando os dados relacionados precisam ser exibidos. Nesse caso, as atribuições de escritório para os instrutores são exibidas.
  • Quando o usuário seleciona um instrutor (Pedro na imagem anterior), as entidades Course relacionadas são exibidas. As entidades Instructor e Course estão em uma relação muitos para muitos. O carregamento adiantado é usado para entidades Course e suas entidades Department relacionadas. Nesse caso, consultas separadas podem ser mais eficientes porque somente os cursos para o instrutor selecionado são necessários. Este exemplo mostra como usar o carregamento adiantado para propriedades de navegação em entidades que estão nas propriedades de navegação.
  • Quando o usuário seleciona um curso (Química na imagem anterior), os dados relacionados da entidade Enrollments são exibidos. Na imagem anterior, o nome do aluno e a nota são exibidos. As entidades Course e Enrollment estão em uma relação um-para-muitos.

Criar um modelo de exibição para a exibição Índice de Instrutor

A página Instrutores mostra dados de três tabelas diferentes. É criado um modelo de exibição que inclui as três entidades que representam as três tabelas.

Na pasta SchoolViewModels, adicione o InstructorIndexData.cs com o seguinte código:

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

Gerar o modelo Instrutor por scaffolding

Siga as instruções em Gere um modelo de aluno por scaffold e use Instructor para a classe de modelo.

O comando anterior gera o modelo Instructor por scaffolding. Execute o aplicativo e navegue para a página Instrutores.

Substitua Pages/Instructors/Index.cshtml.cs pelo seguinte código:

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

O método OnGetAsync aceita dados de rota opcionais para a ID do instrutor selecionado.

Examine a consulta no arquivo 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();

A consulta tem duas inclusões:

Atualizar a página Índice de instrutores

Atualize o Pages/Instructors/Index.cshtml com a seguinte marcação:

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

A marcação anterior faz as seguintes alterações:

  • Atualiza a diretiva page de @page para @page "{id:int?}". "{id:int?}" é um modelo de rota. O modelo de rota altera cadeias de consulta de inteiro na URL para dados de rota. Por exemplo, clicar no link Selecionar de um o instrutor apenas com a diretiva @page produz uma URL semelhante à seguinte:

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

    Quando a diretiva de página é @page "{id:int?}", a URL anterior é:

    http://localhost:1234/Instructors/2

  • O título de página é Instrutores.

  • Adicionou uma coluna Office que exibe item.OfficeAssignment.Location somente se item.OfficeAssignment não é nulo. Como essa é uma relação um para zero ou um, pode não haver uma entidade OfficeAssignment relacionada.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Adicionou uma coluna Courses que exibe os cursos ministrados por cada instrutor. Consulte Transição de linha explícita para saber mais sobre essa sintaxe do Razor.

  • Adicionou um código que adiciona class="success" dinamicamente ao elemento tr do instrutor selecionado. Isso define uma cor da tela de fundo para a linha selecionada usando uma classe Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • Adicionou um novo hiperlink rotulado Selecionar. Este link envia a ID do instrutor selecionado para o método Index e define uma cor da tela de fundo.

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

Execute o aplicativo e selecione a guia Instrutores. A página exibe o Location (Office) da entidade relacionada OfficeAssignment . Se OfficeAssignment é nulo, uma célula de tabela vazia é exibida.

Clique no link Selecionar. O estilo de linha é alterado.

Adicionar cursos ministrados pelo instrutor selecionado

Substitua o método OnGetAsync em Pages/Instructors/Index.cshtml.cs pelo seguinte código:

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

Adicione public int CourseID { get; set; }

public class IndexModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public IndexModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    public InstructorIndexData Instructor { get; set; }
    public int InstructorID { get; set; }
    public int CourseID { get; set; }

    public async Task OnGetAsync(int? id, int? courseID)
    {
        Instructor = new InstructorIndexData();
        Instructor.Instructors = await _context.Instructors
              .Include(i => i.OfficeAssignment)
              .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Department)
              .AsNoTracking()
              .OrderBy(i => i.LastName)
              .ToListAsync();

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

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

Examine a consulta atualizada:

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

A consulta anterior adiciona as entidades Department.

O código a seguir é executado quando o instrutor é selecionado (id != null). O instrutor selecionado é recuperado da lista de instrutores no modelo de exibição. Em seguida, a propriedade Courses do modelo de exibição é carregada com as entidades Course da propriedade de navegação CourseAssignments desse instrutor.

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

O método Where retorna uma coleção. No método Where anterior, uma única entidade Instructor é retornada. O método Single converte a coleção em uma única entidade Instructor. A entidade Instructor fornece acesso à propriedade CourseAssignments. CourseAssignments fornece acesso às entidades Course relacionadas.

Instructor-to-Courses m:M

O método Single é usado em uma coleção quando a coleção tem apenas um item. O método Single gera uma exceção se a coleção está vazia ou se há mais de um item. Uma alternativa é SingleOrDefault, que retorna um valor padrão (nulo, nesse caso) se a coleção está vazia. O uso de SingleOrDefault é uma coleção vazia:

  • Resulta em uma exceção (da tentativa de encontrar uma propriedade Courses em uma referência nula).
  • A mensagem de exceção indica menos claramente a causa do problema.

O seguinte código popula a propriedade Enrollments do modelo de exibição quando um curso é selecionado:

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

Adicione a seguinte marcação ao final da Página do 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>
}

A marcação anterior exibe uma lista de cursos relacionados a um instrutor quando um instrutor é selecionado.

Testar o aplicativo. Clique em um link Selecionar na página Instrutores.

Mostrar dados de alunos

Nesta seção, o aplicativo é atualizado para mostrar os dados de alunos de um curso selecionado.

Substitua a consulta do método OnGetAsync em Pages/Instructors/Index.cshtml.cs pelo seguinte código:

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

Atualizar Pages/Instructors/Index.cshtml. Adicione a seguinte marcação ao final do arquivo:


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

A marcação anterior exibe uma lista dos alunos registrados no curso selecionado.

Atualize a página e selecione um instrutor. Selecione um curso para ver a lista de alunos registrados e suas notas.

Instructors Index page instructor and course selected

Usando Single

O método Single pode passar a condição Where em vez de chamar o método Where separadamente:

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

A abordagem Single anterior não oferece nenhum benefício em relação ao uso de Where. Alguns desenvolvedores preferem o estilo de abordagem Single.

Carregamento explícito

O código atual especifica o carregamento adiantado para Enrollments e 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();

Suponha que os usuários raramente desejem ver registros em um curso. Nesse caso, uma otimização será carregar apenas os dados de registro se eles forem solicitados. Nesta seção, o OnGetAsync é atualizado para usar o carregamento explícito de Enrollments e Students.

Atualize o OnGetAsync com o seguinte código:

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

O código anterior remove as chamadas do método ThenInclude para dados de registro e de alunos. Se um curso é selecionado, o código realçado recupera:

  • As entidades Enrollment para o curso selecionado.
  • As entidades Student para cada Enrollment.

Observe que o código anterior comenta .AsNoTracking(). As propriedades de navegação apenas podem ser carregadas de forma explícita para entidades controladas.

Testar o aplicativo. De uma perspectiva dos usuários, o aplicativo se comporta de forma idêntica à versão anterior.

O próximo tutorial mostra como atualizar os dados relacionados.

Recursos adicionais