Tutorial: Behandeln der Parallelität: ASP.NET MVC mit EF Core

In den vorherigen Tutorials haben Sie gelernt, wie Sie Daten aktualisieren. In diesem Tutorial wird gezeigt, wie Sie Konflikte behandeln, wenn mehrere Benutzer gleichzeitig dieselbe Entität aktualisieren.

Sie erstellen Webseiten, die mit der Entität Department arbeiten, und behandeln Parallelitätsfehler. In der nachfolgenden Abbildung sehen Sie die Seiten „Bearbeiten“ und „Löschen“, einschließlich einiger Meldungen, die angezeigt werden, wenn ein Parallelitätskonflikt auftritt.

Department Edit page

Department Delete page

In diesem Tutorial:

  • Erhalten Sie Informationen über Parallelitätskonflikte
  • Fügen Sie eine Nachverfolgungseigenschaft hinzu
  • Erstellen Sie Abteilungscontroller und -ansichten
  • Aktualisieren Sie die Ansicht „Index“
  • Aktualisieren Sie „Bearbeiten“-Methoden
  • Aktualisieren Sie die Ansicht „Bearbeiten“
  • Testen Sie auf Parallelitätskonflikte
  • Aktualisieren der Seite „Delete“ (Löschen)
  • Aktualisieren der Ansichten „Details“ und „Erstellen“

Voraussetzungen

Nebenläufigkeitskonflikte

Ein Parallelitätskonflikt tritt auf, wenn ein Benutzer die Daten einer Entität anzeigt, um diese zu bearbeiten, und ein anderer Benutzer eben diese Entitätsdaten aktualisiert, bevor die Änderungen des ersten Benutzers in die Datenbank geschrieben wurden. Wenn Sie die Erkennung solcher Konflikte nicht aktivieren, überschreibt das letzte Update der Datenbank die Änderungen des anderen Benutzers. In vielen Anwendungen ist dieses Risiko akzeptabel: Wenn es nur wenige Benutzer bzw. wenige Updates gibt, oder wenn es nicht schlimm ist, dass Änderungen überschrieben werden können, ist es den Aufwand, für die Parallelität zu programmieren, möglicherweise nicht wert. In diesem Fall müssen Sie für die Anwendung keine Behandlung von Nebenläufigkeitskonflikten konfigurieren.

Pessimistische Parallelität (Sperren)

Wenn Ihre Anwendung versehentliche Datenverluste in Parallelitätsszenarios verhindern muss, ist die Verwendung von Datenbanksperren eine Möglichkeit. Man bezeichnet dies als pessimistische Parallelität. Bevor Sie zum Beispiel eine Zeile aus einer Datenbank lesen, fordern Sie eine Sperre für den schreibgeschützten Zugriff oder den Aktualisierungszugriff an. Wenn Sie eine Zeile für den Aktualisierungszugriff sperren, kann kein anderer Benutzer diese Zeile für den schreibgeschützten Zugriff oder den Aktualisierungszugriff sperren, da er eine Kopie der Daten erhalten würde, die gerade geändert werden. Wenn Sie eine Zeile für den schreibgeschützten Zugriff sperren, können andere diese Zeile ebenfalls für den schreibgeschützten Zugriff sperren, aber nicht für den Aktualisierungszugriff.

Das Verwalten von Sperren hat Nachteile. Es kann komplex sein, sie zu programmieren. Dies erfordert erhebliche Datenbankverwaltungsressourcen und kann mit steigender Anzahl der Benutzer einer Anwendung zu Leistungsproblemen führen. Aus diesen Gründen unterstützen nicht alle Datenbankverwaltungssysteme die pessimistische Parallelität. Entity Framework Core enthält keine integrierte Unterstützung, und in diesem Tutorial wird nicht gezeigt, wie Sie sie implementieren.

Optimistische Nebenläufigkeit

Die optimistische Parallelität ist die Alternative zur pessimistischen Parallelität. Die Verwendung der optimistischen Parallelität bedeutet, Nebenläufigkeitskonflikte zu erlauben und entsprechend zu reagieren, wenn diese auftreten. Benutzer1 besucht z.B. die Seite „Abteilung bearbeiten“ und ändert das Budget für die englische Abteilung von $350.000,00 in $0,00.

Changing budget to 0

Bevor Benutzer1 auf Speichern klickt, besucht Benutzer2 dieselbe Seite und ändert das Feld „Startdatum“ von 9.1.2007 in 9.1.2013.

Changing start date to 2013

Benutzer1 klickt zuerst auf Speichern und sieht die Änderungen, wenn der Browser die Indexseite anzeigt.

Budget changed to zero

Dann klickt Benutzer2 auf einer Bearbeitungsseite, die weiterhin ein Budget von $350.000,00 angibt, auf Speichern. Was daraufhin geschieht, ist abhängig davon, wie Sie Nebenläufigkeitskonflikte behandeln.

Einige der Optionen schließen Folgendes ein:

  • Sie können nachverfolgen, welche Eigenschaft ein Benutzer geändert hat und nur die entsprechenden Spalten in der Datenbank aktualisieren.

    Im Beispielszenario würden keine Daten verloren gehen, da verschiedene Eigenschaften von zwei Benutzern aktualisiert wurden. Das nächste Mal, wenn eine Person den englischen Fachbereich durchsucht, wird sie die Änderungen von Benutzer1 und Benutzer2 sehen – das Startdatum 1.9.2013 und ein Budget von 0 Dollar. Diese Methode der Aktualisierung kann die Anzahl von Konflikten reduzieren, die zu Datenverlusten führen können. Sie kann Datenverluste jedoch nicht verhindern, wenn konkurrierende Änderungen an der gleichen Eigenschaft einer Entität vorgenommen werden. Ob Entity Framework auf diese Weise funktioniert, hängt davon ab, wie Sie Ihren Aktualisierungscode implementieren. Oft ist dies in Webanwendungen nicht praktikabel, da es erforderlich sein kann, viele Zustände zu verwalten, um alle ursprünglichen Eigenschaftswerte einer Entität und die neuen Werte im Auge zu behalten. Das Verwalten vieler Zuständen kann sich auf die Leistung der Anwendung auswirken, da es entweder Serverressourcen beansprucht oder in der Webseite selbst (z. B. in ausgeblendeten Feldern) oder in einem cookie enthalten sein muss.

  • Sie können zulassen, dass die Änderungen von Benutzer2 die Änderungen von Benutzer1 überschreiben.

    Das nächste Mal, wenn jemand den englischen Fachbereich durchsucht, wird das Datum 1.9.2013 und der wiederhergestellte Wert $350.000,00 angezeigt. Das ist entweder ein Client gewinnt- oder ein Letzter Schreiber gewinnt-Szenario. (Alle Werte des Clients haben Vorrang vor dem Datenspeicher.) Wie in der Einführung dieses Abschnitts beschrieben, geschieht dies automatisch, wenn Sie für die Behandlung der Parallelität keinen Code schreiben.

  • Sie können verhindern, dass die Änderungen von Benutzer2 in die Datenbank aufgenommen werden.

    In der Regel würden Sie eine Fehlermeldung ausgeben, ihm den aktuellen Zustand der Daten anzeigen und ihm erlauben, seine Änderungen erneut anzuwenden, sofern er dies immer noch machen möchte. Dieses Szenario wird Speicher gewinnt genannt. (Die Werte des Datenspeichers haben Vorrang vor den Werten, die vom Client gesendet werden.) In diesem Tutorial implementieren Sie das Szenario „Speicher gewinnt“. Diese Methode stellt sicher, dass keine Änderung überschrieben wird, ohne dass ein Benutzer darüber benachrichtigt wird.

Erkennen von Nebenläufigkeitskonflikten

Sie können Konflikte auflösen, indem Sie die DbConcurrencyException-Ausnahmen behandeln, die vom Entity Framework ausgelöst werden. Entity Framework muss dazu in der Lage sein, Konflikte zu erkennen, damit es weiß, wann diese Ausnahmen ausgelöst werden sollen. Aus diesem Grund müssen Sie die Datenbank und das Datenmodell entsprechend konfigurieren. Einige der Optionen für das Aktivieren der Konflikterkennung schließen Folgendes ein:

  • Fügen Sie eine Änderungsverfolgungsspalte in die Datenbanktabelle ein, die verwendet werden kann, um zu bestimmen, wenn eine Änderung an einer Zeile vorgenommen wurde. Anschließend können Sie Entity Framework so konfigurieren, dass die Spalte in der Where-Klausel eines SQL-Updates oder eines Delete-Befehls enthalten ist.

    In der Regel ist rowversion der Datentyp der Änderungsverfolgungsspalte. Der Wert rowversion ist eine sequenzielle Zahl, die jedes Mal erhöht wird, wenn die Zeile aktualisiert wird. In einem Update- oder Delete-Befehl enthält die Where-Klausel den ursprünglichen Wert der Änderungsverfolgungsspalte (die ursprüngliche Zeilenversion). Wenn die Zeile, die aktualisiert wird, durch einen anderen Benutzer geändert wurde, unterscheidet sich der Wert in der Spalte rowversion vom ursprünglichen Wert, sodass die Anweisungen „Update“ oder „Delete“ die Zeile, die aktualisiert werden soll, aufgrund der Where-Klausel nicht finden können. Wenn Entity Framework feststellt, dass das Update oder der Delete-Befehl keine Zeilen aktualisiert hat (d.h., wenn die Anzahl der betroffenen Zeilen 0 (null) ist), wird dies als Parallelitätskonflikt interpretiert.

  • Konfigurieren Sie Entity Framework so, dass die ursprünglichen Werte jeder Spalte in der Tabelle in den Where-Klauseln der Update- und Delete-Befehle enthalten sind.

    Wie in der ersten Option gibt die Where-Klausel keine Zeile zum Aktualisieren zurück, wenn etwas in der Zeile geändert wurde, da die Zeile zuerst gelesen wurde, was vom Entity Framework als Parallelitätskonflikt interpretiert wird. Bei Datenbanktabellen mit vielen Spalten kann dieser Ansatz zu sehr großen Where-Klauseln führen, was wiederum dazu führen kann, dass Sie eine große Anzahl von Zuständen verwalten müssen. Wie bereits erwähnt, kann das Verwalten großer Mengen von Zuständen die Anwendungsleistung beeinträchtigen. Deshalb wird dieser Ansatz in der Regel nicht empfohlen, und ist nicht die Methode, die in diesem Tutorial verwendet wird.

    Wenn Sie diesen Ansatz für die Parallelität implementieren wollen, müssen Sie alle nicht primären Schlüsseleigenschaften in der Entität markieren, für die Sie die Parallelität nachverfolgen wollen, indem Sie ihnen das Attribut ConcurrencyCheck hinzufügen. Diese Änderung ermöglicht dem Entity Framework, alle Spalten in der SQL-Where-Klausel der Update- und Delete-Anweisungen einzubeziehen.

Im weiteren Verlauf dieses Tutorials fügen Sie der Fachbereichsentität die Änderungsverfolgungseigenschaft rowversion hinzu, erstellen einen Controller und Ansichten und überprüfen, ob alles ordnungsgemäß funktioniert.

Fügen Sie eine Nachverfolgungseigenschaft hinzu

Fügen Sie der Datei Models/Department.cs eine Nachverfolgungseigenschaft namens „RowVersion“ hinzu:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Das Attribut Timestamp gibt an, dass diese Spalte in die Where-Klausel der Befehle „Update“ und „Delete“ einbezogen wird, die an die Datenbank gesendet werden. Das Attribut wird Timestamp genannt, weil vorherige Versionen von SQL Server einen SQL-timestamp-Datentyp verwendet haben, bevor er durch SQL-rowversion ersetzt wurde. Der .NET-Typ für rowversion ist ein Bytearray.

Wenn Sie die Verwendung der Fluent-API bevorzugen, können Sie die IsConcurrencyToken-Methode (in Data/SchoolContext.cs) verwenden, um die Änderungsverfolgungseigenschaft anzugeben, wie im folgenden Beispiel dargestellt:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

Durch das Hinzufügen einer Eigenschaft ändern Sie das Datenbankmodell, daher müssen Sie eine weitere Migration durchführen.

Speichern Sie ihre Änderungen, erstellen Sie das Projekt, und geben Sie dann die folgenden Befehle in das Befehlsfenster ein:

dotnet ef migrations add RowVersion
dotnet ef database update

Erstellen Sie Abteilungscontroller und -ansichten

Erstellen Sie einen Abteilungscontroller und Ansichten, wie Sie es vorher bereits für Studenten, Kurse und Dozenten getan haben.

Scaffold Department

Ändern Sie in der Datei DepartmentsController.cs „FirstMidName“ an jeder Stelle in „FullName“, damit die Dropdownliste für Abteilungsadministratoren den vollen Namen des Dozenten enthält und nicht nur den Nachnamen.

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);

Aktualisieren Sie die Ansicht „Index“

Die Engine für den Gerüstbau hat eine RowVersion-Spalte in der Indexansicht erstellt, dieses Feld soll jedoch nicht angezeigt werden.

Ersetzen Sie den Code in Views/Departments/Index.cshtml durch folgenden Code.

@model IEnumerable<ContosoUniversity.Models.Department>

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

<h2>Departments</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Administrator)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Damit wird die Überschrift in „Departments“ geändert, die Spalte RowVersion wird gelöscht, und der vollständige Name des Administrators wird anstelle des Vornamens angezeigt.

Aktualisieren Sie „Bearbeiten“-Methoden

Fügen Sie in den HttpGet-Methoden Edit und DetailsAsNoTracking hinzu. Fügen Sie in der HttpGet-Methode Edit für den Administrator Eager Loading hinzu.

var department = await _context.Departments
    .Include(i => i.Administrator)
    .AsNoTracking()
    .FirstOrDefaultAsync(m => m.DepartmentID == id);

Ersetzen Sie den vorhandenen Code für die HttpPost-Methode Edit durch folgenden Code:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
    if (id == null)
    {
        return NotFound();
    }

    var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        await TryUpdateModelAsync(deletedDepartment);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

    if (await TryUpdateModelAsync<Department>(
        departmentToUpdate,
        "",
        s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var exceptionEntry = ex.Entries.Single();
            var clientValues = (Department)exceptionEntry.Entity;
            var databaseEntry = exceptionEntry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                {
                    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
                }
                if (databaseValues.Budget != clientValues.Budget)
                {
                    ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
                }
                if (databaseValues.StartDate != clientValues.StartDate)
                {
                    ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
                }
                if (databaseValues.InstructorID != clientValues.InstructorID)
                {
                    Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
                    ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
                }

                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
                ModelState.Remove("RowVersion");
            }
        }
    }
    ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

Der Code versucht zunächst die Abteilung zu lesen, die aktualisiert werden soll. Wenn die Methode FirstOrDefaultAsync NULL zurückgibt, wurde die Abteilung von einem anderen Benutzer gelöscht. In diesem Fall verwendet der Code die bereitgestellten Formularwerte zum Erstellen einer Department-Entität, damit die Seite „Bearbeiten“ mit einer Fehlermeldung erneut angezeigt werden kann. Alternativ müssen Sie die Entität Department nicht erneut erstellen, wenn Sie nur eine Fehlermeldung anzeigen, ohne die Abteilungsfelder erneut anzuzeigen.

Die Ansicht speichert den ursprünglichen Wert von RowVersion in einem ausgeblendeten Feld, und diese Methode erhält diesen Wert über den Parameter rowVersion. Bevor Sie SaveChanges aufrufen, müssen Sie diesen ursprünglichen Eigenschaftswert von RowVersion in die Auflistung OriginalValues für die Entität einfügen.

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

Wenn Entity Framework dann den SQL-Befehl „Update“ erstellt, enthält dieser Befehl eine Where-Klausel, die nach einer Zeile mit dem ursprünglichen Wert RowVersion sucht. Wenn keine Zeile durch den Befehl „Update“ betroffen ist (keine Zeile enthält den ursprünglichen Wert RowVersion), löst das Entity Framework die Ausnahme DbUpdateConcurrencyException aus.

Der Code im Catch-Block für diese Ausnahme ruft die betroffene Abteilungsentität ab, die die aktualisierten Werte der Eigenschaft Entries auf dem Ausnahmeobjekt enthält.

var exceptionEntry = ex.Entries.Single();

Die Auflistung Entries hat nur ein EntityEntry-Objekt. Sie können dieses Objekt verwenden, um die aktuellen Datenbankwerte und die neuen Werte abzurufen, die von dem Benutzer eingegeben wurden.

var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();

Der Code fügt eine benutzerdefinierte Fehlermeldung für jede Spalte mit Datenbankwerten hinzu, die von den Werten abweichen, die der Benutzer auf der Seite „Bearbeiten“ eingegeben hat (zugunsten der Übersichtlichkeit wird hier nur ein Feld angezeigt).

var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");

Schließlich legt der Code den Wert RowVersion von departmentToUpdate auf den neuen Wert fest, der aus der Datenbank abgerufen wurde. Dieser neue RowVersion-Wert wird in dem ausgeblendeten Feld gespeichert, wenn die Seite „Bearbeiten“ erneut angezeigt wird. Das nächste Mal, wenn der Benutzer auf Speichern klickt, werden nur Parallelitätsfehler abgefangen, die nach dem erneuten Anzeigen der Seite „Bearbeiten“ aufgetreten sind.

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

Die Anweisung ModelState.Remove ist erforderlich, da ModelState über den alten RowVersion-Wert verfügt. In der Ansicht hat der Wert ModelState Vorrang vor den Modelleigenschaftswerten, wenn beide vorhanden sind.

Aktualisieren Sie die Ansicht „Bearbeiten“

Nehmen Sie die folgenden Änderungen in Views/Departments/Edit.cshtml vor:

  • Fügen Sie ein ausgeblendetes Feld zum Speichern des Eigenschaftswerts RowVersion, direkt nach dem ausgeblendeten Feld für die Eigenschaft DepartmentID hinzu.

  • Fügen Sie der Dropdownliste die Option „Select Administrator“ hinzu.

@model ContosoUniversity.Models.Department

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

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="DepartmentID" />
            <input type="hidden" asp-for="RowVersion" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
                <span asp-validation-for="InstructorID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

Testen Sie auf Parallelitätskonflikte

Führen Sie die App aus, und navigieren Sie zur Indexseite „Abteilungen“. Klicken Sie mit der rechten Maustaste auf den Link Bearbeiten für die englische Abteilung und wählen In neuer Registerkarte öffnen aus, klicken Sie dann auf den Link Bearbeiten für die englische Abteilung. Beide Registerkarten zeigen nun die gleichen Informationen im Browser an.

Ändern Sie ein Feld in der ersten Registerkarte, und klicken Sie auf Speichern.

Department Edit page 1 after change

Der Browser zeigt die Indexseite mit dem geänderten Wert an.

Ändern Sie ein Feld in der zweiten Registerkarte.

Department Edit page 2 after change

Klicken Sie auf Speichern. Folgende Fehlermeldung wird angezeigt:

Department Edit page error message

Klicken Sie erneut auf Speichern. Der Wert, den Sie auf der zweiten Registerkarte eingegeben haben, wird gespeichert. Die gespeicherten Werte werden Ihnen auf der Indexseite angezeigt.

Aktualisieren der Seite „Delete“ (Löschen)

Bei der Seite „Löschen“ entdeckt Entity Framework Nebenläufigkeitskonflikte, die durch die Bearbeitung einer Abteilung ausgelöst wurden, auf ähnliche Weise. Wenn die HttpGet-Methode Delete die Bestätigungsansicht anzeigt, enthält diese Ansicht den ursprünglichen Wert von RowVersion in einem ausgeblendeten Feld. Dieser Wert steht dann der HttpPost-Methode Delete zur Verfügung, die aufgerufen wird, wenn der Benutzer das Löschen bestätigt. Wenn Entity Framework den SQL-Befehl „Delete“ erstellt, enthält dieser Befehl eine Where-Klausel mit dem ursprünglichen Wert RowVersion. Wenn der Befehl in 0 (null) betroffenen Zeilen resultiert (d.h. die Zeile wurde geändert, nachdem die Bestätigungsseite „Löschen“ angezeigt wurde), wird eine Parallelitätsausnahme ausgelöst, und die HttpGet-Methode Delete wird mit einem auf TRUE festgelegten Fehlerflag aufgerufen, um die Bestätigungsseite erneut mit einer Fehlermeldung anzuzeigen. Es ist auch möglich, dass 0 (null) Zeilen betroffen sind, weil die Zeile von einem anderen Benutzer gelöscht wurde. In diesem Fall wird keine Fehlermeldung angezeigt.

Aktualisieren der Delete-Methoden im Abteilungscontroller

Ersetzen Sie in der Datei DepartmentsController.cs die HttpGet-Methode Delete durch den folgenden Code:

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

    var department = await _context.Departments
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.DepartmentID == id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction(nameof(Index));
        }
        return NotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

Die Methode akzeptiert einen optionalen Parameter, der angibt, ob die Seite nach einem Parallelitätsfehler erneut angezeigt wird. Wenn dieses Flag auf TRUE festgelegt ist, und die angegebene Abteilung nicht mehr vorhanden ist, wurde sie von einem anderen Benutzer gelöscht. In diesem Fall leitet der Code an eine Indexseite weiter. Wenn dieses Flag auf TRUE festgelegt und die Abteilung vorhanden ist, wurde sie von einem anderen Benutzer geändert. In diesem Fall sendet der Code mithilfe von ViewData eine Fehlermeldung an die Ansicht.

Ersetzen Sie den Code in der HttpPost-Methode Delete (namens DeleteConfirmed) durch den folgenden Code:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
        {
            _context.Departments.Remove(department);
            await _context.SaveChangesAsync();
        }
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateConcurrencyException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
    }
}

In dem eingerüsteten Code, den Sie soeben ersetzt haben, akzeptiert diese Methode nur eine Datensatz-ID:

public async Task<IActionResult> DeleteConfirmed(int id)

Diesen Parameter haben Sie in eine Department-Entitätsinstanz geändert, die durch die Modellbindung erstellt wurde. Dadurch erhält Entity Framework neben dem Datensatzschlüssel auch Zugriff auf den Eigenschaftswert „RowVersion“.

public async Task<IActionResult> Delete(Department department)

Ebenfalls haben Sie den Namen der Aktionsmethode DeleteConfirmed auf Delete geändert. Der eingerüstete Code verwendet den Namen DeleteConfirmed, um der HttpPost-Methode eine eindeutige Signatur zuzuweisen. (Die CLR erfordert überladene Methoden, um verschiedene Methodenparameter zu enthalten.) Da die Signaturen nun eindeutig sind, können Sie weiterhin die MVC-Konventionen verwenden, und den gleichen Namen für die HttpPost- und HttpGet-Delete-Methoden verwenden.

Wenn die Abteilung bereits gelöscht ist, gibt die Methode AnyAsync FALSE zurück, und die Anwendung kehrt zu der Indexmethode zurück.

Wenn ein Parallelitätsfehler abgefangen wird, zeigt der Code erneut die Bestätigungsseite „Löschen“ an, und stellt ein Flag bereit, das angibt, dass eine Fehlermeldung für die Parallelität angezeigt werden soll.

Aktualisieren der Ansicht „Löschen“

Ersetzen Sie den eingerüsteten Code in der Datei Views/Departments/Delete.cshtml durch den folgenden Code, der ein Feld für die Fehlermeldung und ausgeblendete Felder für die Eigenschaften „DepartmentID“ und „RowVersion“ hinzufügt. Die Änderungen werden hervorgehoben.

@model ContosoUniversity.Models.Department

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

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="DepartmentID" />
        <input type="hidden" asp-for="RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

Dadurch werden folgende Änderungen vorgenommen:

  • Fügt eine Fehlermeldung zwischen den Überschriften h2 und h3 hinzu.

  • Ersetzt „FirstMidName“ durch „FullName“ im Feld Administrator.

  • Entfernt das Feld „RowVersion“.

  • Fügt der Eigenschaft RowVersion ein ausgeblendetes Feld zu.

Führen Sie die App aus, und navigieren Sie zur Indexseite „Abteilungen“. Klicken Sie mit der rechten Maustaste auf den Link Löschen für die englische Abteilung, und wählen Sie In neuer Registerkarte öffnen aus. Klicken Sie in der ersten Registerkarte dann auf den Link Bearbeiten für die englische Abteilung.

Ändern Sie im ersten Fenster einen der Werte, und klicken Sie auf Speichern:

Department Edit page after change before delete

Klicken Sie in der zweiten Registerkarte auf Löschen. Ihnen wird eine Fehlermeldung zur Parallelität angezeigt, und die Abteilungswerte werden mit den aktuellen Werten der Datenbank aktualisiert.

Department Delete confirmation page with concurrency error

Wenn Sie erneut auf Löschen klicken, werden Sie auf die Indexseite weitergeleitet, die anzeigt, dass die Abteilung gelöscht wurde.

Aktualisieren der Ansichten „Details“ und „Erstellen“

Optional können Sie den eingerüsteten Code in den Ansichten „Details“ und „Erstellen“ bereinigen.

Ersetzen Sie den Code in der Datei Views/Departments/Details.cshtml, um die Spalte „RowVersion“ zu löschen und den vollständigen Namen des Administrators anzuzeigen.

@model ContosoUniversity.Models.Department

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

<h2>Details</h2>

<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

Ersetzen Sie den Code in der Datei Views/Departments/Create.cshtml, um der Dropdownliste eine Select-Option hinzuzufügen.

@model ContosoUniversity.Models.Department

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

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

Abrufen des Codes

Download or view the completed app (Herunterladen oder anzeigen der vollständigen App).

Zusätzliche Ressourcen

Weitere Informationen zum Behandeln der Parallelität in EF Core finden Sie unter Nebenläufigkeitskonflikte.

Nächste Schritte

In diesem Tutorial:

  • Haben Sie Informationen über Parallelitätskonflikte erhalten
  • Haben Sie eine Nachverfolgungseigenschaft hinzugefügt
  • Haben Sie Abteilungscontroller und Ansichten erstellt
  • Haben Sie die Ansicht „Index“ aktualisiert
  • Haben Sie „Bearbeiten“-Methoden aktualisiert
  • Haben Sie die Ansicht „Bearbeiten“ aktualisiert
  • Haben Sie auf Parallelitätskonflikte getestet
  • Haben Sie die Seite „Löschen“ aktualisiert
  • Haben Sie die Ansichten „Details“ und „Erstellen“ aktualisiert

Fahren Sie mit dem nächsten Tutorial fort, um zu erfahren, wie Sie die TPH-Vererbung (TPH = Table per Hierarchy, Tabelle pro Hierarchie) für die Entitäten „Instructor“ und „Student“ implementieren.