Bölüm 8, Razor ASP.NET Core'da EF Core ile Sayfalar - Eşzamanlılık

Tom Dykstra ve Jon P Smith

Contoso University web uygulaması, EF Core ve Visual Studio kullanarak 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.
  • Başka bir kullanıcı, ilk kullanıcının değişikliği veritabanına yazılmadan önce aynı varlığı güncelleştirir.

Eşzamanlılık algılama etkin değilse, veritabanını son güncelleştiren kişi diğer kullanıcının değişikliklerinin üzerine yazar. Bu risk kabul edilebilirse eşzamanlılık için 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 isteğinde bulunur. Bir satır güncelleştirme erişimi için kilitlendikten sonra, ilk kilit serbest bırakılana kadar başka kullanıcıları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 TL'den 0,00 TL'ye değiştirir.

Changing budget to 0

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.

Changing start date to 2013

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ğinin 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 İngilizce bölümüne göz atacak olan kişi 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ılırsa veri kaybını önleyemezsiniz.
    • 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.

    İngilizce bölümüne bir sonraki göz atışında 1/9/2013 ve getirilen 350.000,00 ABD doları değerini 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şlemez, İ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.

EF Core'da çakışma algılama

Eşzamanlılık belirteçleri 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 EF Core tarafından okunan özgün değerle karşılaştırılır:

  • Değerler eşleşirse 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ında EF Core, bir eşzamanlılık çakışmasını algılamak için ve DELETE deyimlerinin yan tümcesinde 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:

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 dışı veritabanları için 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ımlar. Fluent 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.
  • çağrısında bulunur IsConcurrencyToken. 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 bu yana değişmediğinden emin olmak için özgün değerle karşılaştırılır. Değiştiyse, oluşturulur DbUpdateConcurrencyException ve değişiklikler uygulanmaz.
  • bir varlığı eklerken veya güncelleştirirken özelliği 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 EF Core tarafından 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 ConcurrencyTokenyan tümcesini WHERE gösterir. Veritabanı ConcurrencyToken parametresine ConcurrencyToken@p2eş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 DbUpdateConcurrencyExceptionoluş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öntemine BuildModel aşağıdaki kodu ekler:
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");

yapı iskelesi bölümü sayfaları

Yapı İskelesi Öğrenci sayfalarındaki yönergeleri aşağıdaki özel durumlarla 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, her iki SQLite ad SQL Server ile de ç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ümlerindeki 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 değildir.

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 yerine FullName yazın.

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()
        {
            var deletedDepartment = new Department();
            // 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 Core, özgün ConcurrencyToken değeri içeren bir yan tümcesiyle bir SQL UPDATEWHERE komut oluşturur. Komuttan UPDATE hiçbir satır etkilenmezse, bir DbUpdateConcurrencyException özel durum oluşturulur. Hiçbir satır özgün ConcurrencyToken değere UPDATE sahip olmadığında komutundan 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ğinde Edit varlığın Get getirildiği değerdir. Değer, düzenlenecek varlığı görüntüleyen sayfadaki gizli bir alan Razor tarafından yöntemine sağlanırOnPost. Gizli alan değeri, model bağlayıcısı tarafından öğesine Department.ConcurrencyToken kopyalanır.
  • OriginalValue , EF Core'un yan tümcesinde WHERE kullandığı değerdir. Vurgulanan kod satırı yürütülmeden önce:
    • OriginalValue , bu yöntemde çağrıldığında FirstOrDefaultAsync veritabanında bulunan değere sahiptir.
    • Bu değer, Düzenle sayfasında görüntülenenden farklı olabilir.
  • Vurgulanan kod, EF Core'un WHERE SQL UPDATE deyiminin yan tümcesinde görüntülenen Department varlıktan özgün ConcurrencyToken değeri kullandığından emin olur.

Aşağıdaki kod modeli gösterir Department . Department şu şekilde başlatılır:

  • OnGetAsync yöntemini seçin.
  • OnPostAsyncmodel bağlamasını 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 HTTP GET gelen 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 OnPostAsyncfarklı 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 gerçekleşen 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)}");
    }

ModelState.Remove Önceki ConcurrencyToken değere sahip olduğundan ModelState deyimi gereklidir. Razor Sayfada bir alanın değeri, ModelState her ikisi de mevcut olduğunda model özellik 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 sayfasını güncelleştirme Razor

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 ile @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ş InstructorNameSLolan ile değiştirir.

Düzenleme sayfasıyla eşzamanlılık çakışmalarını test etme

İngilizce departmanında 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.

Department Edit page 1 after change

Tarayıcı, değiştirilen değer ve güncelleştirilmiş ConcurrencyTokengösterge ile Dizin sayfasını gösterir. Güncelleştirilmiş ConcurrencyTokengöstergeyi not edin; diğer sekmedeki ikinci geri göndermede görüntülenir.

İkinci tarayıcı sekmesinde farklı bir alanı değiştirin.

Department Edit page 2 after change

Kaydet’e tıklayın. Veritabanı değerleriyle eşleşmeyen tüm alanlar için hata iletileri görürsünüz:

Department Edit page error message

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.

Kaydet'e yeniden 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. EF Core komutunu oluşturduğunda SQL DELETE , ile where ConcurrencyTokenyan tümcesi içerir. Komut, SQL DELETE etkilenen sıfır satırla sonuçlanırsa:

  • ConcurrencyToken komutundaki SQL DELETE komutu veritabanında eşleşmiyorConcurrencyToken.
  • Bir DbUpdateConcurrencyException özel durum oluşturulur.
  • OnGetAsync ile çağrılır concurrencyError.

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">
            @Utility.GetLastChars(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 ile @page@page "{id:int}"güncelleştirir.
  • Hata iletisi ekler.
  • FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
  • Son bayt görüntülenecek şekilde değişir 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 etme

Test departmanı oluşturma.

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ğer ve güncelleştirilmiş ConcurrencyTokengösterge ile Dizin sayfasını gösterir. Güncelleştirilmiş ConcurrencyTokengöstergeyi not edin; diğer sekmedeki ikinci geri göndermede görüntülenir.

test bölümünü ikinci sekmeden 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

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ö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.
  • Başka bir kullanıcı, ilk kullanıcının değişikliği veritabanına yazılmadan önce aynı varlığı güncelleştirir.

Eşzamanlılık algılama etkin değilse, veritabanını son güncelleştiren kişi diğer kullanıcının değişikliklerinin üzerine yazar. Bu risk kabul edilebilirse eşzamanlılık için 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 isteğinde bulunur. Bir satır güncelleştirme erişimi için kilitlendikten sonra, ilk kilit serbest bırakılana kadar başka kullanıcıları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 TL'den 0,00 TL'ye değiştirir.

Changing budget to 0

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.

Changing start date to 2013

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ğinin 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 İngilizce bölümüne göz atacak olan kişi 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ılırsa veri kaybını önleyemezsiniz.
    • 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şikliğinin Jane'in değişikliğinin üzerine yazmasına izin vekleyebilirsiniz.

    İngilizce bölümüne bir sonraki göz atışında 1/9/2013 ve getirilen 350.000,00 ABD doları değerini 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 uygularsınız. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.

EF Core'da çakışma algılama

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:

  • EF Core'ı 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ın.

    Ç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ümceleriyle sonuçlanabilir 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. rowversion Değer, satır her güncelleştirildiğinde artırılan sıralı bir sayıdır. Güncelleştir veya Sil 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ütundaki rowversion 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. Güncelleştirme veya Silme komutundan hiçbir satır etkilenmediğinde EF Core eşzamanlılık özel durumu oluşturur.

İzleme özelliği ekleme

içinde Models/Department.csRowVersion 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. Fluent 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 sıralı bir satır sürüm numarası oluşturur. Bir Update veya Delete komutunda yan tümcesi Where , 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.
  • yan tümcesi Update getirilen satır sürüm değerini aradığından Where veya Delete komutları satır bulamaz.
  • A DbUpdateConcurrencyException oluşturulur.

Aşağıdaki kod, Bölüm adı güncelleştirildiğinde EF Core tarafından 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 RowVersionyan 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 DbUpdateConcurrencyExceptionoluş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ış kodu BuildModel 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 departman sayfaları

  • Aşağıdaki özel durumlarla birlikte Yapı İskelesi Öğ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.

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 baytası 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>

Düzenleme sayfası modelini 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ığındaki değerle 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, Düzenle sayfasının Get isteğinde ilk 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 öğesine Department.RowVersion kopyalanır.
  • OriginalValue , EF Core'un Where yan tümcesinde kullanacağı şeydir. Vurgulanan kod satırı yürütülmeden önce, OriginalValue bu yöntemde çağrıldığında FirstOrDefaultAsync veritabanında olan değere sahiptir ve bu değer Düzenleme sayfasında görüntülenenden farklı olabilir.
  • Vurgulanan kod, EF Core'un SQL UPDATE deyiminin Where yan tümcesinde görüntülenen Department varlıktan özgün RowVersion değeri kullandığından emin olur.

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 farklı OnPostAsyncveritabanı 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 gerçekleşen 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 bir alanın değeri, ModelState her ikisi de mevcut olduğunda model özellik 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 ile @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ş InstructorNameSLolan ile değiştirir.

Düzenleme sayfasıyla eşzamanlılık çakışmalarını test etme

İngilizce departmanında 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.

Department Edit page 1 after change

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.

Department Edit page 2 after change

Kaydet’e tıklayın. Veritabanı değerleriyle eşleşmeyen tüm alanlar için hata iletileri görürsünüz:

Department Edit page error message

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.

Kaydet'e yeniden 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 RowVersionbir 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şturuldu.
  • OnGetAsync ile çağrılır concurrencyError.

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 ile @page@page "{id:int}"güncelleştirir.
  • Hata iletisi ekler.
  • FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
  • Son bayt görüntülenecek şekilde değişir 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 etme

Test departmanı oluşturma.

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.

test bölümünü ikinci sekmeden 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

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österilmektedir. Çö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.
  • Başka bir kullanıcı, ilk kullanıcının değişikliği db'ye yazılmadan önce 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 TL'den 0,00 TL'ye değiştirir.

Changing budget to 0

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.

Changing start date to 2013

Jane önce Kaydet'e tıklar ve tarayıcı Dizin sayfasını görüntülediğinde değişikliğini görür.

Budget changed to zero

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 İngilizce bölümüne göz atacak olan kişi 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ılırsa veri kaybını önleyemezsiniz.
    • 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şikliğinin Jane'in değişikliğinin üzerine yazmasına izin vekleyebilirsiniz.

    İngilizce bölümüne bir sonraki göz atışında 1/9/2013 ve getirilen 350.000,00 ABD doları değerini 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:

    • Hata iletisi görüntüleme.
    • Verilerin geçerli durumunu gösterir.
    • Kullanıcının değişiklikleri yeniden uygulamasına izin verin.

    Bu, Store Wins senaryosu olarak adlandırılır. (Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir.) Bu öğreticide Store Wins senaryoyu uygularsı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:

VERITABANı ve veri modeli, oluşturma işlemini DbUpdateConcurrencyExceptiondestekleyecek ş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. Data Annotations-ConcurrencyCheck.

[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. Bir Update veya komutunda Where yan tümcesi, getirilen değerini rowversionDelete içerir. Güncelleştirilmekte olan satır değiştiyse:

  • rowversion getirilen değerle eşleşmiyor.
  • Update yan tümcesi getirilen rowversionöğesini içerdiğinden Where veya Delete komutları bir satır bulamaz.
  • A DbUpdateConcurrencyException oluşturulur.

EF Core'da, 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.csRowVersion 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 WhereUpdate dahil olduğunu belirtir. SQL Server'ın önceki sürümleri, SQL türü değiştirmeden önce bir SQL timestamprowversion veri türü kullandığından özniteliği çağrılırTimestamp.

Fluent 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 EF Core tarafından 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 RowVersionyan 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 DbUpdateConcurrencyExceptionoluşturur.

Visual Studio'nun çıkış penceresinde T-SQL EF Core'un ürettiğini 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ış kodu BuildModel 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 son baytası RowVersion görüntülenir. Son baysın benzersiz olması garanti değildir. Gerçek bir uygulama görüntülenmez veya son baytını görüntülemez RowVersionRowVersion.

Dizin sayfasını güncelleştirin:

  • Dizini Departmanlar ile değiştirin.
  • öğesini içeren RowVersion işaretlemeyi değerinin son baytıyla RowVersiondeğ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>

Düzenleme sayfası modelini 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()
        {
            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.");
        }
    }
}

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, veritabanı değerlerinin gönderilenden OnPostAsyncfarklı olduğu her sütun için ö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 gerçekleşen 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 Sayfasında, bir alanın değeri, ModelState her ikisi de mevcut olduğunda model özelliği değerlerinden önceliklidir.

Düzenleme 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 ile @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, kesin olarak türü belirlenmiş InstructorNameSLile değiştirilir.

Düzenleme sayfasıyla eşzamanlılık sınama çakışmaları

İngilizce departmanında 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.

Department Edit page 1 after change

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ğini unutmayın.

İkinci tarayıcı sekmesinde farklı bir alanı değiştirin.

Department Edit page 2 after change

Kaydet’e tıklayın. Veritabanı değerleriyle eşleşmeyen tüm alanlar için hata iletileri görürsünüz:

Department Edit page error message 1

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.

Department Edit page error message 2

Kaydet'e yeniden 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

Sayfa modelini sil'i 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 RowVersionbir 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şturuldu.
  • OnGetAsync ile çağrılır concurrencyError.

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 ile @page@page "{id:int}"güncelleştirir.
  • Hata iletisi ekler.
  • FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
  • Son bayt görüntülenecek şekilde değişir RowVersion .
  • Gizli satır sürümü ekler. RowVersion geri gönder'in değeri bağlaması için eklenmelidir.

Silme sayfasıyla eşzamanlılık çakışmalarını test etme

Test departmanı oluşturma.

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.

test bölümünü ikinci sekmeden 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