Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Contoso University-webbappen visar hur du skapar webbappar för Razor Pages med hjälp av EF Core och Visual Studio. Information om självstudieserien finns i den första självstudien.
Om du stöter på problem som du inte kan lösa, ladda ner den färdiga appen och jämför den koden med den du skapade när du följde handledningen.
Den här självstudien visar hur du hanterar konflikter när flera användare uppdaterar en entitet samtidigt.
Samtidighetskonflikter
En samtidighetskonflikt uppstår när:
- En användare navigerar till redigeringssidan för en entitet.
- En annan användare uppdaterar samma entitet innan den första användarens ändring skrivs till databasen.
Om samtidighetsidentifiering inte är aktiverat skriver den som uppdaterar databasen senast över den andra användarens ändringar. Om den här risken är acceptabel kan kostnaden för programmering för samtidighet uppväga fördelen.
Pessimistisk samtidighet
Ett sätt att förhindra samtidighetskonflikter är att använda databaslås. Detta kallas pessimistisk samtidighet. Innan appen läser en databasrad som den tänker uppdatera begär den ett lås. När en rad har låsts för uppdateringsåtkomst får inga andra användare låsa raden förrän det första låset har släppts.
Att hantera lås har nackdelar. Det kan vara komplext att programmera och kan orsaka prestandaproblem när antalet användare ökar. Entity Framework Core ger inget inbyggt stöd för pessimistisk samtidighet.
Optimistisk konkurrenshantering
Optimistisk samtidighet gör att samtidighetskonflikter kan inträffa och reagerar sedan på rätt sätt när de gör det. Jane besöker till exempel sidan Avdelningsredigering och ändrar budgeten för den engelska avdelningen från 350 000,00 USD till 0,00 USD.
Innan Jane klickar på Sparabesöker John samma sida och ändrar fältet Startdatum från 2007-09-01 till 2013-09-01.
Jane klickar Spara först och ser att hennes ändring börjar gälla, eftersom webbläsaren visar sidan Index med noll som budgetbelopp.
John klickar på Spara på en redigeringssida som fortfarande visar en budget på 350 000,00 USD. Vad som händer härnäst bestäms av hur du hanterar samtidighetskonflikter:
Håll reda på vilken egenskap en användare har ändrat och uppdatera endast motsvarande kolumner i databasen.
I scenariot skulle inga data gå förlorade. De två användarna uppdaterade olika egenskaper. Nästa gång någon bläddrar på den engelska avdelningen ser de både Janes och Johns ändringar. Den här uppdateringsmetoden kan minska antalet konflikter som kan leda till dataförlust. Den här metoden har vissa nackdelar:
- Det går inte att undvika dataförlust om konkurrerande ändringar görs i samma attribut.
- Är vanligtvis inte praktiskt i en webbapp. Det kräver att betydande tillstånd bibehålls för att hålla reda på alla hämtade värden och nya värden. Att upprätthålla stora mängder tillstånd kan påverka appens prestanda.
- Kan öka appkomplexiteten jämfört med samtidighetsidentifiering på en entitet.
Låt Johns förändring skriva över Janes förändring.
Nästa gång någon bläddrar på den engelska avdelningen ser de datumet 2013-01-09 och det hämtade beloppet på 350 000,00 USD. Den här metoden kallas för ett klient vinner eller sista in vinner scenario. Alla värden från klienten har företräde framför vad som finns i datalagret. Den genererade koden hanterar ingen samtidighet, och "Client Wins" sker automatiskt.
Förhindra att Johns ändring uppdateras i databasen. Normalt skulle appen:
- Visa ett felmeddelande.
- Visa datans aktuella tillstånd.
- Tillåt att användaren återanvänder ändringarna.
Detta kallas för ett Store Wins scenario. Datalagringsvärdena har företräde framför de värden som skickas av klienten. "Store Wins-scenariot" används i den här handledningen. Den här metoden säkerställer att inga ändringar skrivs över utan att en användare aviseras.
Konfliktidentifiering i EF Core
Egenskaper som konfigurerats som samtidighetsmarkörer används för att implementera optimistisk samtidighetskontroll. När en uppdaterings- eller borttagningsåtgärd utlöses av SaveChanges eller SaveChangesAsyncjämförs värdet för samtidighetstoken i databasen med det ursprungliga värdet som lästes av EF Core:
- Om värdena matchar kan åtgärden slutföras.
- Om värdena inte matchar förutsätter EF Core att en annan användare har utfört en konfliktåtgärd, avbryter den aktuella transaktionen och genererar en DbUpdateConcurrencyException.
En annan användare eller process som utför en åtgärd som står i konflikt med den aktuella åtgärden kallas samtidighetskonflikt.
I relationsdatabaser kontrollerar EF Core värdet för konkurrenstokenet i WHERE-klausulen i UPDATE- och DELETE-satser för att upptäcka en samtidighetskonflikt.
Datamodellen måste konfigureras för att aktivera konfliktidentifiering genom att inkludera en spårningskolumn som kan användas för att avgöra när en rad har ändrats. EF tillhandahåller två metoder för samtidighetstoken:
Tillämpa
[ConcurrencyCheck]eller IsConcurrencyToken på en egenskap i modellen. Den här metoden rekommenderas inte. Mer information finns i Konkurrenstoken i EF Core.Tillämpa TimestampAttribute eller IsRowVersion på en samtidighetstoken i modellen. Det här är den metod som används i den här handledningen.
SQL Server-metoden och SQLite-implementeringsinformationen skiljer sig något åt. En skillnadsfil presenteras senare i handledningen där skillnaderna listas. Fliken Visual Studio visar SQL Server-metoden. Fliken Visual Studio Code visar metoden för icke-SQL Server-databaser, till exempel SQLite.
- I modellen inkluderar du en spårningskolumn som används för att avgöra när en rad har ändrats.
- Använd TimestampAttribute på samtidighetsegenskapen.
Uppdatera Models/Department.cs-filen med följande markerade kod:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] ConcurrencyToken { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
TimestampAttribute är vad som identifierar kolumnen som en kolumn för samtidighetsspårning. Api:et fluent är ett alternativt sätt att ange spårningsegenskapen:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Attributet [Timestamp] på en entitetsegenskap genererar följande kod i metoden ModelBuilder:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Föregående kod:
- Anger egenskapstypen
ConcurrencyTokentill bytematris.byte[]är den typ som krävs för SQL Server. - Anropar IsConcurrencyToken.
IsConcurrencyTokenkonfigurerar egenskapen som en samtidighetstoken. Vid uppdateringar jämförs värdet för samtidighetstoken i databasen med det ursprungliga värdet för att säkerställa att det inte har ändrats sedan instansen hämtades från databasen. Om den har ändrats genereras en DbUpdateConcurrencyException och ändringar tillämpas inte. - Anropar ValueGeneratedOnAddOrUpdate, som konfigurerar egenskapen
ConcurrencyTokenså att ett värde genereras automatiskt när en entitet läggs till eller uppdateras. -
HasColumnType("rowversion")anger kolumntypen i SQL Server-databasen till rowversion.
Följande kod visar en del av T-SQL som genereras av EF Core när Department namn uppdateras:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Den föregående markerade koden visar WHERE-satsen som innehåller ConcurrencyToken. Om databasen ConcurrencyToken inte är lika med parametern ConcurrencyToken@p2uppdateras inga rader.
Följande markerade kod visar T-SQL som verifierar att exakt en rad har uppdaterats:
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 returnerar antalet rader som påverkades av det senaste uttalandet. Om inga rader uppdateras kastar EF Core ett DbUpdateConcurrencyException.
Lägga till en migrering
Om du lägger till egenskapen ConcurrencyToken ändras datamodellen, vilket kräver en migrering.
Skapa projektet.
Kör följande kommandon i PMC:
Add-Migration RowVersion
Update-Database
Föregående kommandon:
- Skapar
Migrations/{time stamp}_RowVersion.csmigreringsfilen. - Uppdaterar
Migrations/SchoolContextModelSnapshot.cs-filen. Uppdateringen lägger till följande kod i metodenBuildModel:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Sidor för byggställningsavdelningen
Följ anvisningarna på studentsidor med följande undantag:
- Skapa en pages/departments mapp.
- Använd
Departmentför modellklassen. - Använd den befintliga kontextklassen i stället för att skapa en ny.
Lägga till en verktygsklass
I projektmappen skapar du klassen Utility med följande kod:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
Klassen Utility innehåller metoden GetLastChars som används för att visa de sista tecknen i samtidighetstoken. Följande kod visar koden som fungerar med både 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
Det #if SQLiteVersionförprocessordirektivet isolerar skillnaderna i SQLite- och SQL Server-versionerna och hjälper dig:
- Författaren har en kodbas för båda versionerna.
- SQLite-utvecklare distribuerar appen till Azure och använder SQL Azure.
Skapa projektet.
Uppdatera sidan Index
Verktyget scaffolding skapade en ConcurrencyToken kolumn för indexsidan, men det fältet skulle inte visas i en produktionsapp. I den här handledningen visas den sista delen av ConcurrencyToken för att hjälpa till att visa hur samtidighetshantering fungerar. Den sista delen är inte garanterad att vara unik av sig själv.
Uppdatera sidan Pages\Departments\Index.cshtml:
- Ersätt index med avdelningar.
- Ändra koden som innehåller
ConcurrencyTokenså att bara de sista tecknen visas. - Ersätt
FirstMidNamemedFullName.
Följande kod visar den uppdaterade sidan:
@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>
Uppdatera redigeringssidemodellen
Uppdatera Pages/Departments/Edit.cshtml.cs med följande kod:
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.");
}
}
}
Samtidighetsuppdateringarna
OriginalValue uppdateras med värdet ConcurrencyToken från entiteten när det hämtades i metoden OnGetAsync.
EF Core genererar ett SQL UPDATE-kommando med en WHERE-sats som innehåller det ursprungliga ConcurrencyToken-värdet. Om inga rader påverkas av kommandot UPDATE genereras ett DbUpdateConcurrencyException undantag. Inga rader påverkas av kommandot UPDATE när inga rader har det ursprungliga ConcurrencyToken värdet.
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;
I föregående markerade kod:
- Värdet i
Department.ConcurrencyTokenär värdet när entiteten hämtades iGetbegäran för sidanEdit. Värdet anges till metodenOnPostav ett dolt fält på sidan Razor som visar den entitet som ska redigeras. Värdet för det dolda fältet kopieras tillDepartment.ConcurrencyTokenav modellbindaren. -
OriginalValueär vad EF Core använder iWHERE-satsen. Innan den markerade kodraden körs:-
OriginalValuehar värdet som fanns i databasen närFirstOrDefaultAsyncanropades i den här metoden. - Det här värdet kan skilja sig från det som visades på sidan Redigera.
-
- Den markerade koden ser till att EF Core använder det ursprungliga
ConcurrencyToken-värdet från den visadeDepartment-entiteten i SQLUPDATE-instruktionensWHERE-sats.
Följande kod visar Department modellen.
Department är initierad i:
-
OnGetAsync-metoden för EF-fråga. - Metod
OnPostAsyncmed det dolda fältet på Razor-sidan med modellbindning:
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;
Föregående kod visar ConcurrencyToken värdet för den Department entiteten från HTTP POST begäran har angetts till värdet ConcurrencyToken från HTTP GET begäran.
När ett samtidighetsfel inträffar hämtar följande markerade kod klientvärdena (värdena som publicerats till den här metoden) och databasvärdena.
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)}");
}
Följande kod lägger till ett anpassat felmeddelande för varje kolumn som har databasvärden som skiljer sig från vad som publicerades i 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.");
}
Följande markerade kod anger värdet ConcurrencyToken till det nya värdet som hämtats från databasen. Nästa gång användaren klickar på Sparafångas endast samtidighetsfel som inträffar sedan den senaste visningen av sidan Redigera.
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)}");
}
Instruktionen ModelState.Remove krävs eftersom ModelState har det tidigare ConcurrencyToken värdet. På Razor-sidan har värdet ModelState för ett fält företräde framför modellegenskapsvärdena när båda finns.
Skillnader mellan SQL Server och SQLite-kod
Följande visar skillnaderna mellan SQL Server- och SQLite-versionerna:
+ 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;
Uppdatera Redigera-sidan Razor
Uppdatera Pages/Departments/Edit.cshtml med följande kod:
@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");}
}
Föregående kod:
- Uppdaterar
page-direktivet från@pagetill@page "{id:int}". - Lägger till en dold radversion.
ConcurrencyTokenmåste läggas till så att postback binder värdet. - Visar den sista byte av
ConcurrencyTokenför felsökning. - Ersätter
ViewDatamed den tydligt typadeInstructorNameSL.
Testa samtidighetskonflikter med Redigera sidan
Öppna två webbläsarinstanser av Redigera på den engelska avdelningen:
- Kör appen och välj Avdelningar.
- Högerklicka på hyperlänken Redigera för den engelska avdelningen och välj Öppna på ny flik.
- På den första fliken klickar du på hyperlänken Redigera för den engelska avdelningen.
De två webbläsarflikarna visar samma information.
Ändra namnet på den första webbläsarfliken och klicka på Spara.
Webbläsaren visar sidan Index med det ändrade värdet och uppdaterad ConcurrencyTokenindikator. Observera den uppdaterade indikatorn för ConcurrencyToken. Den visas på den andra återställningen på den andra fliken.
Ändra ett annat fält på den andra webbläsarfliken.
Klicka på Spara. Du ser felmeddelanden för alla fält som inte matchar databasvärdena:
Det här webbläsarfönstret hade inte för avsikt att ändra fältet Namn. Kopiera och klistra in det aktuella värdet (språk) i fältet Namn. Ta bort. Verifiering på klientsidan tar bort felmeddelandet.
Klicka på Spara igen. Värdet som du angav på den andra webbläsarfliken sparas. Du ser de sparade värdena på sidan Index.
Uppdatera sidmodellen Ta bort
Uppdatera Pages/Departments/Delete.cshtml.cs med följande kod:
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 });
}
}
}
}
Sidan Ta bort identifierar samtidighetskonflikter när entiteten har ändrats efter att den hämtades.
Department.ConcurrencyToken är radversionen när entiteten hämtades. När EF Core skapar kommandot SQL DELETE innehåller det en WHERE-sats med ConcurrencyToken. Om kommandot SQL DELETE resulterar i att noll rader påverkas:
- Kommandot
ConcurrencyTokeniSQL DELETEmatchar inteConcurrencyTokeni databasen. - Ett
DbUpdateConcurrencyException-undantag kastas. -
OnGetAsyncanropas medconcurrencyError.
Uppdatera sidan Ta bort Razor
Uppdatera Pages/Departments/Delete.cshtml med följande kod:
@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>
Föregående kod gör följande ändringar:
- Uppdaterar
page-direktivet från@pagetill@page "{id:int}". - Lägger till ett felmeddelande.
- Ersätter FirstMidName med FullName i fältet Administrator.
- Ändrar
ConcurrencyTokenför att visa senaste byte. - Lägger till en dold radversion.
ConcurrencyTokenmåste läggas till så att postback binder värdet.
Testa samtidighetskonflikter
Skapa en testavdelning.
Öppna två webbläsarfönster av Delete på testavdelningen.
- Kör appen och välj Avdelningar.
- Högerklicka på hyperlänken Ta bort för testavdelningen och välj Öppna på ny flik.
- Klicka på hyperlänken Redigera för testavdelningen.
De två webbläsarflikarna visar samma information.
Ändra budgeten på den första webbläsarfliken och klicka på Spara.
Webbläsaren visar sidan Index med det ändrade värdet och uppdaterad ConcurrencyTokenindikator. Observera den uppdaterade indikatorn för ConcurrencyToken. Den visas på den andra återställningen på den andra fliken.
Ta bort testavdelningen från den andra fliken. Ett samtidighetsfel visas med de aktuella värdena från databasen. Om du klickar på Ta bort tas entiteten bort, såvida inte ConcurrencyToken har uppdaterats.
Mönster för företagswebbappar
Vägledning om hur du skapar en tillförlitlig, säker, högpresterande, testbar och skalbar ASP.NET Core-app finns i Enterprise-webbappmönster. En komplett exempelwebbapp av produktionskvalitet som implementerar mönstren är tillgänglig.
Ytterligare resurser
Nästa steg
Det här är den sista handledningen i serien. Ytterligare avsnitt beskrivs i MVC-versionen av den här självstudieserien.
Den här självstudien visar hur du hanterar konflikter när flera användare uppdaterar en entitet samtidigt (samtidigt).
Samtidighetskonflikter
En samtidighetskonflikt uppstår när:
- En användare navigerar till redigeringssidan för en entitet.
- En annan användare uppdaterar samma entitet innan den första användarens ändring skrivs till databasen.
Om samtidighetsidentifiering inte är aktiverat skriver den som uppdaterar databasen senast över den andra användarens ändringar. Om den här risken är acceptabel kan kostnaden för programmering för samtidighet uppväga fördelen.
Pessimistisk konkurrens (låsning)
Ett sätt att förhindra samtidighetskonflikter är att använda databaslås. Detta kallas pessimistisk samtidighet. Innan appen läser en databasrad som den tänker uppdatera begär den ett lås. När en rad har låsts för uppdateringsåtkomst får inga andra användare låsa raden förrän det första låset har släppts.
Att hantera lås har nackdelar. Det kan vara komplext att programmera och kan orsaka prestandaproblem när antalet användare ökar. Entity Framework Core har inget inbyggt stöd för det, och den här självstudien visar inte hur du implementerar det.
Optimistisk konkurrenshantering
Optimistisk samtidighet gör att samtidighetskonflikter kan inträffa och reagerar sedan på rätt sätt när de gör det. Jane besöker till exempel sidan Avdelningsredigering och ändrar budgeten för den engelska avdelningen från 350 000,00 USD till 0,00 USD.
Innan Jane klickar på Sparabesöker John samma sida och ändrar fältet Startdatum från 2007-09-01 till 2013-09-01.
Jane klickar Spara först och ser att hennes ändring börjar gälla, eftersom webbläsaren visar sidan Index med noll som budgetbelopp.
John klickar på Spara på en redigeringssida som fortfarande visar en budget på 350 000,00 USD. Vad som händer härnäst bestäms av hur du hanterar samtidighetskonflikter:
Du kan hålla reda på vilken egenskap en användare har ändrat och uppdatera endast motsvarande kolumner i databasen.
I scenariot skulle inga data gå förlorade. De två användarna uppdaterade olika egenskaper. Nästa gång någon bläddrar på den engelska avdelningen ser de både Janes och Johns ändringar. Den här uppdateringsmetoden kan minska antalet konflikter som kan leda till dataförlust. Den här metoden har vissa nackdelar:
- Det går inte att undvika dataförlust om konkurrerande ändringar görs i samma attribut.
- Är vanligtvis inte praktiskt i en webbapp. Det kräver att betydande tillstånd bibehålls för att hålla reda på alla hämtade värden och nya värden. Att upprätthålla stora mängder tillstånd kan påverka appens prestanda.
- Kan öka appkomplexiteten jämfört med samtidighetsidentifiering på en entitet.
Du kan låta Johns ändring skriva över Janes förändring.
Nästa gång någon bläddrar på den engelska avdelningen ser de datumet 2013-01-09 och det hämtade beloppet på 350 000,00 USD. Den här metoden kallas för ett klient vinner eller sista in vinner scenario. (Alla värden från klienten har företräde framför vad som finns i datalagret.) Om du inte kodar för samtidighetshantering sker klientvinster automatiskt.
Du kan förhindra att Johns ändring uppdateras i databasen. Normalt skulle appen:
- Visa ett felmeddelande.
- Visa datans aktuella tillstånd.
- Tillåt att användaren återanvänder ändringarna.
Detta kallas för ett Store Wins scenario. (Datalagringsvärdena har företräde framför de värden som skickas av klienten.) Du implementerar scenariot Store Wins i den här självstudien. Den här metoden säkerställer att inga ändringar skrivs över utan att en användare aviseras.
Konfliktidentifiering i EF Core
EF Core genererar DbConcurrencyException undantag när konflikter identifieras. Datamodellen måste konfigureras för att aktivera konfliktidentifiering. Alternativ för att aktivera konfliktidentifiering är följande:
Konfigurera EF Core så att de ursprungliga värdena för kolumner som har konfigurerats som samtidighetstoken inkluderas i WHERE-klausulen för kommandona Uppdatera och Ta bort.
När
SaveChangesanropas letar Where-satsen efter de ursprungliga värdena för alla egenskaper som kommenterats med attributet ConcurrencyCheckAttribute. Uppdateringsinstrukeringen hittar ingen rad som ska uppdateras om någon av egenskaperna för samtidighetstoken har ändrats sedan raden lästes först. EF Core tolkar detta som en samtidighetskonflikt. För databastabeller som har många kolumner kan den här metoden resultera i mycket stora Where-satser och kan kräva stora mängder tillstånd. Därför rekommenderas inte den här metoden, och det är inte den metod som används i den här självstudien.I databastabellen inkluderar du en spårningskolumn som kan användas för att avgöra när en rad har ändrats.
I en SQL Server-databas är datatypen för spårningskolumnen
rowversion. Värdetrowversionär ett sekventiellt tal som ökas varje gång raden uppdateras. I kommandot Uppdatera eller Ta bort innehåller Where-satsen det ursprungliga värdet för spårningskolumnen (det ursprungliga radversionsnumret). Om raden som uppdateras har ändrats av en annan användare skiljer sig värdet i kolumnenrowversionän det ursprungliga värdet. I så fall kan instruktionen Uppdatera eller Ta bort inte hitta raden som ska uppdateras på grund av Where-satsen. EF Core genererar ett samtidighetsfel när inga rader påverkas av kommandot Uppdatera eller Ta bort.
Lägga till en spårningsegenskap
I Models/Department.cslägger du till en spårningsegenskap med namnet RowVersion:
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; }
}
}
Attributet TimestampAttribute är det som identifierar kolumnen som en samtidighetsspårningskolumn. Api:et fluent är ett alternativt sätt att ange spårningsegenskapen:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
För en SQL Server-databas: [Timestamp]-attributet på en entitetsegenskap definieras som byte-array.
- Gör att kolumnen inkluderas i satserna DELETE och UPDATE WHERE.
- Anger kolumntypen i databasen till rowversion.
Databasen genererar ett sekventiellt radversionsnummer som ökas varje gång raden uppdateras. I ett Update- eller Delete-kommando innehåller Where-satsen det hämtade radversionsvärdet. Om raden som uppdateras har ändrats sedan den hämtades:
- Det aktuella radversionsvärdet matchar inte det hämtade värdet.
- Kommandona
UpdateellerDeletehittar ingen rad eftersomWhere-satsen söker efter värdet för den hämtade radversionen. - En
DbUpdateConcurrencyExceptionkastas.
Följande kod visar en del av T-SQL som genereras av EF Core när avdelningsnamnet uppdateras:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Den föregående markerade koden visar WHERE-satsen som innehåller RowVersion. Om databasen RowVersion inte är lika med parametern RowVersion (@p2) uppdateras inga rader.
Följande markerade kod visar T-SQL som verifierar att exakt en rad har uppdaterats:
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 returnerar antalet rader som påverkades av det senaste uttalandet. Om inga rader uppdateras kastar EF Core ett DbUpdateConcurrencyException.
Uppdatera databasen
Om du lägger till egenskapen RowVersion ändras datamodellen, vilket kräver en migrering.
Skapa projektet.
Kör följande kommando i PMC:
Add-Migration RowVersion
Det här kommandot:
Skapar
Migrations/{time stamp}_RowVersion.csmigreringsfilen.Uppdaterar
Migrations/SchoolContextModelSnapshot.cs-filen. Uppdateringen lägger till följande markerade kod i metodenBuildModel: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"); });
Kör följande kommando i PMC:
Update-Database
Sidor för byggställningsavdelningen
Följ anvisningarna på studentsidor med följande undantag:
Skapa en pages/departments mapp.
Använd
Departmentför modellklassen.- Använd den befintliga kontextklassen i stället för att skapa en ny.
Skapa projektet.
Uppdatera sidan Index
Verktyget scaffolding skapade en RowVersion kolumn för indexsidan, men det fältet skulle inte visas i en produktionsapp. I den här handledningen visas den sista byten av RowVersion för att hjälpa till att visa hur samtidighetshantering fungerar. De sista bytena är inte garanterade att vara unika av sig själva.
Uppdatera sidan Pages\Departments\Index.cshtml:
- Ersätt index med avdelningar.
- Ändra koden som innehåller
RowVersionför att bara visa den sista byteen i bytematrisen. - Ersätt FirstMidName med FullName.
Följande kod visar den uppdaterade sidan:
@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>
Uppdatera redigeringssidemodellen
Uppdatera Pages/Departments/Edit.cshtml.cs med följande kod:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
var deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error
// and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
}
}
OriginalValue uppdateras med värdet rowVersion från entiteten när den hämtades i metoden OnGetAsync.
EF Core genererar ett SQL UPDATE-kommando med en WHERE-sats som innehåller det ursprungliga RowVersion-värdet. Om inga rader påverkas av kommandot UPDATE (inga rader har det ursprungliga RowVersion värdet) genereras ett DbUpdateConcurrencyException undantag.
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;
I föregående markerade kod:
- Värdet i
Department.RowVersionär det som fanns i entiteten när det ursprungligen hämtades i Get-begäran för Edit-sidans. Värdet anges till metodenOnPostav ett dolt fält på sidan Razor som visar den entitet som ska redigeras. Värdet för det dolda fältet kopieras tillDepartment.RowVersionav modellbindaren. -
OriginalValueär vad EF Core kommer att använda i Where-satsen. Innan den markerade kodraden körs harOriginalValuevärdet som fanns i databasen närFirstOrDefaultAsyncanropades i den här metoden, vilket kan skilja sig från det som visades på sidan Redigera. - Den markerade koden ser till att EF Core använder det ursprungliga
RowVersion-värdet från den visadeDepartment-entiteten i SQL UPDATE-instruktionens Where-sats.
När ett samtidighetsfel inträffar hämtar följande markerade kod klientvärdena (värdena som publicerats till den här metoden) och databasvärdena.
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");
}
Följande kod lägger till ett anpassat felmeddelande för varje kolumn som har databasvärden som skiljer sig från vad som publicerades i 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.");
}
Följande markerade kod anger värdet RowVersion till det nya värdet som hämtats från databasen. Nästa gång användaren klickar på Sparafångas endast samtidighetsfel som inträffar sedan den senaste visningen av sidan Redigera.
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");
}
Instruktionen ModelState.Remove krävs eftersom ModelState har det gamla RowVersion värdet. På Razor-sidan har värdet ModelState för ett fält företräde framför modellegenskapsvärdena när båda finns.
Uppdatera sidan Redigera
Uppdatera Pages/Departments/Edit.cshtml med följande kod:
@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");}
}
Föregående kod:
- Uppdaterar
page-direktivet från@pagetill@page "{id:int}". - Lägger till en dold radversion.
RowVersionmåste läggas till så att postback binder värdet. - Visar den sista byte av
RowVersionför felsökning. - Ersätter
ViewDatamed den tydligt typadeInstructorNameSL.
Testa samtidighetskonflikter med Redigera sidan
Öppna två webbläsarinstanser av Redigera på den engelska avdelningen:
- Kör appen och välj Avdelningar.
- Högerklicka på hyperlänken Redigera för den engelska avdelningen och välj Öppna på ny flik.
- På den första fliken klickar du på hyperlänken Redigera för den engelska avdelningen.
De två webbläsarflikarna visar samma information.
Ändra namnet på den första webbläsarfliken och klicka på Spara.
Webbläsaren visar sidan Index med det ändrade värdet och den uppdaterade rowVersion-indikatorn. Observera den uppdaterade rowVersion-indikatorn, den visas vid den andra postbacken på den andra fliken.
Ändra ett annat fält på den andra webbläsarfliken.
Klicka på Spara. Du ser felmeddelanden för alla fält som inte matchar databasvärdena:
Det här webbläsarfönstret hade inte för avsikt att ändra fältet Namn. Kopiera och klistra in det aktuella värdet (språk) i fältet Namn. Ta bort. Verifiering på klientsidan tar bort felmeddelandet.
Klicka på Spara igen. Värdet som du angav på den andra webbläsarfliken sparas. Du ser de sparade värdena på sidan Index.
Uppdatera sidmodellen Ta bort
Uppdatera Pages/Departments/Delete.cshtml.cs med följande kod:
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 });
}
}
}
}
Sidan Ta bort identifierar samtidighetskonflikter när entiteten har ändrats efter att den hämtades.
Department.RowVersion är radversionen när entiteten hämtades. När EF Core skapar SQL DELETE-kommandot innehåller det en WHERE-sats med RowVersion. Om SQL DELETE-kommandot resulterar i att noll rader påverkas:
-
RowVersioni SQL DELETE-kommandot matchar inteRowVersioni databasen. - Ett DbUpdateConcurrencyException-undantag genereras.
-
OnGetAsyncanropas medconcurrencyError.
Uppdatera sidan Ta bort
Uppdatera Pages/Departments/Delete.cshtml med följande kod:
@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>
Föregående kod gör följande ändringar:
- Uppdaterar
page-direktivet från@pagetill@page "{id:int}". - Lägger till ett felmeddelande.
- Ersätter FirstMidName med FullName i fältet Administrator.
- Ändrar
RowVersionför att visa senaste byte. - Lägger till en dold radversion.
RowVersionmåste läggas till så att postback binder värdet.
Testa samtidighetskonflikter
Skapa en testavdelning.
Öppna två webbläsarfönster av Delete på testavdelningen.
- Kör appen och välj Avdelningar.
- Högerklicka på hyperlänken Ta bort för testavdelningen och välj Öppna på ny flik.
- Klicka på hyperlänken Redigera för testavdelningen.
De två webbläsarflikarna visar samma information.
Ändra budgeten på den första webbläsarfliken och klicka på Spara.
Webbläsaren visar sidan Index med det ändrade värdet och den uppdaterade rowVersion-indikatorn. Observera den uppdaterade rowVersion-indikatorn, den visas vid den andra postbacken på den andra fliken.
Ta bort testavdelningen från den andra fliken. Ett samtidighetsfel visas med de aktuella värdena från databasen. Om du klickar på Ta bort tas entiteten bort, såvida inte RowVersion har uppdaterats.
Vägledning om hur du skapar en tillförlitlig, säker, högpresterande, testbar och skalbar ASP.NET Core-app finns i Enterprise-webbappmönster. En komplett exempelwebbapp av produktionskvalitet som implementerar mönstren är tillgänglig.
Ytterligare resurser
Nästa steg
Det här är den sista handledningen i serien. Ytterligare avsnitt beskrivs i MVC-versionen av den här självstudieserien.
Den här självstudien visar hur du hanterar konflikter när flera användare uppdaterar en entitet samtidigt (samtidigt). Om du stöter på problem som du inte kan lösa ladda ned eller visa den färdiga appen.Ladda ned instruktioner.
Samtidighetskonflikter
En samtidighetskonflikt uppstår när:
- En användare navigerar till redigeringssidan för en entitet.
- En annan användare uppdaterar samma entitet innan den första användarens ändring skrivs till databasen.
Om samtidighetsidentifiering inte är aktiverat när samtidiga uppdateringar inträffar:
- Den senaste uppdateringen vinner. De senaste uppdateringsvärdena sparas alltså i databasen.
- Den första av de aktuella uppdateringarna saknas.
Optimistisk konkurrenshantering
Optimistisk samtidighet gör att samtidighetskonflikter kan inträffa och reagerar sedan på rätt sätt när de gör det. Jane besöker till exempel sidan Avdelningsredigering och ändrar budgeten för den engelska avdelningen från 350 000,00 USD till 0,00 USD.
Innan Jane klickar på Sparabesöker John samma sida och ändrar fältet Startdatum från 2007-09-01 till 2013-09-01.
Först klickar Jane på Spara och ser sin ändring när webbläsaren visar indexsidan.
John klickar på Spara på en redigeringssida som fortfarande visar en budget på 350 000,00 USD. Vad som händer härnäst bestäms av hur du hanterar samtidighetskonflikter.
Optimistisk samtidighet innehåller följande alternativ:
Du kan hålla reda på vilken egenskap en användare har ändrat och uppdatera endast motsvarande kolumner i databasen.
I scenariot skulle inga data gå förlorade. De två användarna uppdaterade olika egenskaper. Nästa gång någon bläddrar på den engelska avdelningen ser de både Janes och Johns ändringar. Den här uppdateringsmetoden kan minska antalet konflikter som kan leda till dataförlust. Den här metoden:
- Det går inte att undvika dataförlust om konkurrerande ändringar görs i samma attribut.
- Är vanligtvis inte praktiskt i en webbapp. Det kräver att betydande tillstånd bibehålls för att hålla reda på alla hämtade värden och nya värden. Att upprätthålla stora mängder tillstånd kan påverka appens prestanda.
- Kan öka appkomplexiteten jämfört med samtidighetsidentifiering på en entitet.
Du kan låta Johns ändring skriva över Janes förändring.
Nästa gång någon bläddrar på den engelska avdelningen ser de datumet 2013-01-09 och det hämtade beloppet på 350 000,00 USD. Den här metoden kallas för ett klient vinner eller sista in vinner scenario. (Alla värden från klienten har företräde framför vad som finns i datalagret.) Om du inte kodar för samtidighetshantering sker klientvinster automatiskt.
Du kan förhindra att Johns ändring uppdateras i databasen. Normalt skulle appen:
- Visa ett felmeddelande.
- Visa datans aktuella tillstånd.
- Tillåt att användaren återanvänder ändringarna.
Detta kallas för ett Store Wins scenario. (Datalagringsvärdena har företräde framför de värden som skickas av klienten.) Du implementerar scenariot Store Wins i den här självstudien. Den här metoden säkerställer att inga ändringar skrivs över utan att en användare aviseras.
Hantera samtidighet
När en egenskap har konfigurerats som en samtidighetstoken:
- EF Core verifierar att egenskapen inte har ändrats efter att den hämtades. Kontrollen sker när SaveChanges eller SaveChangesAsync anropas.
- Om egenskapen har ändrats efter att den hämtades utlöses en DbUpdateConcurrencyException.
Databas- och datamodellen måste konfigureras för att kunna generera DbUpdateConcurrencyException.
Identifiera samtidighetskonflikter på ett attribut
Samtidighetskonflikter kan identifieras på egenskapsnivå med attributet ConcurrencyCheck. Attributet kan tillämpas på flera egenskaper i modellen. Mer information finns i Data Annotations - ConcurrencyCheck.
Attributet [ConcurrencyCheck] används inte i den här handledningen.
Identifiera samtidighetskonflikter på en rad
För att identifiera samtidighetskonflikter läggs en radversion spårningskolumn till modellen.
rowversion :
- Är SQL Server specifikt. Andra databaser kanske inte har en liknande funktion.
- Används för att fastställa att en entitet inte har ändrats sedan den hämtades från databasdatabasen.
Databasen genererar ett sekventiellt rowversion tal som ökas varje gång raden uppdateras. I ett Update- eller Delete-kommando innehåller Where-satsen det hämtade värdet för rowversion. Om raden som uppdateras har ändrats:
-
rowversionmatchar inte det hämtade värdet. - Kommandona
UpdateellerDeletehittar ingen rad eftersomWhere-satsen innehåller den hämtaderowversion. - En
DbUpdateConcurrencyExceptionkastas.
I EF Coregenereras ett samtidighetsfel när inga rader har uppdaterats av ett Update- eller Delete-kommando.
Lägga till en spårningsegenskap i entiteten Department
I Models/Department.cslägger du till en spårningsegenskap med namnet RowVersion:
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; }
}
}
Attributet Timestamp anger att den här kolumnen ingår i Where-satsen i kommandona Update och Delete. Attributet kallas Timestamp eftersom tidigare versioner av SQL Server använde en SQL-timestamp datatyp innan SQL-rowversion typ ersatte det.
Api:et fluent kan också ange spårningsegenskapen:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Följande kod visar en del av T-SQL som genereras av EF Core när avdelningsnamnet uppdateras:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Den föregående markerade koden visar WHERE-satsen som innehåller RowVersion. Om db-RowVersion inte är lika med parametern RowVersion (@p2) uppdateras inga rader.
Följande markerade kod visar T-SQL som verifierar att exakt en rad har uppdaterats:
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 returnerar antalet rader som påverkades av det senaste uttalandet. Om inga rader uppdateras, genererar EF Core en DbUpdateConcurrencyException.
Du kan se T-SQL-EF Core som genereras i utdatafönstret i Visual Studio.
Uppdatera databasen
Om du lägger till egenskapen RowVersion ändras DB-modellen, vilket kräver en migrering.
Skapa projektet. Ange följande i ett kommandofönster:
dotnet ef migrations add RowVersion
dotnet ef database update
Föregående kommandon:
Lägger till migreringsfilen
Migrations/{time stamp}_RowVersion.cs.Uppdaterar
Migrations/SchoolContextModelSnapshot.cs-filen. Uppdateringen lägger till följande markerade kod i metodenBuildModel:Kör migreringar för att uppdatera databasen.
Skapa grundstrukturen för avdelningsmodellen
Följ anvisningarna i Stödstrukturera studentmodellen och använd Department för model class.
Föregående kommando bygger upp strukturen för Department-modellen. Öppna projektet i Visual Studio.
Skapa projektet.
Uppdatera sidan Avdelningsindex
Genereringsmotorn skapade en RowVersion-kolumn för Indexsidan, men det fältet ska inte visas. I den här handledningen visas den sista byten av RowVersion för att hjälpa till att förstå samtidighet. Det sista bytet är inte garanterat unikt. En riktig app skulle inte visa RowVersion eller sista bytet av RowVersion.
Uppdatera sidan Index:
- Ersätt index med avdelningar.
- Ersätt markeringen som innehåller
RowVersionmed den sista byten avRowVersion. - Ersätt FirstMidName med FullName.
Följande markering visar den uppdaterade sidan:
@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>
Uppdatera redigeringssidemodellen
Uppdatera Pages/Departments/Edit.cshtml.cs med följande kod:
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.");
}
}
}
För att identifiera ett samtidighetsproblem uppdateras OriginalValue med värdet rowVersion från den entitet som hämtades.
EF Core genererar ett SQL UPDATE-kommando med en WHERE-sats som innehåller det ursprungliga RowVersion-värdet. Om inga rader påverkas av kommandot UPDATE (inga rader har det ursprungliga RowVersion värdet) genereras ett DbUpdateConcurrencyException undantag.
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;
I föregående kod är Department.RowVersion värdet när entiteten hämtades.
OriginalValue är värdet i databasen när FirstOrDefaultAsync anropades i den här metoden.
Följande kod hämtar klientvärdena (värdena som har publicerats till den här metoden) och DB-värdena:
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");
}
Följande kod lägger till ett anpassat felmeddelande för varje kolumn som har db-värden som skiljer sig från det som publicerades i 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.");
}
Följande markerade kod anger värdet RowVersion till det nya värdet som hämtats från databasen. Nästa gång användaren klickar på Sparafångas endast samtidighetsfel som inträffar sedan den senaste visningen av sidan Redigera.
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");
}
Instruktionen ModelState.Remove krävs eftersom ModelState har det gamla RowVersion värdet. På Razor-sidan har värdet ModelState för ett fält företräde framför modellegenskapsvärdena när båda finns.
Uppdatera sidan Redigera
Uppdatera Pages/Departments/Edit.cshtml med följande markering:
@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");}
}
Föregående markering:
- Uppdaterar
page-direktivet från@pagetill@page "{id:int}". - Lägger till en dold radversion.
RowVersionmåste läggas till så att post back binder värdet. - Visar den sista byte av
RowVersionför felsökning. - Ersätter
ViewDatamed den tydligt typadeInstructorNameSL.
Testa samtidighetskonflikter med Redigera sidan
Öppna två webbläsarinstanser av Redigera på den engelska avdelningen:
- Kör appen och välj Avdelningar.
- Högerklicka på hyperlänken Redigera för den engelska avdelningen och välj Öppna på ny flik.
- På den första fliken klickar du på hyperlänken Redigera för den engelska avdelningen.
De två webbläsarflikarna visar samma information.
Ändra namnet på den första webbläsarfliken och klicka på Spara.
Webbläsaren visar sidan Index med det ändrade värdet och den uppdaterade rowVersion-indikatorn. Observera den uppdaterade rowVersion-indikatorn, den visas vid den andra postbacken på den andra fliken.
Ändra ett annat fält på den andra webbläsarfliken.
Klicka på Spara. Du ser felmeddelanden för alla fält som inte matchar DB-värdena:
Det här webbläsarfönstret hade inte för avsikt att ändra fältet Namn. Kopiera och klistra in det aktuella värdet (språk) i fältet Namn. Ta bort. Verifiering på klientsidan tar bort felmeddelandet.
Klicka på Spara igen. Värdet som du angav på den andra webbläsarfliken sparas. Du ser de sparade värdena på sidan Index.
Uppdatera sidan Ta bort
Uppdatera borttagningssidans modell med följande kod:
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 });
}
}
}
}
Sidan Ta bort identifierar samtidighetskonflikter när entiteten har ändrats efter att den hämtades.
Department.RowVersion är radversionen när entiteten hämtades. När EF Core skapar SQL DELETE-kommandot innehåller det en WHERE-sats med RowVersion. Om SQL DELETE-kommandot resulterar i att noll rader påverkas:
-
RowVersioni SQL DELETE-kommandot matchar inteRowVersioni databasen. - Ett DbUpdateConcurrencyException-undantag genereras.
-
OnGetAsyncanropas medconcurrencyError.
Uppdatera sidan Ta bort
Uppdatera Pages/Departments/Delete.cshtml med följande kod:
@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>
Föregående kod gör följande ändringar:
- Uppdaterar
page-direktivet från@pagetill@page "{id:int}". - Lägger till ett felmeddelande.
- Ersätter FirstMidName med FullName i fältet Administrator.
- Ändrar
RowVersionför att visa senaste byte. - Lägger till en dold radversion.
RowVersionmåste läggas till så att post back binder värdet.
Testa samtidighetskonflikter med sidan Ta bort
Skapa en testavdelning.
Öppna två webbläsarfönster av Delete på testavdelningen.
- Kör appen och välj Avdelningar.
- Högerklicka på hyperlänken Ta bort för testavdelningen och välj Öppna på ny flik.
- Klicka på hyperlänken Redigera för testavdelningen.
De två webbläsarflikarna visar samma information.
Ändra budgeten på den första webbläsarfliken och klicka på Spara.
Webbläsaren visar sidan Index med det ändrade värdet och den uppdaterade rowVersion-indikatorn. Observera den uppdaterade rowVersion-indikatorn, den visas vid den andra postbacken på den andra fliken.
Ta bort testavdelningen från den andra fliken. Ett samtidighetsfel visas med de aktuella värdena från databasen. Om du klickar på Ta bort tas entiteten bort, såvida inte RowVersion har uppdaterats.
Se Arv om hur du ärver en datamodell.
Mönster för företagswebbappar
Vägledning om hur du skapar en tillförlitlig, säker, högpresterande, testbar och skalbar ASP.NET Core-app finns i Enterprise-webbappmönster. En komplett exempelwebbapp av produktionskvalitet som implementerar mönstren är tillgänglig.
Ytterligare resurser
ASP.NET Core