Teil 2: Razor-Seiten mit EF Core in ASP.NET Core – CRUD

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 mit FindAsync aufrufen. Wenn also in Beziehung stehende Daten benötigt werden, ist FirstOrDefaultAsync 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. in DateTime 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.

Fiddler adding Secret field

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, ist FindAsync effizienter.
  • OnPostAsync verfügt über einen id-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 SaveChanges-Methode 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 SaveChanges-Methode gibt eine UPDATE-Anweisung aus.

  • Deleted: Die Entität wurde zum Löschen markiert. Die SaveChanges-Methode 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 DbContextverfü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 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 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“ mit saveChangesError=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 mit FindAsync aufrufen. Wenn also in Beziehung stehende Daten benötigt werden, ist FirstOrDefaultAsync 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. in DateTime 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.

Fiddler adding Secret field

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, ist FindAsync effizienter.
  • OnPostAsync verfügt über einen id-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 SaveChanges-Methode 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 SaveChanges-Methode gibt eine UPDATE-Anweisung aus.

  • Deleted: Die Entität wurde zum Löschen markiert. Die SaveChanges-Methode 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 DbContextverfü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 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 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“ mit saveChangesError=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 mit FindAsync aufrufen. Wenn also in Beziehung stehende Daten benötigt werden, ist FirstOrDefaultAsync 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.

Fiddler adding Secret field

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, ist FindAsync effizienter.
  • OnPostAsync verfügt über einen id-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 Methode SaveChanges 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 Methode SaveChanges gibt eine UPDATE-Anweisung aus.

  • Deleted: Die Entität wurde zum Löschen markiert. Die Methode SaveChanges 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 DbContextverfü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 mit saveChangesError=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.

Nächste Schritte