Część 8, Razor strony z EF Core w ASP.NET Core — współbieżność
Aplikacja internetowa Contoso University pokazuje, jak tworzyć Razor aplikacje internetowe stron przy użyciu programu EF Core Visual Studio. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek.
Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację i porównaj ten kod z utworzonymi elementami, wykonując czynności opisane w samouczku.
W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników jednocześnie aktualizuje jednostkę.
Konflikty współbieżności
Konflikt współbieżności występuje, gdy:
- Użytkownik przechodzi do strony edycji jednostki.
- Inny użytkownik aktualizuje tę samą jednostkę przed zapisaniem zmiany pierwszego użytkownika w bazie danych.
Jeśli wykrywanie współbieżności nie jest włączone, kto ostatnio aktualizuje bazę danych, zastępuje zmiany innego użytkownika. Jeśli to ryzyko jest akceptowalne, koszt programowania współbieżności może przeważyć nad korzyścią.
Pesymistyczna współbieżność
Jednym ze sposobów zapobiegania konfliktom współbieżności jest użycie blokad bazy danych. Jest to nazywane pesymistyczną współbieżnością. Zanim aplikacja odczytuje wiersz bazy danych, który zamierza zaktualizować, żąda blokady. Po zablokowaniu wiersza dostępu do aktualizacji żaden inny użytkownik nie może zablokować wiersza do momentu zwolnienia pierwszej blokady.
Zarządzanie blokadami ma wady. Program może być złożony i może powodować problemy z wydajnością w miarę wzrostu liczby użytkowników. Platforma Entity Framework Core nie zapewnia wbudowanej obsługi pesymistycznej współbieżności.
Optymistyczna współbieżność
Optymistyczna współbieżność umożliwia wystąpieniu konfliktów współbieżności, a następnie odpowiednio reaguje podczas ich wykonywania. Na przykład Jane odwiedza stronę edycji Departamentu i zmienia budżet dla działu angielskiego z 350 000,000 USD do 0,00 USD.
Zanim Jane kliknie pozycję Zapisz, Jan odwiedzi tę samą stronę i zmieni pole Data rozpoczęcia z 2007-09-1.2013.
Jane najpierw klika pozycję Zapisz i widzi, że jej zmiana zostanie w życie, ponieważ w przeglądarce zostanie wyświetlona strona Indeks z wartością zero jako kwota budżetu.
Jan klika pozycję Zapisz na stronie Edycja, która nadal pokazuje budżet w wysokości 350 000,000 USD. Co się stanie dalej, zależy od sposobu obsługi konfliktów współbieżności:
Śledź, która właściwość użytkownika zmodyfikowała i zaktualizuj tylko odpowiednie kolumny w bazie danych.
W scenariuszu żadne dane nie zostaną utracone. Różne właściwości zostały zaktualizowane przez dwóch użytkowników. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane's, jak i Johna. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych. Takie podejście ma pewne wady:
- Nie można uniknąć utraty danych, jeśli konkurencyjne zmiany są wprowadzane do tej samej właściwości.
- Zazwyczaj nie jest praktyczne w aplikacji internetowej. Wymaga to utrzymania znaczącego stanu w celu śledzenia wszystkich pobranych wartości i nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji.
- Może zwiększyć złożoność aplikacji w porównaniu z wykrywaniem współbieżności w jednostce.
Zmieńmy zmianę Jane'a.
Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i pobraną wartość $350,000.000. Takie podejście jest nazywane scenariuszem Wins klienta lub Last in Wins . Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych. Kod szkieletowy nie obsługuje współbieżności. Usługa Wins klienta odbywa się automatycznie.
Uniemożliwianie aktualizowania zmiany Johna w bazie danych. Zazwyczaj aplikacja:
- Wyświetl komunikat o błędzie.
- Pokaż bieżący stan danych.
- Zezwalaj użytkownikowi na ponowne zastosowania zmian.
Jest to nazywane scenariuszem Store Wins . Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta. Scenariusz Store Wins jest używany w tym samouczku. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez zgłaszania alertów przez użytkownika.
Wykrywanie konfliktów w programie EF Core
Właściwości skonfigurowane jako tokeny współbieżności są używane do implementowania optymistycznej kontroli współbieżności. Gdy operacja aktualizacji lub usuwania jest wyzwalana przez SaveChanges element lub SaveChangesAsync, wartość tokenu współbieżności w bazie danych jest porównywana z oryginalną wartością odczytaną przez EF Core:
- Jeśli wartości są zgodne, operacja może zakończyć się.
- Jeśli wartości nie są zgodne, zakłada, EF Core że inny użytkownik wykonał operację powodującą konflikt, przerywa bieżącą transakcję i zgłasza błąd DbUpdateConcurrencyException.
Inny użytkownik lub proces wykonujący operację, która powoduje konflikt z bieżącą operacją, jest nazywana konfliktem współbieżności.
W relacyjnych bazach danych EF Core sprawdza wartość tokenu współbieżności w WHERE
klauzuli UPDATE
i DELETE
instrukcji w celu wykrycia konfliktu współbieżności.
Aby umożliwić wykrywanie konfliktów, należy skonfigurować model danych, uwzględniając kolumnę śledzenia, która może służyć do określenia, kiedy wiersz został zmieniony. Platforma EF oferuje dwa podejścia do tokenów współbieżności:
[ConcurrencyCheck]
Stosowanie właściwości lub IsConcurrencyToken do właściwości w modelu. Takie podejście nie jest zalecane. Aby uzyskać więcej informacji, zobacz Tokeny współbieżności w programie EF Core.TimestampAttribute Stosowanie lub IsRowVersion do tokenu współbieżności w modelu. Jest to podejście używane w tym samouczku.
Podejście do programu SQL Server i szczegóły implementacji SQLite są nieco inne. W dalszej części samouczka przedstawiono plik różnic. Na karcie Programu Visual Studio przedstawiono podejście programu SQL Server. Na karcie Visual Studio Code przedstawiono podejście do baz danych innych niż SQL Server, takich jak SQLite.
- W modelu dołącz kolumnę śledzenia, która służy do określania, kiedy wiersz został zmieniony.
- Zastosuj właściwość TimestampAttribute do właściwości współbieżności.
Models/Department.cs
Zaktualizuj plik przy użyciu następującego wyróżnionego kodu:
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; }
}
}
Element TimestampAttribute określa kolumnę jako kolumnę śledzenia współbieżności. Płynny interfejs API to alternatywny sposób określania właściwości śledzenia:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Atrybut [Timestamp]
we właściwości jednostki generuje następujący kod w metodzie ModelBuilder :
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Powyższy kod ma następujące działanie:
- Ustawia typ
ConcurrencyToken
właściwości na tablicę bajtów.byte[]
jest wymaganym typem programu SQL Server. - Wywołuje IsConcurrencyToken.
IsConcurrencyToken
Konfiguruje właściwość jako token współbieżności. W przypadku aktualizacji wartość tokenu współbieżności w bazie danych jest porównywana z oryginalną wartością, aby upewnić się, że nie uległa zmianie od czasu pobrania wystąpienia z bazy danych. W przypadku zmiany zostanie zgłoszony element , a DbUpdateConcurrencyException zmiany nie zostaną zastosowane. - Wywołuje ValueGeneratedOnAddOrUpdatemetodę , która konfiguruje
ConcurrencyToken
właściwość tak, aby wartość została wygenerowana automatycznie podczas dodawania lub aktualizowania jednostki. HasColumnType("rowversion")
Ustawia typ kolumny w bazie danych programu SQL Server na rowversion.
Poniższy kod przedstawia część języka T-SQL wygenerowaną przez EF Core po zaktualizowaniu Department
nazwy:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Powyższy wyróżniony kod przedstawia klauzulę zawierającą WHERE
ConcurrencyToken
. Jeśli baza danych ConcurrencyToken
nie jest równa parametrowi ConcurrencyToken
@p2
, nie są aktualizowane żadne wiersze.
Poniższy wyróżniony kod przedstawia język T-SQL, który weryfikuje dokładnie jeden wiersz został zaktualizowany:
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 zwraca liczbę wierszy, których dotyczy ostatnia instrukcja. Jeśli wiersze nie są aktualizowane, EF Core zwraca wartość DbUpdateConcurrencyException
.
Dodawanie migracji
ConcurrencyToken
Dodanie właściwości zmienia model danych, który wymaga migracji.
Skompiluj projekt.
Uruchom następujące polecenia w usłudze PMC:
Add-Migration RowVersion
Update-Database
Poprzednie polecenia:
Migrations/{time stamp}_RowVersion.cs
Tworzy plik migracji.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje plik. Aktualizacja dodaje następujący kod doBuildModel
metody :
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Strony działu szkieletu
Postępuj zgodnie z instrukcjami na stronach Szkielet studenta z następującymi wyjątkami:
- Utwórz folder Strony/Działy.
- Użyj dla
Department
klasy modelu. - Użyj istniejącej klasy kontekstu zamiast utworzyć nową.
Dodawanie klasy narzędzi
W folderze projektu utwórz klasę Utility
przy użyciu następującego kodu:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
Klasa Utility
udostępnia metodę GetLastChars
używaną do wyświetlania kilku ostatnich znaków tokenu współbieżności. Poniższy kod przedstawia kod, który działa z programem SQL Server w obu usługach SQLite:
#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
Dyrektywa #if SQLiteVersion
preprocesora izoluje różnice w wersjach SQLite i SQL Server i pomaga:
- Autor utrzymuje jedną bazę kodu dla obu wersji.
- Deweloperzy SQLite wdrażają aplikację na platformie Azure i używają Usługi SQL Azure.
Skompiluj projekt.
Aktualizowanie strony Indeks
Narzędzie do tworzenia szkieletu utworzyło kolumnę ConcurrencyToken
dla strony Indeks, ale to pole nie będzie wyświetlane w aplikacji produkcyjnej. W tym samouczku zostanie wyświetlona ostatnia część elementu ConcurrencyToken
, aby pokazać, jak działa obsługa współbieżności. Ostatnia część nie ma gwarancji, że jest unikatowa sama w sobie.
Zaktualizuj stronę Pages\Departments\Index.cshtml :
- Zastąp indeks działem.
- Zmień kod zawierający
ConcurrencyToken
, aby pokazać tylko kilka ostatnich znaków. - Zamień
FirstMidName
naFullName
.
Poniższy kod przedstawia zaktualizowaną stronę:
@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>
Aktualizowanie modelu strony Edycji
Zaktualizuj Pages/Departments/Edit.cshtml.cs
za pomocą następującego kodu:
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.");
}
}
}
Aktualizacje współbieżności
OriginalValue element jest aktualizowany przy użyciu ConcurrencyToken
wartości z jednostki, która została pobrana w metodzie OnGetAsync
. EF Core Generuje SQL UPDATE
polecenie z klauzulą WHERE
zawierającą oryginalną ConcurrencyToken
wartość. Jeśli polecenie nie ma wpływu na UPDATE
żadne wiersze, zgłaszany DbUpdateConcurrencyException
jest wyjątek. Żadne wiersze nie mają wpływu na UPDATE
polecenie, gdy żadne wiersze nie mają oryginalnej ConcurrencyToken
wartości.
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;
W poprzednim wyróżnionym kodzie:
- Wartość w
Department.ConcurrencyToken
pliku to wartość, gdy jednostka została pobrana w żądaniuGet
Edit
dla strony. Wartość jest udostępniana metodzieOnPost
przez ukryte pole na Razor stronie, na których wyświetlana jest jednostka do edycji. Wartość pola ukrytego jest kopiowana przezDepartment.ConcurrencyToken
powiązanie modelu. OriginalValue
to, co EF Core używa w klauzuliWHERE
. Przed wykonaniem wyróżnionego wiersza kodu:OriginalValue
ma wartość, która znajdowała się w bazie danych, gdyFirstOrDefaultAsync
została wywołana w tej metodzie.- Ta wartość może się różnić od tego, co zostało wyświetlone na stronie Edycja.
- Wyróżniony kod zapewnia, że EF Core używa oryginalnej
ConcurrencyToken
wartości z wyświetlanejDepartment
jednostki w klauzuli instrukcjiWHERE
SQLUPDATE
.
Poniższy kod przedstawia Department
model. Department
element jest inicjowany w:
OnGetAsync
metoda przez zapytanie EF.OnPostAsync
metoda według ukrytego pola na Razor stronie przy użyciu powiązania 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;
Powyższy kod pokazuje ConcurrencyToken
wartość jednostki z HTTP POST
żądania jest ustawiona ConcurrencyToken
na wartość z HTTP GET
Department
żądania.
W przypadku wystąpienia błędu współbieżności wyróżniony kod pobiera wartości klienta (wartości opublikowane w tej metodzie) i wartości bazy danych.
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)}");
}
Poniższy kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych inne niż to, co zostało opublikowane w pliku 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.");
}
Poniższy wyróżniony kod ustawia ConcurrencyToken
wartość na nową wartość pobraną z bazy danych. Następnym razem, gdy użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu ostatniego wyświetlenia strony Edytuj.
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)}");
}
Instrukcja jest wymagana ModelState.Remove
, ponieważ ModelState
ma poprzednią ConcurrencyToken
wartość. Na stronie Razor ModelState
wartość pola ma pierwszeństwo przed wartościami właściwości modelu, gdy oba te wartości są obecne.
Różnice w kodzie programu SQL Server a SQLite
Poniżej przedstawiono różnice między wersjami programu SQL Server i 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;
Aktualizowanie strony Edytuj Razor
Zaktualizuj Pages/Departments/Edit.cshtml
za pomocą następującego kodu:
@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");}
}
Powyższy kod ma następujące działanie:
- Aktualizuje dyrektywę
page
z@page
do@page "{id:int}"
. - Dodaje ukrytą wersję wiersza.
ConcurrencyToken
należy dodać wartość , więc postback wiąże wartość. - Wyświetla ostatni bajt
ConcurrencyToken
dla celów debugowania. ViewData
Zastępuje element silnie typizowaneInstructorNameSL
.
Testowanie konfliktów współbieżności ze stroną Edytuj
Otwórz dwa wystąpienia przeglądarki Edit w dziale angielskim:
- Uruchom aplikację i wybierz pozycję Działy.
- Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz polecenie Otwórz na nowej karcie.
- Na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.
Dwie karty przeglądarki wyświetlają te same informacje.
Zmień nazwę na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.
W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym ConcurrencyToken
wskaźnikiem. Zwróć uwagę na zaktualizowany ConcurrencyToken
wskaźnik, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.
Zmień inne pole na drugiej karcie przeglądarki.
Kliknij przycisk Zapisz. Zobaczysz komunikaty o błędach dla wszystkich pól, które nie są zgodne z wartościami bazy danych:
To okno przeglądarki nie zamierzało zmienić pola Nazwa. Skopiuj i wklej bieżącą wartość (Języki) do pola Nazwa. Na karcie. Walidacja po stronie klienta usuwa komunikat o błędzie.
Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości zostaną wyświetlone na stronie Indeks.
Aktualizowanie modelu strony Usuń
Zaktualizuj Pages/Departments/Delete.cshtml.cs
za pomocą następującego kodu:
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 });
}
}
}
}
Strona Usuń wykrywa konflikty współbieżności, gdy jednostka uległa zmianie po jej pobraniu. Department.ConcurrencyToken
to wersja wiersza, gdy jednostka została pobrana. Podczas EF Core tworzenia SQL DELETE
polecenia zawiera klauzulę WHERE z ConcurrencyToken
. SQL DELETE
Jeśli polecenie spowoduje, że nie ma to wpływu na wiersze zerowe:
- Polecenie
ConcurrencyToken
w poleceniuSQL DELETE
nie jest zgodneConcurrencyToken
z bazą danych. - Zgłaszany
DbUpdateConcurrencyException
jest wyjątek. OnGetAsync
element jest wywoływany za pomocą .concurrencyError
Aktualizowanie strony Usuwanie Razor
Zaktualizuj Pages/Departments/Delete.cshtml
za pomocą następującego kodu:
@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>
Powyższy kod wprowadza następujące zmiany:
- Aktualizuje dyrektywę
page
z@page
do@page "{id:int}"
. - Dodaje komunikat o błędzie.
- Zastępuje wartość FirstMidName wartością FullName w polu Administrator .
- Zmiany
ConcurrencyToken
w celu wyświetlenia ostatniego bajtu. - Dodaje ukrytą wersję wiersza.
ConcurrencyToken
należy dodać wartość , więc postback wiąże wartość.
Testowanie konfliktów współbieżności
Utwórz dział testów.
Otwórz dwa wystąpienia przeglądarki Usuń w dziale testowym:
- Uruchom aplikację i wybierz pozycję Działy.
- Kliknij prawym przyciskiem myszy hiperlink Usuń dla działu testów i wybierz polecenie Otwórz na nowej karcie.
- Kliknij hiperlink Edytuj dla działu testów.
Dwie karty przeglądarki wyświetlają te same informacje.
Zmień budżet na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.
W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym ConcurrencyToken
wskaźnikiem. Zwróć uwagę na zaktualizowany ConcurrencyToken
wskaźnik, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.
Usuń dział testów z drugiej karty. Zostanie wyświetlony błąd współbieżności z bieżącymi wartościami z bazy danych. Kliknięcie przycisku Usuń powoduje usunięcie jednostki, chyba że ConcurrencyToken
została zaktualizowana.
Dodatkowe zasoby
- Tokeny współbieżności w programie EF Core
- Obsługa współbieżności w programie EF Core
- Debugowanie źródła ASP.NET Core 2.x
Następne kroki
Jest to ostatni samouczek z serii. Dodatkowe tematy zostały omówione w wersji MVC tej serii samouczków.
W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników jednocześnie aktualizuje jednostkę (jednocześnie).
Konflikty współbieżności
Konflikt współbieżności występuje, gdy:
- Użytkownik przechodzi do strony edycji jednostki.
- Inny użytkownik aktualizuje tę samą jednostkę przed zapisaniem zmiany pierwszego użytkownika w bazie danych.
Jeśli wykrywanie współbieżności nie jest włączone, kto ostatnio aktualizuje bazę danych, zastępuje zmiany innego użytkownika. Jeśli to ryzyko jest akceptowalne, koszt programowania współbieżności może przeważyć nad korzyścią.
Pesymistyczna współbieżność (blokowanie)
Jednym ze sposobów zapobiegania konfliktom współbieżności jest użycie blokad bazy danych. Jest to nazywane pesymistyczną współbieżnością. Zanim aplikacja odczytuje wiersz bazy danych, który zamierza zaktualizować, żąda blokady. Po zablokowaniu wiersza dostępu do aktualizacji żaden inny użytkownik nie może zablokować wiersza do momentu zwolnienia pierwszej blokady.
Zarządzanie blokadami ma wady. Program może być złożony i może powodować problemy z wydajnością w miarę wzrostu liczby użytkowników. Program Entity Framework Core nie zapewnia wbudowanej obsługi, a w tym samouczku nie pokazano, jak ją zaimplementować.
Optymistyczna współbieżność
Optymistyczna współbieżność umożliwia wystąpieniu konfliktów współbieżności, a następnie odpowiednio reaguje podczas ich wykonywania. Na przykład Jane odwiedza stronę edycji Departamentu i zmienia budżet dla działu angielskiego z 350 000,000 USD do 0,00 USD.
Zanim Jane kliknie pozycję Zapisz, Jan odwiedzi tę samą stronę i zmieni pole Data rozpoczęcia z 2007-09-1.2013.
Jane najpierw klika pozycję Zapisz i widzi, że jej zmiana zostanie w życie, ponieważ w przeglądarce zostanie wyświetlona strona Indeks z wartością zero jako kwota budżetu.
Jan klika pozycję Zapisz na stronie Edycja, która nadal pokazuje budżet w wysokości 350 000,000 USD. Co się stanie dalej, zależy od sposobu obsługi konfliktów współbieżności:
Możesz śledzić, która właściwość użytkownika zmodyfikowała i zaktualizować tylko odpowiednie kolumny w bazie danych.
W scenariuszu żadne dane nie zostaną utracone. Różne właściwości zostały zaktualizowane przez dwóch użytkowników. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane's, jak i Johna. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych. Takie podejście ma pewne wady:
- Nie można uniknąć utraty danych, jeśli konkurencyjne zmiany są wprowadzane do tej samej właściwości.
- Zazwyczaj nie jest praktyczne w aplikacji internetowej. Wymaga to utrzymania znaczącego stanu w celu śledzenia wszystkich pobranych wartości i nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji.
- Może zwiększyć złożoność aplikacji w porównaniu z wykrywaniem współbieżności w jednostce.
Możesz pozwolić Jane's change zastąpić zmianę Jane.
Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i pobraną wartość $350,000.000. Takie podejście jest nazywane scenariuszem Wins klienta lub Last in Wins . (Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych). Jeśli nie wykonasz żadnego kodowania na potrzeby obsługi współbieżności, usługa Wins klienta odbywa się automatycznie.
Możesz uniemożliwić aktualizację johna w bazie danych. Zazwyczaj aplikacja:
- Wyświetl komunikat o błędzie.
- Pokaż bieżący stan danych.
- Zezwalaj użytkownikowi na ponowne zastosowania zmian.
Jest to nazywane scenariuszem Store Wins . (Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta). W tym samouczku zaimplementujesz scenariusz Store Wins. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez zgłaszania alertów przez użytkownika.
Wykrywanie konfliktów w programie EF Core
EF CoreDbConcurrencyException
zgłasza wyjątki podczas wykrywania konfliktów. Aby umożliwić wykrywanie konfliktów, należy skonfigurować model danych. Opcje włączania wykrywania konfliktów obejmują następujące elementy:
Skonfiguruj EF Core , aby uwzględnić oryginalne wartości kolumn skonfigurowanych jako tokeny współbieżności w klauzuli Where poleceń Update and Delete.
Po
SaveChanges
wywołaniu klauzula Where wyszukuje oryginalne wartości wszystkich właściwości z adnotacjami z atrybutem ConcurrencyCheckAttribute . Instrukcja update nie znajdzie wiersza do zaktualizowania, jeśli którakolwiek z właściwości tokenu współbieżności została zmieniona, ponieważ wiersz został po raz pierwszy odczytany. EF Core interpretuje to jako konflikt współbieżności. W przypadku tabel baz danych, które mają wiele kolumn, takie podejście może spowodować bardzo duże klauzule Where i może wymagać dużej ilości stanu. W związku z tym takie podejście nie jest zwykle zalecane i nie jest to metoda używana w tym samouczku.W tabeli bazy danych dołącz kolumnę śledzenia, która może służyć do określenia, kiedy wiersz został zmieniony.
W bazie danych programu SQL Server typ danych kolumny śledzenia to
rowversion
. Wartośćrowversion
jest sekwencyjną liczbą, która jest zwiększana za każdym razem, gdy wiersz jest aktualizowany. W poleceniu Aktualizuj lub Usuń klauzula Where zawiera oryginalną wartość kolumny śledzenia (oryginalny numer wersji wiersza). Jeśli aktualizowany wiersz został zmieniony przez innego użytkownika, wartość wrowversion
kolumnie różni się od oryginalnej wartości. W takim przypadku instrukcja Update or Delete nie może odnaleźć wiersza do zaktualizowania z powodu klauzuli Where. EF Core zgłasza wyjątek współbieżności, gdy żadne wiersze nie mają wpływu na polecenie Aktualizuj lub Usuń.
Dodawanie właściwości śledzenia
W Models/Department.cs
pliku dodaj właściwość śledzenia o nazwie 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; }
}
}
Atrybut TimestampAttribute określa kolumnę jako kolumnę śledzenia współbieżności. Płynny interfejs API to alternatywny sposób określania właściwości śledzenia:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
W przypadku bazy danych [Timestamp]
programu SQL Server atrybut właściwości jednostki zdefiniowany jako tablica bajtów:
- Powoduje, że kolumna zostanie uwzględniona w klauzulach DELETE i UPDATE WHERE.
- Ustawia typ kolumny w bazie danych na rowversion.
Baza danych generuje sekwencyjny numer wersji wiersza, który jest zwiększany za każdym razem, gdy wiersz jest aktualizowany. W poleceniu Update
lub Delete
klauzula Where
zawiera pobraną wartość wersji wiersza. Jeśli aktualizowany wiersz uległ zmianie od czasu pobrania:
- Bieżąca wartość wersji wiersza jest niezgodna z pobraną wartością.
- Polecenia
Update
lubDelete
nie znajdują wiersza, ponieważWhere
klauzula szuka pobranej wartości wersji wiersza. - Jest
DbUpdateConcurrencyException
zgłaszany.
Poniższy kod przedstawia część języka T-SQL wygenerowaną przez EF Core po zaktualizowaniu nazwy działu:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Powyższy wyróżniony kod przedstawia klauzulę zawierającą WHERE
RowVersion
. Jeśli baza danych RowVersion
nie jest równa parametrowi RowVersion
(@p2
), nie są aktualizowane żadne wiersze.
Poniższy wyróżniony kod przedstawia język T-SQL, który weryfikuje dokładnie jeden wiersz został zaktualizowany:
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 zwraca liczbę wierszy, których dotyczy ostatnia instrukcja. Jeśli wiersze nie są aktualizowane, EF Core zwraca wartość DbUpdateConcurrencyException
.
Aktualizowanie bazy danych
RowVersion
Dodanie właściwości zmienia model danych, który wymaga migracji.
Skompiluj projekt.
Uruchom następujące polecenie w usłudze PMC:
Add-Migration RowVersion
To polecenie:
Migrations/{time stamp}_RowVersion.cs
Tworzy plik migracji.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje plik. Aktualizacja dodaje następujący wyróżniony kod doBuildModel
metody :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"); });
Uruchom następujące polecenie w usłudze PMC:
Update-Database
Strony działu szkieletu
Postępuj zgodnie z instrukcjami na stronach Szkielet studenta z następującymi wyjątkami:
Utwórz folder Strony/Działy.
Użyj dla
Department
klasy modelu.- Użyj istniejącej klasy kontekstu zamiast utworzyć nową.
Skompiluj projekt.
Aktualizowanie strony Indeks
Narzędzie do tworzenia szkieletu utworzyło kolumnę RowVersion
dla strony Indeks, ale to pole nie będzie wyświetlane w aplikacji produkcyjnej. W tym samouczku zostanie wyświetlony ostatni bajt, RowVersion
aby pokazać, jak działa obsługa współbieżności. Ostatni bajt nie ma gwarancji, że sam jest unikatowy.
Zaktualizuj stronę Pages\Departments\Index.cshtml :
- Zastąp indeks działem.
- Zmień kod zawierający
RowVersion
, aby pokazać tylko ostatni bajt tablicy bajtów. - Zastąp ciąg FirstMidName wartością FullName.
Poniższy kod przedstawia zaktualizowaną stronę:
@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>
Aktualizowanie modelu strony Edycji
Zaktualizuj Pages/Departments/Edit.cshtml.cs
za pomocą następującego kodu:
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.");
}
}
}
Element OriginalValue jest aktualizowany przy użyciu rowVersion
wartości z jednostki, gdy został pobrany w metodzie OnGetAsync
. EF Core Generuje polecenie SQL UPDATE z klauzulą WHERE zawierającą oryginalną RowVersion
wartość. Jeśli polecenie UPDATE nie ma wpływu na żadne wiersze (żadne wiersze nie mają oryginalnej RowVersion
wartości), zgłaszany DbUpdateConcurrencyException
jest wyjątek.
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;
W poprzednim wyróżnionym kodzie:
- Wartość w
Department.RowVersion
pliku to to, co znajdowało się w jednostce, gdy zostało pierwotnie pobrane w żądaniu Get dla strony Edycja. Wartość jest udostępniana metodzieOnPost
przez ukryte pole na Razor stronie, na których wyświetlana jest jednostka do edycji. Wartość pola ukrytego jest kopiowana przezDepartment.RowVersion
powiązanie modelu. OriginalValue
jest to, co EF Core będzie używane w klauzuli Where. Przed wykonaniemOriginalValue
wyróżnionego wiersza kodu ma wartość, która znajdowała się w bazie danych, gdyFirstOrDefaultAsync
została wywołana w tej metodzie, co może się różnić od tego, co było wyświetlane na stronie Edycja.- Wyróżniony kod zapewnia, że EF Core używa oryginalnej
RowVersion
wartości z wyświetlanejDepartment
jednostki w klauzuli Where instrukcji SQL UPDATE.
W przypadku wystąpienia błędu współbieżności wyróżniony kod pobiera wartości klienta (wartości opublikowane w tej metodzie) i wartości bazy danych.
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");
}
Poniższy kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych inne niż to, co zostało opublikowane w pliku 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.");
}
Poniższy wyróżniony kod ustawia RowVersion
wartość na nową wartość pobraną z bazy danych. Następnym razem, gdy użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu ostatniego wyświetlenia strony Edytuj.
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");
}
Instrukcja jest wymagana ModelState.Remove
, ponieważ ModelState
ma starą RowVersion
wartość. Na stronie Razor ModelState
wartość pola ma pierwszeństwo przed wartościami właściwości modelu, gdy oba te wartości są obecne.
Aktualizowanie strony Edytuj
Zaktualizuj Pages/Departments/Edit.cshtml
za pomocą następującego kodu:
@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");}
}
Powyższy kod ma następujące działanie:
- Aktualizuje dyrektywę
page
z@page
do@page "{id:int}"
. - Dodaje ukrytą wersję wiersza.
RowVersion
należy dodać wartość , więc postback wiąże wartość. - Wyświetla ostatni bajt
RowVersion
dla celów debugowania. ViewData
Zastępuje element silnie typizowaneInstructorNameSL
.
Testowanie konfliktów współbieżności ze stroną Edytuj
Otwórz dwa wystąpienia przeglądarki Edit w dziale angielskim:
- Uruchom aplikację i wybierz pozycję Działy.
- Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz polecenie Otwórz na nowej karcie.
- Na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.
Dwie karty przeglądarki wyświetlają te same informacje.
Zmień nazwę na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.
W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.
Zmień inne pole na drugiej karcie przeglądarki.
Kliknij przycisk Zapisz. Zobaczysz komunikaty o błędach dla wszystkich pól, które nie są zgodne z wartościami bazy danych:
To okno przeglądarki nie zamierzało zmienić pola Nazwa. Skopiuj i wklej bieżącą wartość (Języki) do pola Nazwa. Na karcie. Walidacja po stronie klienta usuwa komunikat o błędzie.
Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości zostaną wyświetlone na stronie Indeks.
Aktualizowanie modelu strony Usuń
Zaktualizuj Pages/Departments/Delete.cshtml.cs
za pomocą następującego kodu:
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 });
}
}
}
}
Strona Usuń wykrywa konflikty współbieżności, gdy jednostka uległa zmianie po jej pobraniu. Department.RowVersion
to wersja wiersza, gdy jednostka została pobrana. Podczas EF Core tworzenia polecenia SQL DELETE zawiera klauzulę WHERE z RowVersion
. Jeśli polecenie SQL DELETE powoduje, że nie ma to wpływu na wiersze:
- Polecenie
RowVersion
w poleceniu SQL DELETE nie jest zgodneRowVersion
z bazą danych. - Zgłaszany jest wyjątek DbUpdateConcurrencyException.
OnGetAsync
element jest wywoływany za pomocą .concurrencyError
Aktualizowanie strony Usuwanie
Zaktualizuj Pages/Departments/Delete.cshtml
za pomocą następującego kodu:
@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>
Powyższy kod wprowadza następujące zmiany:
- Aktualizuje dyrektywę
page
z@page
do@page "{id:int}"
. - Dodaje komunikat o błędzie.
- Zastępuje wartość FirstMidName wartością FullName w polu Administrator .
- Zmiany
RowVersion
w celu wyświetlenia ostatniego bajtu. - Dodaje ukrytą wersję wiersza.
RowVersion
należy dodać wartość , więc postback wiąże wartość.
Testowanie konfliktów współbieżności
Utwórz dział testów.
Otwórz dwa wystąpienia przeglądarki Usuń w dziale testowym:
- Uruchom aplikację i wybierz pozycję Działy.
- Kliknij prawym przyciskiem myszy hiperlink Usuń dla działu testów i wybierz polecenie Otwórz na nowej karcie.
- Kliknij hiperlink Edytuj dla działu testów.
Dwie karty przeglądarki wyświetlają te same informacje.
Zmień budżet na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.
W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.
Usuń dział testów z drugiej karty. Zostanie wyświetlony błąd współbieżności z bieżącymi wartościami z bazy danych. Kliknięcie przycisku Usuń powoduje usunięcie jednostki, chyba że RowVersion
została zaktualizowana.
Dodatkowe zasoby
- Tokeny współbieżności w programie EF Core
- Obsługa współbieżności w programie EF Core
- Debugowanie źródła ASP.NET Core 2.x
Następne kroki
Jest to ostatni samouczek z serii. Dodatkowe tematy zostały omówione w wersji MVC tej serii samouczków.
W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników jednocześnie aktualizuje jednostkę (jednocześnie). Jeśli napotkasz problemy, nie możesz rozwiązać, pobierz lub wyświetl ukończoną aplikację. Pobierz instrukcje.
Konflikty współbieżności
Konflikt współbieżności występuje, gdy:
- Użytkownik przechodzi do strony edycji jednostki.
- Inny użytkownik aktualizuje tę samą jednostkę przed zapisaniem zmiany pierwszego użytkownika w bazie danych.
Jeśli wykrywanie współbieżności nie jest włączone, po wystąpieniu współbieżnych aktualizacji:
- Ostatnia aktualizacja wygrywa. Oznacza to, że ostatnie wartości aktualizacji są zapisywane w bazie danych.
- Pierwsze z bieżących aktualizacji zostaną utracone.
Optymistyczna współbieżność
Optymistyczna współbieżność umożliwia wystąpieniu konfliktów współbieżności, a następnie odpowiednio reaguje podczas ich wykonywania. Na przykład Jane odwiedza stronę edycji Departamentu i zmienia budżet dla działu angielskiego z 350 000,000 USD do 0,00 USD.
Zanim Jane kliknie pozycję Zapisz, Jan odwiedzi tę samą stronę i zmieni pole Data rozpoczęcia z 2007-09-1.2013.
Jane klika pozycję Zapisz jako pierwszy i widzi jej zmianę po wyświetleniu strony Indeks w przeglądarce.
Jan klika pozycję Zapisz na stronie Edycja, która nadal pokazuje budżet w wysokości 350 000,000 USD. To, co się stanie dalej, zależy od sposobu obsługi konfliktów współbieżności.
Optymistyczna współbieżność obejmuje następujące opcje:
Możesz śledzić właściwość, którą użytkownik zmodyfikował i zaktualizować tylko odpowiednie kolumny w bazie danych.
W scenariuszu żadne dane nie zostaną utracone. Różne właściwości zostały zaktualizowane przez dwóch użytkowników. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane's, jak i Johna. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych. Takie podejście:
- Nie można uniknąć utraty danych, jeśli konkurencyjne zmiany są wprowadzane do tej samej właściwości.
- Zazwyczaj nie jest praktyczne w aplikacji internetowej. Wymaga to utrzymania znaczącego stanu w celu śledzenia wszystkich pobranych wartości i nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji.
- Może zwiększyć złożoność aplikacji w porównaniu z wykrywaniem współbieżności w jednostce.
Możesz pozwolić Jane's change zastąpić zmianę Jane.
Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i pobraną wartość $350,000.000. Takie podejście jest nazywane scenariuszem Wins klienta lub Last in Wins . (Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych). Jeśli nie wykonasz żadnego kodowania na potrzeby obsługi współbieżności, usługa Wins klienta odbywa się automatycznie.
Możesz uniemożliwić aktualizację johna w bazie danych. Zazwyczaj aplikacja:
- Wyświetl komunikat o błędzie.
- Pokaż bieżący stan danych.
- Zezwalaj użytkownikowi na ponowne zastosowania zmian.
Jest to nazywane scenariuszem Store Wins . (Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta). W tym samouczku zaimplementujesz scenariusz Store Wins. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez zgłaszania alertów przez użytkownika.
Obsługa współbieżności
Gdy właściwość jest skonfigurowana jako token współbieżności:
- EF Core Sprawdza, czy właściwość nie została zmodyfikowana po pobraniu. Sprawdzanie występuje, gdy SaveChanges wywoływana jest funkcja lub SaveChangesAsync .
- Jeśli właściwość została zmieniona po pobraniu, zostanie zwrócona DbUpdateConcurrencyException wartość .
Aby można było zgłaszać wyjątek DbUpdateConcurrencyException
, należy skonfigurować bazę danych i model danych.
Wykrywanie konfliktów współbieżności we właściwości
Konflikty współbieżności można wykryć na poziomie właściwości za pomocą atrybutu ConcurrencyCheck . Atrybut można zastosować do wielu właściwości w modelu. Aby uzyskać więcej informacji, zobacz Data Annotations-ConcurrencyCheck.
Atrybut [ConcurrencyCheck]
nie jest używany w tym samouczku.
Wykrywanie konfliktów współbieżności w wierszu
Aby wykryć konflikty współbieżności, do modelu zostanie dodana kolumna śledzenia rowversion . rowversion
:
- Jest specyficzny dla programu SQL Server. Inne bazy danych mogą nie zapewniać podobnej funkcji.
- Służy do określania, że jednostka nie została zmieniona, ponieważ została pobrana z bazy danych.
Baza danych generuje sekwencyjną rowversion
liczbę, która jest zwiększana za każdym razem, gdy wiersz jest aktualizowany. W poleceniu lub klauzula Where
zawiera pobraną wartość rowversion
.Delete
Update
Jeśli aktualizowany wiersz uległ zmianie:
rowversion
nie jest zgodna z pobraną wartością.- Polecenia
Update
lubDelete
nie znajdują wiersza, ponieważ klauzulaWhere
zawiera pobranyrowversion
element . - Jest
DbUpdateConcurrencyException
zgłaszany.
W EF Coresystemie, gdy nie zaktualizowano wierszy za pomocą Update
polecenia lub Delete
, zgłaszany jest wyjątek współbieżności.
Dodawanie właściwości śledzenia do jednostki Department
W Models/Department.cs
pliku dodaj właściwość śledzenia o nazwie 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; }
}
}
Atrybut Znacznik czasu określa, że ta kolumna jest zawarta w Where
klauzuli Update
i Delete
polecenia. Atrybut jest wywoływany Timestamp
, ponieważ poprzednie wersje programu SQL Server używały typu danych SQL timestamp
przed zastąpieniem go typem SQL rowversion
.
Płynny interfejs API może również określać właściwość śledzenia:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Poniższy kod przedstawia część języka T-SQL wygenerowaną przez EF Core po zaktualizowaniu nazwy działu:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Powyższy wyróżniony kod przedstawia klauzulę zawierającą WHERE
RowVersion
. Jeśli baza danych RowVersion
nie jest równa parametrowi RowVersion
(@p2
), nie są aktualizowane żadne wiersze.
Poniższy wyróżniony kod przedstawia język T-SQL, który weryfikuje dokładnie jeden wiersz został zaktualizowany:
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 zwraca liczbę wierszy, których dotyczy ostatnia instrukcja. W żadnym wierszu nie są aktualizowane, EF Core zgłasza element DbUpdateConcurrencyException
.
W oknie danych wyjściowych programu Visual Studio można zobaczyć generowanie kodu T-SQL EF Core .
Aktualizowanie bazy danych
RowVersion
Dodanie właściwości zmienia model bazy danych, który wymaga migracji.
Skompiluj projekt. Wprowadź następujące polecenie w oknie polecenia:
dotnet ef migrations add RowVersion
dotnet ef database update
Poprzednie polecenia:
Migrations/{time stamp}_RowVersion.cs
Dodaje plik migracji.Migrations/SchoolContextModelSnapshot.cs
Aktualizuje plik. Aktualizacja dodaje następujący wyróżniony kod doBuildModel
metody :Uruchamia migracje w celu zaktualizowania bazy danych.
Tworzenie szkieletu modelu Działy
Postępuj zgodnie z instrukcjami w artykule Tworzenie szkieletu modelu ucznia i używanie go Department
do klasy modelu.
Poprzednie polecenie szkieletuje Department
model. Otwórz projekt w programie Visual Studio.
Skompiluj projekt.
Aktualizowanie strony Indeks działów
Aparat tworzenia szkieletów utworzył kolumnę RowVersion
dla strony Indeks, ale to pole nie powinno być wyświetlane. W tym samouczku zostanie wyświetlony ostatni bajt, RowVersion
aby ułatwić zrozumienie współbieżności. Ostatni bajt nie ma gwarancji, że będzie unikatowy. Rzeczywista aplikacja nie będzie wyświetlana RowVersion
ani ostatnia bajt .RowVersion
Zaktualizuj stronę Indeks:
- Zastąp indeks działem.
- Zastąp znaczniki zawierające
RowVersion
ostatni bajt .RowVersion
- Zastąp ciąg FirstMidName wartością FullName.
Na poniższej adiustacji jest wyświetlana zaktualizowana strona:
@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>
Aktualizowanie modelu strony Edycji
Zaktualizuj Pages/Departments/Edit.cshtml.cs
za pomocą następującego kodu:
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.");
}
}
}
Aby wykryć problem z współbieżnością, OriginalValue element zostanie zaktualizowany o rowVersion
wartość z jednostki, która została pobrana. EF Core Generuje polecenie SQL UPDATE z klauzulą WHERE zawierającą oryginalną RowVersion
wartość. Jeśli polecenie UPDATE nie ma wpływu na żadne wiersze (żadne wiersze nie mają oryginalnej RowVersion
wartości), zgłaszany DbUpdateConcurrencyException
jest wyjątek.
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;
W poprzednim kodzie jest wartością, Department.RowVersion
gdy jednostka została pobrana. OriginalValue
jest wartością w bazie danych, gdy FirstOrDefaultAsync
została wywołana w tej metodzie.
Poniższy kod pobiera wartości klienta (wartości opublikowane w tej metodzie) i wartości bazy danych:
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");
}
Poniższy kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych różniące się od tego, co zostało opublikowane w pliku 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.");
}
Poniższy wyróżniony kod ustawia RowVersion
wartość na nową wartość pobraną z bazy danych. Następnym razem, gdy użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu ostatniego wyświetlenia strony Edytuj.
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");
}
Instrukcja jest wymagana ModelState.Remove
, ponieważ ModelState
ma starą RowVersion
wartość. Na stronie Razor ModelState
wartość pola ma pierwszeństwo przed wartościami właściwości modelu, gdy oba te wartości są obecne.
Aktualizowanie strony Edytuj
Zaktualizuj Pages/Departments/Edit.cshtml
za pomocą następującego znacznika:
@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");}
}
Powyższy znacznik:
- Aktualizuje dyrektywę
page
z@page
do@page "{id:int}"
. - Dodaje ukrytą wersję wiersza.
RowVersion
należy dodać element , aby po powrocie powiązać wartość. - Wyświetla ostatni bajt
RowVersion
dla celów debugowania. ViewData
Zastępuje element silnie typizowaneInstructorNameSL
.
Testowanie konfliktów współbieżności ze stroną Edytuj
Otwórz dwa wystąpienia przeglądarki Edit w dziale angielskim:
- Uruchom aplikację i wybierz pozycję Działy.
- Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz polecenie Otwórz na nowej karcie.
- Na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.
Dwie karty przeglądarki wyświetlają te same informacje.
Zmień nazwę na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.
W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.
Zmień inne pole na drugiej karcie przeglądarki.
Kliknij przycisk Zapisz. Zobaczysz komunikaty o błędach dla wszystkich pól, które nie są zgodne z wartościami bazy danych:
To okno przeglądarki nie zamierzało zmienić pola Nazwa. Skopiuj i wklej bieżącą wartość (Języki) do pola Nazwa. Na karcie. Walidacja po stronie klienta usuwa komunikat o błędzie.
Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości zostaną wyświetlone na stronie Indeks.
Aktualizowanie strony Usuwanie
Zaktualizuj model strony Usuń przy użyciu następującego kodu:
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 });
}
}
}
}
Strona Usuń wykrywa konflikty współbieżności, gdy jednostka uległa zmianie po jej pobraniu. Department.RowVersion
to wersja wiersza, gdy jednostka została pobrana. Podczas EF Core tworzenia polecenia SQL DELETE zawiera klauzulę WHERE z RowVersion
. Jeśli polecenie SQL DELETE powoduje, że nie ma to wpływu na wiersze:
- Polecenie
RowVersion
w poleceniu SQL DELETE nie jest zgodneRowVersion
z bazą danych. - Zgłaszany jest wyjątek DbUpdateConcurrencyException.
OnGetAsync
element jest wywoływany za pomocą .concurrencyError
Aktualizowanie strony Usuwanie
Zaktualizuj Pages/Departments/Delete.cshtml
za pomocą następującego kodu:
@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>
Powyższy kod wprowadza następujące zmiany:
- Aktualizuje dyrektywę
page
z@page
do@page "{id:int}"
. - Dodaje komunikat o błędzie.
- Zastępuje wartość FirstMidName wartością FullName w polu Administrator .
- Zmiany
RowVersion
w celu wyświetlenia ostatniego bajtu. - Dodaje ukrytą wersję wiersza.
RowVersion
należy dodać element , aby po powrocie powiązać wartość.
Testowanie konfliktów współbieżności ze stroną Usuwanie
Utwórz dział testów.
Otwórz dwa wystąpienia przeglądarki Usuń w dziale testowym:
- Uruchom aplikację i wybierz pozycję Działy.
- Kliknij prawym przyciskiem myszy hiperlink Usuń dla działu testów i wybierz polecenie Otwórz na nowej karcie.
- Kliknij hiperlink Edytuj dla działu testów.
Dwie karty przeglądarki wyświetlają te same informacje.
Zmień budżet na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.
W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.
Usuń dział testów z drugiej karty. Zostanie wyświetlony błąd współbieżności z bieżącymi wartościami z bazy danych. Kliknięcie przycisku Usuń powoduje usunięcie jednostki, chyba że RowVersion
została zaktualizowana.
Zobacz Dziedziczenie dotyczące dziedziczenia modelu danych.