Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Tom Dykstra, 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 mit dem Code, den Sie anhand des Tutorials erstellt haben.
Dieses Tutorial zeigt, wie Sie Konflikte behandeln, wenn mehrere Benutzer gleichzeitig dieselbe Entität aktualisieren.
Parallelitätskonflikte
Ein Nebenläufigkeitskonflikt tritt auf, wenn:
- Ein Benutzer zur Bearbeitungsseite für eine Entität navigiert.
- Ein anderer Benutzer aktualisiert dieselbe Entität, bevor die Änderung des ersten Benutzers in die Datenbank geschrieben wird.
Wenn die Nebenläufigkeitserkennung nicht aktiviert ist, überschreibt derjenige, der die Datenbank zuletzt aktualisiert, die Änderungen des anderen Benutzers. Wenn dieses Risiko akzeptabel ist, könnten die Kosten für die Programmierung für Nebenläufigkeit den Nutzen überwiegen.
Pessimistische Nebenläufigkeit
Eine Möglichkeit, Parallelitätskonflikte zu vermeiden, ist die Verwendung von Datenbanksperren. Man bezeichnet dies als pessimistische Nebenläufigkeit. Bevor die App eine Datenbankzeile liest, die aktualisiert werden soll, wird eine Sperre angefordert. Nachdem eine Zeile für den Aktualisierungszugriff gesperrt wurde, dürfen keine anderen Benutzer die Zeile sperren, bis die erste Sperre freigegeben wird.
Das Verwalten von Sperren hat Nachteile. Die Programmierung kann komplex sein und Leistungsprobleme verursachen, wenn sich die Anzahl der Benutzer erhöht. Entity Framework Core bietet keine integrierte Unterstützung für pessimistische Nebenläufigkeit.
Optimistische Parallelität
Optimistische Nebenläufigkeit lässt Nebenläufigkeitskonflikte zu und reagiert entsprechend, wenn diese auftreten. Beispielsweise besucht Jane die Bearbeitungsseite der Abteilung und ändert das Budget für die Abteilung Englisch von 350.000,00 $ auf 0,00 $.
Bevor Jane auf Speichern klickt, besucht John dieselbe Seite und ändert das Feld „Startdatum“ von 9.1.2007 in 9.1.2013.
Jane klickt zuerst auf Speichern und sieht, dass die Änderung wirksam wird, da der Browser die Indexseite mit 0 (null) als Budgetbetrag anzeigt.
John klickt auf einer Bearbeitungsseite auf Speichern, die weiterhin ein Budget von 350.000 $ anzeigt. Was anschließend geschieht, ist abhängig davon, wie Sie Parallelitätskonflikte handhaben:
Verfolgen Sie nach, welche Eigenschaft ein Benutzer geändert hat, und aktualisieren Sie nur die entsprechenden Spalten in der Datenbank.
In diesem Szenario sollten keine Daten verloren gehen. Die beiden Benutzer haben verschiedene Eigenschaften aktualisiert. Das nächste Mal, wenn eine Person den englischen Fachbereich durchsucht, sieht diese sowohl Janes als auch Johns Änderungen. Diese Methode der Aktualisierung kann die Anzahl von Konflikten reduzieren, die zu Datenverlust führen können. Dieser Ansatz bietet einige Nachteile:
- Datenverlust lässt sich nicht vermeiden, wenn konkurrierende Änderungen an der gleichen Eigenschaft gemacht werden.
- Ist in einer Web-App in der Regel nicht praktisch. Dies erfordert, dass ein erheblicher Zustand beibehalten wird, um alle abgerufenen Werte und neuen Werte nachzuverfolgen. Das Verwalten großer Mengen an Status kann die App-Leistung beeinträchtigen.
- Kann die Anwendungskomplexität im Vergleich zur Parallelitätsermittlung für eine Entität erhöhen.
Lassen Sie zu, dass Johns Änderung Janes Änderung überschreibt.
Das nächste Mal, wenn jemand den englischen Fachbereich durchsucht, wird das Datum 9.1.2013 und der abgerufene Wert von 350.000 $ angezeigt. Dieser Ansatz wird als Client Wins- oder Last in Wins-Szenario (Letzter gewinnt) bezeichnet. Alle vom Client stammenden Werte haben Vorrang vor den im Datenspeicher enthaltenen Werten. Der Gerüstcode enthält keine Konfliktbehandlung bei gleichzeitigen Änderungen; „Client Wins“ erfolgt automatisch.
Verhindern Sie, dass Johns Änderung in der Datenbank aktualisiert wird. In der Regel würde die App:
- Eine Fehlermeldung anzeigen
- Den aktuellen Datenstatus anzeigen
- Ermöglichen Sie dem Benutzer, die Änderungen erneut anzuwenden.
Dies wird als Store Wins-Szenario bezeichnet. Die Werte des Datenspeichers besitzen Vorrang gegenüber den Werten, die vom Client gesendet werden. Das Szenario „Store Wins“ (Speicher gewinnt) wird in diesem Tutorial verwendet. Diese Methode stellt sicher, dass keine Änderungen überschrieben werden, ohne dass ein Benutzer darüber benachrichtigt wird.
Konflikterkennung in EF Core
Eigenschaften, die als Nebenläufigkeitstoken konfiguriert sind, werden zur Implementierung von optimistischer Nebenläufigkeitssteuerung verwendet. Wenn ein Aktualisierungs- oder Löschvorgang durch SaveChanges oder SaveChangesAsync ausgelöst wird, wird der Wert des Nebenläufigkeitstokens in der Datenbank mit dem ursprünglichen, von EF Core gelesenen Wert verglichen:
- Falls die Werte übereinstimmen, kann der Vorgang abgeschlossen werden.
- Ist dies jedoch nicht der Fall, geht EF Core davon aus, dass ein anderer Benutzer einen in Konflikt stehenden Vorgang ausgeführt hat, bricht die aktuelle Transaktion ab, und löst eine DbUpdateConcurrencyException aus.
Wenn ein anderer Benutzer oder Prozess einen Vorgang ausführt, der mit dem aktuellen Vorgang in Konflikt steht, wird dies als Parallelitätskonflikt bezeichnet.
Bei relationalen Datenbanken überprüft EF Core den Wert des Nebenläufigkeitstokens in der WHERE-Klausel der UPDATE- und DELETE-Anweisungen, um einen Nebenläufigkeitskonflikt zu erkennen.
Das Datenmodell muss so konfiguriert werden, dass es eine Konflikterkennung ermöglicht, indem eine Nachverfolgungsspalte eingefügt wird, anhand derer festgestellt werden kann, ob eine Zeile geändert wurde. EF bietet zwei Ansätze für Nebenläufigkeitstoken:
Wenden Sie
[ConcurrencyCheck]oder IsConcurrencyToken auf eine Eigenschaft des Modells an. Diese Vorgehensweise wird nicht empfohlen. Weitere Informationen finden Sie unter Nebenläufigkeitstoken in EF Core.Anwenden von TimestampAttribute oder IsRowVersion auf ein Nebenläufigkeitstoken im Modell. Dieser Ansatz wird in diesem Tutorial verwendet.
Der SQL Server-Ansatz und die SQLite-Implementierung unterscheiden sich in den Details geringfügig. Eine Differenzdatei wird später im Tutorial gezeigt, in der die Unterschiede aufgelistet werden. Die Visual Studio-Registerkarte zeigt den SQL Server-Ansatz. Auf dem Visual Studio Code-Register wird der Ansatz für Nicht-SQL Server-Datenbanken (z. B. SQLite) gezeigt.
- Fügen Sie in das Modell eine Nachverfolgungsspalte ein, die dazu dient, festzustellen, wenn eine Zeile geändert wurde.
- Wenden Sie TimestampAttribute auf die Parallelitätseigenschaft an.
Aktualisieren Sie die Models/Department.cs Datei mit dem folgenden hervorgehobenen Code:
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[] ConcurrencyToken { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
TimestampAttribute identifiziert die Spalte als Parallelitätsnachverfolgungsspalte. Die Fluent-API ist eine alternative Methode, um die Nachverfolgungseigenschaft anzugeben:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Das [Timestamp]-Attribut für eine Entitätseigenschaft generiert den folgenden Code in der ModelBuilder-Methode:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Der vorangehende Code:
- Legt den Eigenschaftentyp
ConcurrencyTokenauf Bytearray fest.byte[]ist der erforderliche Typ für SQL Server. - Aufrufe IsConcurrencyToken.
IsConcurrencyTokenkonfiguriert die Eigenschaft als Parallelitätstoken. Bei Aktualisierungen wird der Wert des Parallelitätstokens in der Datenbank mit dem ursprünglichen Wert verglichen, um sicherzustellen, dass er sich nicht geändert hat, nachdem die Instanz aus der Datenbank abgerufen wurde. Wenn es sich geändert hat, wird eine DbUpdateConcurrencyException ausgelöst, und die Änderungen werden nicht übernommen. - Ruft ValueGeneratedOnAddOrUpdate auf, wodurch die
ConcurrencyToken-Eigenschaft so konfiguriert wird, dass beim Hinzufügen oder Aktualisieren einer Entität automatisch ein Wert generiert wird. -
HasColumnType("rowversion")legt den Spaltentyp in der SQL Server-Datenbank auf rowversion fest.
Der folgende Code zeigt einen Teil des T-SQL, das von EF Core generiert wird, wenn der Department-Name aktualisiert wird:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Das vorherige markierte Codebeispiel zeigt die WHERE-Klausel, die ConcurrencyToken enthält. Wenn die Datenbank ConcurrencyToken nicht dem ConcurrencyToken-Parameter @p2 entspricht, werden keine Zeilen aktualisiert.
Der folgende hervorgehobene Code stellt das T-SQL dar, das genau überprüft, ob eine Zeile aktualisiert wurde:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT gibt die Anzahl der von der letzten Anweisung betroffenen Zeilen zurück. Wenn keine Zeilen aktualisiert werden, löst EF Core eine DbUpdateConcurrencyException aus.
Migration hinzufügen
Das Hinzufügen der ConcurrencyToken-Eigenschaft ändert das Datenmodell, das eine Migration erfordert.
Kompilieren Sie das Projekt.
Führen Sie die folgenden Befehle in der PMC aus:
Add-Migration RowVersion
Update-Database
Die vorangehenden Befehle:
- Erstellt die Migrationsdatei
Migrations/{time stamp}_RowVersion.cs. - Aktualisiert die Datei
Migrations/SchoolContextModelSnapshot.cs. Die Aktualisierung fügt derBuildModel-Methode den folgenden Code hinzu:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Vorlage für Abteilungsseiten
Befolgen Sie die Anweisungen auf den Scaffold-Schülerseiten, mit den folgenden Ausnahmen:
- Erstellen Sie einen Ordner Pages/Departments.
- Verwenden Sie
Departmentals Modellklasse. - Verwenden Sie die vorhandene Kontextklasse, anstatt eine neue Klasse zu erstellen.
Hilfsklasse hinzufügen
Erstellen Sie mit dem folgenden Code im Projektordner die Klasse Utility:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
Die Utility-Klasse stellt die GetLastChars-Methode bereit, mit der die letzten paar Zeichen des Nebenläufigkeitstokens angezeigt werden. Der folgende Code funktioniert mit SQLite und SQL Server:
#if SQLiteVersion
using System;
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(Guid token)
{
return token.ToString().Substring(
token.ToString().Length - 3);
}
}
}
#else
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
#endif
Die #if SQLiteVersion isoliert die Unterschiede in den SQLite- und SQL Server-Versionen und wirkt unterstützend:
- Der Autor verwaltet eine Codebasis für beide Versionen.
- SQLite-Entwickler stellen die App in Azure bereit und verwenden SQL Azure.
Erstellen Sie das Projekt.
Index-Seite aktualisieren
Das Scaffoldingtool hat für die Indexseite eine ConcurrencyToken-Spalte erstellt, aber dieses Feld würde in einer Produktions-App nicht angezeigt. In diesem Tutorial wird der letzte Teil von ConcurrencyToken angezeigt, um zu veranschaulichen, wie die Behandlung von Nebenläufigkeit funktioniert. Der letzte Teil ist für sich allein nicht garantiert eindeutig.
Aktualisieren Sie die Seite Pages\Departments\Index.cshtml:
- Ersetzen Sie „Index“ durch „Departments“.
- Ändern Sie den Code, der
ConcurrencyTokenenthält, um nur die letzten Zeichen anzuzeigen. - Ersetzen Sie
FirstMidNamedurchFullName.
Der folgende Code zeigt die aktualisierte Seite an:
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
Token
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department)
{
<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>
@Utility.GetLastChars(item.ConcurrencyToken)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Das Modell der Seite „Bearbeiten“ aktualisieren
Aktualisieren Sie Pages/Departments/Edit.cshtml.cs mit folgendem Code:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
// ModelState contains the posted data because of the deletion error
// and overides the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
Aktualisierungen zur Parallelität
OriginalValue wird mit dem ConcurrencyToken-Wert aus der Entität aktualisiert, die in der OnGetAsync-Methode abgerufen wurde.
EF Core generiert einen SQL UPDATE-Befehl mit einer WHERE-Klausel, die den ursprünglichen ConcurrencyToken-Wert enthält. Wenn keine Zeilen von dem UPDATE-Befehl betroffen sind, wird eine Ausnahme DbUpdateConcurrencyException ausgelöst. Der Befehl UPDATE wirkt sich nicht auf Zeilen aus, wenn keine Zeilen den ursprünglichen Wert ConcurrencyToken aufweisen.
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
Im vorangehend hervorgehobenen Code:
- Der Wert in
Department.ConcurrencyTokenist der Wert zum Zeitpunkt des Abrufs der Entität in derGet-Anforderung für dieEdit-Seite. Der Wert wird der MethodeOnPostüber ein verborgenes Feld in der Razor-Seite bereitgestellt, die die zu bearbeitende Entität anzeigt. Der Wert des ausgeblendeten Felds wird vom Modellbinder nachDepartment.ConcurrencyTokenkopiert. -
OriginalValuegibt an, was EF Core in derWHERE-Klausel verwendet. Bevor die hervorgehobene Codezeile ausgeführt wird:-
OriginalValueweist den Wert auf, der in der Datenbank vorhanden war, alsFirstOrDefaultAsyncin dieser Methode aufgerufen wurde. - Dieser Wert kann sich von dem unterscheiden, der auf der Seite „Bearbeiten“ angezeigt wurde.
-
- Der hervorgehobene Code stellt sicher, dass EF Core den ursprünglichen
ConcurrencyToken-Wert aus der angezeigtenDepartment-Entität in derUPDATE-Klausel der SQL-WHERE-Anweisung verwendet.
Der folgende Code veranschaulicht das Department-Modell.
Department wird in initialisiert:
- in der
OnGetAsync-Methode durch die EF-Abfrage. - in der
OnPostAsync-Methode über das ausgeblendete Feld auf der Razor-Seite mithilfe der Modellbindung:
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
Der Code oben zeigt, dass der ConcurrencyToken-Wert der Department-Entität aus der HTTP POST-Anforderung auf den ConcurrencyToken-Wert aus der HTTP GET-Anforderung festgelegt wird.
Wenn ein Parallelitätsfehler auftritt, ruft der folgende hervorgehobene Code die vom Client übergebenen Werte (die Werte, die an diese Methode übergeben wurden) und die Datenbankwerte ab.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
}
Der folgende Code fügt eine benutzerdefinierte Fehlermeldung für jede Spalte ein, deren Datenbankwerte sich von jenen unterscheiden, die an OnPostAsync gesendet wurden:
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
Der folgende hervorgehobene Code legt den ConcurrencyToken-Wert auf den neuen Wert fest, der aus der Datenbank abgerufen wurde. Wenn der Benutzer das nächste Mal auf Speichern klickt, werden nur Nebenläufigkeitsfehler abgefangen, die aufgetreten sind, seit die Seite „Bearbeiten“ zuletzt angezeigt wurde.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
}
Die ModelState.Remove-Anweisung ist erforderlich, weil ModelState den vorherigen ConcurrencyToken-Wert enthält. Auf der Razor-Seite hat der ModelState-Wert für ein Feld Vorrang vor den Modelleigenschaftswerten, wenn beide vorhanden sind.
Codeunterschiede zwischen SQL Server und SQLite
Im Folgenden werden die Unterschiede zwischen den SQL Server- und SQLite-Versionen gezeigt:
+ using System; // For GUID on SQLite
+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();
_context.Entry(departmentToUpdate)
.Property(d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
- Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
+ Department.ConcurrencyToken = dbValues.ConcurrencyToken;
Aktualisieren Sie die Seite „Edit Razor“
Aktualisieren Sie Pages/Departments/Edit.cshtml mit folgendem Code:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<div class="form-group">
<label>Version</label>
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Der vorangehende Code:
- Aktualisiert die
page-Anweisung von@pageauf@page "{id:int}". - Fügt eine Version einer ausgeblendeten Zeile hinzu.
ConcurrencyTokenmuss hinzugefügt werden, damit der Wert beim Postback gebunden wird. - Zeigt das letzte Byte von
ConcurrencyTokenfür Debuggingzwecke an. - Ersetzt
ViewDatadurch den stark typisiertenInstructorNameSL.
Testen von Nebenläufigkeitskonflikten auf der Bearbeitungsseite
Öffnen Sie für den englischen Fachbereich zwei Browserfenster von Edit:
- Starten Sie die App und wählen Sie „Abteilungen“.
- Klicken Sie mit der rechten Maustaste auf den Link Bearbeiten für den englischen Fachbereich, und wählen Sie In neuer Registerkarte öffnen.
- Klicken Sie im ersten Tab auf den Hyperlink Bearbeiten für die Englischabteilung.
Beide Registerkarten zeigen die gleichen Informationen an.
Ändern Sie den Namen im ersten Browser-Tab, und klicken Sie auf Speichern.
Der Browser zeigt die Seite „Index“ mit dem geänderten Wert und dem aktualisierten ConcurrencyToken Indikator an. Beachten Sie, dass der aktualisierte ConcurrencyToken-Indikator beim zweiten Postback-Ereignis auf der anderen Registerkarte angezeigt wird.
Ändern Sie ein anderes Feld im zweiten Browser-Tab.
Klicken Sie auf "Speichern". Es werden Fehlermeldungen für alle Felder angezeigt, die nicht mit den Datenbankwerten übereinstimmen:
Dieses Browserfenster beabsichtigte nicht, das Feld „Name“ zu ändern. Kopieren Sie den aktuellen Wert (Sprachen) und fügen Sie ihn in das Feld Name ein. Wechseln Sie durch Drücken der TAB-Taste zum nächsten Feld. Im Rahmen der clientseitigen Überprüfung wird die Fehlermeldung entfernt.
Klicken Sie erneut auf Speichern. Der Wert, den Sie im zweiten Browser-Tab eingegeben haben, wird gespeichert. Die gespeicherten Werte werden auf der Indexseite angezeigt.
Aktualisieren Sie das Seitenmodell „Löschen“
Aktualisieren Sie Pages/Departments/Delete.cshtml.cs mit folgendem Code:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.ConcurrencyToken value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Die Seite „Löschen“ erkennt Nebenläufigkeitskonflikte, wenn die Entität geändert wurde, nachdem sie abgerufen wurde. Bei Department.ConcurrencyToken handelt es sich um die Zeilenversion zum Zeitpunkt des Abrufs der Entität. Wenn EF Core den SQL DELETE-Befehl erstellt, umfasst dieser eine WHERE-Klausel mit ConcurrencyToken. Wenn der SQL DELETE-Befehl dazu führt, dass null Zeilen betroffen sind:
- Stimmt
ConcurrencyTokenimSQL DELETE-Befehl nicht mitConcurrencyTokenin der Datenbank überein. - Wird eine
DbUpdateConcurrencyException-Ausnahme ausgelöst. -
OnGetAsyncwird mit demconcurrencyErroraufgerufen.
Aktualisieren der Razor-Seite „Delete“ (Löschen)
Aktualisieren Sie Pages/Departments/Delete.cshtml mit folgendem Code:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.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.Department.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
</dt>
<dd class="col-sm-10">
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Durch den vorhergehenden Code werden folgende Änderungen vorgenommen:
- Aktualisiert die
page-Anweisung von@pageauf@page "{id:int}". - Fügt eine Fehlermeldung hinzu.
- Ersetzt „FirstMidName“ durch „FullName“ im Feld Administrator.
- Ändert
ConcurrencyToken, um das letzte Byte anzuzeigen. - Fügt eine Version einer ausgeblendeten Zeile hinzu.
ConcurrencyTokenmuss hinzugefügt werden, damit der Wert beim Postback gebunden wird.
Parallelitätskonflikte testen
Erstellen Sie eine Testabteilung.
Öffnen Sie im Testfachbereich zwei Browserinstanzen von Delete:
- Starten Sie die App und wählen Sie „Abteilungen“.
- Klicken Sie mit der rechten Maustaste auf den Link Löschen für die Testabteilung und wählen Sie In neuer Registerkarte öffnen aus.
- Klicken Sie auf den Link Bearbeiten für die Testabteilung.
Beide Registerkarten zeigen die gleichen Informationen an.
Ändern Sie das Budget im ersten Browser-Tab, und klicken Sie auf Speichern.
Der Browser zeigt die Index-Seite mit dem geänderten Wert und dem aktualisierten ConcurrencyToken-Indikator an. Beachten Sie, dass der aktualisierte ConcurrencyToken-Indikator beim zweiten Postback-Ereignis auf der anderen Registerkarte angezeigt wird.
Löschen Sie den Testfachbereich aus der zweiten Registerkarte. Ein Parallelitätsfehler wird mit den aktuellen Werten aus der Datenbank angezeigt. Wenn Sie auf Löschen klicken, wird die Entität gelöscht, es sei denn, ConcurrencyToken wurde aktualisiert.
Enterprise Web App-Muster
Anleitungen zum Erstellen einer zuverlässigen, sicheren, leistungsfähigen, testbaren und skalierbaren ASP.NET Core-App finden Sie unter Enterprise Web App-Muster. Eine vollständige Beispielweb-App zur Produktionsqualität, die die Muster implementiert, ist verfügbar.
Dieses Tutorial zeigt, wie Sie Konflikte behandeln, wenn mehrere Benutzer gleichzeitig dieselbe Entität aktualisieren.
Parallelitätskonflikte
Ein Nebenläufigkeitskonflikt tritt auf, wenn:
- Ein Benutzer navigiert zur Bearbeitungsseite für eine Entität.
- Ein anderer Benutzer aktualisiert dieselbe Entität, bevor die Änderung des ersten Benutzers in die Datenbank geschrieben wird.
Wenn die Parallelitätserkennung nicht aktiviert ist, überschreibt derjenige, der die Datenbank zuletzt aktualisiert, die Änderungen des anderen Benutzers. Wenn dieses Risiko akzeptabel ist, könnten die Kosten der nebenläufigen Programmierung den Nutzen überwiegen.
Pessimistische Nebenläufigkeitssteuerung (Sperren)
Eine Möglichkeit, Parallelitätskonflikte zu vermeiden, ist die Verwendung von Datenbanksperren. Man bezeichnet dies als pessimistische Nebenläufigkeit. Bevor die App eine Datenbankzeile liest, die sie aktualisieren will, fordert sie eine Sperre an. Sobald eine Zeile für den Updatezugriff gesperrt ist, kann kein anderer Benutzer die Zeile sperren, bis die erste Sperre aufgehoben wird.
Das Verwalten von Sperren hat Nachteile. Die Programmierung kann komplex sein und Leistungsprobleme verursachen, wenn die Anzahl der Benutzer steigt. Entity Framework Core enthält keine integrierte Unterstützung dafür, und in diesem Tutorial wird nicht gezeigt, wie die Implementierung erfolgt.
Optimistische Parallelität
Optimistische Nebenläufigkeit lässt Nebenläufigkeitskonflikte zu und reagiert entsprechend, wenn diese auftreten. Beispielsweise besucht Jane die Bearbeitungsseite des Fachbereichs und ändert das Budget für den englischen Fachbereich von 350.000 $ in 0 $.
Bevor Benutzer1 auf Speichern klickt, besucht Benutzer2 dieselbe Seite und ändert das Feld „Startdatum“ von 9.1.2007 in 9.1.2013.
Jane klickt zuerst auf Speichern und sieht, dass die Änderung wirksam wird, da der Browser die Indexseite mit 0 (null) als Budgetbetrag anzeigt.
John klickt auf einer Bearbeitungsseite auf Speichern, die weiterhin ein Budget von 350.000 $ anzeigt. Was anschließend geschieht, ist abhängig davon, wie Sie Parallelitätskonflikte handhaben:
Sie können nachverfolgen, welche Eigenschaft ein Benutzer geändert hat und nur die entsprechenden Spalten in der Datenbank aktualisieren.
In diesem Szenario sollten keine Daten verloren gehen. Die beiden Benutzer haben verschiedene Eigenschaften aktualisiert. Das nächste Mal, wenn eine Person den englischen Fachbereich durchsucht, sieht diese sowohl Janes als auch Johns Änderungen. Diese Methode der Aktualisierung kann die Anzahl von Konflikten reduzieren, die zu Datenverlust führen können. Dieser Ansatz bietet einige Nachteile:
- Datenverlust lässt sich nicht vermeiden, wenn konkurrierende Änderungen an der gleichen Eigenschaft gemacht werden.
- Ist in einer Web-App in der Regel nicht praktisch. Dies erfordert, dass ein erheblicher Zustand beibehalten wird, um alle abgerufenen Werte und neuen Werte nachzuverfolgen. Das Verwalten großer Mengen an Status kann die App-Leistung beeinträchtigen.
- Kann die Anwendungskomplexität im Vergleich zur Parallelitätsermittlung für eine Entität erhöhen.
Sie können zulassen, dass Johns Änderung Janes Änderung überschreibt.
Das nächste Mal, wenn jemand den englischen Fachbereich durchsucht, wird das Datum 9.1.2013 und der abgerufene Wert von 350.000 $ angezeigt. Dieses Ansatz wird Client gewinnt- oder Last in Wins-Szenario (Letzter gewinnt) genannt. (Alle Werte des Clients haben Vorrang vor dem Datenspeicher.) Wenn Sie keine Codierung für die Parallelitätsbehandlung durchführen, tritt „Client Wins“ automatisch ein.
Sie können verhindern, dass Johns Änderung in die Datenbank aufgenommen wird. In der Regel würde die App:
- Eine Fehlermeldung anzeigen
- Den aktuellen Datenstatus anzeigen
- Ermöglichen Sie dem Benutzer, die Änderungen erneut anzuwenden.
Dieses Szenario wird Store Wins genannt. (Die Werte des Datenspeichers haben Vorrang gegenüber den Werten, die vom Client gesendet werden). In diesem Tutorial implementieren Sie das Szenario „Store Wins“ (Speicher gewinnt). Diese Methode stellt sicher, dass keine Änderungen überschrieben werden, ohne dass ein Benutzer darüber benachrichtigt wird.
Konflikterkennung in EF Core
EF Core löst DbConcurrencyException-Ausnahmen aus, wenn Konflikte erkannt werden. Das Datenmodell muss konfiguriert werden, um die Konflikterkennung zu aktivieren. Optionen für das Aktivieren der Konflikterkennung schließen Folgendes ein:
Konfigurieren Sie EF Core so, dass die ursprünglichen Werte von Spalten, die als Parallelitätstoken in der Where-Klausel der Update- und Delete-Befehle enthalten sind, einbezogen werden.
Wenn
SaveChangesaufgerufen wird, sucht die Where-Klausel nach den ursprünglichen Werten von Eigenschaften, die als Anmerkung mit dem ConcurrencyCheckAttribute-Attribut versehen wurden. Die Update-Anweisung findet keine zu aktualisierende Zeile, wenn sich eine der Parallelitätstokeneigenschaften geändert hat, seit die Zeile zum ersten Mal gelesen wurde. EF Core interpretiert dies als einen Parallelitätskonflikt. Bei Datenbanktabellen mit vielen Spalten kann dieser Ansatz zu sehr großen Where-Klauseln führen, was wiederum dazu führen kann, dass eine große Anzahl von Zuständen erforderlich ist. Deshalb wird dieser Ansatz in der Regel nicht empfohlen, und ist nicht die Methode, die in diesem Tutorial verwendet wird.Fügen Sie eine Änderungsverfolgungsspalte in die Datenbanktabelle ein, die verwendet werden kann, um zu bestimmen, wenn eine Änderung an einer Zeile vorgenommen wurde.
In einer SQL Server-Datenbank ist
rowversionder Datentyp der Änderungsverfolgungsspalte. Der Wertrowversionist 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 Zeilenversionsnummer). Wenn die zu aktualisierende Zeile von einem anderen Benutzer geändert wurde, unterscheidet sich der Wert in der Spalterowversionvom ursprünglichen Wert. In diesem Fall kann die Update- oder Delete-Anweisung die zu aktualisierende Zeile aufgrund der Where-Klausel nicht finden. EF Core löst eine Parallelitätsausnahme aus, wenn keine Zeilen von einem Update- oder Delete-Befehl betroffen sind.
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 TimestampAttribute-Attribut identifiziert die Spalte als Spalte für Nebenläufigkeitsverfolgung. Die Fluent-API ist eine alternative Methode, um die Nachverfolgungseigenschaft anzugeben:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Für eine SQL Server-Datenbank wird das [Timestamp]-Attribut auf eine als Bytearray definierte Entitätseigenschaft angewendet:
- Bewirkt, dass die Spalte in DELETE- und UPDATE WHERE-Klauseln eingeschlossen wird.
- Legt den Spaltentyp in der Datenbank auf rowversion fest.
Die Datenbank generiert eine sequenzielle Zeilenversionsnummer, die jedes Mal inkrementiert wird, wenn die Zeile aktualisiert wird. In einem Update- oder Delete-Befehl enthält die Where-Klausel den Wert der abgerufenen Zeilenversion. Wenn sich die aktualisierte Zeile seit ihrem Abruf geändert hat, geschieht Folgendes:
- Der Wert der aktuellen Zeilenversion stimmt nicht mit dem abgerufenen Wert überein.
- Die
Update- oderDelete-Befehle finden keine Zeile, da dieWhere-Klausel anhand des abgerufenen Zeilenversionswerts sucht. - Es wird eine
DbUpdateConcurrencyExceptionausgelöst.
Der folgende Code zeigt einen Teil des T-SQL, das von EF Core generiert wurde, wenn der Name von Department aktualisiert wird:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Das vorherige markierte Codebeispiel zeigt die WHERE-Klausel, die RowVersion enthält. Wenn die Datenbank RowVersion nicht dem RowVersion-Parameter (@p2) entspricht, werden keine Zeilen aktualisiert.
Der folgende hervorgehobene Code stellt das T-SQL dar, das genau überprüft, ob eine Zeile aktualisiert wurde:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT gibt die Anzahl der von der letzten Anweisung betroffenen Zeilen zurück. Wenn keine Zeilen aktualisiert werden, löst EF Core eine DbUpdateConcurrencyException aus.
Datenbank aktualisieren
Das Hinzufügen der RowVersion-Eigenschaft ändert das Datenbankmodell, das eine Migration erfordert.
Kompilieren Sie das Projekt.
Führen Sie den folgenden Befehl in der PMC aus:
Add-Migration RowVersion
Dieser Befehl:
Erstellt die
Migrations/{time stamp}_RowVersion.csMigrationsdatei.Aktualisiert die Datei
Migrations/SchoolContextModelSnapshot.cs. Das Update fügt derBuildModel-Methode den folgenden hervorgehobenen Code hinzu:modelBuilder.Entity("ContosoUniversity.Models.Department", b => { b.Property<int>("DepartmentID") .ValueGeneratedOnAdd() .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property<decimal>("Budget") .HasColumnType("money"); b.Property<int?>("InstructorID"); b.Property<string>("Name") .HasMaxLength(50); b.Property<byte[]>("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate(); b.Property<DateTime>("StartDate"); b.HasKey("DepartmentID"); b.HasIndex("InstructorID"); b.ToTable("Department"); });
Führen Sie den folgenden Befehl in der PMC aus:
Update-Database
Abteilungsseiten erstellen
Befolgen Sie die Anweisungen unter Scaffold-Studentenseiten mit den folgenden Ausnahmen:
Legen Sie den Ordner Pages/Departments an.
Verwenden Sie
Departmentals Modellklasse.- Verwenden Sie die vorhandene Kontextklasse, anstatt eine neue Klasse zu erstellen.
Kompilieren Sie das Projekt.
Aktualisieren Sie die Indexseite
Das Gerüstbautool hat eine RowVersion-Spalte für die Index-Seite erstellt, dieses Feld würde jedoch in einer Produktions-App nicht angezeigt werden. In diesem Tutorial wird das letzte Byte der RowVersion angezeigt, damit Sie ein besseres Verständnis für die Funktionsweise der Nebenläufigkeitsbehandlung bekommen. Das letzte Byte ist für sich allein nicht zwingend eindeutig.
Aktualisieren Sie die Seite Pages\Departments\Index.cshtml:
- Ersetzen Sie „Index“ durch „Departments“.
- Ändern Sie den Code, der
RowVersionenthält, um nur das letzte Byte des Bytearrays anzuzeigen. - Ersetzen Sie „FirstMidName“durch „FullName“.
Der folgende Code zeigt die aktualisierte Seite an:
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department)
{
<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>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Aktualisieren des Seitenbearbeitungsmodells
Aktualisieren Sie Pages/Departments/Edit.cshtml.cs mit folgendem Code:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
var deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error
// and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
OriginalValue wird mit dem rowVersion-Wert aus der Entität aktualisiert, der beim Abrufen der Entität in der OnGetAsync-Methode vorlag.
EF Core generiert einen SQL UPDATE-Befehl mit einer WHERE-Klausel mit dem ursprünglichen RowVersion-Wert. Wenn keine Zeilen durch den UPDATE-Befehl betroffen sind (keine Zeile enthält den ursprünglichen RowVersion-Wert), wird eine DbUpdateConcurrencyException-Ausnahme ausgelöst.
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
Im hervorgehobenen Code oben:
- Der Wert in
Department.RowVersionist der Wert, der in der Entität enthalten war, als sie ursprünglich bei der GET-Anforderung für die Bearbeitungsseite abgerufen wurde. Der Wert wird der MethodeOnPostüber ein verborgenes Feld in der Razor-Seite bereitgestellt, die die zu bearbeitende Entität anzeigt. Der Wert des ausgeblendeten Felds wird vom Modellbinder nachDepartment.RowVersionkopiert. -
OriginalValueist das, was EF Core in der WHERE-Klausel verwenden wird. Bevor die hervorgehobene Codezeile ausgeführt wird, enthältOriginalValueden Wert, der in der Datenbank enthalten war, alsFirstOrDefaultAsyncin dieser Methode aufgerufen wurde. Dieser kann sich möglicherweise von dem auf der Edit-Seite angezeigten Wert unterscheiden. - Der hervorgehobene Code stellt sicher, dass EF Core in der WHERE-Klausel der SQL-UPDATE-Anweisung den ursprünglichen
RowVersion-Wert der angezeigtenDepartment-Entität verwendet.
Wenn ein Parallelitätsfehler auftritt, ruft der folgende hervorgehobene Code die vom Client übergebenen Werte (die Werte, die an diese Methode übergeben wurden) und die Datenbankwerte ab.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Der folgende Code fügt eine benutzerdefinierte Fehlermeldung für jede Spalte ein, deren Datenbankwerte sich von jenen unterscheiden, die an OnPostAsync gesendet wurden:
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
Der folgende hervorgehobene Code legt den RowVersion-Wert auf den neuen Wert fest, der aus der Datenbank abgerufen wurde. Wenn der Benutzer das nächste Mal auf Speichern klickt, werden nur Nebenläufigkeitsfehler abgefangen, die aufgetreten sind, seit die Seite „Bearbeiten“ zuletzt angezeigt wurde.
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Die Anweisung ModelState.Remove ist erforderlich, weil ModelState noch den alten RowVersion-Wert hat. Auf der Razor-Seite hat der ModelState-Wert für ein Feld Vorrang vor den Modelleigenschaftswerten, wenn beide vorhanden sind.
Seite „Bearbeiten“ aktualisieren
Aktualisieren Sie Pages/Departments/Edit.cshtml mit folgendem Code:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Der vorangehende Code:
- Aktualisiert die
page-Anweisung von@pageauf@page "{id:int}". - Fügt eine Version einer ausgeblendeten Zeile hinzu.
RowVersionmuss hinzugefügt werden, damit der Wert beim Postback gebunden wird. - Zeigt das letzte Byte von
RowVersionfür Debuggingzwecke an. - Ersetzt
ViewDatadurch den stark typisiertenInstructorNameSL.
Testen von Nebenläufigkeitskonflikten auf der Bearbeitungsseite
Öffnen Sie für den englischen Fachbereich zwei Browserfenster von Edit:
- Starten Sie die App und wählen Sie „Abteilungen“.
- Klicken Sie mit der rechten Maustaste auf den Link Bearbeiten für den englischen Fachbereich, und wählen Sie In neuer Registerkarte öffnen.
- Klicken Sie im ersten Tab auf den Hyperlink Bearbeiten für die Englischabteilung.
Beide Registerkarten zeigen die gleichen Informationen an.
Ändern Sie den Namen im ersten Browser-Tab, und klicken Sie auf Speichern.
Der Browser zeigt die Indexseite mit dem geänderten Wert und dem aktualisierten RowVersion-Indikator an. Beachten Sie den aktualisierten rowVersion-Indikator; er wird beim zweiten Postback auf der anderen Registerkarte angezeigt.
Ändern Sie ein anderes Feld im zweiten Browser-Tab.
Klicken Sie auf "Speichern". Es werden Fehlermeldungen für alle Felder angezeigt, die nicht mit den Datenbankwerten übereinstimmen:
Dieses Browserfenster beabsichtigte nicht, das Feld „Name“ zu ändern. Kopieren Sie den aktuellen Wert (Sprachen) und fügen Sie ihn in das Feld Name ein. Wechseln Sie durch Drücken der TAB-Taste zum nächsten Feld. Im Rahmen der clientseitigen Überprüfung wird die Fehlermeldung entfernt.
Klicken Sie erneut auf Speichern. Der Wert, den Sie im zweiten Browser-Tab eingegeben haben, wird gespeichert. Die gespeicherten Werte werden auf der Indexseite angezeigt.
Aktualisieren Sie das Seitenmodell für „Löschen“
Aktualisieren Sie Pages/Departments/Delete.cshtml.cs mit folgendem Code:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Die Seite „Löschen“ erkennt Nebenläufigkeitskonflikte, wenn die Entität geändert wurde, nachdem sie abgerufen wurde. Bei Department.RowVersion handelt es sich um die Zeilenversion zum Zeitpunkt des Abrufs der Entität. Wenn EF Core den SQL-Befehl DELETE erstellt, umfasst dieser eine WHERE-Klausel mit RowVersion. Wenn der SQL-DELETE-Befehl dazu führt, dass 0 Zeilen betroffen sind:
- Die
RowVersionim SQL-DELETE-Befehl stimmt nicht mitRowVersionin der Datenbank überein. - Eine DbUpdateConcurrencyException-Ausnahme wird ausgelöst.
-
OnGetAsyncwird mitconcurrencyErroraufgerufen.
Aktualisieren der Seite „Delete“ (Löschen)
Aktualisieren Sie Pages/Departments/Delete.cshtml mit folgendem Code:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
Durch den vorangehenden Code werden folgende Änderungen vorgenommen:
- Aktualisiert die
page-Anweisung von@pageauf@page "{id:int}". - Eine Fehlermeldung wird hinzugefügt.
- „FirstMidName“ wird durch „FullName“ im Feld Administrator ersetzt.
-
RowVersionwird geändert, um das letzte Byte anzuzeigen. - Fügt eine ausgeblendete Zeilenversion hinzu.
RowVersionmuss hinzugefügt werden, damit ein Postback-Ereignis den Wert bindet.
Testen Sie auf Parallelitätskonflikte
Erstellen Sie eine Testabteilung.
Öffnen Sie im Testfachbereich zwei Browserinstanzen zum „Löschen“:
- Führen Sie die Anwendung aus, und wählen Sie „Abteilungen“ aus.
- Klicken Sie mit der rechten Maustaste auf den Link Löschen für den Testfachbereich, und klicken Sie aufIn neuer Registerkarte öffnen.
- Klicken Sie auf den Link Bearbeiten für den Testfachbereich.
Beide Registerkarten zeigen die gleichen Informationen an.
Ändern Sie das Budget im ersten Browser-Tab, und klicken Sie auf Speichern.
Der Browser zeigt die Indexseite mit dem geänderten Wert und dem aktualisierten RowVersion-Indikator an. Beachten Sie, dass der aktualisierte RowVersion-Indikator beim zweiten Postback-Ereignis auf der anderen Registerkarte angezeigt wird.
Löschen Sie die Testabteilung aus der zweiten Registerkarte. Ein Nebenläufigkeitsfehler wird mit den aktuellen Werten aus der Datenbank angezeigt. Durch Klicken auf Löschen wird die Entität gelöscht, es sei denn, RowVersion wurde aktualisiert.
Anleitungen zum Erstellen einer zuverlässigen, sicheren, leistungsfähigen, testbaren und skalierbaren ASP.NET Core-App finden Sie unter Enterprise Web App-Muster. Eine vollständige Beispielweb-App zur Produktionsqualität, die die Muster implementiert, ist verfügbar.
Dieses Tutorial zeigt, wie Sie Konflikte behandeln, wenn mehrere Benutzer gleichzeitig dieselbe Entität aktualisieren. Wenn Probleme auftreten, die Sie nicht lösen können, laden Sie die fertige App herunter, oder zeigen Sie diese an. Anweisungen zum Herunterladen.
Parallelitätskonflikte
Ein Nebenläufigkeitskonflikt tritt auf, wenn:
- Ein Benutzer navigiert zur Bearbeitungsseite für eine Entität.
- Ein anderer Benutzer dieselbe Entität aktualisiert, bevor die Änderung des ersten Benutzers in die Datenbank geschrieben wird.
Wenn die Parallelitätserkennung nicht aktiviert ist, geschieht Folgendes, wenn gleichzeitige Aktualisierungen auftreten:
- Das letzte Update gilt. Das bedeutet, dass die neuesten zu aktualisierenden Werte in der Datenbank gespeichert werden.
- Das erste der aktuellen Updates geht verloren.
Optimistische Parallelität
Optimistische Nebenläufigkeit lässt Nebenläufigkeitskonflikte zu und reagiert entsprechend, wenn diese auftreten. Beispielsweise besucht Benutzer1 die Bearbeitungsseite des Fachbereichs und ändert das Budget für den englischen Fachbereich von 350.000 $ in 0 $.
Bevor Jane auf Speichern klickt, besucht John dieselbe Seite und ändert das Feld „Startdatum“ von 9.1.2007 in 9.1.2013.
Benutzer1 klickt zuerst auf Speichern und sieht die Änderungen, wenn im Browser die Indexseite angezeigt wird.
Benutzer2 klickt auf einer Bearbeitungsseite auf Speichern, die weiterhin ein Budget von 350.000 $ anzeigt. Was daraufhin geschieht ist abhängig davon, wie Sie Nebenläufigkeitskonflikte handhaben.
Die optimistische Nebenläufigkeit umfasst die folgenden Optionen:
Sie können Nachverfolgen, welche Eigenschaft ein Benutzer geändert hat, und nur die entsprechenden Spalten in der Datenbank aktualisieren.
In diesem Szenario sollten keine Daten verloren gehen. Von den beiden Benutzern wurden unterschiedliche Eigenschaften aktualisiert. Das nächste Mal, wenn eine Person den englischen Fachbereich durchsucht, sieht diese die Änderungen von Benutzer1 und Benutzer2. Diese Methode der Aktualisierung kann die Anzahl von Konflikten reduzieren, die zu Datenverlust führen können. Dieser Ansatz:
- Kann Datenverlust nicht verhindern, wenn konkurrierende Änderungen an der gleichen Eigenschaft gemacht werden.
- Ist in einer Web-App in der Regel nicht praktisch. Erfordert, dass der maßgebliche Zustand beibehalten wird, um alle abgerufenen Werte und neuen Werte nachzuverfolgen. Das Verwalten großer Mengen an Status kann die Leistung der App beeinträchtigen.
- Kann die Anwendungskomplexität im Vergleich zur Parallelitätsermittlung für eine Entität erhöhen.
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 9.1.2013 und der abgerufene Wert von 350.000,00 $ angezeigt. Dieses Ansatz wird Client gewinnt- oder Last in Wins-Szenario (Letzter gewinnt) genannt. (Alle Werte des Clients haben Vorrang vor dem Datenspeicher.) Wenn Sie keine Codierung für die Parallelitätsbehandlung durchführen, tritt „Client Wins“ automatisch ein.
Sie können verhindern, dass die Änderung von John in die Datenbank aufgenommen wird. In der Regel kann die App:
- Eine Fehlermeldung anzeigen
- Den aktuellen Datenstatus anzeigen
- Ermöglichen Sie dem Benutzer, die Änderungen erneut anzuwenden.
Dieses Szenario wird Store Wins (Speicher gewinnt) genannt. (Die Werte des Datenspeichers haben Vorrang gegenüber den Werten, die vom Client gesendet werden). In diesem Tutorial implementieren Sie das Szenario „Store Wins“ (Speicher gewinnt). Diese Methode stellt sicher, dass keine Änderungen überschrieben werden, ohne dass ein Benutzer darüber benachrichtigt wird.
Umgang mit Nebenläufigkeit
Wenn eine Eigenschaft als ein Parallelitätstoken konfiguriert ist:
- stellt EF Core sicher, dass die Eigenschaft nicht geändert wurde, nachdem sie abgerufen wurde. Die Überprüfung findet statt, wenn SaveChanges oder SaveChangesAsync aufgerufen wird.
- Wenn die Property geändert wurde, nachdem sie abgerufen wurde, wird eine DbUpdateConcurrencyException ausgelöst.
Das Datenbank- und Datenmodell müssen konfiguriert sein, um das Werfen von DbUpdateConcurrencyException zu unterstützen.
Erkennen von Nebenläufigkeitskonflikten mit Eigenschaften
Nebenläufigkeitskonflikte können auf Eigenschaftsebene über das ConcurrencyCheck-Attribut erkannt werden. Das Attribut kann auf mehrere Eigenschaften im Modell angewendet werden. Weitere Informationen finden Sie unter Data Annotations-ConcurrencyCheck.
Das Attribut [ConcurrencyCheck] wird in diesem Tutorial nicht verwendet.
Erkennen von Nebenläufigkeitskonflikten für eine Zeile
Um Nebenläufigkeitskonflikte zu erkennen, wird dem Modell eine Rowversion-Nachverfolgungsspalte (Zeilenversion) hinzugefügt.
rowversion :
- Ist SQL Server-spezifisch. Andere Datenbanken bieten möglicherweise keine ähnliche Funktion.
- Wird verwendet, um zu bestimmen, dass eine Entität, seit dem Abruf aus der Datenbank, nicht geändert wurde.
Die DB generiert eine fortlaufende rowversion Nummer, die bei jeder Aktualisierung der Zeile inkrementiert wird. In einem Update- oder Delete-Befehl enthält die Where-Klausel den abgerufenen Wert von rowversion. Wenn sich die aktualisierte Zeile geändert hat, geschieht Folgendes:
-
rowversionentspricht nicht dem abgerufenen Wert. - Die Befehle
UpdateoderDeletefinden keine Datenzeile, da dieWhere-Klausel die abgerufenerowversionenthält. - Eine
DbUpdateConcurrencyExceptionwird geworfen.
In EF Core wird eine Nebenläufigkeitsausnahme ausgelöst, wenn durch einen Update- oder Delete-Befehl keine Zeilen aktualisiert wurden.
Der Entität „Department“ eine Trackingeigenschaft hinzufügen
Fügen Sie in 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 Timestamp-Attribut gibt an, dass diese Spalte in die Where-Klausel von Update- und Delete-Befehlen aufgenommen wird. Das Attribut wird Timestamp genannt, weil vorherige Versionen von SQL Server einen SQL-timestamp-Datentyp verwendet haben, bevor dieser durch SQL-rowversion ersetzt wurde.
Die Fluent-API kann auch die Nachverfolgungseigenschaft angeben:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Der folgende Code zeigt einen Teil des T-SQL, das von EF Core generiert wurde, wenn der Name von Department aktualisiert wird:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Das vorherige markierte Codebeispiel zeigt die WHERE-Klausel mit RowVersion an. Wenn die Datenbank RowVersion nicht dem RowVersion-Parameter (@p2) entspricht, werden keine Zeilen aktualisiert.
Der folgende hervorgehobene Code stellt das T-SQL dar, das genau überprüft, ob eine Zeile aktualisiert wurde:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT gibt die Anzahl der von der letzten Anweisung betroffenen Zeilen zurück. Wenn keine Zeilen aktualisiert werden, löst EF Core eine DbUpdateConcurrencyException aus.
Sie können das von EF Core generierte T-SQL im Ausgabefenster von Visual Studio sehen.
DB aktualisieren
Das Hinzufügen der RowVersion-Eigenschaft ändert das Datenbankmodell, das eine Migration erfordert.
Erstellen Sie das Projekt. Geben Sie Folgendes in ein Befehlsfenster ein:
dotnet ef migrations add RowVersion
dotnet ef database update
Die obenstehenden Befehle haben folgende Konsequenzen:
Die Migrationsdatei
Migrations/{time stamp}_RowVersion.cswird hinzugefügt.Aktualisiert die Datei
Migrations/SchoolContextModelSnapshot.cs. Über dieses Update wird derBuildModel-Methode der folgende hervorgehobene Code hinzugefügt:Führt Migrationen aus, um die DB zu aktualisieren.
Generieren des Abteilungsmodells
Befolgen Sie die Anweisungen unter Erstellen des Gerüsts für das Studentenmodell, und verwenden Sie Department für die Modellklasse.
Der vorherige Befehl erstellt ein Gerüst für das Department-Modell. Öffnen Sie das Projekt in Visual Studio.
Kompilieren Sie das Projekt.
Aktualisieren der Indexseite für Abteilungen
Die Gerüstbauengine hat eine RowVersion-Spalte auf der Indexseite erstellt, jedoch sollte dieses Feld nicht angezeigt werden. In diesem Tutorial wird das letzte Byte der RowVersion angezeigt, damit Sie ein besseres Verständnis über die Parallelität erlangen. Das letzte Byte ist nicht zwingend eindeutig. Eine echte App würde RowVersion oder das letzte Byte von RowVersion nicht anzeigen.
Aktualisieren der Indexseite:
- Ersetzen Sie „Index“ durch „Departments“.
- Ersetzen Sie das Markup mit
RowVersiondurch das letzte Byte vonRowVersion. - Ersetzen Sie „FirstMidName“durch „FullName“.
Das folgende Markup zeigt die aktualisierte Seite an:
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department) {
<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>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Aktualisieren des Seitenbearbeitungsmodells
Aktualisieren Sie Pages/Departments/Edit.cshtml.cs mit folgendem Code:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
// ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
Um einen Parallelitätskonflikt zu erkennen, wird OriginalValue mit dem rowVersion-Wert der Entität aktualisiert, aus der er abgerufen wurde.
EF Core generiert einen SQL-UPDATE-Befehl mit einer WHERE-Klausel, die den ursprünglichen RowVersion-Wert enthält. Wenn keine Zeilen durch den UPDATE-Befehl betroffen sind (keine Zeile enthält den ursprünglichen RowVersion-Wert), wird eine DbUpdateConcurrencyException-Ausnahme ausgelöst.
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
Im vorstehenden Code ist Department.RowVersion der Wert, den die Entität beim Abrufen hatte.
OriginalValue ist der Wert in der Datenbank, wenn FirstOrDefaultAsync in dieser Methode aufgerufen wurde.
Der folgende Code ruft die Clientwerte (die an diese Methode übergebenen Werte) und die Datenbankwerte ab:
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Der folgende Code fügt für jede Spalte, deren DB-Werte sich von denen unterscheiden, die an OnPostAsync übermittelt wurden, eine benutzerdefinierte Fehlermeldung hinzu:
private async Task SetDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
Der folgende hervorgehobene Code setzt den RowVersion-Wert auf den neuen, aus der DB abgerufenen Wert. Beim nächsten Mal, wenn der Benutzer auf Speichern klickt, werden nur Nebenläufigkeitsfehler abgefangen, die aufgetreten sind, seit die Seite Bearbeiten zuletzt angezeigt wurde.
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./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. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await SetDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
Die Anweisung ModelState.Remove ist erforderlich, da ModelState noch den alten RowVersion-Wert hat. In der Razor-Seite hat der ModelState-Wert für ein Feld Vorrang vor den Modelleigenschaftswerten, wenn beide vorhanden sind.
Seite „Bearbeiten“ aktualisieren
Aktualisieren Sie Pages/Departments/Edit.cshtml mit dem folgenden Markup:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.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-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Das obenstehende Markup:
- Aktualisiert die
page-Anweisung von@pageauf@page "{id:int}". - Fügt eine ausgeblendete Zeilenversion hinzu.
RowVersionmuss hinzugefügt werden, damit über ein Postback-Ereignis der Wert gebunden werden kann. - Zeigt das letzte Byte von
RowVersionzu Debugzwecken an. - Ersetzt
ViewDatadurch den stark typisiertenInstructorNameSL.
Testen von Nebenläufigkeitskonflikten mit der Seite „Bearbeiten“
Öffnen Sie für den englischen Fachbereich zwei Browserinstanzen von „Bearbeiten“:
- Führen Sie die Anwendung aus, und wählen Sie „Abteilungen“ aus.
- Klicken Sie mit der rechten Maustaste auf den Link Bearbeiten für den englischen Fachbereich, und wählen Sie In neuer Registerkarte öffnen.
- Klicken Sie in der ersten Registerkarte auf den Bearbeiten-Link für den Fachbereich Englisch.
Beide Registerkarten zeigen die gleichen Informationen an.
Ändern Sie den Namen in der ersten Registerkarte, und klicken Sie auf Speichern.
Der Browser zeigt die Indexseite mit dem geänderten Wert und dem aktualisierten RowVersion-Indikator an. Beachten Sie, dass der aktualisierte RowVersion-Indikator beim zweiten Postback-Ereignis auf der anderen Registerkarte angezeigt wird.
Ändern Sie ein anderes Feld in der zweiten Registerkarte.
Klicken Sie auf "Speichern". Es werden Fehlermeldungen für alle Felder angezeigt, die nicht mit den Datenbankwerten übereinstimmen:
In diesem Browserfenster sollte nicht das Namensfeld geändert werden. Kopieren Sie den aktuellen Wert (Sprachen) und fügen Sie ihn in das Feld Name ein. Wechseln Sie durch Drücken der TAB-Taste zum nächsten Feld. Im Rahmen der clientseitigen Überprüfung wird die Fehlermeldung entfernt.
Klicken Sie erneut auf Speichern. Der Wert, den Sie auf der zweiten Registerkarte eingegeben haben, wird gespeichert. Die gespeicherten Werte werden auf der Indexseite angezeigt.
Aktualisieren der Seite „Delete“ (Löschen)
Aktualisieren Sie das Seitenmodell „Löschen“ mit dem folgenden Code:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Die Seite „Löschen“ erkennt Nebenläufigkeitskonflikte, wenn die Entität geändert wurde, nachdem sie abgerufen wurde. Bei Department.RowVersion handelt es sich um die Zeilenversion zum Zeitpunkt des Abrufs der Entität. Wenn EF Core den SQL-Befehl DELETE erstellt, umfasst dieser eine WHERE-Klausel mit RowVersion. Wenn der SQL-DELETE-Befehl 0 betroffene Zeilen ergibt:
- Stimmt die
RowVersionim SQL-DELETE-Befehl nicht mitRowVersionin der Datenbank überein. - Wird eine DbUpdateConcurrencyException-Ausnahme ausgelöst.
- Wird
OnGetAsyncmitconcurrencyErroraufgerufen.
Aktualisieren der Seite „Delete“ (Löschen)
Aktualisieren Sie Pages/Departments/Delete.cshtml mit folgendem Code:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
Durch den vorhergehenden Code werden folgende Änderungen vorgenommen:
- Aktualisiert die
page-Anweisung von@pageauf@page "{id:int}". - Eine Fehlermeldung wird hinzugefügt.
- Ersetzt „FirstMidName“ durch „FullName“ im Feld Administrator.
- Ändert
RowVersion, um das letzte Byte anzuzeigen. - Fügt eine ausgeblendete Zeilenversion hinzu.
RowVersionmuss hinzugefügt werden, damit über ein Postback-Ereignis der Wert gebunden werden kann.
Testen von Nebenläufigkeitskonflikten auf der Seite „Löschen“
Erstellen Sie eine Testabteilung.
Öffnen Sie im Testfachbereich zwei Browserinstanzen von Delete:
- Führen Sie die Anwendung aus, und wählen Sie „Abteilungen“ aus.
- Klicken Sie mit der rechten Maustaste auf den Link Löschen für die Testabteilung und wählen Sie In neuer Registerkarte öffnen aus.
- Klicken Sie auf den Link Bearbeiten für die Testabteilung.
Beide Registerkarten zeigen die gleichen Informationen an.
Ändern Sie das Budget in der ersten Registerkarte, und klicken Sie auf Speichern.
Der Browser zeigt die Indexseite mit dem geänderten Wert und dem aktualisierten RowVersion-Indikator an. Beachten Sie den aktualisierten rowVersion-Indikator; er wird beim zweiten Postback auf der anderen Registerkarte angezeigt.
Löschen Sie den Testfachbereich aus der zweiten Registerkarte. Ein Parallelitätsfehler wird mit den aktuellen Werten aus der Datenbank angezeigt. Durch Klicken auf Löschen wird die Entität gelöscht, es sei denn, RowVersion wurde aktualisiert.
Informationen zum Vererben eines Datenmodells finden Sie unter Vererbung.
Enterprise Web App-Muster
Anleitungen zum Erstellen einer zuverlässigen, sicheren, leistungsfähigen, testbaren und skalierbaren ASP.NET Core-App finden Sie unter Enterprise Web App-Muster. Eine vollständige Beispielweb-App zur Produktionsqualität, die die Muster implementiert, ist verfügbar.
Verwandte Inhalte
Nächste Schritte
Dies ist das letzte Tutorial der Serie. Weitere Themen werden in der MVC-Version dieser Tutorialreihe behandelt.