Události
Mistrovství Světa v Power BI DataViz
14. 2. 16 - 31. 3. 16
Se 4 šance na vstup, můžete vyhrát konferenční balíček a udělat to na LIVE Grand Finale v Las Vegas
Další informaceTento prohlížeč se už nepodporuje.
Upgradujte na Microsoft Edge, abyste mohli využívat nejnovější funkce, aktualizace zabezpečení a technickou podporu.
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ě.
Ke konfliktu souběžnosti dochází v případě, že:
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.
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 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:
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:
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.
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:
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.
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:
ConcurrencyToken
vlastnosti na pole bajtů.
byte[]
je požadovaný typ pro SQL Server.IsConcurrencyToken
nakonfiguruje 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í.ConcurrencyToken
př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 .
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.cs
Vytvoří soubor migrace.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje soubor. Aktualizace přidá do BuildModel
metody následující kód: b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:
Department
se pro třídu modelu.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á:
Sestavte projekt.
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 :
ConcurrencyToken
jenom několik posledních znaků.FirstMidName
FullName
.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>
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.");
}
}
}
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:
Department.ConcurrencyToken
hodnota, když byla entita načtena v Get
požadavku na Edit
stránku. Tato hodnota je pro metodu OnPost
poskytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje do Department.ConcurrencyToken
pořadače modelu.OriginalValue
je to, co EF Core se používá v klauzuli WHERE
. Před spuštěním zvýrazněného řádku kódu: OriginalValue
má hodnotu, která byla v databázi, když FirstOrDefaultAsync
byla volána v této metodě.ConcurrencyToken
hodnotu ze zobrazené Department
entity v klauzuli příkazu UPDATE
SQLWHERE
.Následující kód ukazuje Department
model.
Department
inicializuje se v:
OnGetAsync
metodou podle dotazu EF.OnPostAsync
metoda 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.
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;
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:
page
od @page
@page "{id:int}"
ConcurrencyToken
musí být přidán, takže postback sváže hodnotu.ConcurrencyToken
pro účely ladění.ViewData
Nahrazuje silným typem InstructorNameSL
.Otevřete dvě instance prohlížeče pro úpravy v anglickém 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 ConcurrencyToken
indikátorem. Všimněte si aktualizovaného ConcurrencyToken
indiká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.
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ů:
ConcurrencyToken
se SQL DELETE
v databázi neshoduje ConcurrencyToken
.DbUpdateConcurrencyException
se výjimka.OnGetAsync
je volána s concurrencyError
.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:
page
od @page
@page "{id:int}"
ConcurrencyToken
pro zobrazení posledního bajtuConcurrencyToken
musí být přidán, takže postback sváže hodnotu.Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím 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 ConcurrencyToken
indikátorem. Všimněte si aktualizovaného ConcurrencyToken
indiká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.
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.
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).
Ke konfliktu souběžnosti dochází v případě, že:
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.
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 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:
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:
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.
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 SaveChanges
zavolá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 rowversion
sledování . Hodnota rowversion
je 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 ve rowversion
sloupci 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.
V Models/Department.cs
aplikaci 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ů:
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:
Update
Příkazy Delete
nenaleznou řádek, protože Where
klauzule hledá hodnotu verze načteného řádku.DbUpdateConcurrencyException
je 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 .
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.cs
Vytvoří soubor migrace.
Migrations/SchoolContextModelSnapshot.cs
Aktualizuje soubor. Aktualizace přidá do BuildModel
metody 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
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á Department
se pro třídu modelu.
Sestavte projekt.
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 :
RowVersion
tak, aby zobrazoval pouze poslední bajt pole bajtů.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>
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:
Department.RowVersion
to, co byla v entitě, když byla původně načtena v požadavku Získat pro stránku Upravit. Tato hodnota je pro metodu OnPost
poskytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje do Department.RowVersion
pořadače modelu.OriginalValue
je 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, OriginalValue
která byla v databázi, když FirstOrDefaultAsync
byla volána v této metodě, což se může lišit od toho, co se zobrazilo na stránce Upravit.RowVersion
hodnotu ze zobrazené Department
entity 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.
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:
page
od @page
@page "{id:int}"
RowVersion
musí být přidán, takže postback sváže hodnotu.RowVersion
pro účely ladění.ViewData
Nahrazuje silným typem InstructorNameSL
.Otevřete dvě instance prohlížeče pro úpravy v anglickém 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.
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ů:
RowVersion
SQL DELETE se v databázi neshoduje RowVersion
.OnGetAsync
je volána s concurrencyError
.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:
page
od @page
@page "{id:int}"
RowVersion
pro zobrazení posledního bajtuRowVersion
musí být přidán, takže postback sváže hodnotu.Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím 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.
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í
Ke konfliktu souběžnosti dochází v případě, že:
Pokud detekce souběžnosti není povolená, když dojde k souběžné aktualizaci:
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:
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:
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.
Když je vlastnost nakonfigurovaná jako token souběžnosti:
Databáze a datový model musí být nakonfigurovány tak, aby podporovaly vyvolání DbUpdateConcurrencyException
.
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á.
Pokud chcete zjistit konflikty souběžnosti, přidá se do modelu sloupec sledování rowversion .
rowversion
:
Databáze vygeneruje sekvenční rowversion
číslo, které se při každé aktualizaci řádku zvýší.
Update
V příkazu nebo Delete
Where
příkaz obsahuje klauzule načtenou hodnotu rowversion
. Pokud se řádek, který se aktualizuje, změnil:
rowversion
neodpovídá načtené hodnotě.Update
Příkazy Delete
nenaleznou řádek, protože Where
klauzule obsahuje načtený rowversion
.DbUpdateConcurrencyException
je vyhozen.Pokud EF Corepříkaz nebo Update
příkaz neaktualizoval Delete
žádné řádky, vyvolá se výjimka souběžnosti.
V Models/Department.cs
aplikaci 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.
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.cs
Přidá soubor migrace.
Migrations/SchoolContextModelSnapshot.cs
Aktualizuje soubor. Aktualizace přidá do BuildModel
metody následující zvýrazněný kód:
Spustí migrace pro aktualizaci databáze.
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.
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:
RowVersion
poslední bajt .RowVersion
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>
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.
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:
page
od @page
@page "{id:int}"
RowVersion
musí být přidán, takže post back binds hodnotu.RowVersion
pro účely ladění.ViewData
Nahrazuje silným typem InstructorNameSL
.Otevřete dvě instance prohlížeče pro úpravy v anglickém 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.
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ů:
RowVersion
SQL DELETE se v databázi neshoduje RowVersion
.OnGetAsync
je volána s concurrencyError
.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:
page
od @page
@page "{id:int}"
RowVersion
pro zobrazení posledního bajtuRowVersion
musí být přidán, takže post back binds hodnotu.Vytvoření testovacího oddělení
Otevřete dvě instance odstranění v testovacím 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.
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.
Zpětná vazba k produktu ASP.NET Core
ASP.NET Core je open source projekt. Vyberte odkaz pro poskytnutí zpětné vazby:
Události
Mistrovství Světa v Power BI DataViz
14. 2. 16 - 31. 3. 16
Se 4 šance na vstup, můžete vyhrát konferenční balíček a udělat to na LIVE Grand Finale v Las Vegas
Další informaceŠkolení
Modul
Tento modul vás provede procesem ověření kódu pro konzolovou aplikaci jazyka C#. Praktické aktivity poskytují zkušenosti s identifikací problémů logiky, laděním aplikace a implementací zpracování výjimek. K dispozici jsou podrobné pokyny.
Dokumentace
Část 7, Razor Stránky s EF Core v ASP.NET Core – aktualizace souvisejících dat
7 Razor . část série kurzů Stránky a Entity Framework
Část 5, Razor Stránky s EF Core ASP.NET Jádrem – datový model
5 Razor . část série kurzů Stránky a Entity Framework
Část 6, Razor stránky s EF Core ASP.NET jádrem – čtení souvisejících dat
6 Razor . část série kurzů Stránky a Entity Framework