Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Webová aplikace Contoso University ukazuje, jak vytvářet Razor webové aplikace Pages pomocí EF Core sady Visual Studio. Informace o sérii kurzů najdete v prvním kurzu.
Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte si dokončenou aplikaci a porovnejte tento kód s tím, co jste vytvořili podle kurzu.
V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně.
Konflikty souběžnosti
Ke konfliktu souběžnosti dochází v případě, že:
- Uživatel přejde na stránku pro úpravy entity.
- Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.
Pokud detekce souběžnosti není povolená, přepíše změny ostatních uživatelů každý, kdo aktualizuje databázi. Pokud je toto riziko přijatelné, náklady na programování pro souběžnost můžou převažovat nad výhodou.
Pesimistické souběžnosti
Jedním zezpůsobůch Tomu se říká pesimistické souběžnost. Než aplikace přečte řádek databáze, který má v úmyslu aktualizovat, požádá o zámek. Jakmile je řádek uzamčen pro přístup k aktualizacím, nebudou moct ostatní uživatelé řádek uzamknout, dokud nebude vydán první zámek.
Správa zámků má nevýhody. Program může být složitý a může způsobovat problémy s výkonem při nárůstu počtu uživatelů. Entity Framework Core neposkytuje žádnou integrovanou podporu pesimistické souběžnosti.
Optimistická souběžnost
Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.
Jane nejprve klikne na Uložit a uvidí, že se její změna projeví, protože v prohlížeči se jako částka rozpočtu zobrazí stránka Index s nulou.
Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti:
Sledujte, kterou vlastnost uživatel upravil, a aktualizujte pouze odpovídající sloupce v databázi.
V tomto scénáři nedojde ke ztrátě žádných dat. Dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup má určité nevýhody:
- Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
- Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
- Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
Přepíše janovu změnu.
Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje jako klient wins nebo last ve scénáři Wins . Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat. Vygenerovaný kód nezpracovává žádné zpracování souběžnosti, klient wins se provádí automaticky.
Znemožnit aktualizaci Janovy změny v databázi. Aplikace obvykle:
- Zobrazí chybovou zprávu.
- Zobrazí aktuální stav dat.
- Umožňuje uživateli znovu použít změny.
Tomu se říká scénář wins ve Storu. Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem. Scénář Wins pro Store se používá v tomto kurzu. Tato metoda zajišťuje, že se bez upozornění uživatele nepřepíší žádné změny.
Detekce konfliktů v EF Core
Vlastnosti nakonfigurované jako tokeny souběžnosti se používají k implementaci optimistického řízení souběžnosti. Pokud je operace aktualizace nebo odstranění aktivována SaveChanges nebo SaveChangesAsync, hodnota tokenu souběžnosti v databázi se porovná s původní hodnotou přečtenou EF Core:
- Pokud se hodnoty shodují, operace se může dokončit.
- Pokud se hodnoty neshodují, předpokládá, EF Core že jiný uživatel provedl konfliktní operaci, přeruší aktuální transakci a vyvolá DbUpdateConcurrencyException.
Jiný uživatel nebo proces provádějící operaci, která je v konfliktu s aktuální operací, se označuje jako konflikt souběžnosti.
V relačních databázích EF Core kontroluje hodnotu tokenu souběžnosti v WHERE klauzuli UPDATE a DELETE příkazech za účelem zjištění konfliktu souběžnosti.
Datový model musí být nakonfigurovaný tak, aby umožňoval detekci konfliktů zahrnutím sloupce sledování, který lze použít k určení, kdy byl řádek změněn. EF poskytuje dva přístupy pro tokeny souběžnosti:
[ConcurrencyCheck]Použití nebo IsConcurrencyToken na vlastnost v modelu Tento přístup se nedoporučuje. Další informace naleznete v tématu Tokeny souběžnosti v EF Core.TimestampAttribute Použití tokenu souběžnosti v modelu nebo IsRowVersion na token souběžnosti Toto je přístup použitý v tomto kurzu.
Podrobnosti o implementaci SQL Serveru a SQLite se mírně liší. Soubor rozdílů se zobrazí později v kurzu se seznamem rozdílů. Na kartě Visual Studio se zobrazuje přístup k SQL Serveru. Na kartě Visual Studio Code se zobrazuje přístup k databázím, které nejsou sql Serverem, jako je například SQLite.
- Do modelu zahrňte sledovací sloupec, který slouží k určení, kdy byl řádek změněn.
- TimestampAttribute Použijte vlastnost souběžnosti.
Models/Department.cs Aktualizujte soubor následujícím zvýrazněným kódem:
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; }
}
}
Jedná se o TimestampAttribute to, co identifikuje sloupec jako sloupec sledování souběžnosti. Fluent API je alternativní způsob, jak určit vlastnost sledování:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Atribut [Timestamp] vlastnosti entity vygeneruje následující kód v ModelBuilder metodě:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Předchozí kód:
- Nastaví typ
ConcurrencyTokenvlastnosti na pole bajtů.byte[]je požadovaný typ pro SQL Server. - Zavolá metodu IsConcurrencyToken.
IsConcurrencyTokennakonfiguruje vlastnost jako token souběžnosti. Při aktualizacích se hodnota tokenu souběžnosti v databázi porovnává s původní hodnotou, aby se od načtení instance z databáze nezměnila. Pokud došlo ke změně, DbUpdateConcurrencyException vyvolá se a změny se nepoužijí. - Volání ValueGeneratedOnAddOrUpdate, která konfiguruje vlastnost tak, aby měla hodnotu automaticky vygenerovanou
ConcurrencyTokenpři přidání nebo aktualizaci entity. -
HasColumnType("rowversion")nastaví typ sloupce v databázi SQL Serveru na rowversion.
Následující kód ukazuje část T-SQL vygenerovanou při EF CoreDepartment aktualizaci názvu:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Předchozí zvýrazněný kód zobrazuje klauzuli WHERE obsahující ConcurrencyToken. Pokud se databáze ConcurrencyToken nerovná parametruConcurrencyToken, nebudou @p2 aktualizovány žádné řádky.
Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:
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 vrátí počet řádků ovlivněných posledním příkazem. Pokud nejsou aktualizovány žádné řádkyEF Core, DbUpdateConcurrencyException vyvolá chybu .
Přidání migrace
ConcurrencyToken Přidání vlastnosti změní datový model, který vyžaduje migraci.
Sestavte projekt.
V PMC spusťte následující příkazy:
Add-Migration RowVersion
Update-Database
Předchozí příkazy:
-
Migrations/{time stamp}_RowVersion.csVytvoří soubor migrace. -
Migrations/SchoolContextModelSnapshot.csAktualizuje soubor. Aktualizace přidá doBuildModelmetody následující kód:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Stránky oddělení uživatelského rozhraní
Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:
- Vytvořte složku Pages/Departments .
- Používá
Departmentse pro třídu modelu. - Místo vytvoření nové třídy použijte existující třídu kontextu.
Přidání třídy utility
Ve složce projektu vytvořte Utility třídu s následujícím kódem:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
Třída Utility poskytuje metodu GetLastChars použitou k zobrazení posledních několika znaků tokenu souběžnosti. Následující kód ukazuje kód, který funguje s oběma službami 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
Direktiva #if SQLiteVersion preprocesoru izoluje rozdíly ve verzích SQLite a SQL Server a pomáhá:
- Autor udržuje pro obě verze jeden základ kódu.
- Vývojáři SQLite nasazují aplikaci do Azure a používají SQL Azure.
Sestavte projekt.
Aktualizace indexové stránky
Nástroj pro generování uživatelského rozhraní vytvořil ConcurrencyToken sloupec pro indexovou stránku, ale toto pole se v produkční aplikaci nezobrazí. V tomto kurzu se zobrazí poslední část tohoto ConcurrencyToken kurzu, která vám pomůže ukázat, jak funguje zpracování souběžnosti. Poslední část není zaručená, že bude jedinečná sama o sobě.
Aktualizovat stránky\Departments\Index.cshtml :
- Nahraďte index odděleními.
- Změňte kód obsahující
ConcurrencyTokenjenom několik posledních znaků. - Nahraďte
FirstMidNameFullName.
Aktualizovaná stránka se zobrazí v následujícím kódu:
@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>
Aktualizace modelu upravit stránku
Aktualizujte Pages/Departments/Edit.cshtml.cs následujícím kódem:
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.");
}
}
}
Aktualizace souběžnosti
OriginalValue se aktualizuje o ConcurrencyToken hodnotu z entity, když byla načtena v OnGetAsync metodě.
EF Core vygeneruje SQL UPDATE příkaz s WHERE klauzulí obsahující původní ConcurrencyToken hodnotu. Pokud příkaz UPDATE neobsahuje žádné řádky, DbUpdateConcurrencyException vyvolá se výjimka. Příkaz nemá vliv na UPDATE žádné řádky, pokud žádné řádky nemají původní ConcurrencyToken hodnotu.
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;
V předchozím zvýrazněném kódu:
- Hodnota je
Department.ConcurrencyTokenhodnota, když byla entita načtena vGetpožadavku naEditstránku. Tato hodnota je pro metoduOnPostposkytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje doDepartment.ConcurrencyTokenpořadače modelu. -
OriginalValueje to, co EF Core se používá v klauzuliWHERE. Před spuštěním zvýrazněného řádku kódu:-
OriginalValuemá hodnotu, která byla v databázi, kdyžFirstOrDefaultAsyncbyla volána v této metodě. - Tato hodnota se může lišit od toho, co se zobrazilo na stránce Upravit.
-
- Zvýrazněný kód zajišťuje, že EF Core používá původní
ConcurrencyTokenhodnotu ze zobrazenéDepartmententity v klauzuli příkazuUPDATESQLWHERE.
Následující kód ukazuje Department model.
Department inicializuje se v:
-
OnGetAsyncmetodou podle dotazu EF. -
OnPostAsyncmetoda podle skrytého pole na Razor stránce pomocí vazby modelu:
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;
Předchozí kód ukazuje ConcurrencyToken hodnotu entity z Department požadavku je nastavena na HTTP POST hodnotu z ConcurrencyToken požadavku.HTTP GET
Když dojde k chybě souběžnosti, získá následující zvýrazněný kód hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze.
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)}");
}
Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno: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.");
}
Následující zvýrazněný kód nastaví ConcurrencyToken hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.
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)}");
}
Příkaz ModelState.Remove je povinný, protože ModelState má předchozí ConcurrencyToken hodnotu.
Razor Na stránce ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.
Rozdíly v kódu SQL Serveru vs. SQLite
Následující informace ukazují rozdíly mezi verzemi SQL Serveru a SQLite:
+ 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;
Aktualizace stránky Upravit Razor
Aktualizujte Pages/Departments/Edit.cshtml následujícím kódem:
@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");}
}
Předchozí kód:
- Aktualizuje direktivu
pageod@page@page "{id:int}" - Přidá skrytou verzi řádku.
ConcurrencyTokenmusí být přidán, takže postback sváže hodnotu. - Zobrazí poslední bajt
ConcurrencyTokenpro účely ladění. -
ViewDataNahrazuje silným typemInstructorNameSL.
Konflikty souběžnosti testů se stránkou Upravit
Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
- Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte název na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným ConcurrencyTokenindikátorem. Všimněte si aktualizovaného ConcurrencyTokenindikátoru, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Změňte jiné pole na druhé kartě prohlížeče.
Klikněte na Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:
Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.
Znovu klikněte na Uložit . Hodnota, kterou jste zadali na druhé kartě prohlížeče, se uloží. Uložené hodnoty se zobrazí na stránce Index.
Aktualizace modelu odstranění stránky
Aktualizujte Pages/Departments/Delete.cshtml.cs následujícím kódem:
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 });
}
}
}
}
Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila.
Department.ConcurrencyToken je verze řádku, když byla entita načtena. Při EF Core vytváření SQL DELETE příkazu obsahuje klauzuli WHERE s ConcurrencyToken. Pokud má SQL DELETE příkaz za následek nulový počet ovlivněných řádků:
- Příkaz
ConcurrencyTokenseSQL DELETEv databázi neshodujeConcurrencyToken. - Vyvolá
DbUpdateConcurrencyExceptionse výjimka. -
OnGetAsyncje volána sconcurrencyError.
Aktualizace stránky Odstranit Razor
Aktualizujte Pages/Departments/Delete.cshtml následujícím kódem:
@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>
Předchozí kód provede následující změny:
- Aktualizuje direktivu
pageod@page@page "{id:int}" - Přidá chybovou zprávu.
- Nahradí Jméno FirstMidName úplným názvem v poli Správce .
- Změny
ConcurrencyTokenpro zobrazení posledního bajtu - Přidá skrytou verzi řádku.
ConcurrencyTokenmusí být přidán, takže postback sváže hodnotu.
Konflikty souběžnosti testů
Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
- Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte rozpočet na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným ConcurrencyTokenindikátorem. Všimněte si aktualizovaného ConcurrencyTokenindikátoru, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud ConcurrencyToken nebyla aktualizována.
Vzory podnikových webových aplikací
Pokyny k vytvoření spolehlivé, zabezpečené, výkonné, testovatelné a škálovatelné aplikace ASP.NET Core najdete v vzorech podnikových webových aplikací. K dispozici je kompletní ukázková webová aplikace pro produkční kvalitu, která implementuje vzory.
Dodatečné zdroje
Další kroky
Toto je poslední kurz série. Další témata jsou popsána ve verzi MVC této série kurzů.
V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně (ve stejnou dobu).
Konflikty souběžnosti
Ke konfliktu souběžnosti dochází v případě, že:
- Uživatel přejde na stránku pro úpravy entity.
- Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.
Pokud detekce souběžnosti není povolená, přepíše změny ostatních uživatelů každý, kdo aktualizuje databázi. Pokud je toto riziko přijatelné, náklady na programování pro souběžnost můžou převažovat nad výhodou.
Pesimistické souběžnost (uzamykání)
Jedním zezpůsobůch Tomu se říká pesimistické souběžnost. Než aplikace přečte řádek databáze, který má v úmyslu aktualizovat, požádá o zámek. Jakmile je řádek uzamčen pro přístup k aktualizacím, nebudou moct ostatní uživatelé řádek uzamknout, dokud nebude vydán první zámek.
Správa zámků má nevýhody. Program může být složitý a může způsobovat problémy s výkonem při nárůstu počtu uživatelů. Entity Framework Core neposkytuje žádnou integrovanou podporu a tento kurz neukazuje, jak ho implementovat.
Optimistická souběžnost
Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.
Jane nejprve klikne na Uložit a uvidí, že se její změna projeví, protože v prohlížeči se jako částka rozpočtu zobrazí stránka Index s nulou.
Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti:
Můžete sledovat, kterou vlastnost uživatel upravil, a aktualizovat pouze odpovídající sloupce v databázi.
V tomto scénáři nedojde ke ztrátě žádných dat. Dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup má určité nevýhody:
- Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
- Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
- Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
Johnovi můžete dát možnost přepsat Janovu změnu.
Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje jako klient wins nebo last ve scénáři Wins . (Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat.) Pokud neprovádíte žádné kódování pro zpracování souběžnosti, dojde k automatickému zpracování služby Client Wins.
V databázi můžete zabránit aktualizaci změny Johna. Aplikace obvykle:
- Zobrazí chybovou zprávu.
- Zobrazí aktuální stav dat.
- Umožňuje uživateli znovu použít změny.
Tomu se říká scénář wins ve Storu. (Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem.) V tomto kurzu implementujete scénář Wins pro Store. Tato metoda zajišťuje, že se bez upozornění uživatele nepřepíší žádné změny.
Detekce konfliktů v EF Core
EF Core
DbConcurrencyException vyvolá výjimky, když zjistí konflikty. Datový model musí být nakonfigurovaný tak, aby umožňoval detekci konfliktů. Mezi možnosti povolení detekce konfliktů patří:
Nakonfigurujte EF Core tak, aby zahrnovaly původní hodnoty sloupců nakonfigurovaných jako tokeny souběžnosti v klauzuli Where příkazů Update a Delete.
Při
SaveChangeszavolání klauzule Where vyhledá původní hodnoty všech vlastností anotovaných atributem ConcurrencyCheckAttribute . Příkaz update nenajde řádek, který se má aktualizovat, pokud se od prvního čtení řádku změnily některé vlastnosti tokenu souběžnosti. EF Core interpretuje to jako konflikt souběžnosti. U databázových tabulek s mnoha sloupci může tento přístup vést k velmi velkým klauzulem Where a může vyžadovat velké množství stavu. Proto se tento přístup obecně nedoporučuje a není to metoda použitá v tomto kurzu.V tabulce databáze zahrňte sledovací sloupec, který lze použít k určení, kdy byl řádek změněn.
V databázi SQL Serveru je datový typ sloupce
rowversionsledování . Hodnotarowversionje pořadové číslo, které se při každé aktualizaci řádku zvýší. V příkazu Update nebo Delete klauzule Where obsahuje původní hodnotu sloupce sledování (číslo původní verze řádku). Pokud byl řádek aktualizovaný jiným uživatelem změněn, hodnota verowversionsloupci se liší od původní hodnoty. V takovém případě příkaz Update nebo Delete nemůže najít řádek, který se má aktualizovat kvůli klauzuli Where. EF Core vyvolá výjimku souběžnosti, pokud příkaz Update nebo Delete neovlivní žádné řádky.
Přidání vlastnosti sledování
V Models/Department.csaplikaci přidejte sledovací vlastnost s názvem 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; }
}
}
Atribut TimestampAttribute je to, co identifikuje sloupec jako sloupec sledování souběžnosti. Fluent API je alternativní způsob, jak určit vlastnost sledování:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
U databáze [Timestamp] SQL Serveru atribut vlastnosti entity definované jako pole bajtů:
- Způsobí zahrnutí sloupce do klauzulí DELETE a UPDATE WHERE.
- Nastaví typ sloupce v databázi na rowversion.
Databáze vygeneruje sekvenční číslo verze řádku, které se zvýší při každé aktualizaci řádku.
Update V klauzuli nebo Delete příkaz Where obsahuje načtenou hodnotu verze řádku. Pokud se od načtení řádku změnil řádek:
- Hodnota aktuální verze řádku neodpovídá načtené hodnotě.
-
UpdatePříkazyDeletenenaleznou řádek, protožeWhereklauzule hledá hodnotu verze načteného řádku. - A
DbUpdateConcurrencyExceptionje vyhozen.
Následující kód ukazuje část T-SQL vygenerovanou při EF Core aktualizaci názvu oddělení:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Předchozí zvýrazněný kód zobrazuje klauzuli WHERE obsahující RowVersion. Pokud se databáze RowVersion nerovná parametru (RowVersion), nebudou @p2 aktualizovány žádné řádky.
Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:
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 vrátí počet řádků ovlivněných posledním příkazem. Pokud nejsou aktualizovány žádné řádkyEF Core, DbUpdateConcurrencyException vyvolá chybu .
Aktualizace databáze
RowVersion Přidání vlastnosti změní datový model, který vyžaduje migraci.
Sestavte projekt.
V PMC spusťte následující příkaz:
Add-Migration RowVersion
Tento příkaz:
Migrations/{time stamp}_RowVersion.csVytvoří soubor migrace.Migrations/SchoolContextModelSnapshot.csAktualizuje soubor. Aktualizace přidá doBuildModelmetody následující zvýrazněný kód: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"); });
V PMC spusťte následující příkaz:
Update-Database
Stránky oddělení uživatelského rozhraní
Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:
Vytvořte složku Pages/Departments .
Používá
Departmentse pro třídu modelu.- Místo vytvoření nové třídy použijte existující třídu kontextu.
Sestavte projekt.
Aktualizace indexové stránky
Nástroj pro generování uživatelského rozhraní vytvořil RowVersion sloupec pro indexovou stránku, ale toto pole se v produkční aplikaci nezobrazí. V tomto kurzu se zobrazí poslední bajt RowVersion bajtů, který vám pomůže ukázat, jak funguje zpracování souběžnosti. Poslední bajt není zaručený, že bude jedinečný sám.
Aktualizovat stránky\Departments\Index.cshtml :
- Nahraďte index odděleními.
- Změňte kód obsahující
RowVersiontak, aby zobrazoval pouze poslední bajt pole bajtů. - Nahraďte FirstMidName úplným názvem.
Aktualizovaná stránka se zobrazí v následujícím kódu:
@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>
Aktualizace modelu upravit stránku
Aktualizujte Pages/Departments/Edit.cshtml.cs následujícím kódem:
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.");
}
}
}
Aktualizuje se OriginalValue o rowVersion hodnotu z entity, když byla načtena v OnGetAsync metodě.
EF Core vygeneruje příkaz SQL UPDATE s klauzulí WHERE obsahující původní RowVersion hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion hodnotu), DbUpdateConcurrencyException vyvolá se výjimka.
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;
V předchozím zvýrazněném kódu:
- Hodnota je
Department.RowVersionto, co byla v entitě, když byla původně načtena v požadavku Získat pro stránku Upravit. Tato hodnota je pro metoduOnPostposkytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje doDepartment.RowVersionpořadače modelu. -
OriginalValueje to, co EF Core se použije v klauzuli Where. Před spuštěním zvýrazněného řádku kódu má hodnotu,OriginalValuekterá byla v databázi, kdyžFirstOrDefaultAsyncbyla volána v této metodě, což se může lišit od toho, co se zobrazilo na stránce Upravit. - Zvýrazněný kód zajišťuje, že EF Core používá původní
RowVersionhodnotu ze zobrazenéDepartmententity v klauzuli Where příkazu SQL UPDATE.
Když dojde k chybě souběžnosti, získá následující zvýrazněný kód hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze.
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");
}
Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno: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.");
}
Následující zvýrazněný kód nastaví RowVersion hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.
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");
}
Příkaz ModelState.Remove je povinný, protože ModelState má starou RowVersion hodnotu.
Razor Na stránce ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.
Aktualizace stránky Upravit
Aktualizujte Pages/Departments/Edit.cshtml následujícím kódem:
@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");}
}
Předchozí kód:
- Aktualizuje direktivu
pageod@page@page "{id:int}" - Přidá skrytou verzi řádku.
RowVersionmusí být přidán, takže postback sváže hodnotu. - Zobrazí poslední bajt
RowVersionpro účely ladění. -
ViewDataNahrazuje silným typemInstructorNameSL.
Konflikty souběžnosti testů se stránkou Upravit
Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
- Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte název na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Změňte jiné pole na druhé kartě prohlížeče.
Klikněte na Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:
Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.
Znovu klikněte na Uložit . Hodnota, kterou jste zadali na druhé kartě prohlížeče, se uloží. Uložené hodnoty se zobrazí na stránce Index.
Aktualizace modelu odstranění stránky
Aktualizujte Pages/Departments/Delete.cshtml.cs následujícím kódem:
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 });
}
}
}
}
Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila.
Department.RowVersion je verze řádku, když byla entita načtena. Při EF Core vytváření příkazu SQL DELETE obsahuje klauzuli WHERE s RowVersion. Pokud výsledkem příkazu SQL DELETE je nula řádků:
- Příkaz
RowVersionSQL DELETE se v databázi neshodujeRowVersion. - Vyvolá se výjimka DbUpdateConcurrencyException.
-
OnGetAsyncje volána sconcurrencyError.
Aktualizace stránky Odstranit
Aktualizujte Pages/Departments/Delete.cshtml následujícím kódem:
@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>
Předchozí kód provede následující změny:
- Aktualizuje direktivu
pageod@page@page "{id:int}" - Přidá chybovou zprávu.
- Nahradí Jméno FirstMidName úplným názvem v poli Správce .
- Změny
RowVersionpro zobrazení posledního bajtu - Přidá skrytou verzi řádku.
RowVersionmusí být přidán, takže postback sváže hodnotu.
Konflikty souběžnosti testů
Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
- Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte rozpočet na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud RowVersion nebyla aktualizována.
Pokyny k vytvoření spolehlivé, zabezpečené, výkonné, testovatelné a škálovatelné aplikace ASP.NET Core najdete v vzorech podnikových webových aplikací. K dispozici je kompletní ukázková webová aplikace pro produkční kvalitu, která implementuje vzory.
Dodatečné zdroje
Další kroky
Toto je poslední kurz série. Další témata jsou popsána ve verzi MVC této série kurzů.
V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně (ve stejnou dobu). Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte nebo zobrazte dokončenou aplikaci.Pokyny ke stažení
Konflikty souběžnosti
Ke konfliktu souběžnosti dochází v případě, že:
- Uživatel přejde na stránku pro úpravy entity.
- Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.
Pokud detekce souběžnosti není povolená, když dojde k souběžné aktualizaci:
- Poslední aktualizace vyhrává. To znamená, že poslední hodnoty aktualizace se uloží do databáze.
- První zaktuálních
Optimistická souběžnost
Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.
Jane nejprve klikne na Uložit a zobrazí se její změna, když prohlížeč zobrazí stránku Rejstřík.
Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti.
Optimistická souběžnost zahrnuje následující možnosti:
Můžete sledovat, kterou vlastnost uživatel upravil, a aktualizovat pouze odpovídající sloupce v databázi.
V tomto scénáři nedojde ke ztrátě žádných dat. Dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup:
- Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
- Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
- Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
Johnovi můžete dát možnost přepsat Janovu změnu.
Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje jako klient wins nebo last ve scénáři Wins . (Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat.) Pokud neprovádíte žádné kódování pro zpracování souběžnosti, dojde k automatickému zpracování služby Client Wins.
Můžete zabránit tomu, aby se v databázi aktualizovala změna Johna. Aplikace obvykle:
- Zobrazí chybovou zprávu.
- Zobrazí aktuální stav dat.
- Umožňuje uživateli znovu použít změny.
Tomu se říká scénář wins ve Storu. (Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem.) V tomto kurzu implementujete scénář Wins pro Store. Tato metoda zajišťuje, že se bez upozornění uživatele nepřepíší žádné změny.
Zpracování souběžnosti
Když je vlastnost nakonfigurovaná jako token souběžnosti:
- EF Core Ověřuje, že vlastnost nebyla po načtení změněna. Kontrola nastane, když SaveChanges nebo SaveChangesAsync je volána.
- Pokud byla vlastnost po načtení změněna, DbUpdateConcurrencyException vyvolá se.
Databáze a datový model musí být nakonfigurovány tak, aby podporovaly vyvolání DbUpdateConcurrencyException.
Zjišťování konfliktů souběžnosti u vlastnosti
Konflikty souběžnosti lze zjistit na úrovni vlastnosti pomocí atributu ConcurrencyCheck . Atribut lze použít na více vlastností modelu. Další informace naleznete v tématu Datové poznámky- SouběžnostCheck.
Tento [ConcurrencyCheck] atribut se v tomto kurzu nepoužívá.
Zjišťování konfliktů souběžnosti na řádku
Pokud chcete zjistit konflikty souběžnosti, přidá se do modelu sloupec sledování rowversion .
rowversion :
- Je specifický pro SQL Server. Jiné databáze nemusí poskytovat podobnou funkci.
- Slouží k určení, že se entita od načtení z databáze nezměnila.
Databáze vygeneruje sekvenční rowversion číslo, které se při každé aktualizaci řádku zvýší.
Update V příkazu nebo DeleteWhere příkaz obsahuje klauzule načtenou hodnotu rowversion. Pokud se řádek, který se aktualizuje, změnil:
-
rowversionneodpovídá načtené hodnotě. -
UpdatePříkazyDeletenenaleznou řádek, protožeWhereklauzule obsahuje načtenýrowversion. - A
DbUpdateConcurrencyExceptionje vyhozen.
Pokud EF Corepříkaz nebo Update příkaz neaktualizoval Delete žádné řádky, vyvolá se výjimka souběžnosti.
Přidání vlastnosti sledování do entity Oddělení
V Models/Department.csaplikaci přidejte sledovací vlastnost s názvem 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; }
}
}
Atribut časového razítka určuje, že tento sloupec je součástí Where klauzule Update a Delete příkazů. Atribut je volán Timestamp , protože předchozí verze SQL Serveru používaly datový typ SQL timestamp před tím, než ho typ SQL rowversion nahradil.
Fluent API může také zadat vlastnost sledování:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Následující kód ukazuje část T-SQL vygenerovanou při EF Core aktualizaci názvu oddělení:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Předchozí zvýrazněný kód zobrazuje klauzuli WHERE obsahující RowVersion. Pokud se databáze RowVersion nerovná parametru (RowVersion), nebudou @p2 aktualizovány žádné řádky.
Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:
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 vrátí počet řádků ovlivněných posledním příkazem. V žádném řádku nejsou aktualizoványEF Core, DbUpdateConcurrencyException vyvolá chybu .
T-SQL EF Core se zobrazí v okně výstupu sady Visual Studio.
Aktualizace databáze
Přidáním vlastnosti se RowVersion změní model databáze, který vyžaduje migraci.
Sestavte projekt. Do příkazového okna zadejte následující:
dotnet ef migrations add RowVersion
dotnet ef database update
Předchozí příkazy:
Migrations/{time stamp}_RowVersion.csPřidá soubor migrace.Migrations/SchoolContextModelSnapshot.csAktualizuje soubor. Aktualizace přidá doBuildModelmetody následující zvýrazněný kód:Spustí migrace pro aktualizaci databáze.
Generování modelu oddělení
Postupujte podle pokynů vygenerování modelu studenta a použijte Department ho pro třídu modelu.
Předchozí příkaz vygeneruje Department model. Otevřete projekt v sadě Visual Studio.
Sestavte projekt.
Aktualizace indexové stránky Oddělení
Modul generování uživatelského rozhraní vytvořil RowVersion sloupec pro indexovou stránku, ale toto pole by se nemělo zobrazovat. V tomto kurzu se zobrazí poslední bajt, RowVersion který vám pomůže porozumět souběžnosti. Poslední bajt není zaručený jako jedinečný. Skutečná aplikace by se nezobrazila RowVersion ani poslední bajt RowVersion.
Aktualizujte indexovou stránku:
- Nahraďte index odděleními.
- Nahraďte značky obsahující
RowVersionposlední bajt .RowVersion - Nahraďte FirstMidName úplným názvem.
Aktualizovaná stránka zobrazuje následující kód:
@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>
Aktualizace modelu upravit stránku
Aktualizujte Pages/Departments/Edit.cshtml.cs následujícím kódem:
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.");
}
}
}
Pokud chcete zjistit problém se souběžností, aktualizuje OriginalValue se rowVersion hodnota z entity, kterou byla načtena.
EF Core vygeneruje příkaz SQL UPDATE s klauzulí WHERE obsahující původní RowVersion hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion hodnotu), DbUpdateConcurrencyException vyvolá se výjimka.
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;
V předchozím kódu je hodnota při Department.RowVersion načtení entity.
OriginalValue je hodnota v databázi, když FirstOrDefaultAsync byla volána v této metodě.
Následující kód získá hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze:
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");
}
Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno: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.");
}
Následující zvýrazněný kód nastaví RowVersion hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.
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");
}
Příkaz ModelState.Remove je povinný, protože ModelState má starou RowVersion hodnotu.
Razor Na stránce ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.
Aktualizace stránky Upravit
Aktualizujte Pages/Departments/Edit.cshtml následujícím kódem:
@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");}
}
Předchozí kód:
- Aktualizuje direktivu
pageod@page@page "{id:int}" - Přidá skrytou verzi řádku.
RowVersionmusí být přidán, takže post back binds hodnotu. - Zobrazí poslední bajt
RowVersionpro účely ladění. -
ViewDataNahrazuje silným typemInstructorNameSL.
Konflikty souběžnosti testů se stránkou Upravit
Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
- Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte název na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Změňte jiné pole na druhé kartě prohlížeče.
Klikněte na Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:
Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.
Znovu klikněte na Uložit . Hodnota, kterou jste zadali na druhé kartě prohlížeče, se uloží. Uložené hodnoty se zobrazí na stránce Index.
Aktualizace stránky Odstranit
Aktualizujte model stránky Delete následujícím kódem:
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 });
}
}
}
}
Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila.
Department.RowVersion je verze řádku, když byla entita načtena. Při EF Core vytváření příkazu SQL DELETE obsahuje klauzuli WHERE s RowVersion. Pokud výsledkem příkazu SQL DELETE je nula řádků:
- Příkaz
RowVersionSQL DELETE se v databázi neshodujeRowVersion. - Vyvolá se výjimka DbUpdateConcurrencyException.
-
OnGetAsyncje volána sconcurrencyError.
Aktualizace stránky Odstranit
Aktualizujte Pages/Departments/Delete.cshtml následujícím kódem:
@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>
Předchozí kód provede následující změny:
- Aktualizuje direktivu
pageod@page@page "{id:int}" - Přidá chybovou zprávu.
- Nahradí Jméno FirstMidName úplným názvem v poli Správce .
- Změny
RowVersionpro zobrazení posledního bajtu - Přidá skrytou verzi řádku.
RowVersionmusí být přidán, takže post back binds hodnotu.
Test souběžnosti koliduje se stránkou Odstranit
Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím oddělení:
- Spusťte aplikaci a vyberte Oddělení.
- Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
- Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.
Na dvou kartách prohlížeče se zobrazují stejné informace.
Změňte rozpočet na první kartě prohlížeče a klikněte na Uložit.
V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.
Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud RowVersion nebyla aktualizována.
Přečtěte si téma Dědičnost , jak dědit datový model.
Vzory podnikových webových aplikací
Pokyny k vytvoření spolehlivé, zabezpečené, výkonné, testovatelné a škálovatelné aplikace ASP.NET Core najdete v vzorech podnikových webových aplikací. K dispozici je kompletní ukázková webová aplikace pro produkční kvalitu, která implementuje vzory.