Compartir vía


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:

Página de índice de cursos

Página de índice de instructores

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.

    Ejemplo de carga diligente

    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 con Load es más parecido a la carga explícita que a la carga diligente.

    Ejemplo de consultas independientes

    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:

    Ejemplo de carga explícita

  • 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.

Course.Department

Para mostrar el nombre del departamento asignado a un curso:

  • Cargue la entidad Department relacionada en la propiedad de navegación Course.Department.
  • Obtenga el nombre de la propiedad Name de la entidad Department.

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é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.

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 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 entidad Department que se carga en la propiedad de navegación Department:

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

Página de índice de cursos

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).

Página de í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 entidades Instructor y OfficeAssignment se encuentran en una relación de uno a cero o uno. Para las entidades OfficeAssignment 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 entidades Instructor y Course se encuentran en una relación de varios a varios. La carga diligente se usa con las entidades Course y sus entidades Department 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 entidades Course y Enrollment 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 si item.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 elemento tr 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.

Instructor y curso seleccionados en la página de índice de instructores

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:

Página de índice de cursos

Página de índice de instructores

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 y ThenInclude.

    Ejemplo de carga diligente

    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 con Load es más parecido a la carga explícita que a la carga diligente.

    Ejemplo de consultas independientes

    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:

    Ejemplo de carga explícita

  • 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.

Course.Department

Para mostrar el nombre del departamento asignado a un curso:

  • Cargue la entidad Department relacionada en la propiedad de navegación Course.Department.
  • Obtenga el nombre de la propiedad Name de la entidad Department.

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é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.

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 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 entidad Department que se carga en la propiedad de navegación Department:

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

Página de índice de cursos

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).

Página de í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 entidades Instructor y OfficeAssignment se encuentran en una relación de uno a cero o uno. Para las entidades OfficeAssignment 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 entidades Instructor y Course se encuentran en una relación de varios a varios. La carga diligente se usa con las entidades Course y sus entidades Department 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 entidades Course y Enrollment 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.

Relación de varios a varios Instructor-to-Courses

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 si item.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 elemento tr 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.

Instructor y curso seleccionados en la página de índice de instructores

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 cada Enrollment.

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:

Página de índice de cursos

Página de índice de instructores

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 y ThenInclude.

    Ejemplo de carga diligente

    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 con Load es más parecido a la carga explícita que a la carga diligente.

    Ejemplo de consultas independientes

    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:

    Ejemplo de carga explícita

  • 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 entidad Department.
  • La entidad Department procede de la propiedad de navegación Course.Department.

Course.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 entidad Department que se carga en la propiedad de navegación Department:

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

Página de índice de cursos

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.

Página de í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 entidades Instructor y OfficeAssignment se encuentran en una relación de uno a cero o uno. Para las entidades OfficeAssignment 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 entidades Instructor y Course se encuentran en una relación de varios a varios. La carga diligente se usa con las entidades Course y sus entidades Department 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 entidades Course y Enrollment 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 si item.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 elemento tr 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.

Relación de varios a varios Instructor-to-Courses

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.cshtmlRazor:

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

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

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

    </table>
}

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.

Instructor y curso seleccionados en la página de índice de instructores

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 cada Enrollment.

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.

Recursos adicionales