Parte 2, Razor Pagine con EF Core in ASP.NET Core - CRUD
Nota
Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Avviso
Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Importante
Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.
Per la versione corrente, vedere la versione .NET 9 di questo articolo.
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
conFindAsync
. 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 inDateTime
.
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.
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.cs
sostituire 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 parametroid
.- 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. IlSaveChanges
metodo genera un'istruzioneINSERT
.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à. IlSaveChanges
metodo genera un'istruzioneUPDATE
.Deleted
: l'entità è stata contrassegnata per l'eliminazione. IlSaveChanges
metodo genera un'istruzioneDELETE
.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 delOnGetAsync
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 consaveChangesError=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
conFindAsync
. 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 inDateTime
.
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.
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.cs
sostituire 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 parametroid
.- 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. IlSaveChanges
metodo genera un'istruzioneINSERT
.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à. IlSaveChanges
metodo genera un'istruzioneUPDATE
.Deleted
: l'entità è stata contrassegnata per l'eliminazione. IlSaveChanges
metodo genera un'istruzioneDELETE
.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 delOnGetAsync
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 consaveChangesError=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
conFindAsync
. 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.
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.cs
sostituire 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 parametroid
.- 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 metodoSaveChanges
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 metodoSaveChanges
genera un'istruzione UPDATE.Deleted
: l'entità è stata contrassegnata per l'eliminazione. Il metodoSaveChanges
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 consaveChangesError=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.