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:
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.
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 comLoad
é mais parecida com o carregamento explícito do que com o carregamento adiantado.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: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.
Para exibir o nome do departamento atribuído para um curso:
- Carregue a entidade relacionada
Department
na propriedade de navegaçãoCourse.Department
. - Obtenha o nome da propriedade
Department
da entidadeName
.
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étodoOnGetAsync
. O mecanismo de scaffolding especificou o carregamento adiantado para a propriedade de navegaçãoDepartment
. O métodoInclude
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 paraCourses
.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 entidadeDepartment
que é carregada na propriedade de navegaçãoDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Execute o aplicativo e selecione a guia Cursos para ver a lista com nomes de departamentos.
Carregando dados relacionados com Select
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.
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 entidadesInstructor
eOfficeAssignment
estão em uma relação um para zero ou um. O carregamento adiantado é usado para as entidadesOfficeAssignment
. 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 entidadesInstructor
eCourse
estão em uma relação muitos para muitos. O carregamento adiantado é usado para entidadesCourse
e suas entidadesDepartment
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 entidadesCourse
eEnrollment
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 seitem.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 elementotr
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.
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:
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
eThenInclude
.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 comLoad
é mais parecida com o carregamento explícito do que com o carregamento adiantado.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: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.
Para exibir o nome do departamento atribuído para um curso:
- Carregue a entidade relacionada
Department
na propriedade de navegaçãoCourse.Department
. - Obtenha o nome da propriedade
Department
da entidadeName
.
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étodoOnGetAsync
. O mecanismo de scaffolding especificou o carregamento adiantado para a propriedade de navegaçãoDepartment
. O métodoInclude
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 paraCourses
.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 entidadeDepartment
que é carregada na propriedade de navegaçãoDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Execute o aplicativo e selecione a guia Cursos para ver a lista com nomes de departamentos.
Carregando dados relacionados com Select
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.
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 entidadesInstructor
eOfficeAssignment
estão em uma relação um para zero ou um. O carregamento adiantado é usado para as entidadesOfficeAssignment
. 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 entidadesInstructor
eCourse
estão em uma relação muitos para muitos. O carregamento adiantado é usado para entidadesCourse
e suas entidadesDepartment
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 entidadesCourse
eEnrollment
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.
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 seitem.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 elementotr
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.
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 cadaEnrollment
.
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. Instruções de download.
As seguintes ilustrações mostram as páginas concluídas para este tutorial:
Carregamento adiantado, explícito e lento de dados relacionados
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
eThenInclude
.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 comLoad
é mais parecida com o carregamento explícito do que com o carregamento adiantado.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: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 entidadeDepartment
. - A entidade
Department
é obtida da propriedade de navegaçãoCourse.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 entidadeDepartment
que é carregada na propriedade de navegaçãoDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Execute o aplicativo e selecione a guia Cursos para ver a lista com nomes de departamentos.
Carregando dados relacionados com Select
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.
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 entidadesInstructor
eOfficeAssignment
estão em uma relação um para zero ou um. O carregamento adiantado é usado para as entidadesOfficeAssignment
. 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 entidadesInstructor
eCourse
estão em uma relação muitos para muitos. O carregamento adiantado é usado para entidadesCourse
e suas entidadesDepartment
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 entidadesCourse
eEnrollment
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:
OfficeAssignment
: exibido na exibição de instrutores.CourseAssignments
: que exibe os cursos ministrados.
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 seitem.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 elementotr
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.
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.cshtml
Razor:
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.Instructor.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Instructor.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
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.
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 cadaEnrollment
.
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.