Teil 2: Razor-Seiten mit EF Core in ASP.NET Core – CRUD
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der Supportrichtlinie für .NET und .NET Core. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Wichtig
Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Von Tom Dykstra, Jeremy Likness und Jon P. Smith
Die Web-App Contoso University veranschaulicht, wie Razor-Seiten-Web-Apps mit EF Core und Visual Studio erstellt werden können. Informationen zu den Tutorials finden Sie im ersten Tutorial.
Wenn Probleme auftreten, die Sie nicht beheben können, laden Sie die vollständige App herunter, und vergleichen Sie diesen Code mit dem Code, den Sie anhand des Tutorials erstellt haben.
In diesem Tutorial wird der erstellte CRUD-Code (CRUD = Create, Read, Update, Delete; Erstellen, Lesen, Aktualisieren, Löschen) überprüft und angepasst.
Kein Repository
Einige Entwickler verwenden eine Dienstschicht oder ein Repositorymuster, um eine Abstraktionsschicht zwischen der Benutzeroberfläche (Razor Pages) und der Datenzugriffsschicht zu erstellen. In diesem Tutorial ist dies nicht der Fall. Zur Minimierung der Komplexität und damit EF Core im Fokus dieses Tutorials bleibt, wird EF Core-Code den Seitenmodellklassen direkt hinzugefügt.
Aktualisieren der Seite „Details“
Der Gerüstbaucode für die Seite „Students“ enthält keine Registrierungsdaten. In diesem Abschnitt fügen Sie der Seite „Details
“ Registrierungen hinzu.
Lesen von Registrierungen
Um die Registrierungsdaten eines Kursteilnehmers auf der Seite anzuzeigen, müssen die Registrierungsdaten gelesen werden. Der Gerüstcode in Pages/Students/Details.cshtml.cs
liest nur die Student
-Daten ohne die Enrollment
-Daten:
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();
}
Ersetzen Sie die OnGetAsync
-Methode durch den folgenden Code, um Registrierungsdaten für den ausgewählten Studenten zu lesen. Die Änderungen werden hervorgehoben.
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();
}
Die Methoden Include und ThenInclude veranlassen den Kontext, die Navigationseigenschaft Student.Enrollments
und innerhalb jeder Registrierung die Navigationseigenschaft Enrollment.Course
zu laden. Diese Methoden werden im Tutorial zum Lesen relevanter Daten ausführlich untersucht.
Die Methode AsNoTracking verbessert die Leistung in Szenarien, in denen die zurückgegebenen Entitäten nicht im aktuellen Kontext aktualisiert werden. AsNoTracking
wird später in diesem Tutorial behandelt.
Anzeigen von Registrierungen
Ersetzen Sie den Code in Pages/Students/Details.cshtml
durch den folgenden Code, um eine Liste der Registrierungen anzuzeigen. Die Änderungen werden hervorgehoben.
@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>
Der vorangehende Code durchläuft die Entitäten in der Navigationseigenschaft Enrollments
. Für jede Registrierung werden der Kurstitel und die Klasse angezeigt. Der Kurstitel wird von der Course
-Entität abgerufen, die in der Course
-Navigationseigenschaft der Entität „Enrollments“ gespeichert ist.
Führen Sie die App aus, wählen Sie die Registerkarte Studenten aus, und klicken Sie bei einem Studenten auf den Link Details. Die Liste der Kurse und Klassen für den ausgewählten Studenten wird angezeigt.
Möglichkeiten zum Lesen einer einzelnen Entität
Der generierte Code verwendet FirstOrDefaultAsync, um eine Entität zu lesen. Diese Methode gibt NULL zurück, wenn nichts gefunden wurde. Andernfalls wird die erste gefundene Zeile zurückgegeben, die die Abfragefilterkriterien erfüllt. FirstOrDefaultAsync
ist im Allgemeinen eine bessere Wahl als die folgenden Alternativen:
- SingleOrDefaultAsync: Löst eine Ausnahme aus, wenn mehrere Entitäten vorhanden sind, die dem Abfragefilter entsprechen. Um zu ermitteln, ob von der Abfrage mehrere Zeilen zurückgegeben werden können, versucht
SingleOrDefaultAsync
, mehrere Zeilen abzurufen. Diese zusätzliche Arbeit ist nicht erforderlich, wenn die Abfrage nur eine Entität zurückgeben kann, wenn sie nach einem eindeutigen Schlüssel sucht. - FindAsync: Sucht nach einer Entität mit dem Primärschlüssel. Wenn eine Entität mit dem Primärschlüssel vom Kontext nachverfolgt wird, wird sie ohne eine Anforderung an die Datenbank zurückgegeben. Diese Methode ist für die Suche nach einer einzelnen Entität optimiert, aber Sie können
Include
nicht mitFindAsync
aufrufen. Wenn also in Beziehung stehende Daten benötigt werden, istFirstOrDefaultAsync
die bessere Wahl.
Routendaten oder Abfragezeichenfolge
Die URL für die Detailseite lautet https://localhost:<port>/Students/Details?id=1
. Der Primärschlüsselwert der Entität befindet sich in der Abfragezeichenfolge. Einige Entwickler bevorzugen es, den Schlüsselwert in Routendaten zu übergeben: https://localhost:<port>/Students/Details/1
. Weitere Informationen finden Sie unter Aktualisieren des generierten Codes.
Aktualisieren der Seite „Erstellen“
Der OnPostAsync
-Gerüstbaucode für die Seite „Create“ ist anfällig für Overposting. Ersetzen Sie die OnPostAsync
-Methode in Pages/Students/Create.cshtml.cs
durch folgenden Code.
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
Der vorangehende Code erstellt ein Student-Objekt und verwendet dann die bereitgestellten Formularfelder, um die Eigenschaften des Student-Objekts zu aktualisieren. Die TryUpdateModelAsync-Methode:
- Verwendet die bereitgestellten Formularwerte aus der PageContext-Eigenschaft in PageModel.
- Aktualisiert nur die aufgeführten Eigenschaften (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Sucht nach Formularfeldern mit dem Präfix „Student“. Beispielsweise
Student.FirstMidName
. Die Groß-/Kleinschreibung wird hier nicht beachtet. - Verwendet das Modellbindungssystem zum Konvertieren von Formularwerten aus Zeichenfolgen in die Typen im
Student
-Modell.EnrollmentDate
wird z. B. inDateTime
konvertiert.
Führen Sie die App aus, und erstellen Sie eine Student-Entität, um die Seite „Create“ zu testen.
Overposting
Die Verwendung von TryUpdateModel
zur Aktualisierung von Feldern mit bereitgestellten Werten ist eine bewährte Sicherheitsmethode, da Overposting verhindert wird. Angenommen, die Student-Entität enthält die Eigenschaft Secret
, die auf dieser Webseite nicht aktualisiert oder hinzugefügt werden sollte:
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; }
}
Selbst wenn die App auf der Razor Page „Create“ oder „Update“ (Erstellen oder Aktualisieren) nicht über das Feld Secret
verfügt, könnte ein Hacker den Secret
-Wert durch Overposting festlegen. Ein Hacker könnte ein Tool wie Fiddler verwenden oder JavaScript-Code schreiben, um einen Secret
-Formularwert bereitzustellen. Die Felder, die von der Modellbindung bei der Erstellung einer Student-Instanz verwendet werden, werden nicht durch den ursprünglichen Code begrenzt.
Jeder beliebige Wert, der vom Hacker im Secret
-Formularfeld angegeben wird, wird in der Datenbank aktualisiert. Die folgende Abbildung zeigt das Fiddler-Tool beim Hinzufügen des Felds Secret
mit dem Wert „OverPost“ zu den bereitgestellten Formularwerten.
Der Wert „OverPost“ wurde erfolgreich zur Eigenschaft Secret
der eingefügten Zeile hinzugefügt. Dies geschieht, obwohl der App-Designer nie die Absicht hatte, die Eigenschaft Secret
auf der Seite „Create“ festzulegen.
ViewModel-Element
Ansichtsmodelle bieten eine alternative Möglichkeit zum Verhindern von Overposting.
Das Anwendungsmodell wird häufig als Domänenmodell bezeichnet. Das Domänenmodell enthält normalerweise alle Eigenschaften, die die entsprechende Entität in der Datenbank erfordert. Das Ansichtsmodell enthält nur die Eigenschaften, die für die Benutzeroberflächenseite erforderlich sind, z. B. die Seite „Create“.
Neben dem Ansichtsmodell verwenden einige Apps ein Bindungsmodell oder Eingabemodell für die Bereitstellung von Daten zwischen der Seitenmodellklasse auf den Razor Pages und dem Browser.
Betrachten Sie das folgende StudentVM
-Ansichtsmodell:
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
Im folgenden Code wird das StudentVM
-Ansichtsmodell für die Erstellung eines neuen Studenten verwendet:
[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");
}
Die Methode SetValues legt die Werte dieses Objekts fest, indem sie Werte aus einem anderen PropertyValues-Objekt einliest. SetValues
verwendet die Übereinstimmung von Eigenschaftsnamen. Der Ansichtsmodelltyp:
- Muss nicht mit dem Modelltyp verwandt sein.
- Muss übereinstimmende Eigenschaften enthalten.
Die Verwendung von StudentVM
erfordert, dass die Seite „Create“ StudentVM
anstelle von Student
verwendet:
@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");}
}
Aktualisieren der Seite „Bearbeiten“
Ersetzen Sie die Methoden OnGetAsync
und OnPostAsync
in der Datei Pages/Students/Edit.cshtml.cs
durch den folgenden Code.
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();
}
Die Codeänderungen sind bis auf wenige Ausnahmen mit denen auf der Seite „Erstellen“ vergleichbar:
FirstOrDefaultAsync
wurde durch FindAsync ersetzt. Wenn Sie keine in Beziehung stehende Daten einschließen müssen, istFindAsync
effizienter.OnPostAsync
verfügt über einenid
-Parameter.- Anstatt einen leeren Studentendatensatz zu erstellen, wird der aktuelle Student aus der Datenbank abgerufen.
Führen Sie die App aus, und testen Sie sie, indem Sie einen Studenten erstellen und bearbeiten.
Entitätsstatus
Im Datenbankkontext wird nachverfolgt, ob Entitäten im Arbeitsspeicher mit den zugehörigen Zeilen in der Datenbank synchron sind. Die Nachverfolgungsinformationen bestimmen, was geschieht, wenn SaveChangesAsync aufgerufen wird. Wenn beispielsweise eine neue Entität an die Methode AddAsync übergeben wird, wird der Zustand dieser Entität auf Added festgelegt. Wenn SaveChangesAsync
aufgerufen wird, gibt der Datenbankkontext den SQL-Befehl INSERT
aus.
Eine Entität kann einen der folgenden Status aufweisen:
Added
: Die Entität ist noch nicht in der Datenbank vorhanden. DieSaveChanges
-Methode gibt eineINSERT
-Anweisung aus.Unchanged
: Für diese Entität müssen keine Änderungen gespeichert werden. Eine Entität weist diesen Zustand auf, wenn sie von der Datenbank gelesen wird.Modified
: Einige oder alle Eigenschaftswerte der Entität wurden geändert. DieSaveChanges
-Methode gibt eineUPDATE
-Anweisung aus.Deleted
: Die Entität wurde zum Löschen markiert. DieSaveChanges
-Methode gibt eineDELETE
-Anweisung aus.Detached
: Die Entität wird nicht vom Datenbankkontext nachverfolgt.
Zustandsänderungen werden in einer Desktop-App in der Regel automatisch festgelegt. Eine Entität wird gelesen, Änderungen werden vorgenommen, und der Entitätszustand wird automatisch in Modified
geändert. Durch das Aufrufen von SaveChanges
wird eine SQL-UPDATE
-Anweisung generiert, die nur die geänderten Eigenschaften aktualisiert.
In einer Web-App wird der DbContext
verfügbar gemacht, der eine Entität liest und die Daten anzeigt, nachdem eine Seite gerendert wurde. Wenn die Methode OnPostAsync
einer Seite aufgerufen wird, wird eine neue Webanforderung mit einer neuen Instanz von DbContext
gestellt. Durch erneutes Lesen der Entität in diesem neuen Kontext wird die Desktopverarbeitung simuliert.
Aktualisieren der Seite „Delete“ (Löschen)
In diesem Abschnitt implementieren Sie eine benutzerdefinierte Fehlermeldung für den Fall, dass der Aufruf von SaveChanges
zu einem Fehler führt.
Ersetzen Sie den Code in Pages/Students/Delete.cshtml.cs
durch folgenden Code:
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 });
}
}
}
}
Der vorangehende Code:
- Fügt Protokollierung hinzu.
- Fügt der
OnGetAsync
-Methodensignatur den optionalen ParametersaveChangesError
hinzu.saveChangesError
gibt an, ob die Methode nach einem Fehler beim Löschen des Studentenobjekts aufgerufen wurde.
Der Löschvorgang schlägt möglicherweise aufgrund von vorübergehenden Netzwerkproblemen fehl. Vorübergehende Netzwerkfehler treten eher auf, wenn sich die Datenbank in der Cloud befindet. Der saveChangesError
-Parameter ist false
, wenn auf der Seite „Delete“ über die Benutzeroberfläche OnGetAsync
aufgerufen wird. Wenn OnGetAsync
von OnPostAsync
aufgerufen wird (da der Löschvorgang fehlerhaft war), ist der saveChangesError
-Parameter true
.
Die OnPostAsync
-Methode ruft die ausgewählte Entität ab und anschließend die Methode Remove auf, um den Zustand der Entität auf Deleted
festzulegen. Beim Aufruf von SaveChanges
wird der SQL-Befehl DELETE
generiert. Wenn Remove
fehlschlägt:
- Wird die Datenbankausnahme abgefangen.
- Wird die Methode
OnGetAsync
auf der Seite „Löschen“ mitsaveChangesError=true
aufgerufen.
Fügen Sie in Pages/Students/Delete.cshtml
eine Fehlermeldung hinzu:
@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>
Führen Sie die App aus, und löschen Sie einen Studenten, um die Seite „Delete“ zu testen.
Nächste Schritte
In diesem Tutorial wird der erstellte CRUD-Code (CRUD = Create, Read, Update, Delete; Erstellen, Lesen, Aktualisieren, Löschen) überprüft und angepasst.
Kein Repository
Einige Entwickler verwenden eine Dienstschicht oder ein Repositorymuster, um eine Abstraktionsschicht zwischen der Benutzeroberfläche (Razor Pages) und der Datenzugriffsschicht zu erstellen. In diesem Tutorial ist dies nicht der Fall. Zur Minimierung der Komplexität und damit EF Core im Fokus dieses Tutorials bleibt, wird EF Core-Code den Seitenmodellklassen direkt hinzugefügt.
Aktualisieren der Seite „Details“
Der Gerüstbaucode für die Seite „Students“ enthält keine Registrierungsdaten. In diesem Abschnitt fügen Sie der Seite „Details
“ Registrierungen hinzu.
Lesen von Registrierungen
Um die Registrierungsdaten eines Kursteilnehmers auf der Seite anzuzeigen, müssen die Registrierungsdaten gelesen werden. Der Gerüstcode in Pages/Students/Details.cshtml.cs
liest nur die Student
-Daten ohne die Enrollment
-Daten:
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();
}
Ersetzen Sie die OnGetAsync
-Methode durch den folgenden Code, um Registrierungsdaten für den ausgewählten Studenten zu lesen. Die Änderungen werden hervorgehoben.
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();
}
Die Methoden Include und ThenInclude veranlassen den Kontext, die Navigationseigenschaft Student.Enrollments
und innerhalb jeder Registrierung die Navigationseigenschaft Enrollment.Course
zu laden. Diese Methoden werden im Tutorial zum Lesen relevanter Daten ausführlich untersucht.
Die Methode AsNoTracking verbessert die Leistung in Szenarien, in denen die zurückgegebenen Entitäten nicht im aktuellen Kontext aktualisiert werden. AsNoTracking
wird später in diesem Tutorial behandelt.
Anzeigen von Registrierungen
Ersetzen Sie den Code in Pages/Students/Details.cshtml
durch den folgenden Code, um eine Liste der Registrierungen anzuzeigen. Die Änderungen werden hervorgehoben.
@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>
Der vorangehende Code durchläuft die Entitäten in der Navigationseigenschaft Enrollments
. Für jede Registrierung werden der Kurstitel und die Klasse angezeigt. Der Kurstitel wird von der Course
-Entität abgerufen, die in der Course
-Navigationseigenschaft der Entität „Enrollments“ gespeichert ist.
Führen Sie die App aus, wählen Sie die Registerkarte Studenten aus, und klicken Sie bei einem Studenten auf den Link Details. Die Liste der Kurse und Klassen für den ausgewählten Studenten wird angezeigt.
Möglichkeiten zum Lesen einer einzelnen Entität
Der generierte Code verwendet FirstOrDefaultAsync, um eine Entität zu lesen. Diese Methode gibt NULL zurück, wenn nichts gefunden wurde. Andernfalls wird die erste gefundene Zeile zurückgegeben, die die Abfragefilterkriterien erfüllt. FirstOrDefaultAsync
ist im Allgemeinen eine bessere Wahl als die folgenden Alternativen:
- SingleOrDefaultAsync: Löst eine Ausnahme aus, wenn mehrere Entitäten vorhanden sind, die dem Abfragefilter entsprechen. Um zu ermitteln, ob von der Abfrage mehrere Zeilen zurückgegeben werden können, versucht
SingleOrDefaultAsync
, mehrere Zeilen abzurufen. Diese zusätzliche Arbeit ist nicht erforderlich, wenn die Abfrage nur eine Entität zurückgeben kann, wenn sie nach einem eindeutigen Schlüssel sucht. - FindAsync: Sucht nach einer Entität mit dem Primärschlüssel. Wenn eine Entität mit dem Primärschlüssel vom Kontext nachverfolgt wird, wird sie ohne eine Anforderung an die Datenbank zurückgegeben. Diese Methode ist für die Suche nach einer einzelnen Entität optimiert, aber Sie können
Include
nicht mitFindAsync
aufrufen. Wenn also in Beziehung stehende Daten benötigt werden, istFirstOrDefaultAsync
die bessere Wahl.
Routendaten oder Abfragezeichenfolge
Die URL für die Detailseite lautet https://localhost:<port>/Students/Details?id=1
. Der Primärschlüsselwert der Entität befindet sich in der Abfragezeichenfolge. Einige Entwickler bevorzugen es, den Schlüsselwert in Routendaten zu übergeben: https://localhost:<port>/Students/Details/1
. Weitere Informationen finden Sie unter Aktualisieren des generierten Codes.
Aktualisieren der Seite „Erstellen“
Der OnPostAsync
-Gerüstbaucode für die Seite „Create“ ist anfällig für Overposting. Ersetzen Sie die OnPostAsync
-Methode in Pages/Students/Create.cshtml.cs
durch folgenden Code.
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
Der vorangehende Code erstellt ein Student-Objekt und verwendet dann die bereitgestellten Formularfelder, um die Eigenschaften des Student-Objekts zu aktualisieren. Die TryUpdateModelAsync-Methode:
- Verwendet die bereitgestellten Formularwerte aus der PageContext-Eigenschaft in PageModel.
- Aktualisiert nur die aufgeführten Eigenschaften (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Sucht nach Formularfeldern mit dem Präfix „Student“. Beispielsweise
Student.FirstMidName
. Die Groß-/Kleinschreibung wird hier nicht beachtet. - Verwendet das Modellbindungssystem zum Konvertieren von Formularwerten aus Zeichenfolgen in die Typen im
Student
-Modell.EnrollmentDate
wird z. B. inDateTime
konvertiert.
Führen Sie die App aus, und erstellen Sie eine Student-Entität, um die Seite „Create“ zu testen.
Overposting
Die Verwendung von TryUpdateModel
zur Aktualisierung von Feldern mit bereitgestellten Werten ist eine bewährte Sicherheitsmethode, da Overposting verhindert wird. Angenommen, die Student-Entität enthält die Eigenschaft Secret
, die auf dieser Webseite nicht aktualisiert oder hinzugefügt werden sollte:
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; }
}
Selbst wenn die App auf der Razor Page „Create“ oder „Update“ (Erstellen oder Aktualisieren) nicht über das Feld Secret
verfügt, könnte ein Hacker den Secret
-Wert durch Overposting festlegen. Ein Hacker könnte ein Tool wie Fiddler verwenden oder JavaScript-Code schreiben, um einen Secret
-Formularwert bereitzustellen. Die Felder, die von der Modellbindung bei der Erstellung einer Student-Instanz verwendet werden, werden nicht durch den ursprünglichen Code begrenzt.
Jeder beliebige Wert, der vom Hacker im Secret
-Formularfeld angegeben wird, wird in der Datenbank aktualisiert. Die folgende Abbildung zeigt das Fiddler-Tool beim Hinzufügen des Felds Secret
mit dem Wert „OverPost“ zu den bereitgestellten Formularwerten.
Der Wert „OverPost“ wurde erfolgreich zur Eigenschaft Secret
der eingefügten Zeile hinzugefügt. Dies geschieht, obwohl der App-Designer nie die Absicht hatte, die Eigenschaft Secret
auf der Seite „Create“ festzulegen.
ViewModel-Element
Ansichtsmodelle bieten eine alternative Möglichkeit zum Verhindern von Overposting.
Das Anwendungsmodell wird häufig als Domänenmodell bezeichnet. Das Domänenmodell enthält normalerweise alle Eigenschaften, die die entsprechende Entität in der Datenbank erfordert. Das Ansichtsmodell enthält nur die Eigenschaften, die für die Benutzeroberflächenseite erforderlich sind, z. B. die Seite „Create“.
Neben dem Ansichtsmodell verwenden einige Apps ein Bindungsmodell oder Eingabemodell für die Bereitstellung von Daten zwischen der Seitenmodellklasse auf den Razor Pages und dem Browser.
Betrachten Sie das folgende StudentVM
-Ansichtsmodell:
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
Im folgenden Code wird das StudentVM
-Ansichtsmodell für die Erstellung eines neuen Studenten verwendet:
[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");
}
Die Methode SetValues legt die Werte dieses Objekts fest, indem sie Werte aus einem anderen PropertyValues-Objekt einliest. SetValues
verwendet die Übereinstimmung von Eigenschaftsnamen. Der Ansichtsmodelltyp:
- Muss nicht mit dem Modelltyp verwandt sein.
- Muss übereinstimmende Eigenschaften enthalten.
Die Verwendung von StudentVM
erfordert, dass die Seite „Create“ StudentVM
anstelle von Student
verwendet:
@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");}
}
Aktualisieren der Seite „Bearbeiten“
Ersetzen Sie die Methoden OnGetAsync
und OnPostAsync
in der Datei Pages/Students/Edit.cshtml.cs
durch den folgenden Code.
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();
}
Die Codeänderungen sind bis auf wenige Ausnahmen mit denen auf der Seite „Erstellen“ vergleichbar:
FirstOrDefaultAsync
wurde durch FindAsync ersetzt. Wenn Sie keine in Beziehung stehende Daten einschließen müssen, istFindAsync
effizienter.OnPostAsync
verfügt über einenid
-Parameter.- Anstatt einen leeren Studentendatensatz zu erstellen, wird der aktuelle Student aus der Datenbank abgerufen.
Führen Sie die App aus, und testen Sie sie, indem Sie einen Studenten erstellen und bearbeiten.
Entitätsstatus
Im Datenbankkontext wird nachverfolgt, ob Entitäten im Arbeitsspeicher mit den zugehörigen Zeilen in der Datenbank synchron sind. Die Nachverfolgungsinformationen bestimmen, was geschieht, wenn SaveChangesAsync aufgerufen wird. Wenn beispielsweise eine neue Entität an die Methode AddAsync übergeben wird, wird der Zustand dieser Entität auf Added festgelegt. Wenn SaveChangesAsync
aufgerufen wird, gibt der Datenbankkontext den SQL-Befehl INSERT
aus.
Eine Entität kann einen der folgenden Status aufweisen:
Added
: Die Entität ist noch nicht in der Datenbank vorhanden. DieSaveChanges
-Methode gibt eineINSERT
-Anweisung aus.Unchanged
: Für diese Entität müssen keine Änderungen gespeichert werden. Eine Entität weist diesen Zustand auf, wenn sie von der Datenbank gelesen wird.Modified
: Einige oder alle Eigenschaftswerte der Entität wurden geändert. DieSaveChanges
-Methode gibt eineUPDATE
-Anweisung aus.Deleted
: Die Entität wurde zum Löschen markiert. DieSaveChanges
-Methode gibt eineDELETE
-Anweisung aus.Detached
: Die Entität wird nicht vom Datenbankkontext nachverfolgt.
Zustandsänderungen werden in einer Desktop-App in der Regel automatisch festgelegt. Eine Entität wird gelesen, Änderungen werden vorgenommen, und der Entitätszustand wird automatisch in Modified
geändert. Durch das Aufrufen von SaveChanges
wird eine SQL-UPDATE
-Anweisung generiert, die nur die geänderten Eigenschaften aktualisiert.
In einer Web-App wird der DbContext
verfügbar gemacht, der eine Entität liest und die Daten anzeigt, nachdem eine Seite gerendert wurde. Wenn die Methode OnPostAsync
einer Seite aufgerufen wird, wird eine neue Webanforderung mit einer neuen Instanz von DbContext
gestellt. Durch erneutes Lesen der Entität in diesem neuen Kontext wird die Desktopverarbeitung simuliert.
Aktualisieren der Seite „Delete“ (Löschen)
In diesem Abschnitt implementieren Sie eine benutzerdefinierte Fehlermeldung für den Fall, dass der Aufruf von SaveChanges
zu einem Fehler führt.
Ersetzen Sie den Code in Pages/Students/Delete.cshtml.cs
durch folgenden Code:
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 });
}
}
}
}
Der vorangehende Code:
- Fügt Protokollierung hinzu.
- Fügt der
OnGetAsync
-Methodensignatur den optionalen ParametersaveChangesError
hinzu.saveChangesError
gibt an, ob die Methode nach einem Fehler beim Löschen des Studentenobjekts aufgerufen wurde.
Der Löschvorgang schlägt möglicherweise aufgrund von vorübergehenden Netzwerkproblemen fehl. Vorübergehende Netzwerkfehler treten eher auf, wenn sich die Datenbank in der Cloud befindet. Der saveChangesError
-Parameter ist false
, wenn auf der Seite „Delete“ über die Benutzeroberfläche OnGetAsync
aufgerufen wird. Wenn OnGetAsync
von OnPostAsync
aufgerufen wird (da der Löschvorgang fehlerhaft war), ist der saveChangesError
-Parameter true
.
Die OnPostAsync
-Methode ruft die ausgewählte Entität ab und anschließend die Methode Remove auf, um den Zustand der Entität auf Deleted
festzulegen. Beim Aufruf von SaveChanges
wird der SQL-Befehl DELETE
generiert. Wenn Remove
fehlschlägt:
- Wird die Datenbankausnahme abgefangen.
- Wird die Methode
OnGetAsync
auf der Seite „Löschen“ mitsaveChangesError=true
aufgerufen.
Fügen Sie in Pages/Students/Delete.cshtml
eine Fehlermeldung hinzu:
@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>
Führen Sie die App aus, und löschen Sie einen Studenten, um die Seite „Delete“ zu testen.
Nächste Schritte
In diesem Tutorial wird der erstellte CRUD-Code (CRUD = Create, Read, Update, Delete; Erstellen, Lesen, Aktualisieren, Löschen) überprüft und angepasst.
Kein Repository
Einige Entwickler verwenden eine Dienstschicht oder ein Repositorymuster, um eine Abstraktionsschicht zwischen der Benutzeroberfläche (Razor Pages) und der Datenzugriffsschicht zu erstellen. In diesem Tutorial ist dies nicht der Fall. Zur Minimierung der Komplexität und damit EF Core im Fokus dieses Tutorials bleibt, wird EF Core-Code den Seitenmodellklassen direkt hinzugefügt.
Aktualisieren der Seite „Details“
Der Gerüstbaucode für die Seite „Students“ enthält keine Registrierungsdaten. In diesem Abschnitt fügen Sie der Seite „Details“ Registrierungen hinzu.
Lesen von Registrierungen
Um die Registrierungsdaten eines Lernenden auf der Seite anzuzeigen, müssen die Registrierungsdaten gelesen werden. Der Gerüstbaucode in Pages/Students/Details.cshtml.cs
liest nur die Student-Daten ohne die Registrierungsdaten:
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();
}
Ersetzen Sie die OnGetAsync
-Methode durch den folgenden Code, um Registrierungsdaten für den ausgewählten Studenten zu lesen. Die Änderungen werden hervorgehoben.
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();
}
Die Methoden Include und ThenInclude veranlassen den Kontext, die Navigationseigenschaft Student.Enrollments
und innerhalb jeder Registrierung die Navigationseigenschaft Enrollment.Course
zu laden. Diese Methoden werden im Tutorial zum Lesen in Beziehung stehender Daten ausführlich untersucht.
Die Methode AsNoTracking verbessert die Leistung in Szenarien, in denen die zurückgegebenen Entitäten nicht im aktuellen Kontext aktualisiert werden. AsNoTracking
wird später in diesem Tutorial behandelt.
Anzeigen von Registrierungen
Ersetzen Sie den Code in Pages/Students/Details.cshtml
durch den folgenden Code, um eine Liste der Registrierungen anzuzeigen. Die Änderungen werden hervorgehoben.
@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>
Der vorangehende Code durchläuft die Entitäten in der Navigationseigenschaft Enrollments
. Für jede Registrierung werden der Kurstitel und die Klasse angezeigt. Der Kurstitel wird von der Entität „Course“ abgerufen, die in der Navigationseigenschaft Course
der Entität „Enrollments“ gespeichert ist.
Führen Sie die App aus, wählen Sie die Registerkarte Studenten aus, und klicken Sie bei einem Studenten auf den Link Details. Die Liste der Kurse und Klassen für den ausgewählten Studenten wird angezeigt.
Möglichkeiten zum Lesen einer einzelnen Entität
Der generierte Code verwendet FirstOrDefaultAsync, um eine Entität zu lesen. Diese Methode gibt NULL zurück, wenn nichts gefunden wurde. Andernfalls wird die erste gefundene Zeile zurückgegeben, die die Abfragefilterkriterien erfüllt. FirstOrDefaultAsync
ist im Allgemeinen eine bessere Wahl als die folgenden Alternativen:
- SingleOrDefaultAsync: Löst eine Ausnahme aus, wenn mehrere Entitäten vorhanden sind, die dem Abfragefilter entsprechen. Um zu ermitteln, ob von der Abfrage mehrere Zeilen zurückgegeben werden können, versucht
SingleOrDefaultAsync
, mehrere Zeilen abzurufen. Diese zusätzliche Arbeit ist nicht erforderlich, wenn die Abfrage nur eine Entität zurückgeben kann, wenn sie nach einem eindeutigen Schlüssel sucht. - FindAsync: Sucht nach einer Entität mit dem Primärschlüssel. Wenn eine Entität mit dem Primärschlüssel vom Kontext nachverfolgt wird, wird sie ohne eine Anforderung an die Datenbank zurückgegeben. Diese Methode ist für die Suche nach einer einzelnen Entität optimiert, aber Sie können
Include
nicht mitFindAsync
aufrufen. Wenn also in Beziehung stehende Daten benötigt werden, istFirstOrDefaultAsync
die bessere Wahl.
Routendaten oder Abfragezeichenfolge
Die URL für die Detailseite lautet https://localhost:<port>/Students/Details?id=1
. Der Primärschlüsselwert der Entität befindet sich in der Abfragezeichenfolge. Einige Entwickler bevorzugen es, den Schlüsselwert in Routendaten zu übergeben: https://localhost:<port>/Students/Details/1
. Weitere Informationen finden Sie unter Aktualisieren des generierten Codes.
Aktualisieren der Seite „Erstellen“
Der OnPostAsync
-Gerüstbaucode für die Seite „Create“ ist anfällig für Overposting. Ersetzen Sie die OnPostAsync
-Methode in Pages/Students/Create.cshtml.cs
durch folgenden Code.
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
Der vorangehende Code erstellt ein Student-Objekt und verwendet dann die bereitgestellten Formularfelder, um die Eigenschaften des Student-Objekts zu aktualisieren. Die TryUpdateModelAsync-Methode:
- Verwendet die bereitgestellten Formularwerte aus der PageContext-Eigenschaft in PageModel.
- Aktualisiert nur die aufgeführten Eigenschaften (
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
). - Sucht nach Formularfeldern mit dem Präfix „Student“. Beispielsweise
Student.FirstMidName
. Die Groß-/Kleinschreibung wird hier nicht beachtet. - Verwendet das Modellbindungssystem zum Konvertieren von Formularwerten aus Zeichenfolgen in die Typen im
Student
-Modell.EnrollmentDate
muss beispielsweise in DateTime konvertiert werden.
Führen Sie die App aus, und erstellen Sie eine Student-Entität, um die Seite „Create“ zu testen.
Overposting
Die Verwendung von TryUpdateModel
zur Aktualisierung von Feldern mit bereitgestellten Werten ist eine bewährte Sicherheitsmethode, da Overposting verhindert wird. Angenommen, die Student-Entität enthält die Eigenschaft Secret
, die auf dieser Webseite nicht aktualisiert oder hinzugefügt werden sollte:
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; }
}
Selbst wenn die App auf der Razor Page „Create“ oder „Update“ (Erstellen oder Aktualisieren) nicht über das Feld Secret
verfügt, könnte ein Hacker den Secret
-Wert durch Overposting festlegen. Ein Hacker könnte ein Tool wie Fiddler verwenden oder JavaScript-Code schreiben, um einen Secret
-Formularwert bereitzustellen. Die Felder, die von der Modellbindung bei der Erstellung einer Student-Instanz verwendet werden, werden nicht durch den ursprünglichen Code begrenzt.
Jeder beliebige Wert, der vom Hacker im Secret
-Formularfeld angegeben wird, wird in der Datenbank aktualisiert. Die folgende Abbildung zeigt das Fiddler-Tool beim Hinzufügen des Felds Secret
(mit dem Wert „OverPost“) zu den bereitgestellten Formularwerten.
Der Wert „OverPost“ wurde erfolgreich zur Eigenschaft Secret
der eingefügten Zeile hinzugefügt. Dies geschieht, obwohl der App-Designer nie die Absicht hatte, die Eigenschaft Secret
auf der Seite „Create“ festzulegen.
ViewModel-Element
Ansichtsmodelle bieten eine alternative Möglichkeit zum Verhindern von Overposting.
Das Anwendungsmodell wird häufig als Domänenmodell bezeichnet. Das Domänenmodell enthält normalerweise alle Eigenschaften, die die entsprechende Entität in der Datenbank erfordert. Das Ansichtsmodell enthält nur die Eigenschaften, die für die Benutzeroberfläche (z.B. die Seite „Create“) erforderlich sind, für die es verwendet wird.
Neben dem Ansichtsmodell verwenden einige Apps ein Bindungsmodell oder Eingabemodell für die Bereitstellung von Daten zwischen der Seitenmodellklasse auf den Razor Pages und dem Browser.
Betrachten Sie das folgende Student
-Ansichtsmodell:
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; }
}
}
Im folgenden Code wird das StudentVM
-Ansichtsmodell für die Erstellung eines neuen Studenten verwendet:
[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");
}
Die Methode SetValues legt die Werte dieses Objekts fest, indem sie Werte aus einem anderen PropertyValues-Objekt einliest. SetValues
verwendet die Übereinstimmung von Eigenschaftsnamen. Der Ansichtsmodelltyp muss nicht mit dem Modelltyp verknüpft sein. Er muss lediglich über übereinstimmende Eigenschaften verfügen.
Für die Verwendung von StudentVM
muss Create.cshtml aktualisiert werden, damit StudentVM
anstelle von Student
verwendet wird.
Aktualisieren der Seite „Bearbeiten“
Ersetzen Sie die Methoden OnGetAsync
und OnPostAsync
in der Datei Pages/Students/Edit.cshtml.cs
durch den folgenden Code.
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();
}
Die Codeänderungen sind bis auf wenige Ausnahmen mit denen auf der Seite „Erstellen“ vergleichbar:
FirstOrDefaultAsync
wurde durch FindAsync ersetzt. Wenn keine enthaltenen zugehörigen Daten benötigt werden, istFindAsync
effizienter.OnPostAsync
verfügt über einenid
-Parameter.- Anstatt einen leeren Studentendatensatz zu erstellen, wird der aktuelle Student aus der Datenbank abgerufen.
Führen Sie die App aus, und testen Sie sie, indem Sie einen Studenten erstellen und bearbeiten.
Entitätsstatus
Im Datenbankkontext wird nachverfolgt, ob Entitäten im Arbeitsspeicher mit den zugehörigen Zeilen in der Datenbank synchron sind. Die Nachverfolgungsinformationen bestimmen, was geschieht, wenn SaveChangesAsync aufgerufen wird. Wenn beispielsweise eine neue Entität an die Methode AddAsync übergeben wird, wird der Zustand dieser Entität auf Added festgelegt. Wenn SaveChangesAsync
aufgerufen wird, gibt der Datenbankkontext den SQL-Befehl INSERT aus.
Eine Entität kann einen der folgenden Status aufweisen:
Added
: Die Entität ist noch nicht in der Datenbank vorhanden. Die MethodeSaveChanges
gibt eine INSERT-Anweisung aus.Unchanged
: Für diese Entität müssen keine Änderungen gespeichert werden. Eine Entität weist diesen Zustand auf, wenn sie von der Datenbank gelesen wird.Modified
: Einige oder alle Eigenschaftswerte der Entität wurden geändert. Die MethodeSaveChanges
gibt eine UPDATE-Anweisung aus.Deleted
: Die Entität wurde zum Löschen markiert. Die MethodeSaveChanges
gibt eine DELETE-Anweisung aus.Detached
: Die Entität wird nicht vom Datenbankkontext nachverfolgt.
Zustandsänderungen werden in einer Desktop-App in der Regel automatisch festgelegt. Eine Entität wird gelesen, Änderungen werden vorgenommen, und der Entitätszustand wird automatisch in Modified
geändert. Durch das Aufrufen von SaveChanges
wird eine SQL UPDATE-Anweisung generiert, die nur die geänderten Eigenschaften aktualisiert.
In einer Web-App wird der DbContext
verfügbar gemacht, der eine Entität liest und die Daten anzeigt, nachdem eine Seite gerendert wurde. Wenn die Methode OnPostAsync
einer Seite aufgerufen wird, wird eine neue Webanforderung mit einer neuen Instanz von DbContext
gestellt. Durch erneutes Lesen der Entität in diesem neuen Kontext wird die Desktopverarbeitung simuliert.
Aktualisieren der Seite „Delete“ (Löschen)
In diesem Abschnitt implementieren Sie eine benutzerdefinierte Fehlermeldung für den Fall, dass der Aufruf von SaveChanges
fehlschlägt.
Ersetzen Sie den Code in Pages/Students/Delete.cshtml.cs
durch folgenden Code. Die Änderungen werden hervorgehoben (mit Ausnahme der Bereinigung von using
-Anweisungen).
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 });
}
}
}
}
Der vorangehende Code fügt der OnGetAsync
-Methodensignatur den optionalen-Parameter saveChangesError
hinzu. saveChangesError
gibt an, ob die Methode nach einem Fehler beim Löschen des Studentenobjekts aufgerufen wurde. Der Löschvorgang schlägt möglicherweise aufgrund von vorübergehenden Netzwerkproblemen fehl. Vorübergehende Netzwerkfehler treten eher auf, wenn sich die Datenbank in der Cloud befindet. Der saveChangesError
-Parameter ist FALSE, wenn OnGetAsync
auf der Seite „Delete“ über die Benutzeroberfläche aufgerufen wird. Wenn OnGetAsync
von OnPostAsync
aufgerufen wird (aufgrund des fehlgeschlagenen Löschvorgangs), ist der Parameter saveChangesError
„TRUE“.
Die OnPostAsync
-Methode ruft die ausgewählte Entität ab und anschließend die Methode Remove auf, um den Zustand der Entität auf Deleted
festzulegen. Wenn SaveChanges
aufgerufen wird, wird der SQL-Befehl DELETE generiert. Wenn Remove
fehlschlägt:
- Wird die Datenbankausnahme abgefangen.
- Die
OnGetAsync
-Methode der Löschseite wird mitsaveChangesError=true
aufgerufen.
Fügen Sie der Razor Page „Delete“ (Pages/Students/Delete.cshtml
) eine Fehlermeldung hinzu:
@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>
Führen Sie die App aus, und löschen Sie einen Studenten, um die Seite „Delete“ zu testen.