Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
De Contoso University-web-app laat zien hoe u webpagina-apps maakt met Razor en Visual Studio EF Core. Zie de eerste zelfstudie voor meer informatie over de reeks zelfstudies.
Als u problemen ondervindt die u niet kunt oplossen, downloadt u de voltooide app en vergelijkt u die code met wat u hebt gemaakt door de zelfstudie te volgen.
Deze zelfstudie laat zien hoe u conflicten kunt afhandelen wanneer meerdere gebruikers een entiteit gelijktijdig bijwerken.
Gelijktijdigheidsconflicten
Er treedt een gelijktijdigheidsconflict op wanneer:
- Een gebruiker navigeert naar de bewerkingspagina voor een entiteit.
- Een andere gebruiker werkt dezelfde entiteit bij voordat de wijziging van de eerste gebruiker naar de database wordt geschreven.
Als gelijktijdigheidsdetectie niet is ingeschakeld, overschrijft degene die de database voor het laatst bijwerken de wijzigingen van de andere gebruiker. Als dit risico aanvaardbaar is, kunnen de kosten van programmering voor gelijktijdigheid opwegen tegen het voordeel.
Pessimistische gelijktijdigheid
Een manier om gelijktijdigheidsconflicten te voorkomen, is door databasevergrendelingen te gebruiken. Dit wordt pessimistische gelijktijdigheid genoemd. Voordat de app een databaserij leest die deze wil bijwerken, wordt een vergrendeling aangevraagd. Zodra een rij is vergrendeld voor updatetoegang, mogen geen andere gebruikers de rij vergrendelen totdat de eerste vergrendeling is vrijgegeven.
Het beheren van vergrendelingen heeft nadelen. Het kan complex zijn om te programmeren en kan prestatieproblemen veroorzaken naarmate het aantal gebruikers toeneemt. Entity Framework Core biedt geen ingebouwde ondersteuning voor pessimistische gelijktijdigheid.
Optimistische gelijktijdigheid
Optimistische gelijktijdigheid maakt gelijktijdigheidsconflicten mogelijk en reageert op de juiste manier wanneer ze voorkomen. Jane bezoekt bijvoorbeeld de pagina Afdeling bewerken en wijzigt het budget voor de Engelse afdeling van $ 350.000,00 tot $ 0,00.
Voordat Jane op Opslaan klikt, bezoekt John dezelfde pagina en wijzigt het veld Begindatum van 1-9-2007 in 1-9-2013.
Jane klikt eerst op Opslaan en ziet dat de wijziging van kracht wordt, omdat in de browser de pagina Index wordt weergegeven met nul als budgetbedrag.
John klikt op Opslaan op een pagina Bewerken waarop nog steeds een budget van $ 350.000,000 wordt weergegeven. Wat er vervolgens gebeurt, wordt bepaald door de manier waarop u gelijktijdigheidsconflicten afhandelt:
Houd bij welke eigenschap een gebruiker heeft gewijzigd en werk alleen de bijbehorende kolommen in de database bij.
In het scenario gaan er geen gegevens verloren. Er zijn verschillende eigenschappen bijgewerkt door de twee gebruikers. De volgende keer dat iemand door de Engelse afdeling bladert, zien ze zowel Jane's als John's wijzigingen. Deze methode voor het bijwerken kan het aantal conflicten verminderen dat kan leiden tot gegevensverlies. Deze aanpak heeft enkele nadelen:
- Kan geen gegevensverlies voorkomen als concurrerende wijzigingen worden aangebracht in dezelfde eigenschap.
- Is over het algemeen niet praktisch in een web-app. Het vereist het behouden van een aanzienlijke status om alle opgehaalde en nieuwe waarden bij te houden. Het onderhouden van grote hoeveelheden status kan van invloed zijn op de prestaties van apps.
- Kan de complexiteit van de app verhogen in vergelijking met gelijktijdigheidsdetectie op een entiteit.
Laat John's wijziging Jane's wijziging overschrijven.
De volgende keer dat iemand door de Engelse afdeling bladert, ziet hij 9-1-2013 en de opgehaalde waarde van $ 350.000,00. Deze benadering wordt een scenario met client-wins of Last in Wins genoemd. Alle waarden van de client hebben voorrang op wat zich in het gegevensarchief bevinden. De scaffolded code voert geen gelijktijdigheidsafhandeling uit. Client wins vindt automatisch plaats.
Voorkom dat de wijziging van John in de database wordt bijgewerkt. Normaal gesproken zou de app het volgende doen:
- Een foutbericht weergeven.
- De huidige status van de gegevens weergeven.
- Hiermee staat u de gebruiker toe om de wijzigingen opnieuw toe te passen.
Dit wordt een Store Wins-scenario genoemd. De waarden voor het gegevensarchief hebben voorrang op de waarden die door de client zijn ingediend. Het scenario Store Wins wordt in deze zelfstudie gebruikt. Deze methode zorgt ervoor dat er geen wijzigingen worden overschreven zonder dat een gebruiker wordt gewaarschuwd.
Conflictdetectie in EF Core
Eigenschappen die zijn geconfigureerd als gelijktijdigheidstokens worden gebruikt om optimistisch gelijktijdigheidsbeheer te implementeren. Wanneer een update- of verwijderbewerking wordt geactiveerd door SaveChanges of SaveChangesAsync, wordt de waarde van het gelijktijdigheidstoken in de database vergeleken met de oorspronkelijke waarde die wordt gelezen door EF Core:
- Als de waarden overeenkomen, kan de bewerking worden voltooid.
- Als de waarden niet overeenkomen, gaat EF Core ervan uit dat een andere gebruiker een conflicterende bewerking heeft uitgevoerd, wordt de huidige transactie afgebroken, en wordt er een DbUpdateConcurrencyException gegooid.
Een andere gebruiker of een proces dat een bewerking uitvoert die conflicteert met de huidige bewerking, wordt gelijktijdigheidsconflict genoemd.
Bij relationele databases EF Core wordt gecontroleerd op de waarde van het gelijktijdigheidstoken in de WHERE component van UPDATE en DELETE instructies om een gelijktijdigheidsconflict te detecteren.
Het gegevensmodel moet worden geconfigureerd om conflictdetectie mogelijk te maken door een volgkolom op te geven die kan worden gebruikt om te bepalen wanneer een rij is gewijzigd. EF biedt twee benaderingen voor gelijktijdigheidstokens:
Toepassen van
[ConcurrencyCheck]of IsConcurrencyToken op een eigenschap van het model. Deze methode wordt niet aanbevolen. Zie Gelijktijdigheidstokens in EF Corevoor meer informatie.Toepassen van TimestampAttribute of IsRowVersion op een concurrentietoken in het model. Dit is de methode die in deze handleiding wordt gebruikt.
De SQL Server-benadering en SQLite-implementatiedetails zijn iets anders. Later in de tutorial ziet u een verschilbestand dat de verschillen toont. Het tabblad Visual Studio toont de SQL Server-benadering. Op het tabblad Visual Studio Code ziet u de benadering voor niet-SQL Server-databases, zoals SQLite.
- Neem in het model een volgkolom op die wordt gebruikt om te bepalen wanneer een rij is gewijzigd.
- Pas de eigenschap TimestampAttribute gelijktijdigheid toe.
Werk het Models/Department.cs bestand bij met de volgende gemarkeerde 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; }
}
}
Dit TimestampAttribute identificeert de kolom als een kolom voor het volgen van gelijktijdigheid. De Fluent-API is een alternatieve manier om de traceringseigenschap op te geven:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Het [Timestamp] kenmerk van een entiteitseigenschap genereert de volgende code in de ModelBuilder methode:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
De voorgaande code:
- Hiermee stelt u het eigenschapstype
ConcurrencyTokenin op bytematrix.byte[]is het vereiste type voor SQL Server. - Roept IsConcurrencyToken aan.
IsConcurrencyTokenhiermee configureert u de eigenschap als een gelijktijdigheidstoken. Bij updates wordt de gelijktijdigheidstokenwaarde in de database vergeleken met de oorspronkelijke waarde om ervoor te zorgen dat deze niet is gewijzigd sinds het exemplaar is opgehaald uit de database. Als deze is gewijzigd, wordt er een DbUpdateConcurrencyException gegenereerd en worden er geen wijzigingen toegepast. - Aanroepen ValueGeneratedOnAddOrUpdate, waarmee de
ConcurrencyTokeneigenschap zodanig wordt geconfigureerd dat er automatisch een waarde wordt gegenereerd bij het toevoegen of bijwerken van een entiteit. -
HasColumnType("rowversion")stelt het kolomtype in de SQL Server-database in op rowversion.
De onderstaande code toont een gedeelte van de T-SQL die door EF Core wordt gegenereerd wanneer de Department-naam wordt geüpdatet:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
De voorgaande gemarkeerde code toont de WHERE clausule die ConcurrencyToken bevat. Als de database ConcurrencyToken niet gelijk is aan de ConcurrencyToken parameter @p2, worden er geen rijen bijgewerkt.
De volgende gemarkeerde code toont de T-SQL waarmee wordt gecontroleerd of precies één rij is bijgewerkt:
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 retourneert het aantal rijen die zijn beïnvloed door de laatste instructie. Als er geen rijen worden bijgewerkt, zal EF Core een DbUpdateConcurrencyException gooien.
Een migratie toevoegen
Als u de ConcurrencyToken eigenschap toevoegt, wordt het gegevensmodel gewijzigd. Hiervoor is een migratie vereist.
Bouw het project.
Voer de volgende opdrachten uit in de PMC:
Add-Migration RowVersion
Update-Database
De voorgaande opdrachten:
- Hiermee maakt u het
Migrations/{time stamp}_RowVersion.csmigratiebestand. - Werkt het
Migrations/SchoolContextModelSnapshot.csbestand bij. De update voegt de volgende code toe aan deBuildModelmethode:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Scaffoldafdeling-pagina's
Volg de instructies in Scaffold Student pages met de volgende uitzonderingen:
- Maak een map Pagina's/afdelingen .
- Gebruiken
Departmentvoor de modelklasse. - Gebruik de bestaande contextklasse in plaats van een nieuwe te maken.
Een hulpprogrammaklasse toevoegen
Maak in de projectmap de Utility klasse met de volgende code:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
De Utility klasse biedt de GetLastChars methode die wordt gebruikt om de laatste paar tekens van het gelijktijdigheidstoken weer te geven. De volgende code toont de code die werkt met zowel SQLite ad 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
De #if SQLiteVersionpreprocessor-instructie isoleert de verschillen in de SQLite- en SQL Server-versies en helpt het volgende:
- De auteur onderhoudt één codebasis voor beide versies.
- SQLite-ontwikkelaars implementeren de app in Azure en gebruiken SQL Azure.
Bouw het project.
De indexpagina bijwerken
Het hulpprogramma voor scaffolding heeft een ConcurrencyToken kolom gemaakt voor de Index-pagina, maar dat veld wordt niet weergegeven in een productie-app. In deze zelfstudie wordt het laatste gedeelte van ConcurrencyToken weergegeven om te laten zien hoe het afhandelen van gelijktijdigheid werkt. Het laatste gedeelte is niet gegarandeerd uniek.
Pagina's bijwerken\Afdelingen\Index.cshtml-pagina:
- Index vervangen door afdelingen.
- Wijzig de code met
ConcurrencyTokenzodat deze alleen de laatste tekens toont. - Vervang
FirstMidNamedoorFullName.
De volgende code toont de bijgewerkte pagina:
@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>
Het model van de bewerkpagina bijwerken
Werk Pages/Departments/Edit.cshtml.cs bij met de volgende 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.");
}
}
}
De gelijktijdigheidsupdates
OriginalValue wordt bijgewerkt met de ConcurrencyToken waarde van de entiteit wanneer deze is opgehaald in de OnGetAsync methode.
EF Core genereert een SQL UPDATE opdracht met een WHERE component die de oorspronkelijke ConcurrencyToken waarde bevat. Als er geen rijen worden beïnvloed door de UPDATE opdracht, wordt er een DbUpdateConcurrencyException uitzondering gegenereerd. Er worden geen rijen beïnvloed door de UPDATE opdracht wanneer geen rijen de oorspronkelijke ConcurrencyToken waarde hebben.
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;
In de eerder genoemde gemarkeerde code:
- De waarde in
Department.ConcurrencyTokenis de waarde waarin de entiteit is opgehaald in deGetaanvraag voor deEditpagina. De waarde wordt aan deOnPostmethode verstrekt door een verborgen veld op de Razor pagina waarop de entiteit wordt weergegeven die moet worden bewerkt. De waarde van het verborgen veld wordt door de modelbinder naarDepartment.ConcurrencyTokengekopieerd. -
OriginalValueis wat EF Core in deWHEREclausule gebruikt. Voordat de gemarkeerde coderegel wordt uitgevoerd:-
OriginalValueheeft de waarde die in de database stond toenFirstOrDefaultAsyncdeze methode aanriep. - Deze waarde kan afwijken van wat op de pagina Bewerken is weergegeven.
-
- De gemarkeerde code zorgt ervoor dat EF Core de oorspronkelijke
ConcurrencyToken-waarde van de weergegevenDepartment-entiteit gebruikt in deUPDATE-clausule van de SQL-instructieWHERE.
De volgende code toont het Department model.
Department wordt geïnitialiseerd in de
-
OnGetAsync-methode uitgevoerd door de EF-query. -
OnPostAsyncmethode door het verborgen veld op de Razor pagina door modelbinding:
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;
In de voorgaande code wordt de ConcurrencyToken waarde van de Department entiteit uit de HTTP POST aanvraag ingesteld op de ConcurrencyToken waarde van de HTTP GET aanvraag.
Wanneer er een gelijktijdigheidsfout optreedt, worden met de volgende gemarkeerde code de clientwaarden (de waarden die zijn gepost op deze methode) en de databasewaarden opgehaald.
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)}");
}
Met de volgende code wordt een aangepast foutbericht toegevoegd voor elke kolom met databasewaarden die afwijken van de waarden die zijn gepost in OnPostAsync:
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.");
}
Met de volgende gemarkeerde code wordt de ConcurrencyToken waarde ingesteld op de nieuwe waarde die uit de database is opgehaald. De volgende keer dat de gebruiker op Opslaan klikt, worden alleen gelijktijdigheidsfouten die zijn opgetreden sinds de laatste weergave van de Bewerken-pagina opgevangen.
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)}");
}
De ModelState.Remove instructie is vereist omdat ModelState de vorige ConcurrencyToken waarde heeft. Op de Razor pagina heeft de ModelState waarde voor een veld voorrang op de waarden van de modeleigenschap wanneer beide aanwezig zijn.
Verschillen tussen SQL Server en SQLite-code
Hieronder ziet u de verschillen tussen de SQL Server- en SQLite-versies:
+ 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;
De bewerkingspagina bijwerken Razor
Werk Pages/Departments/Edit.cshtml bij met de volgende 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");}
}
De voorgaande code:
- Hiermee wordt de
pagerichtlijn bijgewerkt van@pagenaar@page "{id:int}". - Hiermee voegt u een verborgen rijversie toe.
ConcurrencyTokenmoet worden toegevoegd, zodat postback de waarde bindt. - Geeft de laatste byte weer
ConcurrencyTokenvoor foutopsporingsdoeleinden. -
ViewDataVervangt door de sterk getypteInstructorNameSL.
Gelijktijdigheidsconflicten testen met de pagina Bewerken
Open twee browsers-exemplaren van Edit op de Engelse afdeling:
- Voer de app uit en selecteer Afdelingen.
- Klik met de rechtermuisknop op de hyperlink Bewerken voor de Engelse afdeling en selecteer Openen op nieuw tabblad.
- Klik op het eerste tabblad op de hyperlink Bewerken voor de Engelse afdeling.
De twee browsertabbladen geven dezelfde informatie weer.
Wijzig de naam op het eerste browsertabblad en klik op Opslaan.
De browser toont de pagina Index met de gewijzigde waarde en bijgewerkte ConcurrencyTokenindicator. Let op de bijgewerkte ConcurrencyTokenindicator, deze wordt weergegeven op de tweede postback op het andere tabblad.
Wijzig een ander veld op het tweede browsertabblad.
Klik op Opslaan. U ziet foutberichten voor alle velden die niet overeenkomen met de databasewaarden:
Dit browservenster wilde het veld Naam niet wijzigen. Kopieer en plak de huidige waarde (Talen) in het veld Naam. Gebruik de tabtoets om te verlaten. Validatie aan de klantzijde verwijdert het foutbericht.
Klik nogmaals op Opslaan . De waarde die u hebt ingevoerd op het tweede browsertabblad, wordt opgeslagen. U ziet de opgeslagen waarden op de pagina Index.
Het model van de verwijderingspagina bijwerken
Werk Pages/Departments/Delete.cshtml.cs bij met de volgende 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 });
}
}
}
}
De pagina Verwijderen detecteert gelijktijdigheidsconflicten wanneer de entiteit is gewijzigd nadat deze is opgehaald.
Department.ConcurrencyToken is de versie van de rij op het moment dat de entiteit werd opgehaald. Wanneer EF Core de SQL DELETE opdracht maakt, bevat deze een WHERE-clause met ConcurrencyToken. Als de SQL DELETE opdracht resulteert in nul beïnvloede rijen:
- De
ConcurrencyTokenin deSQL DELETEopdracht komt niet overeen metConcurrencyTokenin de database. - Er wordt een
DbUpdateConcurrencyExceptionuitzondering opgeworpen. -
OnGetAsyncwordt aangeroepen met deconcurrencyError.
De pagina Verwijderen bijwerken Razor
Werk Pages/Departments/Delete.cshtml bij met de volgende 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>
De voorgaande code brengt de volgende wijzigingen aan:
- Hiermee wordt de
pagerichtlijn bijgewerkt van@pagenaar@page "{id:int}". - Hiermee wordt een foutbericht toegevoegd.
- Vervangt FirstMidName door FullName in het veld Administrator .
- Wijzig
ConcurrencyTokenom de laatste byte weer te geven. - Hiermee voegt u een verborgen rijversie toe.
ConcurrencyTokenmoet worden toegevoegd, zodat postback de waarde bindt.
Gelijktijdigheidsconflicten testen
Maak een testafdeling.
Open twee browsersexemplaren van Delete op de testafdeling:
- Voer de app uit en selecteer Afdelingen.
- Klik met de rechtermuisknop op de hyperlink Verwijderen voor de testafdeling en selecteer Openen op nieuw tabblad.
- Klik op de hyperlink Bewerken voor de testafdeling.
De twee browsertabbladen geven dezelfde informatie weer.
Wijzig het budget op het eerste browsertabblad en klik op Opslaan.
De browser toont de pagina Index met de gewijzigde waarde en bijgewerkte ConcurrencyTokenindicator. Let op de bijgewerkte ConcurrencyTokenindicator, deze wordt weergegeven op de tweede postback op het andere tabblad.
Verwijder de testafdeling van het tweede tabblad. Er wordt een gelijktijdigheidsfout weergegeven met de huidige waarden uit de database. Als u op Verwijderen klikt, wordt de entiteit verwijderd, tenzij ConcurrencyToken deze is bijgewerkt.
Patronen voor bedrijfsweb-apps
Zie Enterprise-web-apppatronen voor hulp bij het maken van een betrouwbare, veilige, performante, testbare en schaalbare ASP.NET Core-app. Er is een volledige voorbeeldweb-app van productiekwaliteit beschikbaar waarmee de patronen worden geïmplementeerd.
Aanvullende bronnen
- Gelijktijdigheidstokens in EF Core
- Gelijktijdigheid verwerken in EF Core
- Het debuggen van ASP.NET Core 2.x-broncodes
Volgende stappen
Dit is de laatste tutorial in de reeks. Aanvullende onderwerpen worden behandeld in de MVC-versie van deze tutorialreeks.
Deze zelfstudie laat zien hoe u conflicten kunt afhandelen wanneer meerdere gebruikers een entiteit gelijktijdig bijwerken (tegelijkertijd).
Gelijktijdigheidsconflicten
Er treedt een gelijktijdigheidsconflict op wanneer:
- Een gebruiker navigeert naar de bewerkingspagina voor een entiteit.
- Een andere gebruiker werkt dezelfde entiteit bij voordat de wijziging van de eerste gebruiker naar de database wordt geschreven.
Als gelijktijdigheidsdetectie niet is ingeschakeld, overschrijft degene die de database voor het laatst bijwerken de wijzigingen van de andere gebruiker. Als dit risico aanvaardbaar is, kunnen de kosten van programmering voor gelijktijdigheid opwegen tegen het voordeel.
Pessimistische gelijktijdigheid (vergrendelen)
Een manier om gelijktijdigheidsconflicten te voorkomen, is door databasevergrendelingen te gebruiken. Dit wordt pessimistische gelijktijdigheid genoemd. Voordat de app een databaserij leest die deze wil bijwerken, wordt een vergrendeling aangevraagd. Zodra een rij is vergrendeld voor updatetoegang, mogen geen andere gebruikers de rij vergrendelen totdat de eerste vergrendeling is vrijgegeven.
Het beheren van vergrendelingen heeft nadelen. Het kan complex zijn om te programmeren en kan prestatieproblemen veroorzaken naarmate het aantal gebruikers toeneemt. Entity Framework Core biedt hiervoor geen ingebouwde ondersteuning en deze zelfstudie laat niet zien hoe u deze implementeert.
Optimistische gelijktijdigheid
Optimistische gelijktijdigheid maakt gelijktijdigheidsconflicten mogelijk en reageert op de juiste manier wanneer ze voorkomen. Jane bezoekt bijvoorbeeld de pagina Afdeling bewerken en wijzigt het budget voor de Engelse afdeling van $ 350.000,00 tot $ 0,00.
Voordat Jane op Opslaan klikt, bezoekt John dezelfde pagina en wijzigt het veld Begindatum van 1-9-2007 in 1-9-2013.
Jane klikt eerst op Opslaan en ziet dat de wijziging van kracht wordt, omdat in de browser de pagina Index wordt weergegeven met nul als budgetbedrag.
John klikt op Opslaan op een pagina Bewerken waarop nog steeds een budget van $ 350.000,000 wordt weergegeven. Wat er vervolgens gebeurt, wordt bepaald door de manier waarop u gelijktijdigheidsconflicten afhandelt:
U kunt bijhouden welke eigenschap een gebruiker heeft gewijzigd en alleen de bijbehorende kolommen in de database bijwerken.
In het scenario gaan er geen gegevens verloren. Er zijn verschillende eigenschappen bijgewerkt door de twee gebruikers. De volgende keer dat iemand door de Engelse afdeling bladert, zien ze zowel Jane's als John's wijzigingen. Deze methode voor het bijwerken kan het aantal conflicten verminderen dat kan leiden tot gegevensverlies. Deze aanpak heeft enkele nadelen:
- Kan geen gegevensverlies voorkomen als concurrerende wijzigingen worden aangebracht in dezelfde eigenschap.
- Is over het algemeen niet praktisch in een web-app. Het vereist het behouden van een aanzienlijke status om alle opgehaalde en nieuwe waarden bij te houden. Het onderhouden van grote hoeveelheden status kan van invloed zijn op de prestaties van apps.
- Kan de complexiteit van de app verhogen in vergelijking met gelijktijdigheidsdetectie op een entiteit.
U kunt de wijziging van John overschrijven met die van Jane.
De volgende keer dat iemand door de Engelse afdeling bladert, ziet hij 9-1-2013 en de opgehaalde waarde van $ 350.000,00. Deze benadering wordt een scenario met client-wins of Last in Wins genoemd. (Alle waarden van de client hebben voorrang op wat zich in het gegevensarchief bevinden.) Als u geen codering uitvoert voor gelijktijdigheidsafhandeling, worden client wins automatisch uitgevoerd.
U kunt voorkomen dat De wijziging van John wordt bijgewerkt in de database. Normaal gesproken zou de app het volgende doen:
- Een foutbericht weergeven.
- De huidige status van de gegevens weergeven.
- Hiermee staat u de gebruiker toe om de wijzigingen opnieuw toe te passen.
Dit wordt een Store Wins-scenario genoemd. (De waarden van het gegevensarchief hebben voorrang op de waarden die door de client zijn verzonden.) In deze zelfstudie implementeert u het scenario Store Wins. Deze methode zorgt ervoor dat er geen wijzigingen worden overschreven zonder dat een gebruiker wordt gewaarschuwd.
Conflictdetectie in EF Core
EF Core
DbConcurrencyException genereert uitzonderingen wanneer conflicten worden gedetecteerd. Het gegevensmodel moet worden geconfigureerd om conflictdetectie mogelijk te maken. Opties voor het inschakelen van conflictdetectie zijn onder andere:
Configureer EF Core om de oorspronkelijke waarden op te nemen van kolommen die zijn geconfigureerd als gelijktijdigheidstokens in de WHERE-clausule van bijwerk- en verwijderopdrachten.
Wanneer
SaveChangeswordt aangeroepen, zoekt de Where-component naar de oorspronkelijke waarden van eigenschappen die zijn geannoteerd met het ConcurrencyCheckAttribute kenmerk. De update-instructie vindt geen rij die moet worden bijgewerkt als een van de eigenschappen van het gelijktijdigheidstoken is gewijzigd sinds de rij voor het eerst is gelezen. EF Core interpreteert dat als een gelijktijdigheidsconflict. Voor databasetabellen met veel kolommen kan deze benadering resulteren in zeer grote Where-componenten en kunnen grote hoeveelheden statussen vereisen. Daarom wordt deze benadering over het algemeen niet aanbevolen en is dit niet de methode die in deze zelfstudie wordt gebruikt.Neem in de databasetabel een volgkolom op die kan worden gebruikt om te bepalen wanneer een rij is gewijzigd.
In een SQL Server-database is
rowversionhet gegevenstype van de volgkolom. Derowversionwaarde is een opeenvolgend getal dat steeds wordt verhoogd wanneer de rij wordt bijgewerkt. In een opdracht Bijwerken of Verwijderen bevat de Where-component de oorspronkelijke waarde van de volgkolom (het oorspronkelijke versienummer van de rij). Als de rij die wordt bijgewerkt door een andere gebruiker is gewijzigd, is de waarde in derowversionkolom anders dan de oorspronkelijke waarde. In dat geval kan de instructie Update of Delete de rij die moet worden bijgewerkt niet vinden vanwege de Where-component. EF Core genereert een gelijktijdigheidsuitzondering wanneer er geen rijen worden beïnvloed door een opdracht Bijwerken of Verwijderen.
Een traceringseigenschap toevoegen
Voeg in Models/Department.cs, een traceringseigenschap met de naam RowVersion toe:
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; }
}
}
Het TimestampAttribute kenmerk identificeert de kolom als een kolom voor gelijktijdigheidstracering. De Fluent-API is een alternatieve manier om de traceringseigenschap op te geven:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Voor een SQL Server-database is het [Timestamp] kenmerk van een entiteitseigenschap gedefinieerd als bytematrix:
- Zorgt ervoor dat de kolom wordt opgenomen in DELETE- en UPDATE-WHERE-clausules.
- Hiermee stelt u het kolomtype in de database in op rowversion.
De database genereert een opeenvolgend rijversienummer dat steeds wordt verhoogd wanneer de rij wordt bijgewerkt. In een Update of Delete commando bevat de Where clausule de opgehaalde rijversiewaarde. Als de rij die wordt bijgewerkt, is gewijzigd sinds deze is opgehaald:
- De huidige versie van de rij komt niet overeen met de opgehaalde waarde.
- De
UpdateofDeleteopdrachten vinden geen rij omdat deWhereclausule zoekt naar de opgehaalde rijversiewaarde. - Er is een
DbUpdateConcurrencyExceptionopgeworpen.
De volgende code toont een deel van de T-SQL die door EF Core wordt gegenereerd wanneer de naam van de afdeling wordt bijgewerkt.
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
De voorgaande gemarkeerde code toont de WHERE clausule die RowVersion bevat. Als de database RowVersion niet gelijk is aan de RowVersion parameter (@p2), worden er geen rijen bijgewerkt.
De volgende gemarkeerde code toont de T-SQL waarmee wordt gecontroleerd of precies één rij is bijgewerkt:
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 retourneert het aantal rijen die zijn beïnvloed door de laatste instructie. Als er geen rijen worden bijgewerkt, zal EF Core een DbUpdateConcurrencyException gooien.
De database bijwerken
Als u de RowVersion eigenschap toevoegt, wordt het gegevensmodel gewijzigd. Hiervoor is een migratie vereist.
Bouw het project.
Voer de volgende opdracht uit in de PMC:
Add-Migration RowVersion
Deze opdracht:
Hiermee maakt u het
Migrations/{time stamp}_RowVersion.csmigratiebestand.Werkt het
Migrations/SchoolContextModelSnapshot.csbestand bij. De update voegt de volgende gemarkeerde code toe aan deBuildModelmethode: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"); });
Voer de volgende opdracht uit in de PMC:
Update-Database
Scaffoldafdeling-pagina's
Volg de instructies in Scaffold Student pages met de volgende uitzonderingen:
Maak een map Pagina's/afdelingen .
Gebruiken
Departmentvoor de modelklasse.- Gebruik de bestaande contextklasse in plaats van een nieuwe te maken.
Bouw het project.
De indexpagina bijwerken
Het hulpprogramma voor scaffolding heeft een RowVersion kolom gemaakt voor de Index-pagina, maar dat veld wordt niet weergegeven in een productie-app. In deze zelfstudie wordt de laatste byte van RowVersion weergegeven om te laten zien hoe gelijktijdige verwerking werkt. De laatste byte is niet gegarandeerd uniek.
Pagina's bijwerken\Afdelingen\Index.cshtml-pagina:
- Index vervangen door afdelingen.
- Wijzig de code zodat deze alleen de laatste byte van de bytematrix weergeeft.
- Vervang FirstMidName door FullName.
De volgende code toont de bijgewerkte pagina:
@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>
Het model van de bewerkpagina bijwerken
Werk Pages/Departments/Edit.cshtml.cs bij met de volgende 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.");
}
}
}
De OriginalValue waarde wordt bijgewerkt met de rowVersion waarde van de entiteit toen deze in de OnGetAsync methode werd opgehaald.
EF Core genereert een SQL UPDATE-opdracht met een WHERE-component die de oorspronkelijke RowVersion waarde bevat. Als er geen rijen worden beïnvloed door de opdracht UPDATE (er zijn geen rijen met de oorspronkelijke RowVersion waarde), wordt er een DbUpdateConcurrencyException uitzondering gegenereerd.
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;
In de eerder genoemde gemarkeerde code:
- De waarde in
Department.RowVersionis wat zich in de entiteit bevond toen deze oorspronkelijk werd opgehaald in de Get-aanvraag voor de Bewerken-pagina. De waarde wordt aan deOnPostmethode verstrekt door een verborgen veld op de Razor pagina waarop de entiteit wordt weergegeven die moet worden bewerkt. De waarde van het verborgen veld wordt door de modelbinder naarDepartment.RowVersiongekopieerd. -
OriginalValueis wat EF Core zal gebruiken in de WHERE-clause. Voordat de gemarkeerde coderegel wordt uitgevoerd, heeftOriginalValuede waarde die in de database stond toen de methodeFirstOrDefaultAsyncwerd aangeroepen. Dit kan verschillen van wat op de bewerkpagina werd weergegeven. - De gemarkeerde code zorgt ervoor dat EF Core de oorspronkelijke
RowVersion-waarde van de weergegevenDepartment-entiteit gebruikt in de Where-clausule van de SQL UPDATE-instructie.
Wanneer er een gelijktijdigheidsfout optreedt, worden met de volgende gemarkeerde code de clientwaarden (de waarden die zijn gepost op deze methode) en de databasewaarden opgehaald.
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");
}
Met de volgende code wordt een aangepast foutbericht toegevoegd voor elke kolom met databasewaarden die afwijken van de waarden die zijn gepost in OnPostAsync:
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.");
}
Met de volgende gemarkeerde code wordt de RowVersion waarde ingesteld op de nieuwe waarde die uit de database is opgehaald. De volgende keer dat de gebruiker op Opslaan klikt, worden alleen gelijktijdigheidsfouten die zijn opgetreden sinds de laatste weergave van de Bewerken-pagina opgevangen.
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");
}
De ModelState.Remove instructie is vereist omdat ModelState de oude RowVersion waarde heeft. Op de Razor pagina heeft de ModelState waarde voor een veld voorrang op de waarden van de modeleigenschap wanneer beide aanwezig zijn.
De pagina Bewerken bijwerken
Werk Pages/Departments/Edit.cshtml bij met de volgende 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");}
}
De voorgaande code:
- Hiermee wordt de
pagerichtlijn bijgewerkt van@pagenaar@page "{id:int}". - Hiermee voegt u een verborgen rijversie toe.
RowVersionmoet worden toegevoegd, zodat postback de waarde bindt. - Geeft de laatste byte weer
RowVersionvoor foutopsporingsdoeleinden. -
ViewDataVervangt door de sterk getypteInstructorNameSL.
Gelijktijdigheidsconflicten testen met de pagina Bewerken
Open twee browsers-exemplaren van Edit op de Engelse afdeling:
- Voer de app uit en selecteer Afdelingen.
- Klik met de rechtermuisknop op de hyperlink Bewerken voor de Engelse afdeling en selecteer Openen op nieuw tabblad.
- Klik op het eerste tabblad op de hyperlink Bewerken voor de Engelse afdeling.
De twee browsertabbladen geven dezelfde informatie weer.
Wijzig de naam op het eerste browsertabblad en klik op Opslaan.
In de browser wordt de pagina Index weergegeven met de gewijzigde waarde en de bijgewerkte rowVersion-indicator. Let op de bijgewerkte rowVersion-indicator. Deze wordt weergegeven bij de tweede postback in het andere tabblad.
Wijzig een ander veld op het tweede browsertabblad.
Klik op Opslaan. U ziet foutberichten voor alle velden die niet overeenkomen met de databasewaarden:
Dit browservenster wilde het veld Naam niet wijzigen. Kopieer en plak de huidige waarde (Talen) in het veld Naam. Gebruik de tabtoets om te verlaten. Validatie aan de klantzijde verwijdert het foutbericht.
Klik nogmaals op Opslaan . De waarde die u hebt ingevoerd op het tweede browsertabblad, wordt opgeslagen. U ziet de opgeslagen waarden op de pagina Index.
Het model van de verwijderingspagina bijwerken
Werk Pages/Departments/Delete.cshtml.cs bij met de volgende 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 });
}
}
}
}
De pagina Verwijderen detecteert gelijktijdigheidsconflicten wanneer de entiteit is gewijzigd nadat deze is opgehaald.
Department.RowVersion is de versie van de rij op het moment dat de entiteit werd opgehaald. Wanneer EF Core de SQL DELETE-opdracht maakt, bevat deze een WHERE-clausule met RowVersion. Als de SQL DELETE-opdracht resulteert in nul rijen:
- Het
RowVersionin het SQL DELETE-commando komt niet overeen metRowVersionin de database. - Er wordt een DbUpdateConcurrencyException-uitzondering gegenereerd.
-
OnGetAsyncwordt aangeroepen met deconcurrencyError.
De pagina Verwijderen bijwerken
Werk Pages/Departments/Delete.cshtml bij met de volgende 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>
De voorgaande code brengt de volgende wijzigingen aan:
- Hiermee wordt de
pagerichtlijn bijgewerkt van@pagenaar@page "{id:int}". - Hiermee wordt een foutbericht toegevoegd.
- Vervangt FirstMidName door FullName in het veld Administrator .
- Wijzig
RowVersionom de laatste byte weer te geven. - Hiermee voegt u een verborgen rijversie toe.
RowVersionmoet worden toegevoegd, zodat postback de waarde bindt.
Gelijktijdigheidsconflicten testen
Maak een testafdeling.
Open twee browsersexemplaren van Delete op de testafdeling:
- Voer de app uit en selecteer Afdelingen.
- Klik met de rechtermuisknop op de hyperlink Verwijderen voor de testafdeling en selecteer Openen op nieuw tabblad.
- Klik op de hyperlink Bewerken voor de testafdeling.
De twee browsertabbladen geven dezelfde informatie weer.
Wijzig het budget op het eerste browsertabblad en klik op Opslaan.
In de browser wordt de pagina Index weergegeven met de gewijzigde waarde en de bijgewerkte rowVersion-indicator. Let op de bijgewerkte rowVersion-indicator. Deze wordt weergegeven bij de tweede postback in het andere tabblad.
Verwijder de testafdeling van het tweede tabblad. Er wordt een gelijktijdigheidsfout weergegeven met de huidige waarden uit de database. Als u op Verwijderen klikt, wordt de entiteit verwijderd, tenzij RowVersion deze is bijgewerkt.
Zie Enterprise-web-apppatronen voor hulp bij het maken van een betrouwbare, veilige, performante, testbare en schaalbare ASP.NET Core-app. Er is een volledige voorbeeldweb-app van productiekwaliteit beschikbaar waarmee de patronen worden geïmplementeerd.
Aanvullende bronnen
- Gelijktijdigheidstokens in EF Core
- Gelijktijdigheid verwerken in EF Core
- Het debuggen van ASP.NET Core 2.x-broncodes
Volgende stappen
Dit is de laatste tutorial in de reeks. Aanvullende onderwerpen worden behandeld in de MVC-versie van deze tutorialreeks.
Deze zelfstudie laat zien hoe u conflicten kunt afhandelen wanneer meerdere gebruikers een entiteit gelijktijdig bijwerken (tegelijkertijd). Als u problemen tegenkomt die u niet kunt oplossen, kunt u de voltooide app downloaden of weergeven.Downloadinstructies.
Gelijktijdigheidsconflicten
Er treedt een gelijktijdigheidsconflict op wanneer:
- Een gebruiker navigeert naar de bewerkingspagina voor een entiteit.
- Een andere gebruiker werkt dezelfde entiteit bij voordat de wijziging van de eerste gebruiker naar de database wordt geschreven.
Als gelijktijdigheidsdetectie niet is ingeschakeld, gebeurt het volgende wanneer gelijktijdige updates plaatsvinden:
- De laatste update wint. Dat wil gezegd, worden de laatste updatewaarden opgeslagen in de database.
- De eerste van de huidige updates gaan verloren.
Optimistische gelijktijdigheid
Optimistische gelijktijdigheid maakt gelijktijdigheidsconflicten mogelijk en reageert op de juiste manier wanneer ze voorkomen. Jane bezoekt bijvoorbeeld de pagina Afdeling bewerken en wijzigt het budget voor de Engelse afdeling van $ 350.000,00 tot $ 0,00.
Voordat Jane op Opslaan klikt, bezoekt John dezelfde pagina en wijzigt het veld Begindatum van 1-9-2007 in 1-9-2013.
Jane klikt eerst op Opslaan en ziet haar wijziging wanneer de browser de indexpagina weergeeft.
John klikt op Opslaan op een pagina Bewerken waarop nog steeds een budget van $ 350.000,000 wordt weergegeven. Wat er vervolgens gebeurt, wordt bepaald door hoe u gelijktijdigheidsconflicten afhandelt.
Optimistische gelijktijdigheid omvat de volgende opties:
U kunt bijhouden welke eigenschap een gebruiker heeft gewijzigd en alleen de bijbehorende kolommen in de database bijwerken.
In het scenario gaan er geen gegevens verloren. Er zijn verschillende eigenschappen bijgewerkt door de twee gebruikers. De volgende keer dat iemand door de Engelse afdeling bladert, zien ze zowel Jane's als John's wijzigingen. Deze methode voor het bijwerken kan het aantal conflicten verminderen dat kan leiden tot gegevensverlies. Deze aanpak:
- Kan geen gegevensverlies voorkomen als concurrerende wijzigingen worden aangebracht in dezelfde eigenschap.
- Is over het algemeen niet praktisch in een web-app. Het vereist het behouden van een aanzienlijke status om alle opgehaalde en nieuwe waarden bij te houden. Het onderhouden van grote hoeveelheden status kan van invloed zijn op de prestaties van apps.
- Kan de complexiteit van de app verhogen in vergelijking met gelijktijdigheidsdetectie op een entiteit.
U kunt de wijziging van John overschrijven met die van Jane.
De volgende keer dat iemand door de Engelse afdeling bladert, ziet hij 9-1-2013 en de opgehaalde waarde van $ 350.000,00. Deze benadering wordt een scenario met client-wins of Last in Wins genoemd. (Alle waarden van de client hebben voorrang op wat zich in het gegevensarchief bevinden.) Als u geen codering uitvoert voor gelijktijdigheidsafhandeling, worden client wins automatisch uitgevoerd.
U kunt voorkomen dat De wijziging van John wordt bijgewerkt in de database. Normaal gesproken zou de app het volgende doen:
- Een foutbericht weergeven.
- De huidige status van de gegevens weergeven.
- Hiermee staat u de gebruiker toe om de wijzigingen opnieuw toe te passen.
Dit wordt een Store Wins-scenario genoemd. (De waarden van het gegevensarchief hebben voorrang op de waarden die door de client zijn verzonden.) In deze zelfstudie implementeert u het scenario Store Wins. Deze methode zorgt ervoor dat er geen wijzigingen worden overschreven zonder dat een gebruiker wordt gewaarschuwd.
Gelijktijdigheid verwerken
Wanneer een eigenschap is geconfigureerd als een gelijktijdigheidstoken:
- EF Core Controleert of de eigenschap niet is gewijzigd nadat deze is opgehaald. De controle treedt op wanneer SaveChanges of SaveChangesAsync wordt aangeroepen.
- Als de eigenschap is gewijzigd nadat deze is opgehaald, wordt er een DbUpdateConcurrencyException gegenereerd.
De database en het gegevensmodel moeten worden geconfigureerd om DbUpdateConcurrencyException te ondersteunen.
Gelijktijdigheidsconflicten op een eigenschap detecteren
Gelijktijdigheidsconflicten kunnen worden gedetecteerd op eigenschapsniveau met het kenmerk ConcurrencyCheck . Het kenmerk kan worden toegepast op meerdere eigenschappen op het model. Zie Data Annotations-ConcurrencyCheck voor meer informatie.
Het [ConcurrencyCheck] kenmerk wordt niet gebruikt in deze zelfstudie.
Gelijktijdigheidsconflicten in een rij detecteren
Om gelijktijdigheidsconflicten te detecteren, wordt er een kolom voor het bijhouden van rijversies toegevoegd aan het model.
rowversion :
- Is specifiek voor SQL Server. Andere databases bieden mogelijk geen vergelijkbare functie.
- Wordt gebruikt om te bepalen dat een entiteit niet is gewijzigd sinds deze is opgehaald uit de database.
De database genereert een opeenvolgend rowversion getal dat steeds wordt verhoogd wanneer de rij wordt bijgewerkt. In een Update of Delete opdracht bevat de Where component de opgehaalde waarde van rowversion. Als de rij die wordt bijgewerkt, is gewijzigd:
-
rowversionkomt niet overeen met de opgehaalde waarde. - De
UpdateofDeleteopdrachten vinden geen rij omdat deWhereclausule het opgehaalderowversiononderdeel bevat. - Er is een
DbUpdateConcurrencyExceptionopgeworpen.
In EF Core wanneer er geen rijen zijn bijgewerkt door een Update of Delete opdracht, wordt een concurrency-exceptie gegenereerd.
Een traceringseigenschap toevoegen aan de entiteit Afdeling
Voeg in Models/Department.cs, een traceringseigenschap met de naam RowVersion toe:
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; }
}
}
Het kenmerk Timestamp geeft aan dat deze kolom is opgenomen in de Where component van Update en Delete opdrachten. Het kenmerk wordt aangeroepen Timestamp omdat eerdere versies van SQL Server een SQL-gegevenstype timestamp hebben gebruikt voordat het SQL-type rowversion dit heeft vervangen.
De fluent-API kan ook de traceringseigenschap opgeven:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
De volgende code toont een deel van de T-SQL die door EF Core wordt gegenereerd wanneer de naam van de afdeling wordt bijgewerkt.
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
De voorgaande gemarkeerde code toont de WHERE clausule die RowVersion bevat. Als de database RowVersion niet gelijk is aan de RowVersion parameter (@p2), worden er geen rijen bijgewerkt.
De volgende gemarkeerde code toont de T-SQL waarmee wordt gecontroleerd of precies één rij is bijgewerkt:
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 retourneert het aantal rijen die zijn beïnvloed door de laatste instructie. Als er geen rijen worden bijgewerkt, EF Core wordt een DbUpdateConcurrencyException gegooid.
U ziet dat de T-SQL EF Core wordt gegenereerd in het uitvoervenster van Visual Studio.
De database bijwerken
Als u de RowVersion eigenschap toevoegt, wordt het DB-model gewijzigd. Hiervoor is een migratie vereist.
Bouw het project. Voer het volgende in een opdrachtvenster in:
dotnet ef migrations add RowVersion
dotnet ef database update
De voorgaande opdrachten:
Hiermee voegt u het
Migrations/{time stamp}_RowVersion.csmigratiebestand toe.Werkt het
Migrations/SchoolContextModelSnapshot.csbestand bij. De update voegt de volgende gemarkeerde code toe aan deBuildModelmethode:Voert migraties uit om de database bij te werken.
Het model Afdelingen opzetten
Volg de instructies in Scaffold het studentmodel en gebruik Department voor de model-klasse.
De voorgaande opdracht genereert de basisstructuur voor het Department model. Open het project in Visual Studio.
Bouw het project.
De pagina Afdelingenindex bijwerken
De scaffolding-engine heeft een RowVersion kolom voor de indexpagina gemaakt, maar dat veld mag niet worden weergegeven. In deze zelfstudie wordt de laatste byte van RowVersion weergegeven om gelijktijdigheid beter te begrijpen. De laatste byte is niet gegarandeerd uniek. Een echte app zou RowVersion of de laatste byte van RowVersion niet weergeven.
Werk de indexpagina bij:
- Index vervangen door afdelingen.
- Vervang de markup die
RowVersionbevat door de laatste byte vanRowVersion. - Vervang FirstMidName door FullName.
In de volgende markeringen ziet u de bijgewerkte pagina:
@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>
Het model van de bewerkpagina bijwerken
Werk Pages/Departments/Edit.cshtml.cs bij met de volgende 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.");
}
}
}
Als u een gelijktijdigheidsprobleem wilt detecteren, wordt de OriginalValue waarde bijgewerkt met de waarde van de rowVersion entiteit die is opgehaald.
EF Core genereert een SQL UPDATE-opdracht met een WHERE-component die de oorspronkelijke RowVersion waarde bevat. Als er geen rijen worden beïnvloed door de opdracht UPDATE (er zijn geen rijen met de oorspronkelijke RowVersion waarde), wordt er een DbUpdateConcurrencyException uitzondering gegenereerd.
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;
In de voorgaande code Department.RowVersion is dit de waarde wanneer de entiteit is opgehaald.
OriginalValue is de waarde in de database toen FirstOrDefaultAsync in deze methode werd aangeroepen.
Met de volgende code worden de clientwaarden (de waarden die zijn gepost op deze methode) en de DB-waarden opgehaald:
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");
}
Met de volgende code wordt een aangepast foutbericht toegevoegd voor elke kolom met DB-waarden die afwijken van de waarden die zijn gepost in OnPostAsync:
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.");
}
Met de volgende gemarkeerde code wordt de RowVersion waarde ingesteld op de nieuwe waarde die is opgehaald uit de database. De volgende keer dat de gebruiker op Opslaan klikt, worden alleen gelijktijdigheidsfouten die zijn opgetreden sinds de laatste weergave van de Bewerken-pagina opgevangen.
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");
}
De ModelState.Remove instructie is vereist omdat ModelState de oude RowVersion waarde heeft. Op de Razor pagina heeft de ModelState waarde voor een veld voorrang op de waarden van de modeleigenschap wanneer beide aanwezig zijn.
De pagina Bewerken bijwerken
Werk Pages/Departments/Edit.cshtml bij met de volgende markeringen:
@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");}
}
De voorgaande markeringen:
- Hiermee wordt de
pagerichtlijn bijgewerkt van@pagenaar@page "{id:int}". - Hiermee voegt u een verborgen rijversie toe.
RowVersionmoet worden toegevoegd, zodat postback de waarde bindt. - Geeft de laatste byte weer
RowVersionvoor foutopsporingsdoeleinden. -
ViewDataVervangt door de sterk getypteInstructorNameSL.
Gelijktijdigheidsconflicten testen met de pagina Bewerken
Open twee browsers-exemplaren van Edit op de Engelse afdeling:
- Voer de app uit en selecteer Afdelingen.
- Klik met de rechtermuisknop op de hyperlink Bewerken voor de Engelse afdeling en selecteer Openen op nieuw tabblad.
- Klik op het eerste tabblad op de hyperlink Bewerken voor de Engelse afdeling.
De twee browsertabbladen geven dezelfde informatie weer.
Wijzig de naam op het eerste browsertabblad en klik op Opslaan.
In de browser wordt de pagina Index weergegeven met de gewijzigde waarde en de bijgewerkte rowVersion-indicator. Let op de bijgewerkte rowVersion-indicator. Deze wordt weergegeven bij de tweede postback in het andere tabblad.
Wijzig een ander veld op het tweede browsertabblad.
Klik op Opslaan. U ziet foutberichten voor alle velden die niet overeenkomen met de DB-waarden:
Dit browservenster wilde het veld Naam niet wijzigen. Kopieer en plak de huidige waarde (Talen) in het veld Naam. Gebruik de tabtoets om te verlaten. Validatie aan de klantzijde verwijdert het foutbericht.
Klik nogmaals op Opslaan . De waarde die u hebt ingevoerd op het tweede browsertabblad, wordt opgeslagen. U ziet de opgeslagen waarden op de pagina Index.
De pagina Verwijderen bijwerken
Werk het paginamodel Verwijderen bij met de volgende 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 });
}
}
}
}
De pagina Verwijderen detecteert gelijktijdigheidsconflicten wanneer de entiteit is gewijzigd nadat deze is opgehaald.
Department.RowVersion is de versie van de rij op het moment dat de entiteit werd opgehaald. Wanneer EF Core de SQL DELETE-opdracht maakt, bevat deze een WHERE-clausule met RowVersion. Als de SQL DELETE-opdracht resulteert in nul rijen:
- De
RowVersionopdracht SQL DELETE komt niet overeen metRowVersionin de database. - Er wordt een DbUpdateConcurrencyException-uitzondering gegenereerd.
-
OnGetAsyncwordt aangeroepen met deconcurrencyError.
De pagina Verwijderen bijwerken
Werk Pages/Departments/Delete.cshtml bij met de volgende 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>
De voorgaande code brengt de volgende wijzigingen aan:
- Hiermee wordt de
pagerichtlijn bijgewerkt van@pagenaar@page "{id:int}". - Hiermee wordt een foutbericht toegevoegd.
- Vervangt FirstMidName door FullName in het veld Administrator .
- Wijzig
RowVersionom de laatste byte weer te geven. - Hiermee voegt u een verborgen rijversie toe.
RowVersionmoet worden toegevoegd, zodat postback de waarde bindt.
Gelijktijdigheidsconflicten testen met de pagina Verwijderen
Maak een testafdeling.
Open twee browsersexemplaren van Delete op de testafdeling:
- Voer de app uit en selecteer Afdelingen.
- Klik met de rechtermuisknop op de hyperlink Verwijderen voor de testafdeling en selecteer Openen op nieuw tabblad.
- Klik op de hyperlink Bewerken voor de testafdeling.
De twee browsertabbladen geven dezelfde informatie weer.
Wijzig het budget op het eerste browsertabblad en klik op Opslaan.
In de browser wordt de pagina Index weergegeven met de gewijzigde waarde en de bijgewerkte rowVersion-indicator. Let op de bijgewerkte rowVersion-indicator. Deze wordt weergegeven bij de tweede postback in het andere tabblad.
Verwijder de testafdeling van het tweede tabblad. Er wordt een gelijktijdigheidsfout weergegeven met de huidige waarden uit de database. Als u op Verwijderen klikt, wordt de entiteit verwijderd, tenzij RowVersion deze is bijgewerkt.
Zie Overname over het overnemen van een gegevensmodel.
Patronen voor bedrijfsweb-apps
Zie Enterprise-web-apppatronen voor hulp bij het maken van een betrouwbare, veilige, performante, testbare en schaalbare ASP.NET Core-app. Er is een volledige voorbeeldweb-app van productiekwaliteit beschikbaar waarmee de patronen worden geïmplementeerd.