Parte 2, Razor Pagine con EF Core in ASP.NET Core - CRUD

Di Tom Dykstra, Jeremy Likness e Jon P Smith

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.

In questa esercitazione viene esaminato e personalizzato il codice CRUD (Create, Read, Update, Delete) con scaffolding.

Nessun repository

Alcuni sviluppatori usano un modello di servizio o repository per creare un livello di astrazione tra l'interfaccia utente (Razor Pagine) e il livello di accesso ai dati. Questa esercitazione non segue questo approccio. Per ridurre al minimo la complessità e mantenere attiva l'esercitazione su EF Core, EF Core il codice viene aggiunto direttamente alle classi del modello di pagina.

Aggiornare la pagina Details

Il codice con scaffolding per le pagine Students non include i dati di iscrizione. In questa sezione le registrazioni vengono aggiunte alla Details pagina.

Leggere le iscrizioni

Per visualizzare i dati di registrazione di uno studente nella pagina, i dati di registrazione devono essere letti. Il codice sottoposto a scaffolding in Pages/Students/Details.cshtml.cs legge solo i Student dati, senza i Enrollment dati:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Sostituire il metodo OnGetAsync con il codice seguente per leggere i dati di iscrizione per lo studente selezionato. Le modifiche sono evidenziate.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

I Include metodi e ThenInclude determinano il caricamento del contesto della Student.Enrollments proprietà di navigazione e all'interno di ogni registrazione della Enrollment.Course proprietà di navigazione. Questi metodi vengono esaminati in dettaglio nell'esercitazione Leggere i dati correlati.

Il AsNoTracking metodo migliora le prestazioni negli scenari in cui le entità restituite non vengono aggiornate nel contesto corrente. AsNoTracking è descritto più avanti in questa esercitazione.

Visualizzare le iscrizioni

Sostituire il codice in Pages/Students/Details.cshtml con il codice seguente per visualizzare un elenco di registrazioni. Le modifiche sono evidenziate.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

Il codice precedente esegue il ciclo nelle entità nella proprietà di navigazione Enrollments. Per ogni registrazione, il codice visualizza il titolo del corso e il voto. Il titolo del corso viene recuperato dall'entità Course archiviata nella Course proprietà di navigazione dell'entità Enrollments.

Eseguire l'app, selezionare la scheda Students e fare clic sul collegamento Details relativo a uno studente. Viene visualizzato l'elenco dei corsi e dei voti dello studente selezionato.

Modalità di lettura di un'entità

Il codice generato usa FirstOrDefaultAsync per leggere un'entità. Questo metodo restituisce Null se non viene trovato alcun elemento. In caso contrario, viene restituita la prima riga trovata che soddisfa i criteri di filtro della query. FirstOrDefaultAsync è in genere una scelta migliore rispetto alle alternative seguenti:

  • SingleOrDefaultAsync - Genera un'eccezione se è presente più di un'entità che soddisfa il filtro di query. Per determinare se la query può restituire più di una riga, SingleOrDefaultAsync tenta di recuperare più righe. Questa operazione aggiuntiva non è necessario se la query può restituire solo un'entità, ad esempio quando esegue la ricerca in base a una chiave univoca.
  • FindAsync - Trova un'entità con la chiave primaria. Se il contesto rileva un'entità con la chiave primaria, l'entità viene restituita senza una richiesta al database. Questo metodo è ottimizzato per la ricerca di una singola entità, ma non è possibile chiamare Include con FindAsync. Se sono necessari dati correlati, FirstOrDefaultAsync è quindi la scelta migliore.

Dati di route o stringa di query

L'URL della pagina Details è https://localhost:<port>/Students/Details?id=1. Il valore della chiave primaria dell'entità si trova nella stringa di query. Alcuni sviluppatori preferiscono passare il valore della chiave nei dati della route: https://localhost:<port>/Students/Details/1. Per altre informazioni, vedere Aggiornare il codice generato.

Aggiornare la pagina Create

Il codice OnPostAsync con scaffolding per la pagina Create è vulnerabile all'overposting. Sostituire il OnPostAsync metodo in Pages/Students/Create.cshtml.cs con il codice seguente.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

Il codice precedente crea un oggetto Student e quindi usa i campi del modulo pubblicati per aggiornare le proprietà dell'oggetto Student. Il metodo TryUpdateModelAsync:

  • Usa i valori del modulo inviati dalla PageContext proprietà in PageModel.
  • Aggiorna solo le proprietà elencate (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Cerca i campi del modulo con il prefisso "student". Ad esempio, Student.FirstMidName. Non viene fatta distinzione tra maiuscole e minuscole.
  • Usa il sistema di associazione di modelli per convertire i valori dei moduli da stringa ai tipi nel modello Student. Ad esempio, EnrollmentDate viene convertito in DateTime.

Eseguire l'app e creare un'entità Student per testare la pagina Create.

Overposting

L'uso di TryUpdateModel per l'aggiornamento dei campi con i valori inviati è una procedura di sicurezza consigliata poiché impedisce l'overposting. Ad esempio, si supponga che l'entità Student includa una proprietà Secret che la pagina Web non deve aggiornare o aggiungere:

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Anche se l'app non ha un Secret campo nella pagina di creazione o aggiornamento Razor , un hacker potrebbe impostare il Secret valore sovraposto. Un hacker potrebbe usare uno strumento come Fiddler oppure scrivere codice JavaScript per inviare un valore di modulo Secret. Il codice originale non limita i campi usati dallo strumento di associazione di modelli durante la creazione di un'istanza di Student.

Qualsiasi valore specificato dall'hacker per il campo di modulo Secret viene aggiornato nel database. L'immagine seguente mostra lo strumento Fiddler che aggiunge il Secret campo con il valore "OverPost" ai valori del modulo pubblicati.

Fiddler adding Secret field

Il valore "OverPost" è stato aggiunto alla proprietà Secret della riga inserita. Ciò accade anche se il progettista dell'app non ha mai previsto che la proprietà Secret venisse impostata con la pagina Create.

Modello di visualizzazione

I modelli di visualizzazione rappresentano un altro metodo per impedire l'overposting.

Il modello di applicazione è spesso chiamato modello di dominio. Il modello di dominio contiene in genere tutte le proprietà richieste dall'entità corrispondente nel database. Il modello di visualizzazione contiene solo le proprietà necessarie per la pagina dell'interfaccia utente, ad esempio la pagina Crea.

Oltre al modello di visualizzazione, alcune app usano un modello di associazione o un modello di input per passare i dati tra la Razor classe del modello di pagina Pages e il browser.

Si consideri il modello di visualizzazione StudentVM seguente:

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

Il codice seguente usa il modello di visualizzazione StudentVM per creare un nuovo studente:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

Il metodo SetValues imposta i valori di questo oggetto leggendo i valori da un altro PropertyValues oggetto. SetValues usa la corrispondenza dei nomi di proprietà. Tipo di modello di visualizzazione:

  • Non è necessario essere correlati al tipo di modello.
  • Deve avere proprietà corrispondenti.

L'uso StudentVM di richiede l'uso StudentVM della pagina Crea anziché Student:

@page
@model CreateVMModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Aggiornare la pagina Edit

In Pages/Students/Edit.cshtml.cssostituire i OnGetAsync metodi e OnPostAsync con il codice seguente.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Le modifiche al codice sono simili alla pagina Create con alcune eccezioni:

  • FirstOrDefaultAsync è stato sostituito con FindAsync. Quando non è necessario includere dati correlati, FindAsync è più efficiente.
  • OnPostAsync ha un parametro id.
  • Lo studente corrente viene recuperato dal database senza creare uno studente vuoto.

Eseguire l'app e testarla creando e modificando uno studente.

Stati di entità

Il contesto del database tiene traccia della sincronizzazione delle entità in memoria con le righe corrispondenti nel database. Queste informazioni di traccia determinano le operazioni eseguite quando viene chiamato SaveChangesAsync. Ad esempio, quando una nuova entità viene passata al metodo AddAsync, lo stato dell'entità viene impostato su Added. Quando SaveChangesAsync viene chiamato, il contesto del database genera un comando SQL INSERT .

Un'entità può essere in uno dei seguenti stati:

  • Added: l'entità non esiste ancora nel database. Il SaveChanges metodo genera un'istruzione INSERT .

  • Unchanged: non è necessario salvare alcuna modifica con questa entità. Un'entità ha questo stato quando viene letta dal database.

  • Modified: sono stati modificati alcuni o tutti i valori di proprietà dell'entità. Il SaveChanges metodo genera un'istruzione UPDATE .

  • Deleted: l'entità è stata contrassegnata per l'eliminazione. Il SaveChanges metodo genera un'istruzione DELETE .

  • Detached: l'entità non viene rilevata dal contesto del database.

In un'applicazione desktop le modifiche dello stato vengono in genere impostate automaticamente. Viene letta un'entità, vengono apportate le modifiche e lo stato dell'entità viene modificato automaticamente in Modified. La chiamata SaveChanges genera un'istruzione SQL UPDATE che aggiorna solo le proprietà modificate.

In un'app Web il DbContext che legge un'entità e visualizza i dati viene eliminato dopo il rendering di una pagina. Quando viene chiamato il metodo OnPostAsync di una pagina, viene effettuata una nuova richiesta Web con una nuova istanza di DbContext. La rilettura dell'entità nel nuovo contesto simula l'elaborazione desktop.

Aggiornare la pagina Delete (Elimina)

In questa sezione viene implementato un messaggio di errore personalizzato quando la chiamata a SaveChanges non riesce.

Sostituire il codice in Pages/Students/Delete.cshtml.cs con il codice seguente:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Il codice precedente:

  • Aggiunge la registrazione.
  • Aggiunge il parametro saveChangesError facoltativo alla firma del OnGetAsync metodo. saveChangesError indica se il metodo è stato chiamato dopo un errore di eliminazione dell'oggetto Student.

L'operazione di eliminazione potrebbe non riuscire a causa di problemi di rete temporanei. Gli errori di rete temporanei sono più probabili quando il database è nel cloud. Il saveChangesError parametro è false quando viene chiamata la pagina OnGetAsync Elimina dall'interfaccia utente. Quando OnGetAsync viene chiamato da OnPostAsync perché l'operazione di eliminazione non è riuscita, il saveChangesError parametro è true.

Il metodo OnPostAsync recupera l'entità selezionata, quindi chiama il metodo Remove per impostare lo stato dell'entità su Deleted. Quando SaveChanges viene chiamato, viene generato un comando SQL DELETE . Se Remove ha esito negativo:

  • Viene rilevata l'eccezione del database.
  • Il metodo OnGetAsync delle pagine Delete viene chiamato con saveChangesError=true.

Aggiungere un messaggio di errore a Pages/Students/Delete.cshtml:

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Eseguire l'app ed eliminare uno studente per testare la pagina Delete.

Passaggi successivi

In questa esercitazione viene esaminato e personalizzato il codice CRUD (Create, Read, Update, Delete) con scaffolding.

Nessun repository

Alcuni sviluppatori usano un modello di servizio o repository per creare un livello di astrazione tra l'interfaccia utente (Razor Pagine) e il livello di accesso ai dati. Questa esercitazione non segue questo approccio. Per ridurre al minimo la complessità e mantenere attiva l'esercitazione su EF Core, EF Core il codice viene aggiunto direttamente alle classi del modello di pagina.

Aggiornare la pagina Details

Il codice con scaffolding per le pagine Students non include i dati di iscrizione. In questa sezione le registrazioni vengono aggiunte alla Details pagina.

Leggere le iscrizioni

Per visualizzare i dati di registrazione di uno studente nella pagina, i dati di registrazione devono essere letti. Il codice sottoposto a scaffolding in Pages/Students/Details.cshtml.cs legge solo i Student dati, senza i Enrollment dati:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Sostituire il metodo OnGetAsync con il codice seguente per leggere i dati di iscrizione per lo studente selezionato. Le modifiche sono evidenziate.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

I Include metodi e ThenInclude determinano il caricamento del contesto della Student.Enrollments proprietà di navigazione e all'interno di ogni registrazione della Enrollment.Course proprietà di navigazione. Questi metodi vengono esaminati in dettaglio nell'esercitazione Leggere i dati correlati.

Il AsNoTracking metodo migliora le prestazioni negli scenari in cui le entità restituite non vengono aggiornate nel contesto corrente. AsNoTracking è descritto più avanti in questa esercitazione.

Visualizzare le iscrizioni

Sostituire il codice in Pages/Students/Details.cshtml con il codice seguente per visualizzare un elenco di registrazioni. Le modifiche sono evidenziate.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

Il codice precedente esegue il ciclo nelle entità nella proprietà di navigazione Enrollments. Per ogni registrazione, il codice visualizza il titolo del corso e il voto. Il titolo del corso viene recuperato dall'entità Course archiviata nella Course proprietà di navigazione dell'entità Enrollments.

Eseguire l'app, selezionare la scheda Students e fare clic sul collegamento Details relativo a uno studente. Viene visualizzato l'elenco dei corsi e dei voti dello studente selezionato.

Modalità di lettura di un'entità

Il codice generato usa FirstOrDefaultAsync per leggere un'entità. Questo metodo restituisce Null se non viene trovato alcun elemento. In caso contrario, viene restituita la prima riga trovata che soddisfa i criteri di filtro della query. FirstOrDefaultAsync è in genere una scelta migliore rispetto alle alternative seguenti:

  • SingleOrDefaultAsync - Genera un'eccezione se è presente più di un'entità che soddisfa il filtro di query. Per determinare se la query può restituire più di una riga, SingleOrDefaultAsync tenta di recuperare più righe. Questa operazione aggiuntiva non è necessario se la query può restituire solo un'entità, ad esempio quando esegue la ricerca in base a una chiave univoca.
  • FindAsync - Trova un'entità con la chiave primaria. Se il contesto rileva un'entità con la chiave primaria, l'entità viene restituita senza una richiesta al database. Questo metodo è ottimizzato per la ricerca di una singola entità, ma non è possibile chiamare Include con FindAsync. Se sono necessari dati correlati, FirstOrDefaultAsync è quindi la scelta migliore.

Dati di route o stringa di query

L'URL della pagina Details è https://localhost:<port>/Students/Details?id=1. Il valore della chiave primaria dell'entità si trova nella stringa di query. Alcuni sviluppatori preferiscono passare il valore della chiave nei dati della route: https://localhost:<port>/Students/Details/1. Per altre informazioni, vedere Aggiornare il codice generato.

Aggiornare la pagina Create

Il codice OnPostAsync con scaffolding per la pagina Create è vulnerabile all'overposting. Sostituire il OnPostAsync metodo in Pages/Students/Create.cshtml.cs con il codice seguente.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

Il codice precedente crea un oggetto Student e quindi usa i campi del modulo pubblicati per aggiornare le proprietà dell'oggetto Student. Il metodo TryUpdateModelAsync:

  • Usa i valori del modulo inviati dalla PageContext proprietà in PageModel.
  • Aggiorna solo le proprietà elencate (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Cerca i campi del modulo con il prefisso "student". Ad esempio, Student.FirstMidName. Non viene fatta distinzione tra maiuscole e minuscole.
  • Usa il sistema di associazione di modelli per convertire i valori dei moduli da stringa ai tipi nel modello Student. Ad esempio, EnrollmentDate viene convertito in DateTime.

Eseguire l'app e creare un'entità Student per testare la pagina Create.

Overposting

L'uso di TryUpdateModel per l'aggiornamento dei campi con i valori inviati è una procedura di sicurezza consigliata poiché impedisce l'overposting. Ad esempio, si supponga che l'entità Student includa una proprietà Secret che la pagina Web non deve aggiornare o aggiungere:

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Anche se l'app non ha un Secret campo nella pagina di creazione o aggiornamento Razor , un hacker potrebbe impostare il Secret valore sovraposto. Un hacker potrebbe usare uno strumento come Fiddler oppure scrivere codice JavaScript per inviare un valore di modulo Secret. Il codice originale non limita i campi usati dallo strumento di associazione di modelli durante la creazione di un'istanza di Student.

Qualsiasi valore specificato dall'hacker per il campo di modulo Secret viene aggiornato nel database. L'immagine seguente mostra lo strumento Fiddler che aggiunge il Secret campo con il valore "OverPost" ai valori del modulo pubblicati.

Fiddler adding Secret field

Il valore "OverPost" è stato aggiunto alla proprietà Secret della riga inserita. Ciò accade anche se il progettista dell'app non ha mai previsto che la proprietà Secret venisse impostata con la pagina Create.

Modello di visualizzazione

I modelli di visualizzazione rappresentano un altro metodo per impedire l'overposting.

Il modello di applicazione è spesso chiamato modello di dominio. Il modello di dominio contiene in genere tutte le proprietà richieste dall'entità corrispondente nel database. Il modello di visualizzazione contiene solo le proprietà necessarie per la pagina dell'interfaccia utente, ad esempio la pagina Crea.

Oltre al modello di visualizzazione, alcune app usano un modello di associazione o un modello di input per passare i dati tra la Razor classe del modello di pagina Pages e il browser.

Si consideri il modello di visualizzazione StudentVM seguente:

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

Il codice seguente usa il modello di visualizzazione StudentVM per creare un nuovo studente:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

Il metodo SetValues imposta i valori di questo oggetto leggendo i valori da un altro PropertyValues oggetto. SetValues usa la corrispondenza dei nomi di proprietà. Tipo di modello di visualizzazione:

  • Non è necessario essere correlati al tipo di modello.
  • Deve avere proprietà corrispondenti.

L'uso StudentVM di richiede l'uso StudentVM della pagina Crea anziché Student:

@page
@model CreateVMModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Aggiornare la pagina Edit

In Pages/Students/Edit.cshtml.cssostituire i OnGetAsync metodi e OnPostAsync con il codice seguente.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Le modifiche al codice sono simili alla pagina Create con alcune eccezioni:

  • FirstOrDefaultAsync è stato sostituito con FindAsync. Quando non è necessario includere dati correlati, FindAsync è più efficiente.
  • OnPostAsync ha un parametro id.
  • Lo studente corrente viene recuperato dal database senza creare uno studente vuoto.

Eseguire l'app e testarla creando e modificando uno studente.

Stati di entità

Il contesto del database tiene traccia della sincronizzazione delle entità in memoria con le righe corrispondenti nel database. Queste informazioni di traccia determinano le operazioni eseguite quando viene chiamato SaveChangesAsync. Ad esempio, quando una nuova entità viene passata al metodo AddAsync, lo stato dell'entità viene impostato su Added. Quando SaveChangesAsync viene chiamato, il contesto del database genera un comando SQL INSERT .

Un'entità può essere in uno dei seguenti stati:

  • Added: l'entità non esiste ancora nel database. Il SaveChanges metodo genera un'istruzione INSERT .

  • Unchanged: non è necessario salvare alcuna modifica con questa entità. Un'entità ha questo stato quando viene letta dal database.

  • Modified: sono stati modificati alcuni o tutti i valori di proprietà dell'entità. Il SaveChanges metodo genera un'istruzione UPDATE .

  • Deleted: l'entità è stata contrassegnata per l'eliminazione. Il SaveChanges metodo genera un'istruzione DELETE .

  • Detached: l'entità non viene rilevata dal contesto del database.

In un'applicazione desktop le modifiche dello stato vengono in genere impostate automaticamente. Viene letta un'entità, vengono apportate le modifiche e lo stato dell'entità viene modificato automaticamente in Modified. La chiamata SaveChanges genera un'istruzione SQL UPDATE che aggiorna solo le proprietà modificate.

In un'app Web il DbContext che legge un'entità e visualizza i dati viene eliminato dopo il rendering di una pagina. Quando viene chiamato il metodo OnPostAsync di una pagina, viene effettuata una nuova richiesta Web con una nuova istanza di DbContext. La rilettura dell'entità nel nuovo contesto simula l'elaborazione desktop.

Aggiornare la pagina Delete (Elimina)

In questa sezione viene implementato un messaggio di errore personalizzato quando la chiamata a SaveChanges non riesce.

Sostituire il codice in Pages/Students/Delete.cshtml.cs con il codice seguente:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Il codice precedente:

  • Aggiunge la registrazione.
  • Aggiunge il parametro saveChangesError facoltativo alla firma del OnGetAsync metodo. saveChangesError indica se il metodo è stato chiamato dopo un errore di eliminazione dell'oggetto Student.

L'operazione di eliminazione potrebbe non riuscire a causa di problemi di rete temporanei. Gli errori di rete temporanei sono più probabili quando il database è nel cloud. Il saveChangesError parametro è false quando viene chiamata la pagina OnGetAsync Elimina dall'interfaccia utente. Quando OnGetAsync viene chiamato da OnPostAsync perché l'operazione di eliminazione non è riuscita, il saveChangesError parametro è true.

Il metodo OnPostAsync recupera l'entità selezionata, quindi chiama il metodo Remove per impostare lo stato dell'entità su Deleted. Quando SaveChanges viene chiamato, viene generato un comando SQL DELETE . Se Remove ha esito negativo:

  • Viene rilevata l'eccezione del database.
  • Il metodo OnGetAsync delle pagine Delete viene chiamato con saveChangesError=true.

Aggiungere un messaggio di errore a Pages/Students/Delete.cshtml:

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Eseguire l'app ed eliminare uno studente per testare la pagina Delete.

Passaggi successivi

In questa esercitazione viene esaminato e personalizzato il codice CRUD (Create, Read, Update, Delete) con scaffolding.

Nessun repository

Alcuni sviluppatori usano un modello di servizio o repository per creare un livello di astrazione tra l'interfaccia utente (Razor Pagine) e il livello di accesso ai dati. Questa esercitazione non segue questo approccio. Per ridurre al minimo la complessità e mantenere attiva l'esercitazione su EF Core, EF Core il codice viene aggiunto direttamente alle classi del modello di pagina.

Aggiornare la pagina Details

Il codice con scaffolding per le pagine Students non include i dati di iscrizione. In questa sezione le registrazioni vengono aggiunte alla pagina Dettagli.

Leggere le iscrizioni

Per visualizzare i dati di registrazione di uno studente nella pagina, i dati di registrazione devono essere letti. Il codice sottoposto a scaffolding in Pages/Students/Details.cshtml.cs legge solo i dati student, senza i dati di registrazione:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

Sostituire il metodo OnGetAsync con il codice seguente per leggere i dati di iscrizione per lo studente selezionato. Le modifiche sono evidenziate.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

I Include metodi e ThenInclude determinano il caricamento del contesto della Student.Enrollments proprietà di navigazione e all'interno di ogni registrazione della Enrollment.Course proprietà di navigazione. Questi metodi vengono esaminati in dettaglio nell'esercitazione Lettura dei dati correlati.

Il AsNoTracking metodo migliora le prestazioni negli scenari in cui le entità restituite non vengono aggiornate nel contesto corrente. AsNoTracking è descritto più avanti in questa esercitazione.

Visualizzare le iscrizioni

Sostituire il codice in Pages/Students/Details.cshtml con il codice seguente per visualizzare un elenco di registrazioni. Le modifiche sono evidenziate.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

Il codice precedente esegue il ciclo nelle entità nella proprietà di navigazione Enrollments. Per ogni registrazione, il codice visualizza il titolo del corso e il voto. Il titolo del corso viene recuperato dall'entità Course memorizzata nella proprietà di navigazione Course dell'entità Enrollments.

Eseguire l'app, selezionare la scheda Students e fare clic sul collegamento Details relativo a uno studente. Viene visualizzato l'elenco dei corsi e dei voti dello studente selezionato.

Modalità di lettura di un'entità

Il codice generato usa FirstOrDefaultAsync per leggere un'entità. Questo metodo restituisce Null se non viene trovato alcun elemento. In caso contrario, viene restituita la prima riga trovata che soddisfa i criteri di filtro della query. FirstOrDefaultAsync è in genere una scelta migliore rispetto alle alternative seguenti:

  • SingleOrDefaultAsync - Genera un'eccezione se è presente più di un'entità che soddisfa il filtro di query. Per determinare se la query può restituire più di una riga, SingleOrDefaultAsync tenta di recuperare più righe. Questa operazione aggiuntiva non è necessario se la query può restituire solo un'entità, ad esempio quando esegue la ricerca in base a una chiave univoca.
  • FindAsync - Trova un'entità con la chiave primaria. Se il contesto rileva un'entità con la chiave primaria, l'entità viene restituita senza una richiesta al database. Questo metodo è ottimizzato per la ricerca di una singola entità, ma non è possibile chiamare Include con FindAsync. Se sono necessari dati correlati, FirstOrDefaultAsync è quindi la scelta migliore.

Dati di route o stringa di query

L'URL della pagina Details è https://localhost:<port>/Students/Details?id=1. Il valore della chiave primaria dell'entità si trova nella stringa di query. Alcuni sviluppatori preferiscono passare il valore della chiave nei dati della route: https://localhost:<port>/Students/Details/1. Per altre informazioni, vedere Aggiornare il codice generato.

Aggiornare la pagina Create

Il codice OnPostAsync con scaffolding per la pagina Create è vulnerabile all'overposting. Sostituire il OnPostAsync metodo in Pages/Students/Create.cshtml.cs con il codice seguente.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

Il codice precedente crea un oggetto Student e quindi usa i campi del modulo pubblicati per aggiornare le proprietà dell'oggetto Student. Il metodo TryUpdateModelAsync:

  • Usa i valori del modulo inviati dalla PageContext proprietà in PageModel.
  • Aggiorna solo le proprietà elencate (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Cerca i campi del modulo con il prefisso "student". Ad esempio, Student.FirstMidName. Non viene fatta distinzione tra maiuscole e minuscole.
  • Usa il sistema di associazione di modelli per convertire i valori dei moduli da stringa ai tipi nel modello Student. Ad esempio, EnrollmentDate deve essere convertito in DateTime.

Eseguire l'app e creare un'entità Student per testare la pagina Create.

Overposting

L'uso di TryUpdateModel per l'aggiornamento dei campi con i valori inviati è una procedura di sicurezza consigliata poiché impedisce l'overposting. Ad esempio, si supponga che l'entità Student includa una proprietà Secret che la pagina Web non deve aggiornare o aggiungere:

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Anche se l'app non ha un Secret campo nella pagina di creazione o aggiornamento Razor , un hacker potrebbe impostare il Secret valore sovraposto. Un hacker potrebbe usare uno strumento come Fiddler oppure scrivere codice JavaScript per inviare un valore di modulo Secret. Il codice originale non limita i campi usati dallo strumento di associazione di modelli durante la creazione di un'istanza di Student.

Qualsiasi valore specificato dall'hacker per il campo di modulo Secret viene aggiornato nel database. L'immagine seguente illustra lo strumento Fiddler che aggiunge il campo Secret (con il valore "OverPost") ai valori di modulo inviati.

Fiddler adding Secret field

Il valore "OverPost" è stato aggiunto alla proprietà Secret della riga inserita. Ciò accade anche se il progettista dell'app non ha mai previsto che la proprietà Secret venisse impostata con la pagina Create.

Modello di visualizzazione

I modelli di visualizzazione rappresentano un altro metodo per impedire l'overposting.

Il modello di applicazione è spesso chiamato modello di dominio. Il modello di dominio contiene in genere tutte le proprietà richieste dall'entità corrispondente nel database. Il modello di visualizzazione contiene solo le proprietà necessarie per l'interfaccia utente per cui viene usato, ad esempio la pagina Create.

Oltre al modello di visualizzazione, alcune app usano un modello di associazione o un modello di input per passare i dati tra la Razor classe del modello di pagina Pages e il browser.

Si consideri il modello di visualizzazione Student seguente:

using System;

namespace ContosoUniversity.Models
{
    public class StudentVM
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

Il codice seguente usa il modello di visualizzazione StudentVM per creare un nuovo studente:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

Il metodo SetValues imposta i valori di questo oggetto leggendo i valori da un altro PropertyValues oggetto. SetValues usa la corrispondenza dei nomi di proprietà. Poiché il tipo di modello di visualizzazione non deve essere correlato al tipo di modello, è sufficiente che abbia proprietà corrispondenti.

Se si usa StudentVM è necessario che Create.cshtml venga aggiornato per l'uso di StudentVM anziché Student.

Aggiornare la pagina Edit

In Pages/Students/Edit.cshtml.cssostituire i OnGetAsync metodi e OnPostAsync con il codice seguente.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

Le modifiche al codice sono simili alla pagina Create con alcune eccezioni:

  • FirstOrDefaultAsync è stato sostituito con FindAsync. Quando non sono necessari dati correlati inclusi, FindAsync è più efficiente.
  • OnPostAsync ha un parametro id.
  • Lo studente corrente viene recuperato dal database senza creare uno studente vuoto.

Eseguire l'app e testarla creando e modificando uno studente.

Stati di entità

Il contesto del database tiene traccia della sincronizzazione delle entità in memoria con le righe corrispondenti nel database. Queste informazioni di traccia determinano le operazioni eseguite quando viene chiamato SaveChangesAsync. Ad esempio, quando una nuova entità viene passata al metodo AddAsync, lo stato dell'entità viene impostato su Added. Quando viene chiamato SaveChangesAsync, il contesto del database genera un comando SQL INSERT.

Un'entità può essere in uno dei seguenti stati:

  • Added: l'entità non esiste ancora nel database. Il metodo SaveChanges genera un'istruzione INSERT.

  • Unchanged: non è necessario salvare alcuna modifica con questa entità. Un'entità ha questo stato quando viene letta dal database.

  • Modified: sono stati modificati alcuni o tutti i valori di proprietà dell'entità. Il metodo SaveChanges genera un'istruzione UPDATE.

  • Deleted: l'entità è stata contrassegnata per l'eliminazione. Il metodo SaveChanges genera un'istruzione DELETE.

  • Detached: l'entità non viene rilevata dal contesto del database.

In un'applicazione desktop le modifiche dello stato vengono in genere impostate automaticamente. Viene letta un'entità, vengono apportate le modifiche e lo stato dell'entità viene modificato automaticamente in Modified. La chiamata di SaveChanges genera un'istruzione SQL UPDATE che aggiorna solo le proprietà modificate.

In un'app Web il DbContext che legge un'entità e visualizza i dati viene eliminato dopo il rendering di una pagina. Quando viene chiamato il metodo OnPostAsync di una pagina, viene effettuata una nuova richiesta Web con una nuova istanza di DbContext. La rilettura dell'entità nel nuovo contesto simula l'elaborazione desktop.

Aggiornare la pagina Delete (Elimina)

In questa sezione viene implementato un messaggio di errore personalizzato quando la chiamata a SaveChanges ha esito negativo.

Sostituire il codice in Pages/Students/Delete.cshtml.cs con il codice seguente. Le modifiche vengono evidenziate (a eccezione della pulizia delle istruzioni using).

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

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

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = "Delete failed. Try again";
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

Il codice precedente aggiunge il parametro facoltativo saveChangesError alla firma del metodo OnGetAsync. saveChangesError indica se il metodo è stato chiamato dopo un errore di eliminazione dell'oggetto Student. L'operazione di eliminazione potrebbe non riuscire a causa di problemi di rete temporanei. Gli errori di rete temporanei sono più probabili quando il database è nel cloud. Il parametrosaveChangesError è false quando si chiama OnGetAsync della pagina Delete dall'interfaccia utente. Quando OnGetAsync viene chiamato da OnPostAsync (perché l'operazione di eliminazione ha avuto esito negativo), il parametro saveChangesError ha valore true.

Il metodo OnPostAsync recupera l'entità selezionata, quindi chiama il metodo Remove per impostare lo stato dell'entità su Deleted. Quando viene chiamato SaveChanges, viene generato un comando SQL DELETE. Se Remove ha esito negativo:

  • Viene rilevata l'eccezione del database.
  • Il metodo della OnGetAsync pagina Delete viene chiamato con saveChangesError=true.

Aggiungere un messaggio di errore alla pagina Elimina Razor (Pages/Students/Delete.cshtml):

@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Eseguire l'app ed eliminare uno studente per testare la pagina Delete.

Passaggi successivi