Parte 6, Razor Pagine con EF Core in ASP.NET Core - Leggere i dati correlati

Di Tom Dykstra, Jon P Smith e Rick Anderson

L'app Web Contoso University illustra come creare Razor app Web Pages usando EF Core e Visual Studio. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione.

Se si verificano problemi che non è possibile risolvere, scaricare l'app completata e confrontare tale codice con quello creato seguendo questa esercitazione.

Questa esercitazione illustra come leggere e visualizzare dati correlati. I dati correlati sono dati EF Core caricati nelle proprietà di navigazione.

Le figure seguenti mostrano le pagine completate per questa esercitazione:

Pagina di indice dei corsi

Pagina di indice degli insegnanti

Caricamento eager, esplicito e lazy

Esistono diversi modi per EF Core caricare i dati correlati nelle proprietà di navigazione di un'entità:

  • Caricamento eager. Il caricamento eager si verifica quando una query per un solo tipo di entità carica anche entità correlate. Quando viene letta un'entità, vengono recuperati i dati correlati corrispondenti. Ciò in genere ha come risultato una query join singola che recupera tutti i dati necessari. EF Core eseguirà più query per alcuni tipi di caricamento eager. L'esecuzione di più query può essere più efficiente rispetto a una singola query di grandi dimensioni. Il caricamento eager viene specificato con i metodi Include e ThenInclude.

    Esempio di caricamento eager

    Il caricamento eager invia più query quando è inclusa una navigazione di raccolte:

    • Una query per la query principale
    • Una query per ogni raccolta "perimetrale" nell'albero del caricamento.
  • Query separate con Load: i dati possono essere recuperati in query separate e EF Core "corregge" le proprietà di navigazione. "Correzioni" indica che EF Core popola automaticamente le proprietà di navigazione. Le query separate con Load sono più simili a un caricamento esplicito che a un caricamento eager.

    Esempio di query separate

    Nota:EF Core corregge automaticamente le proprietà di navigazione in tutte le altre entità caricate in precedenza nell'istanza di contesto. Anche se i dati per una proprietà di navigazione non sono inclusi in modo esplicito, la proprietà può comunque essere popolata se alcune o tutte le entità correlate sono state caricate in precedenza.

  • Caricamento esplicito Quando un'entità viene letta per la prima volta, i dati correlati non vengono recuperati. Per recuperare i dati correlati quando necessario, è necessario scrivere codice. Il caricamento esplicito con query separate ha come risultato l'invio di più query al database. Con il caricamento esplicito, il codice specifica le proprietà di navigazione da caricare. Per eseguire il caricamento esplicito, usare il metodo Load. Ad esempio:

    Esempio di caricamento esplicito

  • Caricamento lazy. Quando un'entità viene letta per la prima volta, i dati correlati non vengono recuperati. La prima volta che si accede a una proprietà di navigazione, i dati necessari per quest'ultima vengono recuperati automaticamente. Ogni volta che si accede a una proprietà di navigazione per la prima volta, viene inviata una query al database. Il caricamento differita può compromettere le prestazioni, ad esempio quando gli sviluppatori usano query N+1. Le query N+1 caricano un elemento padre ed enumerare gli elementi figlio.

Creare pagine Course

L'entità Course include una proprietà di navigazione che contiene l'entità Department correlata.

Course.Department

Per visualizzare il nome del dipartimento assegnato per un corso:

  • Caricare l'entità Department correlata nella proprietà di navigazione Course.Department.
  • Ottenere il nome dalla proprietà Name dell'entità Department.

Scaffolding delle pagine Course

  • Seguire le istruzioni in Scaffolding delle pagine Student con le eccezioni seguenti:

    • Creare una cartella Pages/Courses.
    • Usare Course per la classe del modello.
    • Usare la classe di contesto esistente anziché crearne una nuova.
  • Aprire Pages/Courses/Index.cshtml.cs ed esaminare il OnGetAsync metodo . Il motore di scaffolding ha specificato il caricamento eager per la proprietà di navigazione Department. Il metodo Include specifica il caricamento eager.

  • Eseguire l'app e selezionare il collegamento Courses (Corsi). La colonna dei dipartimenti visualizza il DepartmentID, che non è utile.

Visualizzare il nome del dipartimento

Aggiornare Pages/Courses/Index.cshtml.cs con il codice seguente:

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

Il codice precedente modifica la proprietà Course in Courses e aggiunge AsNoTracking.

Le query senza rilevamento sono utili quando i risultati vengono usati in uno scenario di sola lettura. In genere sono più veloci da eseguire perché non è necessario configurare le informazioni sul rilevamento delle modifiche. Se non è necessario aggiornare le entità recuperate dal database, è probabile che una query di rilevamento non funzioni meglio di una query di rilevamento.

In alcuni casi una query di rilevamento è più efficiente di una query senza rilevamento. Per altre informazioni, vedere Tracking vs. No-Tracking Queries.For more information, see Tracking vs. No-Tracking Queries. Nel codice precedente viene AsNoTracking chiamato perché le entità non vengono aggiornate nel contesto corrente.

Aggiornare Pages/Courses/Index.cshtml con il codice seguente.

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

Al codice con scaffolding sono state apportate le modifiche seguenti:

  • Il nome della proprietà Course è stato modificato in Courses.

  • È stata aggiunta la colonna Number (Numero) con il valore della proprietà CourseID. Per impostazione predefinita, non viene eseguito lo scaffolding delle chiavi primarie, perché in genere non sono significative per gli utenti finali. In questo caso, tuttavia, la chiave primaria è significativa.

  • Modificare la colonna Department (Dipartimento) per visualizzare il nome del dipartimento. Il codice visualizza la proprietà Name dell'entità Department che viene caricata nella proprietà di navigazione Department:

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

Eseguire l'app e selezionare la scheda Courses (Corsi) per visualizzare l'elenco con i nomi dei dipartimenti.

Pagina di indice dei corsi

Il metodo OnGetAsync carica i dati correlati con il metodo Include. Il metodo Select è un'alternativa che carica solo i dati correlati necessari. Per i singoli elementi, ad esempio , Department.Name usa un oggetto SQL INNER JOIN. Per le raccolte usa un altro accesso al database, ma anche l'operatore Include fa lo stesso sulle raccolte.

Il codice seguente carica dati correlati con il metodo 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();
}

Il codice precedente non restituisce alcun tipo di entità, pertanto non viene eseguito alcun rilevamento. Per altre informazioni sul rilevamento di Entity Framework, vedere Tracking vs. No-Tracking Queries.For more information about the EF tracking, see Tracking vs. No-Tracking Queries.

CourseViewModel:

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

Per le pagine completeRazor, vedere IndexSelectModel.

Creare pagine Instructor

Questa sezione descrive come eseguire lo scaffolding delle pagine Instructor e come aggiungere corsi e iscrizioni correlati alla pagina di indice degli insegnanti.

Pagina di indice degli insegnanti

Questa pagina legge e visualizza dati correlati nei modi seguenti:

  • L'elenco di insegnanti visualizza dati correlati provenienti dall'entità OfficeAssignment, Office (Ufficio) nella figura precedente. Tra le entità Instructor e OfficeAssignment c'è una relazione uno-a-zero-o-uno. Per le entità OfficeAssignment viene usato il caricamento eager. Il caricamento eager è in genere più efficiente quando i dati correlati devono essere visualizzati. In questo caso, devono essere visualizzate le assegnazioni di ufficio degli insegnanti.
  • Quando l'utente seleziona un insegnante, vengono visualizzate le entità Course correlate. Tra le entità Instructor e Course esiste una relazione molti-a-molti. Il caricamento eager viene usato per le entità Course e le entità Department correlate. In questo caso, query separate potrebbero essere più efficienti, perché sono necessari solo i corsi per l'insegnante selezionato. Questo esempio illustra come usare il caricamento eager per le proprietà di navigazione di entità all'interno di proprietà di navigazione.
  • Quando l'utente seleziona un corso, vengono visualizzati i dati correlati dall'entità Enrollments. Nella figura precedente, vengono visualizzati il voto e il nome degli studenti. Tra le entità Course e Enrollment esiste una relazione uno-a-molti.

Creare un modello di visualizzazione

La pagina Instructors (Insegnanti) mostra i dati di tre tabelle diverse. È necessario un modello di visualizzazione che includa tre proprietà che rappresentano le tre tabelle.

Creare Models/SchoolViewModels/InstructorIndexData.cs con il codice seguente:

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 delle pagine Instructor

  • Seguire le istruzioni in Scaffolding delle pagine Student con le eccezioni seguenti:

    • Creare una cartella Pages/Instructors.
    • Usare Instructor per la classe del modello.
    • Usare la classe di contesto esistente anziché crearne una nuova.

Eseguire l'app e passare alla pagina Instructors (Insegnanti).

Aggiornare Pages/Instructors/Index.cshtml.cs con il codice seguente:

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

Il metodo OnGetAsync accetta i dati di route facoltativi per l'ID dell'insegnante selezionato.

Esaminare la query nel Pages/Instructors/Index.cshtml.cs file:

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

Il codice specifica il caricamento eager delle proprietà di navigazione seguenti:

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

Il codice seguente viene eseguito quando viene selezionato un insegnante, id != nullovvero .

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

L'insegnante selezionato viene recuperato dall'elenco di insegnanti nel modello di visualizzazione. La proprietà del modello di Courses visualizzazione viene caricata con le Course entità dalla proprietà di navigazione dell'insegnante Courses selezionato.

Il metodo Where restituisce una raccolta. In questo caso, il filtro seleziona una singola entità, quindi viene chiamato il Single metodo per convertire la raccolta in una singola Instructor entità. L'entità Instructor fornisce l'accesso alla proprietà di Course navigazione.

Il metodo Single viene usato in una raccolta quando quest'ultima ha un solo elemento. Il metodo Single genera un'eccezione se la raccolta è vuota o se contiene più elementi. Un'alternativa è SingleOrDefault, che restituisce un valore predefinito se la raccolta è vuota. Per questa query, null nel valore predefinito restituito.

Il codice seguente popola la proprietà Enrollments del modello di visualizzazione quando è selezionato un corso:

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

Aggiornare la pagina di indice degli insegnanti

Aggiornare Pages/Instructors/Index.cshtml con il codice seguente.

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

Il codice precedente apporta le modifiche seguenti:

  • Aggiorna la page direttiva a @page "{id:int?}". "{id:int?}" è un modello di route. Il modello di route modifica le stringhe di query integer nell'URL per instradare i dati. Se ad esempio si fa clic su sul collegamento Select (Seleziona) per un insegnante con la sola direttiva @page, viene generato un URL come il seguente:

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

    Quando la direttiva di pagina è @page "{id:int?}", l'URL è: https://localhost:5001/Instructors/2

  • Aggiunge una colonna Office che visualizza item.OfficeAssignment.Location solo se item.OfficeAssignment non è Null. Poiché questa è una relazione uno-a-zero-o-uno, potrebbe non esserci un'entità OfficeAssignment correlata.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Aggiunge una colonna Courses che visualizza i corsi tenuti da ogni insegnante. Per altre informazioni su questa razor sintassi, vedere Transizione di riga esplicita.

  • Aggiunge codice che aggiunge class="table-success" in modo dinamico all'elemento tr dell'insegnante e del corso selezionati. In questo modo viene impostato un colore di sfondo per la riga selezionata tramite una classe Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Aggiunge un nuovo collegamento ipertestuale con etichetta Select. Questo collegamento invia l'ID dell'insegnante selezionato al metodo Index e imposta un colore di sfondo.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Aggiunge una tabella di corsi per l'insegnante selezionato.

  • Aggiunge una tabella di iscrizioni degli studenti per il corso selezionato.

Eseguire l'app e selezionare la scheda Instructors (Insegnanti). La pagina visualizza ( Location office) dall'entità correlata OfficeAssignment . Se OfficeAssignment è null, viene visualizzata una cella di tabella vuota.

Fare clic sul collegamento Select per un insegnante. Verranno visualizzate le modifiche dello stile delle righe e i corsi assegnati a tale insegnante.

Selezionare un corso per visualizzare l'elenco degli studenti iscritti e i voti corrispondenti.

Pagina di indice degli insegnanti con un corso selezionato

Passaggi successivi

La prossima esercitazione illustra come aggiornare i dati correlati.

Questa esercitazione illustra come leggere e visualizzare dati correlati. I dati correlati sono dati EF Core caricati nelle proprietà di navigazione.

Le figure seguenti mostrano le pagine completate per questa esercitazione:

Pagina di indice dei corsi

Pagina di indice degli insegnanti

Caricamento eager, esplicito e lazy

Esistono diversi modi per EF Core caricare i dati correlati nelle proprietà di navigazione di un'entità:

  • Caricamento eager. Il caricamento eager si verifica quando una query per un solo tipo di entità carica anche entità correlate. Quando viene letta un'entità, vengono recuperati i dati correlati corrispondenti. Ciò in genere ha come risultato una query join singola che recupera tutti i dati necessari. EF Core eseguirà più query per alcuni tipi di caricamento eager. L'esecuzione di più query può essere più efficiente rispetto a una singola query gigante. Il caricamento eager viene specificato con i metodi Include e ThenInclude.

    Esempio di caricamento eager

    Il caricamento eager invia più query quando è inclusa una navigazione di raccolte:

    • Una query per la query principale
    • Una query per ogni raccolta "perimetrale" nell'albero del caricamento.
  • Query separate con Load: i dati possono essere recuperati in query separate e EF Core "corregge" le proprietà di navigazione. "Correzioni" indica che EF Core popola automaticamente le proprietà di navigazione. Le query separate con Load sono più simili a un caricamento esplicito che a un caricamento eager.

    Esempio di query separate

    Nota:EF Core corregge automaticamente le proprietà di navigazione in tutte le altre entità caricate in precedenza nell'istanza di contesto. Anche se i dati per una proprietà di navigazione non sono inclusi in modo esplicito, la proprietà può comunque essere popolata se alcune o tutte le entità correlate sono state caricate in precedenza.

  • Caricamento esplicito Quando un'entità viene letta per la prima volta, i dati correlati non vengono recuperati. Per recuperare i dati correlati quando necessario, è necessario scrivere codice. Il caricamento esplicito con query separate ha come risultato l'invio di più query al database. Con il caricamento esplicito, il codice specifica le proprietà di navigazione da caricare. Per eseguire il caricamento esplicito, usare il metodo Load. Ad esempio:

    Esempio di caricamento esplicito

  • Caricamento lazy. Quando un'entità viene letta per la prima volta, i dati correlati non vengono recuperati. La prima volta che si accede a una proprietà di navigazione, i dati necessari per quest'ultima vengono recuperati automaticamente. Ogni volta che si accede a una proprietà di navigazione per la prima volta, viene inviata una query al database. Il caricamento differita può compromettere le prestazioni, ad esempio quando gli sviluppatori usano modelli N+1, caricando un elemento padre ed enumerando gli elementi figlio.

Creare pagine Course

L'entità Course include una proprietà di navigazione che contiene l'entità Department correlata.

Course.Department

Per visualizzare il nome del dipartimento assegnato per un corso:

  • Caricare l'entità Department correlata nella proprietà di navigazione Course.Department.
  • Ottenere il nome dalla proprietà Name dell'entità Department.

Scaffolding delle pagine Course

  • Seguire le istruzioni in Scaffolding delle pagine Student con le eccezioni seguenti:

    • Creare una cartella Pages/Courses.
    • Usare Course per la classe del modello.
    • Usare la classe di contesto esistente anziché crearne una nuova.
  • Aprire Pages/Courses/Index.cshtml.cs ed esaminare il OnGetAsync metodo . Il motore di scaffolding ha specificato il caricamento eager per la proprietà di navigazione Department. Il metodo Include specifica il caricamento eager.

  • Eseguire l'app e selezionare il collegamento Courses (Corsi). La colonna dei dipartimenti visualizza il DepartmentID, che non è utile.

Visualizzare il nome del dipartimento

Aggiornare Pages/Courses/Index.cshtml.cs con il codice seguente:

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

Il codice precedente modifica la proprietà Course in Courses e aggiunge AsNoTracking. AsNoTracking migliora le prestazioni, perché le entità restituite non vengono registrate, dato che non vengono aggiornate nel contesto corrente.

Aggiornare Pages/Courses/Index.cshtml con il codice seguente.

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

Al codice con scaffolding sono state apportate le modifiche seguenti:

  • Il nome della proprietà Course è stato modificato in Courses.

  • È stata aggiunta la colonna Number (Numero) con il valore della proprietà CourseID. Per impostazione predefinita, non viene eseguito lo scaffolding delle chiavi primarie, perché in genere non sono significative per gli utenti finali. In questo caso, tuttavia, la chiave primaria è significativa.

  • Modificare la colonna Department (Dipartimento) per visualizzare il nome del dipartimento. Il codice visualizza la proprietà Name dell'entità Department che viene caricata nella proprietà di navigazione Department:

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

Eseguire l'app e selezionare la scheda Courses (Corsi) per visualizzare l'elenco con i nomi dei dipartimenti.

Pagina di indice dei corsi

Il metodo OnGetAsync carica i dati correlati con il metodo Include. Il metodo Select è un'alternativa che carica solo i dati correlati necessari. Per singoli elementi, ad esempio per Department.Name usa un INNER JOIN SQL. Per le raccolte usa un altro accesso al database, ma anche l'operatore Include fa lo stesso sulle raccolte.

Il codice seguente carica dati correlati con il metodo 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();
}

Il codice precedente non restituisce alcun tipo di entità, pertanto non viene eseguito alcun rilevamento. Per altre informazioni sul rilevamento di Entity Framework, vedere Tracking vs. No-Tracking Queries.For more information about the EF tracking, see Tracking vs. No-Tracking Queries.

CourseViewModel:

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

Per un esempio completo, vedere IndexSelect.cshtml e IndexSelect.cshtml.cs.

Creare pagine Instructor

Questa sezione descrive come eseguire lo scaffolding delle pagine Instructor e come aggiungere corsi e iscrizioni correlati alla pagina di indice degli insegnanti.

Pagina di indice degli insegnanti

Questa pagina legge e visualizza dati correlati nei modi seguenti:

  • L'elenco di insegnanti visualizza dati correlati provenienti dall'entità OfficeAssignment, Office (Ufficio) nella figura precedente. Tra le entità Instructor e OfficeAssignment c'è una relazione uno-a-zero-o-uno. Per le entità OfficeAssignment viene usato il caricamento eager. Il caricamento eager è in genere più efficiente quando i dati correlati devono essere visualizzati. In questo caso, devono essere visualizzate le assegnazioni di ufficio degli insegnanti.
  • Quando l'utente seleziona un insegnante, vengono visualizzate le entità Course correlate. Tra le entità Instructor e Course esiste una relazione molti-a-molti. Il caricamento eager viene usato per le entità Course e le entità Department correlate. In questo caso, query separate potrebbero essere più efficienti, perché sono necessari solo i corsi per l'insegnante selezionato. Questo esempio illustra come usare il caricamento eager per le proprietà di navigazione di entità all'interno di proprietà di navigazione.
  • Quando l'utente seleziona un corso, vengono visualizzati i dati correlati dall'entità Enrollments. Nella figura precedente, vengono visualizzati il voto e il nome degli studenti. Tra le entità Course e Enrollment esiste una relazione uno-a-molti.

Creare un modello di visualizzazione

La pagina Instructors (Insegnanti) mostra i dati di tre tabelle diverse. È necessario un modello di visualizzazione che includa tre proprietà che rappresentano le tre tabelle.

Creare SchoolViewModels/InstructorIndexData.cs con il codice seguente:

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 delle pagine Instructor

  • Seguire le istruzioni in Scaffolding delle pagine Student con le eccezioni seguenti:

    • Creare una cartella Pages/Instructors.
    • Usare Instructor per la classe del modello.
    • Usare la classe di contesto esistente anziché crearne una nuova.

Per visualizzare l'aspetto della pagina con scaffolding prima di aggiornarla, eseguire l'app e passare alla pagina Instructors.

Aggiornare Pages/Instructors/Index.cshtml.cs con il codice seguente:

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

Il metodo OnGetAsync accetta i dati di route facoltativi per l'ID dell'insegnante selezionato.

Esaminare la query nel Pages/Instructors/Index.cshtml.cs file:

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

Il codice specifica il caricamento eager delle proprietà di navigazione seguenti:

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

Si noti la ripetizione dei metodi Include e ThenInclude per CourseAssignments e Course. Questa ripetizione è necessaria per specificare il caricamento eager per due proprietà di navigazione dell'entità Course.

Il codice seguente viene eseguito quando viene selezionato un insegnante (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);
}

L'insegnante selezionato viene recuperato dall'elenco di insegnanti nel modello di visualizzazione. La proprietà Courses del modello di visualizzazione viene caricata con le entità Course dalla proprietà di navigazione CourseAssignments di tale insegnante.

Il metodo Where restituisce una raccolta. In questo caso, tuttavia, il filtro selezionerà una singola entità, quindi viene chiamato il Single metodo per convertire la raccolta in una singola Instructor entità. L'entità Instructor consente l'accesso alla proprietà CourseAssignments. CourseAssignments consente l'accesso alle entità Course correlate.

Relazione m:M tra insegnante e corsi

Il metodo Single viene usato in una raccolta quando quest'ultima ha un solo elemento. Il metodo Single genera un'eccezione se la raccolta è vuota o se contiene più elementi. In alternativa, è possibile usare SingleOrDefault, che restituisce un valore predefinito (Null in questo caso) se la raccolta è vuota.

Il codice seguente popola la proprietà Enrollments del modello di visualizzazione quando è selezionato un corso:

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

Aggiornare la pagina di indice degli insegnanti

Aggiornare Pages/Instructors/Index.cshtml con il codice seguente.

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

Il codice precedente apporta le modifiche seguenti:

  • Aggiorna la direttiva page da @page a @page "{id:int?}". "{id:int?}" è un modello di route. Il modello di route cambia le stringhe di query di tipo integer nell'URL in dati di route. Se ad esempio si fa clic su sul collegamento Select (Seleziona) per un insegnante con la sola direttiva @page, viene generato un URL come il seguente:

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

    Quando la direttiva della pagina è @page "{id:int?}", l'URL è:

    https://localhost:5001/Instructors/2

  • Aggiunge una colonna Office che visualizza item.OfficeAssignment.Location solo se item.OfficeAssignment non è Null. Poiché questa è una relazione uno-a-zero-o-uno, potrebbe non esserci un'entità OfficeAssignment correlata.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Aggiunge una colonna Courses che visualizza i corsi tenuti da ogni insegnante. Per altre informazioni su questa razor sintassi, vedere Transizione di riga esplicita.

  • Aggiunge codice che aggiunge class="table-success" in modo dinamico all'elemento tr dell'insegnante e del corso selezionati. In questo modo viene impostato un colore di sfondo per la riga selezionata tramite una classe Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "table-success";
    }
    <tr class="@selectedRow">
    
  • Aggiunge un nuovo collegamento ipertestuale con etichetta Select. Questo collegamento invia l'ID dell'insegnante selezionato al metodo Index e imposta un colore di sfondo.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    
  • Aggiunge una tabella di corsi per l'insegnante selezionato.

  • Aggiunge una tabella di iscrizioni degli studenti per il corso selezionato.

Eseguire l'app e selezionare la scheda Instructors (Insegnanti). La pagina visualizza ( Location office) dall'entità correlata OfficeAssignment . Se OfficeAssignment è null, viene visualizzata una cella di tabella vuota.

Fare clic sul collegamento Select per un insegnante. Verranno visualizzate le modifiche dello stile delle righe e i corsi assegnati a tale insegnante.

Selezionare un corso per visualizzare l'elenco degli studenti iscritti e i voti corrispondenti.

Pagina di indice degli insegnanti con un corso selezionato

Usare Single

Il metodo Single può passare la condizione Where anziché chiamare il metodo Where separatamente:

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

L'uso di Single con una condizione Where è una questione di preferenza personale. Non offre alcun vantaggio rispetto all'uso del metodo Where.

Caricamento esplicito

Il codice corrente specifica il caricamento eager per Enrollments e Students:

InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
    .AsNoTracking()
    .OrderBy(i => i.LastName)
    .ToListAsync();

Si supponga che gli utenti richiedano raramente la visualizzazione delle iscrizioni a un corso. In questo caso, per ottimizzare il funzionamento dell'app è utile caricare i dati delle iscrizioni solo se vengono richiesti. In questa sezione, viene eseguito l'aggiornamento di OnGetAsync in modo da usare il caricamento esplicito di Enrollments e Students.

Aggiornare Pages/Instructors/Index.cshtml.cs con il codice seguente.

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

Il codice precedente rilascia le chiamate del metodo ThenInclude per i dati delle iscrizioni e degli studenti. Se è selezionato un corso, il codice di caricamento esplicito recupera:

  • Le entità Enrollment per il corso selezionato.
  • Le entità Student per ogni entità Enrollment.

Si noti che nel codice precedente .AsNoTracking() è impostato come commento. Le proprietà di navigazione possono solo essere caricate in modo esplicito per le entità registrate.

Test dell'app. Dal punto di vista dell'utente, l'app si comporta in modo identico alla versione precedente.

Passaggi successivi

La prossima esercitazione illustra come aggiornare i dati correlati.

In questa esercitazione vengono letti e visualizzati dati correlati. I dati correlati sono dati EF Core caricati nelle proprietà di navigazione.

Se si verificano problemi non è possibile risolvere, scaricare o visualizzare l'app completata. Istruzioni per il download.

Le figure seguenti mostrano le pagine completate per questa esercitazione:

Pagina di indice dei corsi

Pagina di indice degli insegnanti

Esistono diversi modi per EF Core caricare i dati correlati nelle proprietà di navigazione di un'entità:

  • Caricamento eager. Il caricamento eager si verifica quando una query per un solo tipo di entità carica anche entità correlate. Quando l'entità viene letta, vengono recuperati i dati correlati corrispondenti. Ciò in genere ha come risultato una query join singola che recupera tutti i dati necessari. EF Core eseguirà più query per alcuni tipi di caricamento eager. La generazione di più query può essere più efficiente rispetto al caso di alcune query in EF6, in cui era presente una singola query. Il caricamento eager viene specificato con i metodi Include e ThenInclude.

    Esempio di caricamento eager

    Il caricamento eager invia più query quando è inclusa una navigazione di raccolte:

    • Una query per la query principale
    • Una query per ogni raccolta "perimetrale" nell'albero del caricamento.
  • Query separate con Load: i dati possono essere recuperati in query separate e EF Core "corregge" le proprietà di navigazione. "fixes up" significa che EF Core popola automaticamente le proprietà di navigazione. Le query separate con Load sono più simili a un caricamento esplicito che a un caricamento eager.

    Esempio di query separate

    Nota: EF Core corregge automaticamente le proprietà di navigazione in tutte le altre entità caricate in precedenza nell'istanza di contesto. Anche se i dati per una proprietà di navigazione non sono inclusi in modo esplicito, la proprietà può comunque essere popolata se alcune o tutte le entità correlate sono state caricate in precedenza.

  • Caricamento esplicito Quando un'entità viene letta per la prima volta, i dati correlati non vengono recuperati. Per recuperare i dati correlati quando necessario, è necessario scrivere codice. Il caricamento esplicito con query separate ha come risultato l'invio di più query al database. Con il caricamento esplicito, il codice specifica le proprietà di navigazione da caricare. Per eseguire il caricamento esplicito, usare il metodo Load. Ad esempio:

    Esempio di caricamento esplicito

  • Caricamento lazy. Il caricamento differita è stato aggiunto a EF Core nella versione 2.1. Quando un'entità viene letta per la prima volta, i dati correlati non vengono recuperati. La prima volta che si accede a una proprietà di navigazione, i dati necessari per quest'ultima vengono recuperati automaticamente. Ogni volta che si accede a una proprietà di navigazione per la prima volta, viene inviata una query al database.

  • L'operatore Select carica solo i dati correlati necessari.

Creare una pagina Course che visualizza il nome dei dipartimenti

L'entità Course include una proprietà di navigazione che contiene l'entità Department. L'entità Department contiene il dipartimento a cui il corso è assegnato.

Per visualizzare il nome del dipartimento assegnato in un elenco dei corsi:

  • Ottenere la proprietà Name dall'entità Department.
  • L'entità Department proviene dalla proprietà di navigazione Course.Department.

Course.Department

Scaffolding del modello Course

Seguire le istruzioni in Eseguire lo scaffolding del modello Student (Studente) e usare Course per la classe modello.

Il comando precedente esegue lo scaffolding del modello Course. Aprire il progetto in Visual Studio.

Aprire Pages/Courses/Index.cshtml.cs ed esaminare il OnGetAsync metodo . Il motore di scaffolding ha specificato il caricamento eager per la proprietà di navigazione Department. Il metodo Include specifica il caricamento eager.

Eseguire l'app e selezionare il collegamento Courses (Corsi). La colonna dei dipartimenti visualizza il DepartmentID, che non è utile.

Aggiornare il metodo OnGetAsync con il codice seguente:

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

Il codice precedente aggiunge AsNoTracking. AsNoTracking migliora le prestazioni, perché le entità restituite non vengono registrate, dato che non vengono aggiornate nel contesto corrente.

Eseguire l'aggiornamento Pages/Courses/Index.cshtml con il markup evidenziato seguente:

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

Al codice con scaffolding sono state apportate le modifiche seguenti:

  • Il titolo è stato modificato da Index (Indice) a Courses (Corsi).

  • È stata aggiunta la colonna Number (Numero) con il valore della proprietà CourseID. Per impostazione predefinita, non viene eseguito lo scaffolding delle chiavi primarie, perché in genere non sono significative per gli utenti finali. In questo caso, tuttavia, la chiave primaria è significativa.

  • Modificare la colonna Department (Dipartimento) per visualizzare il nome del dipartimento. Il codice visualizza la proprietà Name dell'entità Department che viene caricata nella proprietà di navigazione Department:

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

Eseguire l'app e selezionare la scheda Courses (Corsi) per visualizzare l'elenco con i nomi dei dipartimenti.

Pagina di indice dei corsi

Il metodo OnGetAsync carica i dati correlati con il metodo Include:

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

L'operatore Select carica solo i dati correlati necessari. Per singoli elementi, ad esempio per Department.Name usa un INNER JOIN SQL. Per le raccolte usa un altro accesso al database, ma anche l'operatore Include fa lo stesso sulle raccolte.

Il codice seguente carica dati correlati con il metodo Select:

public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
            .Select(p => new CourseViewModel
            {
                CourseID = p.CourseID,
                Title = p.Title,
                Credits = p.Credits,
                DepartmentName = p.Department.Name
            }).ToListAsync();
}

CourseViewModel:

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

Per un esempio completo, vedere IndexSelect.cshtml e IndexSelect.cshtml.cs.

Creare una pagina Instructors (Insegnanti) che mostri i corsi e le iscrizioni

In questa sezione viene creata la pagina Instructors (Insegnanti).

Pagina di indice degli insegnanti

Questa pagina legge e visualizza dati correlati nei modi seguenti:

  • L'elenco di insegnanti visualizza dati correlati provenienti dall'entità OfficeAssignment, Office (Ufficio) nella figura precedente. Tra le entità Instructor e OfficeAssignment c'è una relazione uno-a-zero-o-uno. Per le entità OfficeAssignment viene usato il caricamento eager. Il caricamento eager è in genere più efficiente quando i dati correlati devono essere visualizzati. In questo caso, devono essere visualizzate le assegnazioni di ufficio degli insegnanti.
  • Quando l'utente seleziona un insegnante (Harui nella figura precedente), vengono visualizzate le entità Course correlate. Tra le entità Instructor e Course esiste una relazione molti-a-molti. Il caricamento eager viene usato per le entità Course e le entità Department correlate. In questo caso, query separate potrebbero essere più efficienti, perché sono necessari solo i corsi per l'insegnante selezionato. Questo esempio illustra come usare il caricamento eager per le proprietà di navigazione di entità all'interno di proprietà di navigazione.
  • Quando l'utente seleziona un corso (Chemistry (Chimica) nella figura precedente), vengono visualizzati i dati correlati dell'entità Enrollments. Nella figura precedente, vengono visualizzati il voto e il nome degli studenti. Tra le entità Course e Enrollment esiste una relazione uno-a-molti.

Creare un modello per la visualizzazione dell'indice degli insegnanti

La pagina Instructors (Insegnanti) mostra i dati di tre tabelle diverse. Viene creato un modello di visualizzazione che include le tre entità che rappresentano le tre tabelle.

Nella cartella SchoolViewModels creare InstructorIndexData.cs con il codice seguente:

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 del modello Instructor

Seguire le istruzioni in Eseguire lo scaffolding del modello Student (Studente) e usare Instructor per la classe modello.

Il comando precedente esegue lo scaffolding del modello Instructor. Eseguire l'app e passare alla pagina Instructors (Insegnanti).

Sostituisci Pages/Instructors/Index.cshtml.cs con il seguente codice:

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

Il metodo OnGetAsync accetta i dati di route facoltativi per l'ID dell'insegnante selezionato.

Esaminare la query nel Pages/Instructors/Index.cshtml.cs file:

Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

La query ha due istruzioni Include, che riguardano:

Aggiornare la pagina di indice degli insegnanti

Eseguire l'aggiornamento Pages/Instructors/Index.cshtml con il markup seguente:

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

Il markup precedente apporta le modifiche seguenti:

  • Aggiorna la direttiva page da @page a @page "{id:int?}". "{id:int?}" è un modello di route. Il modello di route cambia le stringhe di query di tipo integer nell'URL in dati di route. Se ad esempio si fa clic su sul collegamento Select (Seleziona) per un insegnante con la sola direttiva @page, viene generato un URL come il seguente:

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

    Quando la direttiva della pagina è @page "{id:int?}", l'URL precedente è:

    http://localhost:1234/Instructors/2

  • Il titolo pagina è Instructors (Insegnanti).

  • È stata aggiunta la colonna Office (Ufficio) che visualizza item.OfficeAssignment.Location solo se item.OfficeAssignment non è Null. Poiché questa è una relazione uno-a-zero-o-uno, potrebbe non esserci un'entità OfficeAssignment correlata.

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • È stata aggiunta la colonna Courses (Corsi) che visualizza i corsi tenuti da ogni insegnante. Per altre informazioni su questa razor sintassi, vedere Transizione di riga esplicita.

  • È stato aggiunto codice che aggiunge class="success" in modo dinamico all'elemento tr dell'insegnante selezionato. In questo modo viene impostato un colore di sfondo per la riga selezionata tramite una classe Bootstrap.

    string selectedRow = "";
    if (item.CourseID == Model.CourseID)
    {
        selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • È stato aggiunto un nuovo collegamento ipertestuale con etichetta Select (Seleziona). Questo collegamento invia l'ID dell'insegnante selezionato al metodo Index e imposta un colore di sfondo.

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

Eseguire l'app e selezionare la scheda Instructors (Insegnanti). La pagina visualizza ( Location office) dall'entità correlata OfficeAssignment . Se OfficeAssignment è Null, nella tabella viene visualizzata una cella vuota.

Fare clic sul collegamento Select (Seleziona). Lo stile delle righe cambia.

Aggiungere corsi tenuti dall'insegnante selezionato

Aggiornare il OnGetAsync metodo in Pages/Instructors/Index.cshtml.cs con il codice seguente:

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

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

Esaminare la query aggiornata:

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

La query precedente aggiunge l'entità Department.

Il codice seguente viene eseguito quando viene selezionato un insegnante (id != null). L'insegnante selezionato viene recuperato dall'elenco di insegnanti nel modello di visualizzazione. La proprietà Courses del modello di visualizzazione viene caricata con le entità Course dalla proprietà di navigazione CourseAssignments di tale insegnante.

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

Il metodo Where restituisce una raccolta. Nel metodo Where precedente viene restituita una sola entità Instructor. Il metodo Single converte la raccolta in un'unica entità Instructor. L'entità Instructor consente l'accesso alla proprietà CourseAssignments. CourseAssignments consente l'accesso alle entità Course correlate.

Relazione m:M tra insegnante e corsi

Il metodo Single viene usato in una raccolta quando quest'ultima ha un solo elemento. Il metodo Single genera un'eccezione se la raccolta è vuota o se contiene più elementi. In alternativa, è possibile usare SingleOrDefault, che restituisce un valore predefinito (Null in questo caso) se la raccolta è vuota. L'uso di SingleOrDefault per una raccolta vuota:

  • Genera un'eccezione, a causa del tentativo di cercare la proprietà Courses in un riferimento Null.
  • Il messaggio di eccezione indica meno chiaramente la causa del problema.

Il codice seguente popola la proprietà Enrollments del modello di visualizzazione quando è selezionato un corso:

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

Aggiungere il markup seguente alla fine della Pages/Instructors/Index.cshtmlRazor pagina:

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

Quando è selezionato un insegnante, il markup precedente visualizza un elenco dei corsi correlati all'insegnante stesso.

Test dell'app. Fare clic su un collegamento Select (Seleziona) nella pagina Instructors (Insegnanti).

Visualizzare i dati degli studenti

In questa sezione, l'app viene aggiornata in modo da visualizzare i dati degli studenti per il corso selezionato.

Aggiornare la query nel OnGetAsync metodo in Pages/Instructors/Index.cshtml.cs con il codice seguente:

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

Aggiornare Pages/Instructors/Index.cshtml. Aggiungere il markup seguente alla fine del file:


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

Il markup precedente visualizza l'elenco degli studenti iscritti al corso selezionato.

Aggiornare la pagina e selezionare un insegnante. Selezionare un corso per visualizzare l'elenco degli studenti iscritti e i voti corrispondenti.

Pagina di indice degli insegnanti con un corso selezionato

Usare Single

Il metodo Single può passare la condizione Where anziché chiamare il metodo Where separatamente:

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

L'approccio Single precedente non offre alcun vantaggio rispetto all'uso di Where. Alcuni sviluppatori preferiscono lo stile dell'approccio Single.

Caricamento esplicito

Il codice corrente specifica il caricamento eager per Enrollments e Students:

Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)                 
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

Si supponga che gli utenti richiedano raramente la visualizzazione delle iscrizioni a un corso. In questo caso, per ottimizzare il funzionamento dell'app è utile caricare i dati delle iscrizioni solo se vengono richiesti. In questa sezione, viene eseguito l'aggiornamento di OnGetAsync in modo da usare il caricamento esplicito di Enrollments e Students.

Aggiornare OnGetAsync con il codice seguente:

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

Il codice precedente rilascia le chiamate del metodo ThenInclude per i dati delle iscrizioni e degli studenti. Se è selezionato un corso, il codice evidenziato recupera:

  • Le entità Enrollment per il corso selezionato.
  • Le entità Student per ogni entità Enrollment.

Si noti che nel codice precedente .AsNoTracking() è commentato. Le proprietà di navigazione possono solo essere caricate in modo esplicito per le entità registrate.

Test dell'app. Dal punto di vista degli utenti, l'app si comporta in modo identico alla versione precedente.

La prossima esercitazione illustra come aggiornare i dati correlati.

Risorse aggiuntive