Bölüm 8, Razor ASP.NET Core'da bulunan EF Core sayfalar - Eşzamanlılık
Contoso University web uygulaması, ve Visual Studio kullanarak EF Core Sayfalar web uygulamalarının nasıl oluşturulacağını Razor gösterir. Öğretici serisi hakkında bilgi için ilk öğreticiye bakın.
Çözemediğiniz sorunlarla karşılaşırsanız, tamamlanmış uygulamayı indirin ve öğreticiyi izleyerek bu kodu oluşturduğunuz kodla karşılaştırın.
Bu öğreticide, birden çok kullanıcı bir varlığı eşzamanlı olarak güncelleştirdiğinde çakışmaların nasıl işleneceği gösterilmektedir.
Eşzamanlılık çakışmaları
Eşzamanlılık çakışması şu durumlarda oluşur:
- Kullanıcı bir varlığın düzenleme sayfasına gider.
- İlk kullanıcının değişikliği veritabanına yazılmadan önce başka bir kullanıcı aynı varlığı güncelleştirir.
Eşzamanlılık algılama etkin değilse, veritabanını güncelleştiren kişi son olarak diğer kullanıcının değişikliklerinin üzerine yazar. Bu risk kabul edilebilirse eşzamanlılık programlama maliyeti avantajdan daha ağır basabilir.
Kötümser eşzamanlılık
Eşzamanlılık çakışmalarını önlemenin bir yolu veritabanı kilitlerini kullanmaktır. Buna kötümser eşzamanlılık denir. Uygulama, güncelleştirmek istediği bir veritabanı satırını okumadan önce bir kilit istemektedir. Bir satır güncelleştirme erişimi için kilitlendikten sonra, ilk kilit serbest bırakılana kadar başka hiçbir kullanıcının satırı kilitlemesine izin verilmez.
Kilitleri yönetmenin dezavantajları vardır. Programlama karmaşık olabilir ve kullanıcı sayısı arttıkça performans sorunlarına neden olabilir. Entity Framework Core, kötümser eşzamanlılık için yerleşik destek sağlamaz.
İyimser eşzamanlılık
İyimser eşzamanlılık eşzamanlılık çakışmalarının gerçekleşmesine olanak tanır ve bu çakışmalar gerçekleştiğinde uygun şekilde tepki verir. Örneğin, Jane Departman düzenleme sayfasını ziyaret ederek İngilizce bölümünün bütçesini 350.000,00 ABD doları olan 0,00 TL olarak değiştirir.
Jane Kaydet'e tıklamadan önce, John aynı sayfayı ziyaret eder ve Başlangıç Tarihi alanını 1/9/2007'den 1/9/2013'e değiştirir.
Jane önce Kaydet'e tıklar ve tarayıcı Bütçe tutarı olarak sıfırla Dizin sayfasını görüntülediğinden değişikliğin geçerli olduğunu görür.
John, 350.000,00 TL'lik bütçeyi gösteren Düzenleme sayfasında Kaydet'e tıklar. Bundan sonra ne olacağı eşzamanlılık çakışmalarını nasıl işlediğinize göre belirlenir:
Kullanıcının hangi özelliği değiştirdiğini izleyin ve yalnızca veritabanındaki ilgili sütunları güncelleştirin.
Senaryoda hiçbir veri kaybolmaz. İki kullanıcı tarafından farklı özellikler güncelleştirildi. Bir dahaki sefere birisi İngilizce bölümüne göz atarsa hem Jane'in hem de John'un değişikliklerini görür. Bu güncelleştirme yöntemi, veri kaybına neden olabilecek çakışma sayısını azaltabilir. Bu yaklaşımın bazı dezavantajları vardır:
- Aynı özellikte rakip değişiklikler yapıldığında veri kaybı önlenemez.
- Genellikle bir web uygulamasında pratik değildir. Getirilen tüm değerleri ve yeni değerleri izlemek için önemli bir durumun korunmasını gerektirir. Büyük miktarlarda durumun korunması uygulama performansını etkileyebilir.
- Bir varlıkta eşzamanlılık algılamaya kıyasla uygulama karmaşıklığını artırabilir.
John değiştirsin, Jane'in değişikliğinin üzerine yazsın.
Bir daha İngilizce bölümüne göz atıldığında, 1/9/2013 ve getirilen 350.000,00 ABD doları değeri görür. Bu yaklaşım İstemci Kazanır veya Wins senaryosunda Son olarak adlandırılır. İstemcideki tüm değerler, veri deposundakilerden önceliklidir. yapı iskelesi oluşturulmuş kod eşzamanlılık işlemesi yapmaz, İstemci Kazançları otomatik olarak gerçekleşir.
John'un değişikliğinin veritabanında güncelleştirilmesini önleyin. Genellikle uygulama şunları yapar:
- Bir hata iletisi görüntüleyin.
- Verilerin geçerli durumunu gösterir.
- Kullanıcının değişiklikleri yeniden uygulamasına izin verin.
Buna Store Wins senaryosu adı verilir. Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir. Store Wins senaryosu bu öğreticide kullanılır. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.
içinde çakışma algılama EF Core
Eşzamanlılık belirteci olarak yapılandırılan özellikler, iyimser eşzamanlılık denetimi uygulamak için kullanılır. veya SaveChangesAsynctarafından SaveChanges bir güncelleştirme veya silme işlemi tetiklendiğinde, veritabanındaki eşzamanlılık belirtecinin değeri tarafından EF Coreokunan özgün değerle karşılaştırılır:
- Değerler eşleşiyorsa işlem tamamlanabilir.
- Değerler eşleşmiyorsa, EF Core başka bir kullanıcının çakışan bir işlem gerçekleştirdiğini varsayar, geçerli işlemi durdurur ve bir DbUpdateConcurrencyExceptionoluşturur.
Geçerli işlemle çakışan bir işlem gerçekleştiren başka bir kullanıcı veya işlem eşzamanlılık çakışması olarak bilinir.
İlişkisel veritabanlarındaEF Core, eşzamanlılık çakışmasını algılamak için ve DELETE
deyimleri yan tümcesindeki WHERE
eşzamanlılık belirtecinin UPDATE
değerini denetler.
Veri modeli, bir satırın ne zaman değiştirildiğini belirlemek için kullanılabilecek bir izleme sütunu ekleyerek çakışma algılamayı etkinleştirecek şekilde yapılandırılmalıdır. EF eşzamanlılık belirteçleri için iki yaklaşım sağlar:
Modeldeki
[ConcurrencyCheck]
bir özelliğe veya IsConcurrencyToken uygulama. Bu yaklaşım önerilmez. Daha fazla bilgi için bkz . içindeki EF CoreEşzamanlılık Belirteçleri.Modelde TimestampAttribute eşzamanlılık belirtecine veya IsRowVersion uygulama. Bu öğreticide kullanılan yaklaşım budur.
SQL Server yaklaşımı ve SQLite uygulama ayrıntıları biraz farklıdır. Daha sonra öğreticide farkları listeleyen bir fark dosyası gösterilir. Visual Studio sekmesinde SQL Server yaklaşımı gösterilir. Visual Studio Code sekmesi, SQLite gibi SQL Server olmayan veritabanlarına yönelik yaklaşımı gösterir.
- Modelde, bir satırın ne zaman değiştirildiğini belirlemek için kullanılan bir izleme sütunu ekleyin.
- TimestampAttribute eşzamanlılık özelliğine uygulayın.
Models/Department.cs
Dosyayı aşağıdaki vurgulanmış kodla güncelleştirin:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] ConcurrencyToken { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
TimestampAttribute, sütunu eşzamanlılık izleme sütunu olarak tanımlayan sütundur. Akıcı API, izleme özelliğini belirtmenin alternatif bir yoludur:
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
[Timestamp]
Bir varlık özelliğindeki özniteliği yönteminde ModelBuilder aşağıdaki kodu oluşturur:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Yukarıdaki kod:
- Özellik türünü
ConcurrencyToken
bayt dizisi olarak ayarlar.byte[]
SQL Server için gerekli türdür. - IsConcurrencyToken çağrısı yapar.
IsConcurrencyToken
özelliği eşzamanlılık belirteci olarak yapılandırılır. Güncelleştirmelerde, veritabanındaki eşzamanlılık belirteci değeri, örneğin veritabanından alınmasından sonra değişmediğinden emin olmak için özgün değerle karşılaştırılır. Değiştiyse, bir DbUpdateConcurrencyException oluşturulur ve değişiklikler uygulanmaz. - Özelliğini, bir varlık eklerken veya güncelleştirirken otomatik olarak oluşturulan bir değere sahip olacak şekilde yapılandıran
ConcurrencyToken
öğesini çağırırValueGeneratedOnAddOrUpdate. HasColumnType("rowversion")
SQL Server veritabanındaki sütun türünü rowversion olarak ayarlar.
Aşağıdaki kod, ad güncelleştirildiğinde tarafından EF Core oluşturulan T-SQL'in Department
bir bölümünü gösterir:
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Yukarıdaki vurgulanan kod, öğesini içeren ConcurrencyToken
yan tümcesini WHERE
gösterir. Veritabanı ConcurrencyToken
parametresine ConcurrencyToken
@p2
eşit değilse, hiçbir satır güncelleştirilmez.
Aşağıdaki vurgulanmış kod, tam olarak bir satırın güncelleştirildiğini doğrulayan T-SQL'i gösterir:
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, son deyimden etkilenen satır sayısını döndürür. Hiçbir satır güncelleştirilmezse, EF Core bir DbUpdateConcurrencyException
oluşturur.
Geçiş ekleme
özelliğinin ConcurrencyToken
eklenmesi, geçiş gerektiren veri modelini değiştirir.
Projeyi derleyin.
PMC'de aşağıdaki komutları çalıştırın:
Add-Migration RowVersion
Update-Database
Önceki komutlar:
Migrations/{time stamp}_RowVersion.cs
Geçiş dosyasını oluşturur.Migrations/SchoolContextModelSnapshot.cs
Dosyayı güncelleştirir. Güncelleştirme yöntemineBuildModel
aşağıdaki kodu ekler:
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
yapı iskelesi Bölümü sayfaları
Aşağıdaki özel durumlarla birlikte yapı iskelesi Öğrenci sayfalarındaki yönergeleri izleyin:
- Sayfalar/Departmanlar klasörü oluşturun.
- Model sınıfı için kullanın
Department
. - Yeni bir tane oluşturmak yerine mevcut bağlam sınıfını kullanın.
Yardımcı program sınıfı ekleme
Proje klasöründe aşağıdaki kodla sınıfını oluşturun Utility
:
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
sınıfı, Utility
eşzamanlılık belirtecinin GetLastChars
son birkaç karakterini görüntülemek için kullanılan yöntemi sağlar. Aşağıdaki kod, hem SQLite ad SQL Server ile çalışan kodu gösterir:
#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
#if SQLiteVersion
Önişlemci yönergesi, SQLite ve SQL Server sürümleri arasındaki farkları yalıtarak aşağıdakilere yardımcı olur:
- Yazar her iki sürüm için de bir kod tabanı tutar.
- SQLite geliştiricileri uygulamayı Azure'a dağıtır ve SQL Azure'ı kullanır.
Projeyi derleyin.
Dizin sayfasını güncelleştirme
yapı iskelesi aracı Dizin sayfası için bir ConcurrencyToken
sütun oluşturdu, ancak bu alan üretim uygulamasında görüntülenmez. Bu öğreticide, eşzamanlılık işlemenin ConcurrencyToken
nasıl çalıştığını göstermeye yardımcı olmak için öğesinin son bölümü görüntülenir. Son bölümün tek başına benzersiz olması garanti edilmemektedir.
Pages\Departments\Index.cshtml sayfasını güncelleştirin:
- Dizini Departmanlar ile değiştirin.
- yalnızca son birkaç karakteri gösterecek şekilde içeren
ConcurrencyToken
kodu değiştirin. FirstMidName
öğesiniFullName
ile değiştirin.
Aşağıdaki kod güncelleştirilmiş sayfayı gösterir:
@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>
Sayfa modelini düzenle'yi güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml.cs
:
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.");
}
}
}
Eşzamanlılık güncelleştirmeleri
OriginalValue yönteminde ConcurrencyToken
getirildiğinde OnGetAsync
varlığın değeriyle güncelleştirilir. EF CoreSQL UPDATE
özgün ConcurrencyToken
değeri içeren bir yan tümcesi olan bir WHERE
komut oluşturur. Komuttan hiçbir satır etkilenmezse UPDATE
, bir DbUpdateConcurrencyException
özel durum oluşturulur. Hiçbir satır özgün ConcurrencyToken
değere UPDATE
sahip olmadığında komuttan hiçbir satır etkilenmez.
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;
Yukarıdaki vurgulanan kodda:
- içindeki
Department.ConcurrencyToken
değeri, sayfa isteğindeEdit
varlığınGet
getirildiği değerdir. Değer, düzenlenecek varlığı görüntüleyen sayfada gizli bir alan Razor tarafından yöntemine sağlanırOnPost
. Gizli alan değeri, model bağlayıcısı tarafından öğesineDepartment.ConcurrencyToken
kopyalanır. OriginalValue
yan tümcesindeWHERE
kullanılırEF Core. Vurgulanan kod satırı yürütülmeden önce:OriginalValue
, bu yöntemde çağrıldığındaFirstOrDefaultAsync
veritabanında olan değere sahiptir.- Bu değer, Düzenle sayfasında görüntülenen değerden farklı olabilir.
- Vurgulanan kod, SQL
UPDATE
deyimininWHERE
yan tümcesinde görüntülenenDepartment
varlıktan özgünConcurrencyToken
değerin kullanılmasını sağlarEF Core.
Aşağıdaki kod modeli gösterir Department
. Department
şu şekilde başlatılır:
OnGetAsync
yöntemini seçin.OnPostAsync
model bağlama kullanarak sayfadaki gizli alana Razor göre yöntemi:
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;
Yukarıdaki kod, istekten gelen varlığın Department
değerinin istekten gelen HTTP GET
ConcurrencyToken
değere ayarlandığını gösterirConcurrencyToken
.HTTP POST
Eşzamanlılık hatası oluştuğunda, aşağıdaki vurgulanan kod istemci değerlerini (bu yönteme gönderilen değerler) ve veritabanı değerlerini alır.
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)}");
}
Aşağıdaki kod, her sütun için gönderilenden OnPostAsync
farklı veritabanı değerlerine sahip özel bir hata iletisi ekler:
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.");
}
Aşağıdaki vurgulanmış kod, değeri veritabanından alınan yeni değere ayarlar ConcurrencyToken
. Kullanıcı Kaydet'e bir sonraki tıklayışında, yalnızca Düzenle sayfasının son görüntüsünden bu yana oluşan eşzamanlılık hataları yakalanacaktır.
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)}");
}
Önceki ModelState.Remove
ConcurrencyToken
değere sahip olduğundan ModelState
deyimi gereklidir. Razor Sayfada, ModelState
her ikisi de mevcut olduğunda bir alanın değeri model özelliği değerlerinden önceliklidir.
SQL Server ile SQLite kod farklılıkları karşılaştırması
Aşağıda SQL Server ile SQLite sürümleri arasındaki farklar gösterilmektedir:
+ 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;
Düzenle Razor sayfasını güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml
:
@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");}
}
Yukarıdaki kod:
- yönergesini
page
olarak@page
@page "{id:int}"
güncelleştirir. - Gizli satır sürümü ekler.
ConcurrencyToken
geri göndermenin değeri bağlaması için eklenmelidir. - Hata ayıklama amacıyla son baytını
ConcurrencyToken
görüntüler. ViewData
değerini, kesin olarak türü belirlenmişInstructorNameSL
olan ile değiştirir.
Düzenleme sayfasıyla eşzamanlılık çakışmalarını test edin
İngilizce bölümünde Düzenle'nin iki tarayıcı örneğini açın:
- Uygulamayı çalıştırın ve Departmanlar'ı seçin.
- İngilizce bölümü için Düzenle köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
- İlk sekmede İngilizce bölümü için Düzenle köprüsüne tıklayın.
İki tarayıcı sekmesi aynı bilgileri görüntüler.
İlk tarayıcı sekmesinde adı değiştirin ve Kaydet'e tıklayın.
Tarayıcı, değeri değiştirilmiş ve güncelleştirilmiş ConcurrencyToken
göstergeyi içeren Dizin sayfasını gösterir. Güncelleştirilmiş ConcurrencyToken
göstergeye dikkat edin; diğer sekmedeki ikinci geri göndermede görüntülenir.
İkinci tarayıcı sekmesinde farklı bir alanı değiştirin.
Kaydet'e tıklayın. Veritabanı değerleriyle eşleşmeyen tüm alanlar için hata iletileri görürsünüz:
Bu tarayıcı penceresi Ad alanını değiştirmeyi amaçlamadı. Geçerli değeri (Diller) kopyalayıp Ad alanına yapıştırın. Sekme tuşuyla çıkın. İstemci tarafı doğrulama hata iletisini kaldırır.
Yeniden Kaydet'e tıklayın. İkinci tarayıcı sekmesine girdiğiniz değer kaydedilir. Kaydedilen değerleri Dizin sayfasında görürsünüz.
Sayfa modelini sil'i güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml.cs
:
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 });
}
}
}
}
Sil sayfası, varlık getirildikten sonra değiştiğinde eşzamanlılık çakışmalarını algılar. Department.ConcurrencyToken
varlık getirildiğinde satır sürümüdür. komutu oluşturulduğunda EF Core SQL DELETE
, ile ConcurrencyToken
bir WHERE yan tümcesi içerir. SQL DELETE
Komut, etkilenen sıfır satırla sonuçlanırsa:
ConcurrencyToken
komutu veritabanındaSQL DELETE
eşleşmiyorConcurrencyToken
.- Bir
DbUpdateConcurrencyException
özel durum oluşturulur. OnGetAsync
ile çağrılırconcurrencyError
.
Sil Razor sayfasını güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml
:
@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>
Yukarıdaki kod aşağıdaki değişiklikleri yapar:
- yönergesini
page
olarak@page
@page "{id:int}"
güncelleştirir. - Bir hata iletisi ekler.
- FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
- Son bayta görüntülenecek değişiklikler
ConcurrencyToken
. - Gizli satır sürümü ekler.
ConcurrencyToken
geri göndermenin değeri bağlaması için eklenmelidir.
Eşzamanlılık çakışmalarını test edin
Bir test departmanı oluşturun.
Test departmanında delete'in iki tarayıcı örneğini açın:
- Uygulamayı çalıştırın ve Departmanlar'ı seçin.
- Test departmanı için Sil köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
- Test departmanı için Düzenle köprüsüne tıklayın.
İki tarayıcı sekmesi aynı bilgileri görüntüler.
İlk tarayıcı sekmesinde bütçeyi değiştirin ve Kaydet'e tıklayın.
Tarayıcı, değeri değiştirilmiş ve güncelleştirilmiş ConcurrencyToken
göstergeyi içeren Dizin sayfasını gösterir. Güncelleştirilmiş ConcurrencyToken
göstergeye dikkat edin; diğer sekmedeki ikinci geri göndermede görüntülenir.
İkinci sekmeden test bölümünü silin. Veritabanındaki geçerli değerlerle eşzamanlılık hatası görüntülenir. Sil'e tıklanması, güncelleştirilmediği sürece ConcurrencyToken
varlığı siler.
Ek kaynaklar
- içinde Eşzamanlılık Belirteçleri EF Core
- içinde eşzamanlılığı işleme EF Core
- ASP.NET Core 2.x kaynağında hata ayıklama
Sonraki adımlar
Bu, serideki son öğreticidir. Bu öğretici serisinin MVC sürümünde ek konular ele alınmıştır.
Bu öğreticide, birden çok kullanıcı bir varlığı eşzamanlı olarak güncelleştirdiğinde (aynı anda) çakışmaların nasıl işleneceği gösterilir.
Eşzamanlılık çakışmaları
Eşzamanlılık çakışması şu durumlarda oluşur:
- Kullanıcı bir varlığın düzenleme sayfasına gider.
- İlk kullanıcının değişikliği veritabanına yazılmadan önce başka bir kullanıcı aynı varlığı güncelleştirir.
Eşzamanlılık algılama etkin değilse, veritabanını güncelleştiren kişi son olarak diğer kullanıcının değişikliklerinin üzerine yazar. Bu risk kabul edilebilirse eşzamanlılık programlama maliyeti avantajdan daha ağır basabilir.
Kötümser eşzamanlılık (kilitleme)
Eşzamanlılık çakışmalarını önlemenin bir yolu veritabanı kilitlerini kullanmaktır. Buna kötümser eşzamanlılık denir. Uygulama, güncelleştirmek istediği bir veritabanı satırını okumadan önce bir kilit istemektedir. Bir satır güncelleştirme erişimi için kilitlendikten sonra, ilk kilit serbest bırakılana kadar başka hiçbir kullanıcının satırı kilitlemesine izin verilmez.
Kilitleri yönetmenin dezavantajları vardır. Programlama karmaşık olabilir ve kullanıcı sayısı arttıkça performans sorunlarına neden olabilir. Entity Framework Core bunun için yerleşik destek sağlamaz ve bu öğreticide nasıl uygulandığı gösterilmez.
İyimser eşzamanlılık
İyimser eşzamanlılık eşzamanlılık çakışmalarının gerçekleşmesine olanak tanır ve bu çakışmalar gerçekleştiğinde uygun şekilde tepki verir. Örneğin, Jane Departman düzenleme sayfasını ziyaret ederek İngilizce bölümünün bütçesini 350.000,00 ABD doları olan 0,00 TL olarak değiştirir.
Jane Kaydet'e tıklamadan önce, John aynı sayfayı ziyaret eder ve Başlangıç Tarihi alanını 1/9/2007'den 1/9/2013'e değiştirir.
Jane önce Kaydet'e tıklar ve tarayıcı Bütçe tutarı olarak sıfırla Dizin sayfasını görüntülediğinden değişikliğin geçerli olduğunu görür.
John, 350.000,00 TL'lik bütçeyi gösteren Düzenleme sayfasında Kaydet'e tıklar. Bundan sonra ne olacağı eşzamanlılık çakışmalarını nasıl işlediğinize göre belirlenir:
Kullanıcının hangi özelliği değiştirdiğini izleyebilir ve yalnızca veritabanındaki ilgili sütunları güncelleştirebilirsiniz.
Senaryoda hiçbir veri kaybolmaz. İki kullanıcı tarafından farklı özellikler güncelleştirildi. Bir dahaki sefere birisi İngilizce bölümüne göz atarsa hem Jane'in hem de John'un değişikliklerini görür. Bu güncelleştirme yöntemi, veri kaybına neden olabilecek çakışma sayısını azaltabilir. Bu yaklaşımın bazı dezavantajları vardır:
- Aynı özellikte rakip değişiklikler yapıldığında veri kaybı önlenemez.
- Genellikle bir web uygulamasında pratik değildir. Getirilen tüm değerleri ve yeni değerleri izlemek için önemli bir durumun korunmasını gerektirir. Büyük miktarlarda durumun korunması uygulama performansını etkileyebilir.
- Bir varlıkta eşzamanlılık algılamaya kıyasla uygulama karmaşıklığını artırabilir.
John'un değişiminin Jane'in değişikliğinin üzerine yazmasına izin vekleyebilirsiniz.
Bir daha İngilizce bölümüne göz atıldığında, 1/9/2013 ve getirilen 350.000,00 ABD doları değeri görür. Bu yaklaşım İstemci Kazanır veya Wins senaryosunda Son olarak adlandırılır. (İstemcideki tüm değerler veri deposundakilerden önceliklidir.) Eşzamanlılık işleme için herhangi bir kodlama yapmazsanız, İstemci Kazançları otomatik olarak gerçekleşir.
John'un değişikliğinin veritabanında güncelleştirilmesini engelleyebilirsiniz. Genellikle uygulama şunları yapar:
- Bir hata iletisi görüntüleyin.
- Verilerin geçerli durumunu gösterir.
- Kullanıcının değişiklikleri yeniden uygulamasına izin verin.
Buna Store Wins senaryosu adı verilir. (Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir.) Bu öğreticide Store Wins senaryosunu uygulayacaksınız. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.
içinde çakışma algılama EF Core
EF Core çakışmaları DbConcurrencyException
algıladığında özel durumlar oluşturur. Çakışma algılamayı etkinleştirmek için veri modelinin yapılandırılması gerekir. Çakışma algılamayı etkinleştirme seçenekleri şunlardır:
Güncelleştirme ve Silme komutlarının Where yan tümcesine eşzamanlılık belirteci olarak yapılandırılmış sütunların özgün değerlerini içerecek şekilde yapılandırınEF Core.
Çağrıldığında
SaveChanges
, Where yan tümcesi özniteliğiyle ConcurrencyCheckAttribute ek açıklama ekli özelliklerin özgün değerlerini arar. Satır ilk okunduktan sonra eşzamanlılık belirteci özelliklerinden herhangi biri değiştiyse update deyimi güncelleştirilecek bir satır bulamaz. EF Core bunu eşzamanlılık çakışması olarak yorumlar. Çok sayıda sütunu olan veritabanı tablolarında bu yaklaşım çok büyük Where yan tümcelerine neden olabilir ve büyük miktarlarda durum gerektirebilir. Bu nedenle bu yaklaşım genellikle önerilmez ve bu öğreticide kullanılan yöntem değildir.Veritabanı tablosunda, bir satırın ne zaman değiştirildiğini belirlemek için kullanılabilecek bir izleme sütunu ekleyin.
SQL Server veritabanında, izleme sütununun veri türü şeklindedir
rowversion
. Değerrowversion
, satır her güncelleştirildiğinde artırılan sıralı bir sayıdır. Update veya Delete komutunda Where yan tümcesi, izleme sütununun özgün değerini (özgün satır sürüm numarası) içerir. Güncelleştirilmekte olan satır başka bir kullanıcı tarafından değiştirildiyse, sütundakirowversion
değer özgün değerden farklıdır. Bu durumda, Update veya Delete deyimi Where yan tümcesi nedeniyle güncelleştirilecek satırı bulamıyor. EF Core Güncelleştir veya Sil komutundan hiçbir satır etkilenmediğinde eşzamanlılık özel durumu oluşturur.
İzleme özelliği ekleme
içinde Models/Department.cs
RowVersion adlı bir izleme özelliği ekleyin:
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; }
}
}
TimestampAttribute özniteliği, sütunu eşzamanlılık izleme sütunu olarak tanımlayan öğedir. Akıcı API, izleme özelliğini belirtmenin alternatif bir yoludur:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
SQL Server veritabanı için bayt [Timestamp]
dizisi olarak tanımlanan varlık özelliğindeki özniteliği:
- Sütunun DELETE ve UPDATE WHERE yan tümcelerine eklenmesine neden olur.
- Veritabanındaki sütun türünü rowversion olarak ayarlar.
Veritabanı, satır her güncelleştirildiğinde artırılan bir sıralı satır sürüm numarası oluşturur. Update
bir veya Delete
komutunda yan tümcesiWhere
, getirilen satır sürümü değerini içerir. Güncelleştirilmekte olan satır getirildikten sonra değiştiyse:
- Geçerli satır sürümü değeri getirilen değerle eşleşmiyor.
Update
yan tümcesi getirilen satır sürümü değerini aradığındanWhere
veyaDelete
komutları bir satır bulamaz.- A
DbUpdateConcurrencyException
atılır.
Aşağıdaki kod, Bölüm adı güncelleştirildiğinde tarafından EF Core oluşturulan T-SQL'in bir bölümünü gösterir:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Yukarıdaki vurgulanan kod, öğesini içeren RowVersion
yan tümcesini WHERE
gösterir. Veritabanı RowVersion
parametresine RowVersion
()@p2
eşit değilse, hiçbir satır güncelleştirilmez.
Aşağıdaki vurgulanmış kod, tam olarak bir satırın güncelleştirildiğini doğrulayan T-SQL'i gösterir:
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, son deyimden etkilenen satır sayısını döndürür. Hiçbir satır güncelleştirilmezse, EF Core bir DbUpdateConcurrencyException
oluşturur.
Veritabanını güncelleştirme
özelliğinin RowVersion
eklenmesi, geçiş gerektiren veri modelini değiştirir.
Projeyi derleyin.
PMC'de aşağıdaki komutu çalıştırın:
Add-Migration RowVersion
Şu komut:
Migrations/{time stamp}_RowVersion.cs
Geçiş dosyasını oluşturur.Migrations/SchoolContextModelSnapshot.cs
Dosyayı güncelleştirir. Güncelleştirme yöntemine aşağıdaki vurgulanmış koduBuildModel
ekler: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"); });
PMC'de aşağıdaki komutu çalıştırın:
Update-Database
yapı iskelesi Bölümü sayfaları
Projeyi derleyin.
Dizin sayfasını güncelleştirme
yapı iskelesi aracı Dizin sayfası için bir RowVersion
sütun oluşturdu, ancak bu alan üretim uygulamasında görüntülenmez. Bu öğreticide, eşzamanlılık işlemenin RowVersion
nasıl çalıştığını göstermeye yardımcı olmak için öğesinin son baytı görüntülenir. Son bayt tek başına benzersiz olması garanti değildir.
Pages\Departments\Index.cshtml sayfasını güncelleştirin:
- Dizini Departmanlar ile değiştirin.
- Bayt dizisinin yalnızca son baytını gösterecek şekilde içeren
RowVersion
kodu değiştirin. - FirstMidName değerini FullName ile değiştirin.
Aşağıdaki kod güncelleştirilmiş sayfayı gösterir:
@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>
Sayfa modelini düzenle'yi güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml.cs
:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
var deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error
// and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
}
}
OriginalValue yönteminde rowVersion
getirildiğinde OnGetAsync
varlığın değeriyle güncelleştirilir. EF Core özgün RowVersion
değeri içeren WHERE yan tümcesine sahip bir SQL UPDATE komutu oluşturur. UPDATE komutundan hiçbir satır etkilenmezse (hiçbir satır özgün RowVersion
değere sahip değilse), bir DbUpdateConcurrencyException
özel durum oluşturulur.
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;
Yukarıdaki vurgulanan kodda:
- içindeki
Department.RowVersion
değeri, başlangıçta Düzenle sayfasının Get isteğinde getirildiğinde varlığın içindeki değerdir. Değer, düzenlenecek varlığı görüntüleyen sayfada gizli bir alan Razor tarafından yöntemine sağlanırOnPost
. Gizli alan değeri, model bağlayıcısı tarafından öğesineDepartment.RowVersion
kopyalanır. OriginalValue
EF Core Where yan tümcesinde kullanılacaktır. Vurgulanan kod satırı yürütülmeden önce,OriginalValue
bu yöntemde çağrıldığındaFirstOrDefaultAsync
veritabanında olan değere sahiptir ve bu değer Düzenleme sayfasında görüntülenenden farklı olabilir.- Vurgulanan kod, SQL UPDATE deyiminin Where yan tümcesinde görüntülenen
Department
varlıktan özgünRowVersion
değerin kullanılmasını sağlarEF Core.
Eşzamanlılık hatası oluştuğunda, aşağıdaki vurgulanan kod istemci değerlerini (bu yönteme gönderilen değerler) ve veritabanı değerlerini alır.
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");
}
Aşağıdaki kod, her sütun için gönderilenden OnPostAsync
farklı veritabanı değerlerine sahip özel bir hata iletisi ekler:
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.");
}
Aşağıdaki vurgulanmış kod, değeri veritabanından alınan yeni değere ayarlar RowVersion
. Kullanıcı Kaydet'e bir sonraki tıklayışında, yalnızca Düzenle sayfasının son görüntüsünden bu yana oluşan eşzamanlılık hataları yakalanacaktır.
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");
}
ModelState.Remove
Eski RowVersion
değere sahip olduğundan ModelState
deyimi gereklidir. Razor Sayfada, ModelState
her ikisi de mevcut olduğunda bir alanın değeri model özelliği değerlerinden önceliklidir.
Düzenle sayfasını güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml
:
@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");}
}
Yukarıdaki kod:
- yönergesini
page
olarak@page
@page "{id:int}"
güncelleştirir. - Gizli satır sürümü ekler.
RowVersion
geri göndermenin değeri bağlaması için eklenmelidir. - Hata ayıklama amacıyla son baytını
RowVersion
görüntüler. ViewData
değerini, kesin olarak türü belirlenmişInstructorNameSL
olan ile değiştirir.
Düzenleme sayfasıyla eşzamanlılık çakışmalarını test edin
İngilizce bölümünde Düzenle'nin iki tarayıcı örneğini açın:
- Uygulamayı çalıştırın ve Departmanlar'ı seçin.
- İngilizce bölümü için Düzenle köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
- İlk sekmede İngilizce bölümü için Düzenle köprüsüne tıklayın.
İki tarayıcı sekmesi aynı bilgileri görüntüler.
İlk tarayıcı sekmesinde adı değiştirin ve Kaydet'e tıklayın.
Tarayıcı, değiştirilen değeri ve güncelleştirilmiş rowVersion göstergesini içeren Dizin sayfasını gösterir. Güncelleştirilmiş rowVersion göstergesinin, diğer sekmedeki ikinci geri göndermede görüntülendiğine dikkat edin.
İkinci tarayıcı sekmesinde farklı bir alanı değiştirin.
Kaydet'e tıklayın. Veritabanı değerleriyle eşleşmeyen tüm alanlar için hata iletileri görürsünüz:
Bu tarayıcı penceresi Ad alanını değiştirmeyi amaçlamadı. Geçerli değeri (Diller) kopyalayıp Ad alanına yapıştırın. Sekme tuşuyla çıkın. İstemci tarafı doğrulama hata iletisini kaldırır.
Yeniden Kaydet'e tıklayın. İkinci tarayıcı sekmesine girdiğiniz değer kaydedilir. Kaydedilen değerleri Dizin sayfasında görürsünüz.
Sayfa modelini sil'i güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml.cs
:
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 });
}
}
}
}
Sil sayfası, varlık getirildikten sonra değiştiğinde eşzamanlılık çakışmalarını algılar. Department.RowVersion
varlık getirildiğinde satır sürümüdür. EF Core SQL DELETE komutunu oluşturduğunda, ile RowVersion
bir WHERE yan tümcesi içerir. SQL DELETE komutu etkilenen sıfır satırla sonuçlanırsa:
RowVersion
SQL DELETE komutundaki komutu veritabanında eşleşmiyorRowVersion
.- DbUpdateConcurrencyException özel durumu oluşturulur.
OnGetAsync
ile çağrılırconcurrencyError
.
Sil sayfasını güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml
:
@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>
Yukarıdaki kod aşağıdaki değişiklikleri yapar:
- yönergesini
page
olarak@page
@page "{id:int}"
güncelleştirir. - Bir hata iletisi ekler.
- FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
- Son bayta görüntülenecek değişiklikler
RowVersion
. - Gizli satır sürümü ekler.
RowVersion
geri göndermenin değeri bağlaması için eklenmelidir.
Eşzamanlılık çakışmalarını test edin
Bir test departmanı oluşturun.
Test departmanında delete'in iki tarayıcı örneğini açın:
- Uygulamayı çalıştırın ve Departmanlar'ı seçin.
- Test departmanı için Sil köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
- Test departmanı için Düzenle köprüsüne tıklayın.
İki tarayıcı sekmesi aynı bilgileri görüntüler.
İlk tarayıcı sekmesinde bütçeyi değiştirin ve Kaydet'e tıklayın.
Tarayıcı, değiştirilen değeri ve güncelleştirilmiş rowVersion göstergesini içeren Dizin sayfasını gösterir. Güncelleştirilmiş rowVersion göstergesinin, diğer sekmedeki ikinci geri göndermede görüntülendiğine dikkat edin.
İkinci sekmeden test bölümünü silin. Veritabanındaki geçerli değerlerle eşzamanlılık hatası görüntülenir. Sil'e tıklanması, güncelleştirilmediği sürece RowVersion
varlığı siler.
Ek kaynaklar
- içinde Eşzamanlılık Belirteçleri EF Core
- içinde eşzamanlılığı işleme EF Core
- ASP.NET Core 2.x kaynağında hata ayıklama
Sonraki adımlar
Bu, serideki son öğreticidir. Bu öğretici serisinin MVC sürümünde ek konular ele alınmıştır.
Bu öğreticide, birden çok kullanıcı bir varlığı eşzamanlı olarak güncelleştirdiğinde (aynı anda) çakışmaların nasıl işleneceği gösterilir. Çözemediğiniz sorunlarla karşılaşırsanız tamamlanmış uygulamayı indirin veya görüntüleyin. İndirme yönergeleri.
Eşzamanlılık çakışmaları
Eşzamanlılık çakışması şu durumlarda oluşur:
- Kullanıcı bir varlığın düzenleme sayfasına gider.
- İlk kullanıcının değişikliği DB'ye yazılmadan önce başka bir kullanıcı aynı varlığı güncelleştirir.
Eşzamanlılık algılama etkin değilse, eşzamanlı güncelleştirmeler gerçekleştiğinde:
- Son güncelleştirme kazanır. Yani, son güncelleştirme değerleri db'ye kaydedilir.
- Geçerli güncelleştirmelerin ilki kaybolur.
İyimser eşzamanlılık
İyimser eşzamanlılık eşzamanlılık çakışmalarının gerçekleşmesine olanak tanır ve bu çakışmalar gerçekleştiğinde uygun şekilde tepki verir. Örneğin, Jane Departman düzenleme sayfasını ziyaret ederek İngilizce bölümünün bütçesini 350.000,00 ABD doları olan 0,00 TL olarak değiştirir.
Jane Kaydet'e tıklamadan önce, John aynı sayfayı ziyaret eder ve Başlangıç Tarihi alanını 1/9/2007'den 1/9/2013'e değiştirir.
Jane önce Kaydet'e tıklar ve tarayıcı Dizin sayfasını görüntülediğinde değişikliğini görür.
John, 350.000,00 TL'lik bütçeyi gösteren Düzenleme sayfasında Kaydet'e tıklar. Bundan sonra ne olacağı eşzamanlılık çakışmalarını nasıl işlediğinize göre belirlenir.
İyimser eşzamanlılık aşağıdaki seçenekleri içerir:
Kullanıcının hangi özelliği değiştirdiğini izleyebilir ve yalnızca db'deki ilgili sütunları güncelleştirebilirsiniz.
Senaryoda hiçbir veri kaybolmaz. İki kullanıcı tarafından farklı özellikler güncelleştirildi. Bir dahaki sefere birisi İngilizce bölümüne göz atarsa hem Jane'in hem de John'un değişikliklerini görür. Bu güncelleştirme yöntemi, veri kaybına neden olabilecek çakışma sayısını azaltabilir. Bu yaklaşım:
- Aynı özellikte rakip değişiklikler yapıldığında veri kaybı önlenemez.
- Genellikle bir web uygulamasında pratik değildir. Getirilen tüm değerleri ve yeni değerleri izlemek için önemli bir durumun korunmasını gerektirir. Büyük miktarlarda durumun korunması uygulama performansını etkileyebilir.
- Bir varlıkta eşzamanlılık algılamaya kıyasla uygulama karmaşıklığını artırabilir.
John'un değişiminin Jane'in değişikliğinin üzerine yazmasına izin vekleyebilirsiniz.
Bir daha İngilizce bölümüne göz atıldığında, 1/9/2013 ve getirilen 350.000,00 ABD doları değeri görür. Bu yaklaşım İstemci Kazanır veya Wins senaryosunda Son olarak adlandırılır. (İstemcideki tüm değerler veri deposundakilerden önceliklidir.) Eşzamanlılık işleme için herhangi bir kodlama yapmazsanız, İstemci Kazançları otomatik olarak gerçekleşir.
John'un değişikliğinin db'de güncelleştirilmesini engelleyebilirsiniz. Genellikle uygulama şunları yapar:
- Bir hata iletisi görüntüleyin.
- Verilerin geçerli durumunu gösterir.
- Kullanıcının değişiklikleri yeniden uygulamasına izin verin.
Buna Store Wins senaryosu adı verilir. (Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir.) Bu öğreticide Store Wins senaryosunu uygulayacaksınız. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.
Eşzamanlılığı işleme
Bir özellik eşzamanlılık belirteci olarak yapılandırıldığında:
- EF Core özelliğin getirildikten sonra değiştirilmediğini doğrular. Denetim, veya SaveChangesAsync çağrıldığında SaveChanges gerçekleşir.
- Özellik getirildikten sonra değiştirildiyse, bir DbUpdateConcurrencyException oluşturulur.
Veritabanı ve veri modeli, oluşturma işlemini DbUpdateConcurrencyException
destekleyecek şekilde yapılandırılmalıdır.
Bir özellikte eşzamanlılık çakışmalarını algılama
Eşzamanlılık çakışmaları, ConcurrencyCheck özniteliğiyle özellik düzeyinde algılanabilir. Özniteliği modeldeki birden çok özelliğe uygulanabilir. Daha fazla bilgi için bkz . Veri Ek Açıklamaları-Eşzamanlılık Denetimi.
[ConcurrencyCheck]
Özniteliği bu öğreticide kullanılmaz.
Satırdaki eşzamanlılık çakışmalarını algılama
Eşzamanlılık çakışmalarını algılamak için modele bir rowversion izleme sütunu eklenir. rowversion
:
- SQL Server'a özgüdür. Diğer veritabanları benzer bir özellik sağlamayabilir.
- Bir varlığın veritabanından getirildikten sonra değiştirilmediğini belirlemek için kullanılır.
Veritabanı, satır her güncelleştirildiğinde artırılan bir sıralı rowversion
sayı oluşturur. veya Update
Delete
komutunda yan tümcesi Where
getirilen değerini rowversion
içerir. Güncelleştirilmekte olan satır değiştiyse:
rowversion
getirilen değerle eşleşmiyor.Update
yan tümcesi getirilenrowversion
öğesini içerdiğindenWhere
veyaDelete
komutları bir satır bulamaz.- A
DbUpdateConcurrencyException
atılır.
içinde EF Core, bir Update
veya Delete
komutu tarafından hiçbir satır güncelleştirilmemişse eşzamanlılık özel durumu oluşturulur.
Departman varlığına izleme özelliği ekleme
içinde Models/Department.cs
RowVersion adlı bir izleme özelliği ekleyin:
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; }
}
}
Timestamp özniteliği, bu sütunun ve Delete
komutlarının yan tümcesine Where
Update
dahil olduğunu belirtir. SQL Server'ın önceki sürümleri SQL türü değiştirmeden önce bir SQL timestamp
rowversion
veri türü kullandığından özniteliği çağrılırTimestamp
.
Akıcı API, izleme özelliğini de belirtebilir:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
Aşağıdaki kod, Bölüm adı güncelleştirildiğinde tarafından EF Core oluşturulan T-SQL'in bir bölümünü gösterir:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Yukarıdaki vurgulanan kod, öğesini içeren RowVersion
yan tümcesini WHERE
gösterir. Veritabanı RowVersion
parametresine RowVersion
()@p2
eşit değilse, hiçbir satır güncelleştirilmez.
Aşağıdaki vurgulanmış kod, tam olarak bir satırın güncelleştirildiğini doğrulayan T-SQL'i gösterir:
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, son deyimden etkilenen satır sayısını döndürür. Hiçbir satır güncelleştirilmezse, EF Core bir DbUpdateConcurrencyException
oluşturur.
T-SQL'in EF Core ürettiğini Visual Studio'nun çıkış penceresinde görebilirsiniz.
Db'yi güncelleştirme
özelliğinin RowVersion
eklenmesi, geçiş gerektiren VERITABANı modelini değiştirir.
Projeyi derleyin. Komut penceresine aşağıdakileri girin:
dotnet ef migrations add RowVersion
dotnet ef database update
Önceki komutlar:
Migrations/{time stamp}_RowVersion.cs
Geçiş dosyasını ekler.Migrations/SchoolContextModelSnapshot.cs
Dosyayı güncelleştirir. Güncelleştirme yöntemine aşağıdaki vurgulanmış koduBuildModel
ekler:Db'yi güncelleştirmek için geçişleri çalıştırır.
Departmanlar modelinin iskelesini oluşturma
Öğrenci modelinin iskelesini oluşturma ve model sınıfı için kullanma Department
başlığı altında yer alan yönergeleri izleyin.
Yukarıdaki komut modelin iskelesini Department
oluşturur. Projeyi Visual Studio'da açın.
Projeyi derleyin.
Departmanlar Dizini sayfasını güncelleştirme
yapı iskelesi altyapısı Dizin sayfası için bir RowVersion
sütun oluşturdu, ancak bu alan görüntülenmemelidir. Bu öğreticide, eşzamanlılığın anlaşılmasına yardımcı olmak için öğesinin RowVersion
son baytı görüntülenir. Son baysın benzersiz olması garanti değildir. Gerçek bir uygulama görüntülenmez veya son baytı RowVersion
görüntülenmezRowVersion
.
Dizin sayfasını güncelleştirin:
- Dizini Departmanlar ile değiştirin.
- öğesini içeren
RowVersion
işaretlemeyi son bayt ileRowVersion
değiştirin. - FirstMidName değerini FullName ile değiştirin.
Aşağıdaki işaretleme güncelleştirilmiş sayfayı gösterir:
@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>
Sayfa modelini düzenle'yi güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml.cs
:
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.");
}
}
}
Eşzamanlılık sorununu algılamak için , OriginalValue getirildiği varlığın rowVersion
değeriyle güncelleştirilir. EF Core özgün RowVersion
değeri içeren WHERE yan tümcesine sahip bir SQL UPDATE komutu oluşturur. UPDATE komutundan hiçbir satır etkilenmezse (hiçbir satır özgün RowVersion
değere sahip değilse), bir DbUpdateConcurrencyException
özel durum oluşturulur.
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;
Yukarıdaki kodda, Department.RowVersion
varlık getirildiğinde değerdir. OriginalValue
, bu yöntemde çağrıldığında FirstOrDefaultAsync
db'deki değerdir.
Aşağıdaki kod istemci değerlerini (bu yönteme gönderilen değerler) ve VERITABANı değerlerini alır:
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");
}
Aşağıdaki kod, her sütun için gönderilenden farklı OnPostAsync
VERITABANı değerlerine sahip özel bir hata iletisi ekler:
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.");
}
Aşağıdaki vurgulanmış kod, değeri DB'den alınan yeni değere ayarlar RowVersion
. Kullanıcı Kaydet'e bir sonraki tıklayışında, yalnızca Düzenle sayfasının son görüntüsünden bu yana oluşan eşzamanlılık hataları yakalanacaktır.
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");
}
ModelState.Remove
Eski RowVersion
değere sahip olduğundan ModelState
deyimi gereklidir. Razor Sayfada, ModelState
her ikisi de mevcut olduğunda bir alanın değeri model özelliği değerlerinden önceliklidir.
Düzenle sayfasını güncelleştirme
Aşağıdaki işaretlemeyle güncelleştirin Pages/Departments/Edit.cshtml
:
@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");}
}
Yukarıdaki işaretleme:
- yönergesini
page
olarak@page
@page "{id:int}"
güncelleştirir. - Gizli satır sürümü ekler.
RowVersion
geri göndermenin değeri bağlaması için eklenmelidir. - Hata ayıklama amacıyla son baytını
RowVersion
görüntüler. ViewData
değerini, kesin olarak türü belirlenmişInstructorNameSL
olan ile değiştirir.
Düzenleme sayfasıyla eşzamanlılık çakışmalarını test edin
İngilizce bölümünde Düzenle'nin iki tarayıcı örneğini açın:
- Uygulamayı çalıştırın ve Departmanlar'ı seçin.
- İngilizce bölümü için Düzenle köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
- İlk sekmede İngilizce bölümü için Düzenle köprüsüne tıklayın.
İki tarayıcı sekmesi aynı bilgileri görüntüler.
İlk tarayıcı sekmesinde adı değiştirin ve Kaydet'e tıklayın.
Tarayıcı, değiştirilen değeri ve güncelleştirilmiş rowVersion göstergesini içeren Dizin sayfasını gösterir. Güncelleştirilmiş rowVersion göstergesinin, diğer sekmedeki ikinci geri göndermede görüntülendiğine dikkat edin.
İkinci tarayıcı sekmesinde farklı bir alanı değiştirin.
Kaydet'e tıklayın. Veritabanı değerleriyle eşleşmeyen tüm alanlar için hata iletileri görürsünüz:
Bu tarayıcı penceresi Ad alanını değiştirmeyi amaçlamadı. Geçerli değeri (Diller) kopyalayıp Ad alanına yapıştırın. Sekme tuşuyla çıkın. İstemci tarafı doğrulama hata iletisini kaldırır.
Yeniden Kaydet'e tıklayın. İkinci tarayıcı sekmesine girdiğiniz değer kaydedilir. Kaydedilen değerleri Dizin sayfasında görürsünüz.
Sil sayfasını güncelleştirme
Sayfayı sil modelini aşağıdaki kodla güncelleştirin:
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 });
}
}
}
}
Sil sayfası, varlık getirildikten sonra değiştiğinde eşzamanlılık çakışmalarını algılar. Department.RowVersion
varlık getirildiğinde satır sürümüdür. EF Core SQL DELETE komutunu oluşturduğunda, ile RowVersion
bir WHERE yan tümcesi içerir. SQL DELETE komutu etkilenen sıfır satırla sonuçlanırsa:
RowVersion
SQL DELETE komutundaki komutu veritabanında eşleşmiyorRowVersion
.- DbUpdateConcurrencyException özel durumu oluşturulur.
OnGetAsync
ile çağrılırconcurrencyError
.
Sil sayfasını güncelleştirme
Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml
:
@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>
Yukarıdaki kod aşağıdaki değişiklikleri yapar:
- yönergesini
page
olarak@page
@page "{id:int}"
güncelleştirir. - Bir hata iletisi ekler.
- FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
- Son bayta görüntülenecek değişiklikler
RowVersion
. - Gizli satır sürümü ekler.
RowVersion
geri göndermenin değeri bağlaması için eklenmelidir.
Silme sayfasıyla eşzamanlılık çakışmalarını test et
Bir test departmanı oluşturun.
Test departmanında delete'in iki tarayıcı örneğini açın:
- Uygulamayı çalıştırın ve Departmanlar'ı seçin.
- Test departmanı için Sil köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
- Test departmanı için Düzenle köprüsüne tıklayın.
İki tarayıcı sekmesi aynı bilgileri görüntüler.
İlk tarayıcı sekmesinde bütçeyi değiştirin ve Kaydet'e tıklayın.
Tarayıcı, değiştirilen değeri ve güncelleştirilmiş rowVersion göstergesini içeren Dizin sayfasını gösterir. Güncelleştirilmiş rowVersion göstergesinin, diğer sekmedeki ikinci geri göndermede görüntülendiğine dikkat edin.
İkinci sekmeden test bölümünü silin. Db'den geçerli değerlerle bir eşzamanlılık hatası görüntülenir. Sil'e tıklanması, güncelleştirilmediği sürece RowVersion
varlığı siler.
Bkz . Veri modelini devralma hakkında devralma .
Ek kaynaklar
ASP.NET Core