Parte 6. Razor Pages con EF Core en EF Core: Lectura de datos relacionados
Por Tom Dykstra, Jon P Smith y Rick Anderson
En la aplicación web Contoso University se muestra cómo crear aplicaciones web Razor Pages con EF Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
Si surgen problemas que no puede resolver, descargue la aplicación completada y compare ese código con el que ha creado siguiendo el tutorial.
En este tutorial se muestra cómo leer y mostrar datos relacionados. Los datos relacionados son los que EF Core carga en las propiedades de navegación.
En las ilustraciones siguientes se muestran las páginas completadas para este tutorial:
Carga diligente, explícita y diferida
EF Core puede cargar datos relacionados en las propiedades de navegación de una entidad de varias maneras:
Carga diligente. La carga diligente es cuando una consulta para un tipo de entidad también carga las entidades relacionadas. Cuando se lee una entidad, se recuperan sus datos relacionados. Esto normalmente da como resultado una única consulta de combinación en la que se recuperan todos los datos que se necesitan. EF Core emitirá varias consultas para algunos tipos de carga diligente. La emisión de varias consultas puede ser más eficaz que una sola consulta grande. La carga diligente se especifica con los métodos Include y ThenInclude.
La carga diligente envía varias consultas cuando se incluye una propiedad de navegación de colección:
- Una consulta para la consulta principal
- Una consulta para cada colección "perimetral" en el árbol de la carga.
Separe las consultas con
Load
: los datos se pueden recuperar en distintas consultas y EF Core "corrige" las propiedades de navegación. "Corregir" significa que EF Core rellena de forma automática las propiedades de navegación. La separación de las consultas conLoad
es más parecido a la carga explícita que a la carga diligente.Nota:EF Core corrige automáticamente las propiedades de navegación para todas las entidades que se cargaron previamente en la instancia del contexto. Incluso si los datos de una propiedad de navegación no se incluyen explícitamente, es posible que la propiedad se siga rellenando si algunas o todas las entidades relacionadas se cargaron previamente.
Carga explícita. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Se debe escribir código para recuperar los datos relacionados cuando sea necesario. La carga explícita con consultas independientes da como resultado varias consultas que se envían a la base de datos. Con la carga explícita, el código especifica las propiedades de navegación que se van a cargar. Use el método
Load
para realizar la carga explícita. Por ejemplo:Carga diferida. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. La primera vez que se obtiene acceso a una propiedad de navegación, se recuperan automáticamente los datos necesarios para esa propiedad de navegación. Cada vez que se accede por primera vez a una propiedad de navegación, se envía una consulta a la base de datos. La carga diferida puede perjudicar el rendimiento, por ejemplo, cuando los desarrolladores usan consultas N+1. Las consultas N+1 cargan un elemento primario y se enumeran a través de elementos secundarios.
Creación de las páginas Course
La entidad Course
incluye una propiedad de navegación que contiene la entidad Department
relacionada.
Para mostrar el nombre del departamento asignado a un curso:
- Cargue la entidad
Department
relacionada en la propiedad de navegaciónCourse.Department
. - Obtenga el nombre de la propiedad
Name
de la entidadDepartment
.
Scaffolding de las páginas Course
Siga las instrucciones de Scaffolding de las páginas Student con las siguientes excepciones:
- Cree una carpeta Pages/Courses.
- Use
Course
para la clase del modelo. - Use la clase de contexto existente en lugar de crear una.
Abra
Pages/Courses/Index.cshtml.cs
y examine el métodoOnGetAsync
. El motor de scaffolding especificado realiza la carga diligente de la propiedad de navegaciónDepartment
. El métodoInclude
especifica la carga diligente.Ejecute la aplicación y haga clic en el vínculo Courses. En la columna Department se muestra el
DepartmentID
, lo que no resulta útil.
Representación del nombre del departamento
Actualice Pages/Courses/Index.cshtml.cs con el código siguiente:
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();
}
}
}
En el código anterior se cambia la propiedad Course
a Courses
y se agrega AsNoTracking
.
Las consultas de no seguimiento son útiles cuando los resultados se usan en un escenario de solo lectura. Su ejecución suele ser más rápida porque no es necesario configurar la información de seguimiento de cambios. Si no es necesario actualizar las entidades recuperadas de la base de datos, es probable que una consulta de no seguimiento funcione mejor que una consulta de seguimiento.
En algunos casos, una consulta de seguimiento es más eficaz que una consulta de no seguimiento. Para obtener más información, vea Consultas de seguimiento frente a consultas de no seguimiento.
En el código anterior, se llama a AsNoTracking
porque las entidades no se actualizan en el contexto actual.
Actualice Pages/Courses/Index.cshtml
con el código siguiente.
@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>
Se han realizado los cambios siguientes en el código con scaffolding:
Se ha cambiado el nombre de la propiedad
Course
aCourses
.Ha agregado una columna Number en la que se muestra el valor de propiedad
CourseID
. De forma predeterminada, las claves principales no tienen scaffolding porque normalmente no tienen sentido para los usuarios finales. Pero en este caso, la clave principal es significativa.Ha cambiado la columna Department para mostrar el nombre del departamento. El código muestra la propiedad
Name
de la entidadDepartment
que se carga en la propiedad de navegaciónDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Ejecute la aplicación y haga clic en la pestaña Courses para ver la lista con los nombres de departamento.
Carga de datos relacionados con Select
El método OnGetAsync
carga los datos relacionados con el método Include
. El método Select
es una alternativa que solo carga los datos relacionados necesarios. Para elementos individuales, como el Department.Name
, se usa SQL INNER JOIN
. En las colecciones, se usa otro acceso de base de datos, como también hace el operador Include
en las colecciones.
En el código siguiente se cargan los datos relacionados con el 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();
}
El código anterior no devuelve ningún tipo de entidad, por lo que no se realiza ningún seguimiento. Para más información sobre el seguimiento de EF, consulte Consultas de seguimiento frente a consultas de no seguimiento.
El 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 obtener la instancia de Razor Pages completa.
Creación de las páginas Instructor
Esta sección se aplica scaffolding a las páginas Instructor y se agregan entidades Course y Enrollment a la página Instructors Index (índice de instructores).
En esta página se leen y muestran los datos relacionados de las maneras siguientes:
- En la lista de instructores se muestran datos relacionados de la entidad
OfficeAssignment
(Office en la imagen anterior). Las entidadesInstructor
yOfficeAssignment
se encuentran en una relación de uno a cero o uno. Para las entidadesOfficeAssignment
se usa la carga diligente. Normalmente la carga diligente es más eficaz cuando es necesario mostrar los datos relacionados. En este caso, se muestran las asignaciones de oficina para los instructores. - Cuando el usuario selecciona un instructor, se muestran las entidades
Course
relacionadas. Las entidadesInstructor
yCourse
se encuentran en una relación de varios a varios. La carga diligente se usa con las entidadesCourse
y sus entidadesDepartment
relacionadas. En este caso, es posible que las consultas independientes sean más eficaces porque solo se necesitan cursos para el instructor seleccionado. En este ejemplo se muestra cómo usar la carga diligente para propiedades de navegación en entidades que se encuentran en propiedades de navegación. - Cuando el usuario selecciona un curso, se muestran los datos relacionados de la entidad
Enrollments
. En la imagen anterior, se muestra el nombre del alumno y la calificación. Las entidadesCourse
yEnrollment
se encuentran en una relación uno a varios.
Creación de un modelo de vista
En la página Instructors se muestran datos de tres tablas diferentes. Se necesita un modelo de vista que incluya tres entidades que representen las tres tablas.
Cree el archivo Models/SchoolViewModels/InstructorIndexData.cs
con el siguiente 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; }
}
}
Scaffolding de las páginas Instructor
Siga las instrucciones de Scaffolding de las páginas Student con las siguientes excepciones:
- Cree una carpeta Pages/Instructors.
- Use
Instructor
para la clase del modelo. - Use la clase de contexto existente en lugar de crear una.
Ejecute la aplicación y vaya a la página Instructores.
Actualice Pages/Instructors/Index.cshtml.cs
con el siguiente 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;
}
}
}
}
El método OnGetAsync
acepta datos de ruta opcionales para el identificador del instructor seleccionado.
Examine la consulta en el archivo 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();
El código especifica la carga diligente para las propiedades de navegación siguientes:
Instructor.OfficeAssignment
Instructor.Courses
Course.Department
El código siguiente se ejecuta cuando se selecciona un instructor, es decir, id != null
.
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}
El instructor seleccionado se recupera de la lista de instructores del modelo de vista. Se carga la propiedad Courses
del modelo de vista con las entidades Course
de la propiedad de navegación Courses
del instructor seleccionado.
El método Where
devuelve una colección. En este caso, el filtro seleccionará una sola entidad, por lo que se llamará al método Single
para convertir la colección en una sola entidad Instructor
. La entidad Instructor
proporciona acceso a la propiedad de navegación Course
.
El método Single se usa en una colección cuando la colección tiene un solo elemento. El método Single
inicia una excepción si la colección está vacía o hay más de un elemento. Una alternativa es SingleOrDefault, que devuelve una valor predeterminado si la colección está vacía. En esta consulta, null
en el valor predeterminado devuelto.
El código siguiente rellena la propiedad Enrollments
del modelo de vista cuando se selecciona un curso:
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;
}
Actualizar la página de índice de instructores
Actualice Pages/Instructors/Index.cshtml
con el código siguiente.
@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>
}
En el código anterior se realizan los cambios siguientes:
Actualiza la directiva
page
a@page "{id:int?}"
."{id:int?}"
es una plantilla de ruta. La plantilla de ruta cambia las cadenas de consulta enteras de la dirección URL por datos de ruta. Por ejemplo, al hacer clic en el vínculo Select de un instructor con únicamente la directiva@page
, se genera una dirección URL similar a la siguiente:https://localhost:5001/Instructors?id=2
Cuando la directiva de página es
@page "{id:int?}"
, la dirección URL es:https://localhost:5001/Instructors/2
Se agrega una columna Office en la que se muestra
item.OfficeAssignment.Location
solo siitem.OfficeAssignment
no es NULL. Dado que se trata de una relación de uno a cero o uno, es posible que no haya una entidad OfficeAssignment relacionada.@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Se agrega una columna Courses en la que se muestran los cursos que imparte cada instructor. Para obtener más información sobre esta sintaxis razor, consulta Transición de línea explícita.
Se agrega código que agrega de forma dinámica
class="table-success"
al elementotr
del instructor y curso seleccionados. Esto establece el color de fondo de la fila seleccionada mediante una clase de arranque.string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "table-success"; } <tr class="@selectedRow">
Se agrega un hipervínculo nuevo con la etiqueta Select. Este vínculo envía el identificador del instructor seleccionado al método
Index
y establece un color de fondo.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Se agrega una tabla de cursos para el instructor seleccionado.
Se agrega una tabla de inscripciones de alumnos para el curso seleccionado.
Ejecuta la aplicación y haz clic en la pestaña Instructors. En la página se muestra la Location
(oficina) de la entidad OfficeAssignment
relacionada. Si OfficeAssignment
es NULL, se muestra una celda de tabla vacía.
Haz clic en el vínculo Select de un instructor. Se muestran los cambios de estilo de fila y los cursos asignados a ese instructor.
Selecciona un curso para ver la lista de los estudiantes inscritos y sus calificaciones.
Pasos siguientes
En el siguiente tutorial se muestra cómo actualizar datos relacionados.
En este tutorial se muestra cómo leer y mostrar datos relacionados. Los datos relacionados son los que EF Core carga en las propiedades de navegación.
En las ilustraciones siguientes se muestran las páginas completadas para este tutorial:
Carga diligente, explícita y diferida
EF Core puede cargar datos relacionados en las propiedades de navegación de una entidad de varias maneras:
Carga diligente. La carga diligente es cuando una consulta para un tipo de entidad también carga las entidades relacionadas. Cuando se lee una entidad, se recuperan sus datos relacionados. Esto normalmente da como resultado una única consulta de combinación en la que se recuperan todos los datos que se necesitan. EF Core emitirá varias consultas para algunos tipos de carga diligente. La emisión de varias consultas puede ser más eficaz que una sola consulta gigante. La carga diligente se especifica con los métodos
Include
yThenInclude
.La carga diligente envía varias consultas cuando se incluye una propiedad de navegación de colección:
- Una consulta para la consulta principal
- Una consulta para cada colección "perimetral" en el árbol de la carga.
Separa las consultas con
Load
: los datos se pueden recuperar en distintas consultas y EF Core "corrige" las propiedades de navegación. "Corregir" significa que EF Core rellena de forma automática las propiedades de navegación. La separación de las consultas conLoad
es más parecido a la carga explícita que a la carga diligente.Nota:EF Core corrige automáticamente las propiedades de navegación para todas las entidades que se cargaron previamente en la instancia del contexto. Incluso si los datos de una propiedad de navegación no se incluyen explícitamente, es posible que la propiedad se siga rellenando si algunas o todas las entidades relacionadas se cargaron previamente.
Carga explícita. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Se debe escribir código para recuperar los datos relacionados cuando sea necesario. La carga explícita con consultas independientes da como resultado varias consultas que se envían a la base de datos. Con la carga explícita, el código especifica las propiedades de navegación que se van a cargar. Use el método
Load
para realizar la carga explícita. Por ejemplo:Carga diferida. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. La primera vez que se obtiene acceso a una propiedad de navegación, se recuperan automáticamente los datos necesarios para esa propiedad de navegación. Cada vez que se accede por primera vez a una propiedad de navegación, se envía una consulta a la base de datos. La carga diferida puede afectar negativamente al rendimiento, por ejemplo, cuando los desarrolladores usan patrones N+1, al cargar un elemento primario y enumerar los elementos secundarios.
Creación de las páginas Course
La entidad Course
incluye una propiedad de navegación que contiene la entidad Department
relacionada.
Para mostrar el nombre del departamento asignado a un curso:
- Cargue la entidad
Department
relacionada en la propiedad de navegaciónCourse.Department
. - Obtenga el nombre de la propiedad
Name
de la entidadDepartment
.
Scaffolding de las páginas Course
Siga las instrucciones de Scaffolding de las páginas Student con las siguientes excepciones:
- Cree una carpeta Pages/Courses.
- Use
Course
para la clase del modelo. - Use la clase de contexto existente en lugar de crear una.
Abra
Pages/Courses/Index.cshtml.cs
y examine el métodoOnGetAsync
. El motor de scaffolding especificado realiza la carga diligente de la propiedad de navegaciónDepartment
. El métodoInclude
especifica la carga diligente.Ejecute la aplicación y haga clic en el vínculo Courses. En la columna Department se muestra el
DepartmentID
, lo que no resulta útil.
Representación del nombre del departamento
Actualice Pages/Courses/Index.cshtml.cs con el código siguiente:
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();
}
}
}
En el código anterior se cambia la propiedad Course
a Courses
y se agrega AsNoTracking
. AsNoTracking
mejora el rendimiento porque no se realiza el seguimiento de las entidades devueltas. No es necesario realizar el seguimiento de las entidades porque no se actualizan en el contexto actual.
Actualice Pages/Courses/Index.cshtml
con el código siguiente.
@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>
Se han realizado los cambios siguientes en el código con scaffolding:
Se ha cambiado el nombre de la propiedad
Course
aCourses
.Ha agregado una columna Number en la que se muestra el valor de propiedad
CourseID
. De forma predeterminada, las claves principales no tienen scaffolding porque normalmente no tienen sentido para los usuarios finales. Pero en este caso, la clave principal es significativa.Ha cambiado la columna Department para mostrar el nombre del departamento. El código muestra la propiedad
Name
de la entidadDepartment
que se carga en la propiedad de navegaciónDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Ejecute la aplicación y haga clic en la pestaña Courses para ver la lista con los nombres de departamento.
Carga de datos relacionados con Select
El método OnGetAsync
carga los datos relacionados con el método Include
. El método Select
es una alternativa que solo carga los datos relacionados necesarios. Para elementos individuales, como el Department.Name
, se usa INNER JOIN de SQL. En las colecciones, se usa otro acceso de base de datos, como también hace el operador Include
en las colecciones.
En el código siguiente se cargan los datos relacionados con el 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();
}
El código anterior no devuelve ningún tipo de entidad, por lo que no se realiza ningún seguimiento. Para más información sobre el seguimiento de EF, consulte Consultas de seguimiento frente a consultas de no seguimiento.
El CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
Vea IndexSelect.cshtml e IndexSelect.cshtml.cs para obtener un ejemplo completo.
Creación de las páginas Instructor
Esta sección se aplica scaffolding a las páginas Instructor y se agregan entidades Course y Enrollment a la página Instructors Index (índice de instructores).
En esta página se leen y muestran los datos relacionados de las maneras siguientes:
- En la lista de instructores se muestran datos relacionados de la entidad
OfficeAssignment
(Office en la imagen anterior). Las entidadesInstructor
yOfficeAssignment
se encuentran en una relación de uno a cero o uno. Para las entidadesOfficeAssignment
se usa la carga diligente. Normalmente la carga diligente es más eficaz cuando es necesario mostrar los datos relacionados. En este caso, se muestran las asignaciones de oficina para los instructores. - Cuando el usuario selecciona un instructor, se muestran las entidades
Course
relacionadas. Las entidadesInstructor
yCourse
se encuentran en una relación de varios a varios. La carga diligente se usa con las entidadesCourse
y sus entidadesDepartment
relacionadas. En este caso, es posible que las consultas independientes sean más eficaces porque solo se necesitan cursos para el instructor seleccionado. En este ejemplo se muestra cómo usar la carga diligente para propiedades de navegación en entidades que se encuentran en propiedades de navegación. - Cuando el usuario selecciona un curso, se muestran los datos relacionados de la entidad
Enrollments
. En la imagen anterior, se muestra el nombre del alumno y la calificación. Las entidadesCourse
yEnrollment
se encuentran en una relación uno a varios.
Creación de un modelo de vista
En la página Instructors se muestran datos de tres tablas diferentes. Se necesita un modelo de vista que incluya tres entidades que representen las tres tablas.
Cree el archivo SchoolViewModels/InstructorIndexData.cs
con el siguiente 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; }
}
}
Scaffolding de las páginas Instructor
Siga las instrucciones de Scaffolding de las páginas Student con las siguientes excepciones:
- Cree una carpeta Pages/Instructors.
- Use
Instructor
para la clase del modelo. - Use la clase de contexto existente en lugar de crear una.
Para ver el aspecto de la página con scaffolding antes de actualizarla, ejecute la aplicación y vaya a la página de instructores.
Actualice Pages/Instructors/Index.cshtml.cs
con el siguiente 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;
}
}
}
}
El método OnGetAsync
acepta datos de ruta opcionales para el identificador del instructor seleccionado.
Examine la consulta en el archivo 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();
El código especifica la carga diligente para las propiedades de navegación siguientes:
Instructor.OfficeAssignment
Instructor.CourseAssignments
CourseAssignments.Course
Course.Department
Course.Enrollments
Enrollment.Student
Observe la repetición de los métodos Include
y ThenInclude
para CourseAssignments
y Course
. Esta repetición es necesaria para especificar la carga diligente para dos propiedades de navegación de la entidad Course
.
El código siguiente se ejecuta cuando se selecciona un instructor (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);
}
El instructor seleccionado se recupera de la lista de instructores del modelo de vista. Se carga la propiedad Courses
del modelo de vista con las entidades Course
de la propiedad de navegación CourseAssignments
de ese instructor.
El método Where
devuelve una colección. Sin embargo, en este caso, el filtro seleccionará una sola entidad, por lo que se llamará al método Single
para convertir la colección en una sola entidad Instructor
. La entidad Instructor
proporciona acceso a la propiedad CourseAssignments
. CourseAssignments
proporciona acceso a las entidades Course
relacionadas.
El método Single
se usa en una colección cuando la colección tiene un solo elemento. El método Single
inicia una excepción si la colección está vacía o hay más de un elemento. Una alternativa es SingleOrDefault
, que devuelve una valor predeterminado (NULL, en este caso) si la colección está vacía.
El código siguiente rellena la propiedad Enrollments
del modelo de vista cuando se selecciona un curso:
if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = InstructorData.Courses
.Where(x => x.CourseID == courseID).Single();
InstructorData.Enrollments = selectedCourse.Enrollments;
}
Actualizar la página de índice de instructores
Actualice Pages/Instructors/Index.cshtml
con el código siguiente.
@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>
}
En el código anterior se realizan los cambios siguientes:
Se actualiza la directiva
page
de@page
a@page "{id:int?}"
."{id:int?}"
es una plantilla de ruta. La plantilla de ruta cambia las cadenas de consulta enteras de la dirección URL por datos de ruta. Por ejemplo, al hacer clic en el vínculo Select de un instructor con únicamente la directiva@page
, se genera una dirección URL similar a la siguiente:https://localhost:5001/Instructors?id=2
Cuando la directiva de página es
@page "{id:int?}"
, la dirección URL es:https://localhost:5001/Instructors/2
Se agrega una columna Office en la que se muestra
item.OfficeAssignment.Location
solo siitem.OfficeAssignment
no es NULL. Dado que se trata de una relación de uno a cero o uno, es posible que no haya una entidad OfficeAssignment relacionada.@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Se agrega una columna Courses en la que se muestran los cursos que imparte cada instructor. Para obtener más información sobre esta sintaxis razor, consulta Transición de línea explícita.
Se agrega código que agrega de forma dinámica
class="table-success"
al elementotr
del instructor y curso seleccionados. Esto establece el color de fondo de la fila seleccionada mediante una clase de arranque.string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "table-success"; } <tr class="@selectedRow">
Se agrega un hipervínculo nuevo con la etiqueta Select. Este vínculo envía el identificador del instructor seleccionado al método
Index
y establece un color de fondo.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Se agrega una tabla de cursos para el instructor seleccionado.
Se agrega una tabla de inscripciones de alumnos para el curso seleccionado.
Ejecute la aplicación y haga clic en la pestaña Instructors. En la página se muestra la Location
(oficina) de la entidad OfficeAssignment
relacionada. Si OfficeAssignment
es NULL, se muestra una celda de tabla vacía.
Haga clic en el vínculo Select de un instructor. Se muestran los cambios de estilo de fila y los cursos asignados a ese instructor.
Seleccione un curso para ver la lista de los estudiantes inscritos y sus calificaciones.
Uso de Single
Se puede pasar el método Single
en la condición Where
en lugar de llamar al método Where
por separado:
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;
}
}
El uso de Single
con una condición Where es una cuestión de preferencia personal. No proporciona ninguna ventaja sobre el uso del método Where
.
Carga explícita
En el código actual se especifica la carga diligente para Enrollments
y 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();
Imagine que los usuarios rara vez querrán ver las inscripciones en un curso. En ese caso, una optimización sería cargar solamente los datos de inscripción si se solicitan. En esta sección, se actualiza OnGetAsync
para usar la carga explícita de Enrollments
y Students
.
Actualice Pages/Instructors/Index.cshtml.cs
con el código siguiente.
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;
}
}
}
}
En el código anterior se quitan las llamadas al método ThenInclude para los datos de inscripción y estudiantes. Si se selecciona un curso, el código de carga explícita recupera lo siguiente:
- Las entidades
Enrollment
para el curso seleccionado. - Las entidades
Student
para cadaEnrollment
.
Observe que en el código anterior .AsNoTracking()
se convierte en comentario. Las propiedades de navegación solo se pueden cargar explícitamente para las entidades sometidas a seguimiento.
Pruebe la aplicación. Desde la perspectiva del usuario, la aplicación se comporta exactamente igual a la versión anterior.
Pasos siguientes
En el siguiente tutorial se muestra cómo actualizar datos relacionados.
En este tutorial, se leen y se muestran datos relacionados. Los datos relacionados son los que EF Core carga en las propiedades de navegación.
Si experimenta problemas que no puede resolver, descargue o vea la aplicación completada. Instrucciones de descarga.
En las ilustraciones siguientes se muestran las páginas completadas para este tutorial:
Carga diligente, explícita y diferida de datos relacionados
EF Core puede cargar datos relacionados en las propiedades de navegación de una entidad de varias maneras:
Carga diligente. La carga diligente es cuando una consulta para un tipo de entidad también carga las entidades relacionadas. Cuando se lee la entidad, se recuperan sus datos relacionados. Esto normalmente da como resultado una única consulta de combinación en la que se recuperan todos los datos que se necesitan. EF Core emitirá varias consultas para algunos tipos de carga diligente. La emisión de varias consultas puede ser más eficaz de lo que eran algunas consultas de EF6 cuando había una sola consulta. La carga diligente se especifica con los métodos
Include
yThenInclude
.La carga diligente envía varias consultas cuando se incluye una propiedad de navegación de colección:
- Una consulta para la consulta principal
- Una consulta para cada colección "perimetral" en el árbol de la carga.
Separe las consultas con
Load
: los datos se pueden recuperar en distintas consultas y EF Core "corrige" las propiedades de navegación. "Corregir" significa que EF Core rellena de forma automática las propiedades de navegación. La separación de las consultas conLoad
es más parecido a la carga explícita que a la carga diligente.Nota: EF Core corrige automáticamente las propiedades de navegación para todas las entidades que se cargaron previamente en la instancia del contexto. Incluso si los datos de una propiedad de navegación no se incluyen explícitamente, es posible que la propiedad se siga rellenando si algunas o todas las entidades relacionadas se cargaron previamente.
Carga explícita. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. Se debe escribir código para recuperar los datos relacionados cuando sea necesario. La carga explícita con consultas independientes da como resultado varias consultas que se envían a la base de datos. Con la carga explícita, el código especifica las propiedades de navegación que se van a cargar. Use el método
Load
para realizar la carga explícita. Por ejemplo:Carga diferida. Se ha agregado la carga diferida a EF Core en la versión 2.1. Cuando la entidad se lee por primera vez, no se recuperan datos relacionados. La primera vez que se obtiene acceso a una propiedad de navegación, se recuperan automáticamente los datos necesarios para esa propiedad de navegación. Cada vez que se obtiene acceso a una propiedad de navegación, se envía una consulta a la base de datos.
El operador
Select
solo carga los datos relacionados necesarios.
Crear una página de cursos en la que se muestre el nombre de departamento
La entidad Course incluye una propiedad de navegación que contiene la entidad Department
. La entidad Department
contiene el departamento al que se asigna el curso.
Para mostrar el nombre del departamento asignado en una lista de cursos:
- Obtenga la propiedad
Name
desde la entidadDepartment
. - La entidad
Department
procede de la propiedad de navegaciónCourse.Department
.
Aplicar scaffolding al modelo de Course
Siga las instrucciones que encontrará en Aplicación de scaffolding al modelo de alumnos y use Course
para la clase de modelo.
El comando anterior aplica scaffolding al modelo Course
. Abra el proyecto en Visual Studio.
Abra Pages/Courses/Index.cshtml.cs
y examine el método OnGetAsync
. El motor de scaffolding especificado realiza la carga diligente de la propiedad de navegación Department
. El método Include
especifica la carga diligente.
Ejecute la aplicación y haga clic en el vínculo Courses. En la columna Department se muestra el DepartmentID
, lo que no resulta útil.
Actualice el método OnGetAsync
con el código siguiente:
public async Task OnGetAsync()
{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
El código anterior agrega AsNoTracking
. AsNoTracking
mejora el rendimiento porque no se realiza el seguimiento de las entidades devueltas. No se realiza el seguimiento de las entidades porque no se actualizan en el contexto actual.
Actualice Pages/Courses/Index.cshtml
con el marcado resaltado siguiente:
@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>
Se han realizado los cambios siguientes en el código con scaffolding:
Ha cambiado el título de Index a Courses.
Ha agregado una columna Number en la que se muestra el valor de propiedad
CourseID
. De forma predeterminada, las claves principales no tienen scaffolding porque normalmente no tienen sentido para los usuarios finales. Pero en este caso, la clave principal es significativa.Ha cambiado la columna Department para mostrar el nombre del departamento. El código muestra la propiedad
Name
de la entidadDepartment
que se carga en la propiedad de navegaciónDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Ejecute la aplicación y haga clic en la pestaña Courses para ver la lista con los nombres de departamento.
Carga de datos relacionados con Select
El método OnGetAsync
carga los datos relacionados con el método Include
:
public async Task OnGetAsync()
{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
El operador Select
solo carga los datos relacionados necesarios. Para elementos individuales, como el Department.Name
, se usa INNER JOIN de SQL. En las colecciones, se usa otro acceso de base de datos, como también hace el operador Include
en las colecciones.
En el código siguiente se cargan los datos relacionados con el 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();
}
El CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
Vea IndexSelect.cshtml e IndexSelect.cshtml.cs para obtener un ejemplo completo.
Crear una página de instructores en la que se muestran los cursos y las inscripciones
En esta sección, se crea la página de instructores.
En esta página se leen y muestran los datos relacionados de las maneras siguientes:
- En la lista de instructores se muestran datos relacionados de la entidad
OfficeAssignment
(Office en la imagen anterior). Las entidadesInstructor
yOfficeAssignment
se encuentran en una relación de uno a cero o uno. Para las entidadesOfficeAssignment
se usa la carga diligente. Normalmente la carga diligente es más eficaz cuando es necesario mostrar los datos relacionados. En este caso, se muestran las asignaciones de oficina para los instructores. - Cuando el usuario selecciona un instructor (Harui en la imagen anterior), se muestran las entidades
Course
relacionadas. Las entidadesInstructor
yCourse
se encuentran en una relación de varios a varios. La carga diligente se usa con las entidadesCourse
y sus entidadesDepartment
relacionadas. En este caso, es posible que las consultas independientes sean más eficaces porque solo se necesitan cursos para el instructor seleccionado. En este ejemplo se muestra cómo usar la carga diligente para propiedades de navegación en entidades que se encuentran en propiedades de navegación. - Cuando el usuario selecciona un curso (Chemistry [Química] en la imagen anterior), se muestran los datos relacionados de la entidad
Enrollments
. En la imagen anterior, se muestra el nombre del alumno y la calificación. Las entidadesCourse
yEnrollment
se encuentran en una relación uno a varios.
Crear un modelo de vista para la vista de índice de instructores
En la página Instructors se muestran datos de tres tablas diferentes. Se crea un modelo de vista que incluye las tres entidades que representan las tres tablas.
En la carpeta SchoolViewModels, cree InstructorIndexData.cs
con el código siguiente:
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 scaffolding al modelo de Instructor
Siga las instrucciones que encontrará en Aplicación de scaffolding al modelo de alumnos y use Instructor
para la clase de modelo.
El comando anterior aplica scaffolding al modelo Instructor
.
Ejecute la aplicación y vaya a la página de instructores.
Reemplaza Pages/Instructors/Index.cshtml.cs
por el código siguiente:
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;
}
}
}
}
El método OnGetAsync
acepta datos de ruta opcionales para el identificador del instructor seleccionado.
Examine la consulta en el archivo 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();
La consulta tiene dos instrucciones include:
OfficeAssignment
: se muestra en la vista de instructores.CourseAssignments
: muestra los cursos impartidos.
Actualizar la página de índice de instructores
Actualice Pages/Instructors/Index.cshtml
con el marcado siguiente:
@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>
En el marcado anterior se realizan los cambios siguientes:
Se actualiza la directiva
page
de@page
a@page "{id:int?}"
."{id:int?}"
es una plantilla de ruta. La plantilla de ruta cambia las cadenas de consulta enteras de la dirección URL por datos de ruta. Por ejemplo, al hacer clic en el vínculo Select de un instructor con únicamente la directiva@page
, se genera una dirección URL similar a la siguiente:http://localhost:1234/Instructors?id=2
Cuando la directiva de página es
@page "{id:int?}"
, la dirección URL anterior es:http://localhost:1234/Instructors/2
El título de página es Instructors.
Se ha agregado una columna Office en la que se muestra
item.OfficeAssignment.Location
solo siitem.OfficeAssignment
no es NULL. Dado que se trata de una relación de uno a cero o uno, es posible que no haya una entidad OfficeAssignment relacionada.@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Se ha agregado una columna Courses en la que se muestran los cursos que imparte cada instructor. Para obtener más información sobre esta sintaxis razor, consulta Transición de línea explícita.
Ha agregado código que agrega dinámicamente
class="success"
al elementotr
del instructor seleccionado. Esto establece el color de fondo de la fila seleccionada mediante una clase de arranque.string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "success"; } <tr class="@selectedRow">
Se ha agregado un hipervínculo nuevo con la etiqueta Select. Este vínculo envía el identificador del instructor seleccionado al método
Index
y establece un color de fondo.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Ejecute la aplicación y haga clic en la pestaña Instructors. En la página se muestra la Location
(oficina) de la entidad OfficeAssignment
relacionada. Si OfficeAssignment es NULL, se muestra una celda de tabla vacía.
Haga clic en el vínculo Select. El estilo de la fila cambia.
Agregar cursos impartidos por el instructor seleccionado
Actualice el método OnGetAsync
en Pages/Instructors/Index.cshtml.cs
con el código siguiente:
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;
}
}
Agregue 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 la consulta actualizada:
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();
En la consulta anterior se agregan las entidades Department
.
El código siguiente se ejecuta cuando se selecciona un instructor (id != null
). El instructor seleccionado se recupera de la lista de instructores del modelo de vista. Se carga la propiedad Courses
del modelo de vista con las entidades Course
de la propiedad de navegación CourseAssignments
de ese instructor.
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);
}
El método Where
devuelve una colección. En el método Where
anterior, solo se devuelve una entidad Instructor
. El método Single
convierte la colección en una sola entidad Instructor
. La entidad Instructor
proporciona acceso a la propiedad CourseAssignments
. CourseAssignments
proporciona acceso a las entidades Course
relacionadas.
El método Single
se usa en una colección cuando la colección tiene un solo elemento. El método Single
inicia una excepción si la colección está vacía o hay más de un elemento. Una alternativa es SingleOrDefault
, que devuelve una valor predeterminado (NULL, en este caso) si la colección está vacía. El uso de SingleOrDefault
en una colección vacía:
- Inicia una excepción (al tratar de buscar una propiedad
Courses
en una referencia nula). - El mensaje de excepción indicará con menos claridad la causa del problema.
El código siguiente rellena la propiedad Enrollments
del modelo de vista cuando se selecciona un curso:
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Agregue el marcado siguiente al final de la página de 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>
}
En el marcado anterior se muestra una lista de cursos relacionados con un instructor cuando se selecciona un instructor.
Pruebe la aplicación. Haga clic en un vínculo Select en la página de instructores.
Mostrar datos de estudiante
En esta sección, la aplicación se actualiza para mostrar los datos de estudiante para un curso seleccionado.
Actualice la consulta del método OnGetAsync
en Pages/Instructors/Index.cshtml.cs
con el código siguiente:
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();
Actualice Pages/Instructors/Index.cshtml
. Agregue el marcado siguiente al final del archivo:
@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>
}
En el marcado anterior se muestra una lista de los estudiantes que están inscritos en el curso seleccionado.
Actualice la página y seleccione un instructor. Seleccione un curso para ver la lista de los estudiantes inscritos y sus calificaciones.
Uso de Single
Se puede pasar el método Single
en la condición Where
en lugar de llamar al método Where
por separado:
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;
}
}
El enfoque de Single
anterior no ofrece ninguna ventaja con respecto a Where
. Algunos desarrolladores prefieren el estilo del enfoque de Single
.
Carga explícita
En el código actual se especifica la carga diligente para Enrollments
y 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();
Imagine que los usuarios rara vez querrán ver las inscripciones en un curso. En ese caso, una optimización sería cargar solamente los datos de inscripción si se solicitan. En esta sección, se actualiza OnGetAsync
para usar la carga explícita de Enrollments
y Students
.
Actualice OnGetAsync
con el código siguiente:
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;
}
}
En el código anterior se quitan las llamadas al método ThenInclude para los datos de inscripción y estudiantes. Si se selecciona un curso, el código resaltado recupera lo siguiente:
- Las entidades
Enrollment
para el curso seleccionado. - Las entidades
Student
para cadaEnrollment
.
Tenga en cuenta que en el código anterior .AsNoTracking()
se convierte en comentario. Las propiedades de navegación solo se pueden cargar explícitamente para las entidades sometidas a seguimiento.
Pruebe la aplicación. Desde la perspectiva de los usuarios, la aplicación se comporta exactamente igual a la versión anterior.
En el siguiente tutorial se muestra cómo actualizar datos relacionados.