Aracılığıyla paylaş


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

Tom Dykstra ve Jon P Smith

Contoso University web uygulaması, ve Visual Studio kullanarak EF Core Sayfalar web uygulamalarının nasıl oluşturulacağını Razor gösterir. Öğretici serisi hakkında bilgi için ilk öğreticiye bakın.

Çözemediğiniz sorunlarla karşılaşırsanız, tamamlanmış uygulamayı indirin ve öğreticiyi izleyerek bu kodu oluşturduğunuz kodla karşılaştırın.

Bu öğreticide, birden çok kullanıcı bir varlığı eşzamanlı olarak güncelleştirdiğinde çakışmaların nasıl işleneceği gösterilmektedir.

Eşzamanlılık çakışmaları

Eşzamanlılık çakışması şu durumlarda oluşur:

  • Kullanıcı bir varlığın düzenleme sayfasına gider.
  • İlk kullanıcının değişikliği veritabanına yazılmadan önce başka bir kullanıcı aynı varlığı güncelleştirir.

Eşzamanlılık algılama etkin değilse, veritabanını güncelleştiren kişi son olarak diğer kullanıcının değişikliklerinin üzerine yazar. Bu risk kabul edilebilirse eşzamanlılık programlama maliyeti avantajdan daha ağır basabilir.

Kötümser eşzamanlılık

Eşzamanlılık çakışmalarını önlemenin bir yolu veritabanı kilitlerini kullanmaktır. Buna kötümser eşzamanlılık denir. Uygulama, güncelleştirmek istediği bir veritabanı satırını okumadan önce bir kilit istemektedir. Bir satır güncelleştirme erişimi için kilitlendikten sonra, ilk kilit serbest bırakılana kadar başka hiçbir kullanıcının satırı kilitlemesine izin verilmez.

Kilitleri yönetmenin dezavantajları vardır. Programlama karmaşık olabilir ve kullanıcı sayısı arttıkça performans sorunlarına neden olabilir. Entity Framework Core, kötümser eşzamanlılık için yerleşik destek sağlamaz.

İyimser eşzamanlılık

İyimser eşzamanlılık eşzamanlılık çakışmalarının gerçekleşmesine olanak tanır ve bu çakışmalar gerçekleştiğinde uygun şekilde tepki verir. Örneğin, Jane Departman düzenleme sayfasını ziyaret ederek İngilizce bölümünün bütçesini 350.000,00 ABD doları olan 0,00 TL olarak değiştirir.

Bütçeyi 0 olarak değiştirme

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.

Başlangıç tarihini 2013 olarak değiştirme

Jane önce Kaydet'e tıklar ve tarayıcı Bütçe tutarı olarak sıfırla Dizin sayfasını görüntülediğinden değişikliğin geçerli olduğunu görür.

John, 350.000,00 TL'lik bütçeyi gösteren Düzenleme sayfasında Kaydet'e tıklar. Bundan sonra ne olacağı eşzamanlılık çakışmalarını nasıl işlediğinize göre belirlenir:

  • Kullanıcının hangi özelliği değiştirdiğini izleyin ve yalnızca veritabanındaki ilgili sütunları güncelleştirin.

    Senaryoda hiçbir veri kaybolmaz. İki kullanıcı tarafından farklı özellikler güncelleştirildi. Bir dahaki sefere birisi İngilizce bölümüne göz atarsa hem Jane'in hem de John'un değişikliklerini görür. Bu güncelleştirme yöntemi, veri kaybına neden olabilecek çakışma sayısını azaltabilir. Bu yaklaşımın bazı dezavantajları vardır:

    • Aynı özellikte rakip değişiklikler yapıldığında veri kaybı önlenemez.
    • Genellikle bir web uygulamasında pratik değildir. Getirilen tüm değerleri ve yeni değerleri izlemek için önemli bir durumun korunmasını gerektirir. Büyük miktarlarda durumun korunması uygulama performansını etkileyebilir.
    • Bir varlıkta eşzamanlılık algılamaya kıyasla uygulama karmaşıklığını artırabilir.
  • John değiştirsin, Jane'in değişikliğinin üzerine yazsın.

    Bir daha İngilizce bölümüne göz atıldığında, 1/9/2013 ve getirilen 350.000,00 ABD doları değeri görür. Bu yaklaşım İstemci Kazanır veya Wins senaryosunda Son olarak adlandırılır. İstemcideki tüm değerler, veri deposundakilerden önceliklidir. yapı iskelesi oluşturulmuş kod eşzamanlılık işlemesi yapmaz, İstemci Kazançları otomatik olarak gerçekleşir.

  • John'un değişikliğinin veritabanında güncelleştirilmesini önleyin. Genellikle uygulama şunları yapar:

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

    Buna Store Wins senaryosu adı verilir. Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir. Store Wins senaryosu bu öğreticide kullanılır. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.

içinde çakışma algılama EF Core

Eşzamanlılık belirteci olarak yapılandırılan özellikler, iyimser eşzamanlılık denetimi uygulamak için kullanılır. veya SaveChangesAsynctarafından SaveChanges bir güncelleştirme veya silme işlemi tetiklendiğinde, veritabanındaki eşzamanlılık belirtecinin değeri tarafından EF Coreokunan özgün değerle karşılaştırılır:

  • Değerler eşleşiyorsa işlem tamamlanabilir.
  • Değerler eşleşmiyorsa, EF Core başka bir kullanıcının çakışan bir işlem gerçekleştirdiğini varsayar, geçerli işlemi durdurur ve bir DbUpdateConcurrencyExceptionoluşturur.

Geçerli işlemle çakışan bir işlem gerçekleştiren başka bir kullanıcı veya işlem eşzamanlılık çakışması olarak bilinir.

İlişkisel veritabanlarındaEF Core, eşzamanlılık çakışmasını algılamak için ve DELETE deyimleri yan tümcesindeki WHERE eşzamanlılık belirtecinin UPDATE değerini denetler.

Veri modeli, bir satırın ne zaman değiştirildiğini belirlemek için kullanılabilecek bir izleme sütunu ekleyerek çakışma algılamayı etkinleştirecek şekilde yapılandırılmalıdır. EF eşzamanlılık belirteçleri için iki yaklaşım sağlar:

SQL Server yaklaşımı ve SQLite uygulama ayrıntıları biraz farklıdır. Daha sonra öğreticide farkları listeleyen bir fark dosyası gösterilir. Visual Studio sekmesinde SQL Server yaklaşımı gösterilir. Visual Studio Code sekmesi, SQLite gibi SQL Server olmayan veritabanlarına yönelik yaklaşımı gösterir.

  • Modelde, bir satırın ne zaman değiştirildiğini belirlemek için kullanılan bir izleme sütunu ekleyin.
  • TimestampAttribute eşzamanlılık özelliğine uygulayın.

Models/Department.cs Dosyayı aşağıdaki vurgulanmış kodla güncelleştirin:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                       ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] ConcurrencyToken { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

TimestampAttribute, sütunu eşzamanlılık izleme sütunu olarak tanımlayan sütundur. Akıcı API, izleme özelliğini belirtmenin alternatif bir yoludur:

modelBuilder.Entity<Department>()
  .Property<byte[]>("ConcurrencyToken")
  .IsRowVersion();

[Timestamp] Bir varlık özelliğindeki özniteliği yönteminde ModelBuilder aşağıdaki kodu oluşturur:

 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");

Yukarıdaki kod:

  • Özellik türünü ConcurrencyToken bayt dizisi olarak ayarlar. byte[] SQL Server için gerekli türdür.
  • IsConcurrencyToken çağrısı yapar. IsConcurrencyToken özelliği eşzamanlılık belirteci olarak yapılandırılır. Güncelleştirmelerde, veritabanındaki eşzamanlılık belirteci değeri, örneğin veritabanından alınmasından sonra değişmediğinden emin olmak için özgün değerle karşılaştırılır. Değiştiyse, bir DbUpdateConcurrencyException oluşturulur ve değişiklikler uygulanmaz.
  • Özelliğini, bir varlık eklerken veya güncelleştirirken otomatik olarak oluşturulan bir değere sahip olacak şekilde yapılandıran ConcurrencyToken öğesini çağırırValueGeneratedOnAddOrUpdate.
  • HasColumnType("rowversion")SQL Server veritabanındaki sütun türünü rowversion olarak ayarlar.

Aşağıdaki kod, ad güncelleştirildiğinde tarafından EF Core oluşturulan T-SQL'in Department bir bölümünü gösterir:

SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

Yukarıdaki vurgulanan kod, öğesini içeren 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ı

Aşağıdaki özel durumlarla birlikte yapı iskelesi Öğrenci sayfalarındaki yönergeleri izleyin:

  • Sayfalar/Departmanlar klasörü oluşturun.
  • Model sınıfı için kullanın Department .
  • Yeni bir tane oluşturmak yerine mevcut bağlam sınıfını kullanın.

Yardımcı program sınıfı ekleme

Proje klasöründe aşağıdaki kodla sınıfını oluşturun Utility :

namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(byte[] token)
        {
            return token[7].ToString();
        }
    }
}

sınıfı, Utility eşzamanlılık belirtecinin GetLastChars son birkaç karakterini görüntülemek için kullanılan yöntemi sağlar. Aşağıdaki kod, hem SQLite ad SQL Server ile çalışan kodu gösterir:

#if SQLiteVersion
using System;

namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(Guid token)
        {
            return token.ToString().Substring(
                                    token.ToString().Length - 3);
        }
    }
}
#else
namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(byte[] token)
        {
            return token[7].ToString();
        }
    }
}
#endif

#if SQLiteVersion Önişlemci yönergesi, SQLite ve SQL Server sürümleri arasındaki farkları yalıtarak aşağıdakilere yardımcı olur:

  • Yazar her iki sürüm için de bir kod tabanı tutar.
  • SQLite geliştiricileri uygulamayı Azure'a dağıtır ve SQL Azure'ı kullanır.

Projeyi derleyin.

Dizin sayfasını güncelleştirme

yapı iskelesi aracı Dizin sayfası için bir ConcurrencyToken sütun oluşturdu, ancak bu alan üretim uygulamasında görüntülenmez. Bu öğreticide, eşzamanlılık işlemenin ConcurrencyToken nasıl çalıştığını göstermeye yardımcı olmak için öğesinin son bölümü görüntülenir. Son bölümün tek başına benzersiz olması garanti edilmemektedir.

Pages\Departments\Index.cshtml sayfasını güncelleştirin:

  • Dizini Departmanlar ile değiştirin.
  • yalnızca son birkaç karakteri gösterecek şekilde içeren ConcurrencyToken kodu değiştirin.
  • FirstMidName öğesini 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>
                Token
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Department)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    @Utility.GetLastChars(item.ConcurrencyToken)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Sayfa modelini düzenle'yi güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml.cs :

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            // Fetch current department from DB.
            // ConcurrencyToken may have changed.
            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            // Set ConcurrencyToken to value read in OnGetAsync
            _context.Entry(departmentToUpdate).Property(
                 d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current ConcurrencyToken so next postback
                    // matches unless an new concurrency issue happens.
                    Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
                    // Clear the model error for the next postback.
                    ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error
            // and overides the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task SetDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}

Eşzamanlılık güncelleştirmeleri

OriginalValue yönteminde ConcurrencyToken getirildiğinde OnGetAsync varlığın değeriyle güncelleştirilir. EF CoreSQL UPDATE özgün ConcurrencyToken değeri içeren bir yan tümcesi olan bir WHERE komut oluşturur. Komuttan hiçbir satır etkilenmezse UPDATE , bir DbUpdateConcurrencyException özel durum oluşturulur. Hiçbir satır özgün ConcurrencyToken değere UPDATE sahip olmadığında komuttan hiçbir satır etkilenmez.

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // Fetch current department from DB.
    // ConcurrencyToken may have changed.
    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Set ConcurrencyToken to value read in OnGetAsync
    _context.Entry(departmentToUpdate).Property(
         d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

Yukarıdaki vurgulanan kodda:

  • içindeki Department.ConcurrencyToken değeri, sayfa isteğinde Edit varlığın Get getirildiği değerdir. Değer, düzenlenecek varlığı görüntüleyen sayfada gizli bir alan Razor tarafından yöntemine sağlanırOnPost. Gizli alan değeri, model bağlayıcısı tarafından öğesine Department.ConcurrencyToken kopyalanır.
  • OriginalValueyan tümcesinde WHERE kullanılırEF Core. Vurgulanan kod satırı yürütülmeden önce:
    • OriginalValue , bu yöntemde çağrıldığında FirstOrDefaultAsync veritabanında olan değere sahiptir.
    • Bu değer, Düzenle sayfasında görüntülenen değerden farklı olabilir.
  • Vurgulanan kod, SQL UPDATE deyiminin WHERE yan tümcesinde görüntülenen Department varlıktan özgün ConcurrencyToken değerin kullanılmasını sağlarEF Core.

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

  • OnGetAsync yöntemini seçin.
  • OnPostAsyncmodel bağlama kullanarak sayfadaki gizli alana Razor göre yöntemi:
public class EditModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public EditModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Department Department { get; set; }
    // Replace ViewData["InstructorID"] 
    public SelectList InstructorNameSL { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Department = await _context.Departments
            .Include(d => d.Administrator)  // eager loading
            .AsNoTracking()                 // tracking not required
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (Department == null)
        {
            return NotFound();
        }

        // Use strongly typed data rather than ViewData.
        InstructorNameSL = new SelectList(_context.Instructors,
            "ID", "FirstMidName");

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch current department from DB.
        // ConcurrencyToken may have changed.
        var departmentToUpdate = await _context.Departments
            .Include(i => i.Administrator)
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (departmentToUpdate == null)
        {
            return HandleDeletedDepartment();
        }

        // Set ConcurrencyToken to value read in OnGetAsync
        _context.Entry(departmentToUpdate).Property(
             d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

Yukarıdaki kod, istekten gelen varlığın Department değerinin istekten gelen HTTP GET ConcurrencyToken değere ayarlandığını gösterirConcurrencyToken.HTTP POST

Eşzamanlılık hatası oluştuğunda, aşağıdaki vurgulanan kod istemci değerlerini (bu yönteme gönderilen değerler) ve veritabanı değerlerini alır.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await SetDbErrorMessage(dbValues, clientValues, _context);

        // Save the current ConcurrencyToken so next postback
        // matches unless an new concurrency issue happens.
        Department.ConcurrencyToken = dbValues.ConcurrencyToken;
        // Clear the model error for the next postback.
        ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
    }

Aşağıdaki kod, her sütun için gönderilenden 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 oluşan eşzamanlılık hataları yakalanacaktır.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await SetDbErrorMessage(dbValues, clientValues, _context);

        // Save the current ConcurrencyToken so next postback
        // matches unless an new concurrency issue happens.
        Department.ConcurrencyToken = dbValues.ConcurrencyToken;
        // Clear the model error for the next postback.
        ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
    }

Önceki ModelState.Remove ConcurrencyToken değere sahip olduğundan ModelState deyimi gereklidir. Razor Sayfada, ModelState her ikisi de mevcut olduğunda bir alanın değeri model özelliği değerlerinden önceliklidir.

SQL Server ile SQLite kod farklılıkları karşılaştırması

Aşağıda SQL Server ile SQLite sürümleri arasındaki farklar gösterilmektedir:

+ using System;    // For GUID on SQLite

+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

 _context.Entry(departmentToUpdate)
    .Property(d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

- Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
+ Department.ConcurrencyToken = dbValues.ConcurrencyToken;

Düzenle Razor sayfasını güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml :

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.ConcurrencyToken" />
            <div class="form-group">
                <label>Version</label>
                @Utility.GetLastChars(Model.Department.ConcurrencyToken)
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Yukarıdaki kod:

  • yönergesini page olarak @page @page "{id:int}"güncelleştirir.
  • Gizli satır sürümü ekler. ConcurrencyToken geri göndermenin değeri bağlaması için eklenmelidir.
  • Hata ayıklama amacıyla son baytını ConcurrencyToken görüntüler.
  • ViewData değerini, kesin olarak türü belirlenmiş InstructorNameSLolan ile değiştirir.

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

İngilizce bölümünde Düzenle'nin iki tarayıcı örneğini açın:

  • Uygulamayı çalıştırın ve Departmanlar'ı seçin.
  • İngilizce bölümü için Düzenle köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
  • İlk sekmede İngilizce bölümü için Düzenle köprüsüne tıklayın.

İki tarayıcı sekmesi aynı bilgileri görüntüler.

İlk tarayıcı sekmesinde adı değiştirin ve Kaydet'e tıklayın.

Değişiklik sonrasında Bölüm Düzenleme sayfası 1

Tarayıcı, değeri değiştirilmiş ve güncelleştirilmiş ConcurrencyTokengöstergeyi içeren Dizin sayfasını gösterir. Güncelleştirilmiş ConcurrencyTokengöstergeye dikkat edin; diğer sekmedeki ikinci geri göndermede görüntülenir.

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

Değişiklik sonrasında Bölüm Düzenleme sayfası 2

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

Bölüm Düzenleme sayfası hata iletisi

Bu tarayıcı penceresi Ad alanını değiştirmeyi amaçlamadı. Geçerli değeri (Diller) kopyalayıp Ad alanına yapıştırın. Sekme tuşuyla çıkın. İstemci tarafı doğrulama hata iletisini kaldırır.

Yeniden Kaydet'e tıklayın. İkinci tarayıcı sekmesine girdiğiniz değer kaydedilir. Kaydedilen değerleri Dizin sayfasında görürsünüz.

Sayfa modelini sil'i güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml.cs :

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.ConcurrencyToken value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

Sil sayfası, varlık getirildikten sonra değiştiğinde eşzamanlılık çakışmalarını algılar. Department.ConcurrencyToken varlık getirildiğinde satır sürümüdür. komutu oluşturulduğunda EF Core SQL DELETE , ile ConcurrencyTokenbir WHERE yan tümcesi içerir. SQL DELETE Komut, etkilenen sıfır satırla sonuçlanırsa:

  • ConcurrencyToken komutu veritabanında SQL DELETE 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">
            @Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
        </dt>
        <dd class="col-sm-10">
            @Utility.GetLastChars(Model.Department.ConcurrencyToken)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.ConcurrencyToken" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Yukarıdaki kod aşağıdaki değişiklikleri yapar:

  • yönergesini page olarak @page @page "{id:int}"güncelleştirir.
  • Bir hata iletisi ekler.
  • FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
  • Son bayta görüntülenecek değişiklikler ConcurrencyToken .
  • Gizli satır sürümü ekler. ConcurrencyToken geri göndermenin değeri bağlaması için eklenmelidir.

Eşzamanlılık çakışmalarını test edin

Bir test departmanı oluşturun.

Test departmanında delete'in iki tarayıcı örneğini açın:

  • Uygulamayı çalıştırın ve Departmanlar'ı seçin.
  • Test departmanı için Sil köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
  • Test departmanı için Düzenle köprüsüne tıklayın.

İki tarayıcı sekmesi aynı bilgileri görüntüler.

İlk tarayıcı sekmesinde bütçeyi değiştirin ve Kaydet'e tıklayın.

Tarayıcı, değeri değiştirilmiş ve güncelleştirilmiş ConcurrencyTokengöstergeyi içeren Dizin sayfasını gösterir. Güncelleştirilmiş ConcurrencyTokengöstergeye dikkat edin; diğer sekmedeki ikinci geri göndermede görüntülenir.

İkinci sekmeden test bölümünü silin. Veritabanındaki geçerli değerlerle eşzamanlılık hatası görüntülenir. Sil'e tıklanması, güncelleştirilmediği sürece ConcurrencyToken varlığı siler.

Ek kaynaklar

Sonraki adımlar

Bu, serideki son öğreticidir. Bu öğretici serisinin MVC sürümünde ek konular ele alınmıştır.

Bu öğreticide, birden çok kullanıcı bir varlığı eşzamanlı olarak güncelleştirdiğinde (aynı anda) çakışmaların nasıl işleneceği gösterilir.

Eşzamanlılık çakışmaları

Eşzamanlılık çakışması şu durumlarda oluşur:

  • Kullanıcı bir varlığın düzenleme sayfasına gider.
  • İlk kullanıcının değişikliği veritabanına yazılmadan önce başka bir kullanıcı aynı varlığı güncelleştirir.

Eşzamanlılık algılama etkin değilse, veritabanını güncelleştiren kişi son olarak diğer kullanıcının değişikliklerinin üzerine yazar. Bu risk kabul edilebilirse eşzamanlılık programlama maliyeti avantajdan daha ağır basabilir.

Kötümser eşzamanlılık (kilitleme)

Eşzamanlılık çakışmalarını önlemenin bir yolu veritabanı kilitlerini kullanmaktır. Buna kötümser eşzamanlılık denir. Uygulama, güncelleştirmek istediği bir veritabanı satırını okumadan önce bir kilit istemektedir. Bir satır güncelleştirme erişimi için kilitlendikten sonra, ilk kilit serbest bırakılana kadar başka hiçbir kullanıcının satırı kilitlemesine izin verilmez.

Kilitleri yönetmenin dezavantajları vardır. Programlama karmaşık olabilir ve kullanıcı sayısı arttıkça performans sorunlarına neden olabilir. Entity Framework Core bunun için yerleşik destek sağlamaz ve bu öğreticide nasıl uygulandığı gösterilmez.

İyimser eşzamanlılık

İyimser eşzamanlılık eşzamanlılık çakışmalarının gerçekleşmesine olanak tanır ve bu çakışmalar gerçekleştiğinde uygun şekilde tepki verir. Örneğin, Jane Departman düzenleme sayfasını ziyaret ederek İngilizce bölümünün bütçesini 350.000,00 ABD doları olan 0,00 TL olarak değiştirir.

Bütçeyi 0 olarak değiştirme

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.

Başlangıç tarihini 2013 olarak değiştirme

Jane önce Kaydet'e tıklar ve tarayıcı Bütçe tutarı olarak sıfırla Dizin sayfasını görüntülediğinden değişikliğin geçerli olduğunu görür.

John, 350.000,00 TL'lik bütçeyi gösteren Düzenleme sayfasında Kaydet'e tıklar. Bundan sonra ne olacağı eşzamanlılık çakışmalarını nasıl işlediğinize göre belirlenir:

  • Kullanıcının hangi özelliği değiştirdiğini izleyebilir ve yalnızca veritabanındaki ilgili sütunları güncelleştirebilirsiniz.

    Senaryoda hiçbir veri kaybolmaz. İki kullanıcı tarafından farklı özellikler güncelleştirildi. Bir dahaki sefere birisi İngilizce bölümüne göz atarsa hem Jane'in hem de John'un değişikliklerini görür. Bu güncelleştirme yöntemi, veri kaybına neden olabilecek çakışma sayısını azaltabilir. Bu yaklaşımın bazı dezavantajları vardır:

    • Aynı özellikte rakip değişiklikler yapıldığında veri kaybı önlenemez.
    • Genellikle bir web uygulamasında pratik değildir. Getirilen tüm değerleri ve yeni değerleri izlemek için önemli bir durumun korunmasını gerektirir. Büyük miktarlarda durumun korunması uygulama performansını etkileyebilir.
    • Bir varlıkta eşzamanlılık algılamaya kıyasla uygulama karmaşıklığını artırabilir.
  • John'un değişiminin Jane'in değişikliğinin üzerine yazmasına izin vekleyebilirsiniz.

    Bir daha İngilizce bölümüne göz atıldığında, 1/9/2013 ve getirilen 350.000,00 ABD doları değeri görür. Bu yaklaşım İstemci Kazanır veya Wins senaryosunda Son olarak adlandırılır. (İstemcideki tüm değerler veri deposundakilerden önceliklidir.) Eşzamanlılık işleme için herhangi bir kodlama yapmazsanız, İstemci Kazançları otomatik olarak gerçekleşir.

  • John'un değişikliğinin veritabanında güncelleştirilmesini engelleyebilirsiniz. Genellikle uygulama şunları yapar:

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

    Buna Store Wins senaryosu adı verilir. (Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir.) Bu öğreticide Store Wins senaryosunu uygulayacaksınız. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.

içinde çakışma algılama EF Core

EF Core çakışmaları DbConcurrencyException algıladığında özel durumlar oluşturur. Çakışma algılamayı etkinleştirmek için veri modelinin yapılandırılması gerekir. Çakışma algılamayı etkinleştirme seçenekleri şunlardır:

  • Güncelleştirme ve Silme komutlarının Where yan tümcesine eşzamanlılık belirteci olarak yapılandırılmış sütunların özgün değerlerini içerecek şekilde yapılandırınEF Core.

    Çağrıldığında SaveChanges , Where yan tümcesi özniteliğiyle ConcurrencyCheckAttribute ek açıklama ekli özelliklerin özgün değerlerini arar. Satır ilk okunduktan sonra eşzamanlılık belirteci özelliklerinden herhangi biri değiştiyse update deyimi güncelleştirilecek bir satır bulamaz. EF Core bunu eşzamanlılık çakışması olarak yorumlar. Çok sayıda sütunu olan veritabanı tablolarında bu yaklaşım çok büyük Where yan tümcelerine neden olabilir ve büyük miktarlarda durum gerektirebilir. Bu nedenle bu yaklaşım genellikle önerilmez ve bu öğreticide kullanılan yöntem değildir.

  • Veritabanı tablosunda, bir satırın ne zaman değiştirildiğini belirlemek için kullanılabilecek bir izleme sütunu ekleyin.

    SQL Server veritabanında, izleme sütununun veri türü şeklindedir rowversion. Değer rowversion , satır her güncelleştirildiğinde artırılan sıralı bir sayıdır. Update veya Delete komutunda Where yan tümcesi, izleme sütununun özgün değerini (özgün satır sürüm numarası) içerir. Güncelleştirilmekte olan satır başka bir kullanıcı tarafından değiştirildiyse, sü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. EF Core Güncelleştir veya Sil komutundan hiçbir satır etkilenmediğinde eşzamanlılık özel durumu oluşturur.

İzleme özelliği ekleme

içinde Models/Department.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. Akıcı API, izleme özelliğini belirtmenin alternatif bir yoludur:

modelBuilder.Entity<Department>()
  .Property<byte[]>("RowVersion")
  .IsRowVersion();

SQL Server veritabanı için bayt [Timestamp] dizisi olarak tanımlanan varlık özelliğindeki özniteliği:

  • Sütunun DELETE ve UPDATE WHERE yan tümcelerine eklenmesine neden olur.
  • Veritabanındaki sütun türünü rowversion olarak ayarlar.

Veritabanı, satır her güncelleştirildiğinde artırılan bir sıralı satır sürüm numarası oluşturur. Update bir veya Delete komutunda yan tümcesiWhere, getirilen satır sürümü değerini içerir. Güncelleştirilmekte olan satır getirildikten sonra değiştiyse:

  • Geçerli satır sürümü değeri getirilen değerle eşleşmiyor.
  • Update yan tümcesi getirilen satır sürümü değerini aradığından Where veya Delete komutları bir satır bulamaz.
  • A DbUpdateConcurrencyException atılır.

Aşağıdaki kod, Bölüm adı güncelleştirildiğinde tarafından EF Core oluşturulan T-SQL'in bir bölümünü gösterir:

SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

Yukarıdaki vurgulanan kod, öğesini içeren 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 Bölümü sayfaları

  • Aşağıdaki özel durumlarla birlikte yapı iskelesi Öğrenci sayfalarındaki yönergeleri izleyin:

  • Sayfalar/Departmanlar klasörü oluşturun.

  • Model sınıfı için kullanın Department .

    • Yeni bir tane oluşturmak yerine mevcut bağlam sınıfını kullanın.

Projeyi derleyin.

Dizin sayfasını güncelleştirme

yapı iskelesi aracı Dizin sayfası için bir RowVersion sütun oluşturdu, ancak bu alan üretim uygulamasında görüntülenmez. Bu öğreticide, eşzamanlılık işlemenin RowVersion nasıl çalıştığını göstermeye yardımcı olmak için öğesinin son baytı görüntülenir. Son bayt tek başına benzersiz olması garanti değildir.

Pages\Departments\Index.cshtml sayfasını güncelleştirin:

  • Dizini Departmanlar ile değiştirin.
  • Bayt dizisinin yalnızca son baytını gösterecek şekilde içeren RowVersion kodu değiştirin.
  • FirstMidName değerini FullName ile değiştirin.

Aşağıdaki kod güncelleştirilmiş sayfayı gösterir:

@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
    ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Department)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    @item.RowVersion[7]
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Sayfa modelini düzenle'yi güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml.cs :

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await setDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            var deletedDepartment = new Department();
            // ModelState contains the posted data because of the deletion error
            // and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task setDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}

OriginalValue yönteminde rowVersion getirildiğinde OnGetAsync varlığın değeriyle güncelleştirilir. EF Core özgün RowVersion değeri içeren WHERE yan tümcesine sahip bir SQL UPDATE komutu oluşturur. UPDATE komutundan hiçbir satır etkilenmezse (hiçbir satır özgün RowVersion değere sahip değilse), bir DbUpdateConcurrencyException özel durum oluşturulur.

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;

Yukarıdaki vurgulanan kodda:

  • içindeki Department.RowVersion değeri, başlangıçta Düzenle sayfasının Get isteğinde getirildiğinde varlığın içindeki değerdir. Değer, düzenlenecek varlığı görüntüleyen sayfada gizli bir alan Razor tarafından yöntemine sağlanırOnPost. Gizli alan değeri, model bağlayıcısı tarafından öğesine Department.RowVersion kopyalanır.
  • OriginalValueEF Core Where yan tümcesinde kullanılacaktır. 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, SQL UPDATE deyiminin Where yan tümcesinde görüntülenen Department varlıktan özgün RowVersion değerin kullanılmasını sağlarEF Core.

Eşzamanlılık hatası oluştuğunda, aşağıdaki vurgulanan kod istemci değerlerini (bu yönteme gönderilen değerler) ve veritabanı değerlerini alır.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await setDbErrorMessage(dbValues, clientValues, _context);

        // Save the current RowVersion so next postback
        // matches unless an new concurrency issue happens.
        Department.RowVersion = (byte[])dbValues.RowVersion;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }

Aşağıdaki kod, her sütun için gönderilenden 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 RowVersion . Kullanıcı Kaydet'e bir sonraki tıklayışında, yalnızca Düzenle sayfasının son görüntüsünden bu yana oluşan eşzamanlılık hataları yakalanacaktır.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await setDbErrorMessage(dbValues, clientValues, _context);

        // Save the current RowVersion so next postback
        // matches unless an new concurrency issue happens.
        Department.RowVersion = (byte[])dbValues.RowVersion;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }

ModelState.Remove Eski RowVersion değere sahip olduğundan ModelState deyimi gereklidir. Razor Sayfada, ModelState her ikisi de mevcut olduğunda bir alanın değeri model özelliği değerlerinden önceliklidir.

Düzenle sayfasını güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml :

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Yukarıdaki kod:

  • yönergesini page olarak @page @page "{id:int}"güncelleştirir.
  • Gizli satır sürümü ekler. RowVersion geri göndermenin değeri bağlaması için eklenmelidir.
  • Hata ayıklama amacıyla son baytını RowVersion görüntüler.
  • ViewData değerini, kesin olarak türü belirlenmiş InstructorNameSLolan ile değiştirir.

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

İngilizce bölümünde Düzenle'nin iki tarayıcı örneğini açın:

  • Uygulamayı çalıştırın ve Departmanlar'ı seçin.
  • İngilizce bölümü için Düzenle köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
  • İlk sekmede İngilizce bölümü için Düzenle köprüsüne tıklayın.

İki tarayıcı sekmesi aynı bilgileri görüntüler.

İlk tarayıcı sekmesinde adı değiştirin ve Kaydet'e tıklayın.

Değişiklik sonrasında Bölüm Düzenleme sayfası 1

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.

Değişiklik sonrasında Bölüm Düzenleme sayfası 2

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

Bölüm Düzenleme sayfası hata iletisi

Bu tarayıcı penceresi Ad alanını değiştirmeyi amaçlamadı. Geçerli değeri (Diller) kopyalayıp Ad alanına yapıştırın. Sekme tuşuyla çıkın. İstemci tarafı doğrulama hata iletisini kaldırır.

Yeniden Kaydet'e tıklayın. İkinci tarayıcı sekmesine girdiğiniz değer kaydedilir. Kaydedilen değerleri Dizin sayfasında görürsünüz.

Sayfa modelini sil'i güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Delete.cshtml.cs :

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

Sil sayfası, varlık getirildikten sonra değiştiğinde eşzamanlılık çakışmalarını algılar. Department.RowVersion varlık getirildiğinde satır sürümüdür. EF Core SQL DELETE komutunu oluşturduğunda, ile 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şturulur.
  • 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 olarak @page @page "{id:int}"güncelleştirir.
  • Bir hata iletisi ekler.
  • FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
  • Son bayta görüntülenecek değişiklikler RowVersion .
  • Gizli satır sürümü ekler. RowVersion geri göndermenin değeri bağlaması için eklenmelidir.

Eşzamanlılık çakışmalarını test edin

Bir test departmanı oluşturun.

Test departmanında delete'in iki tarayıcı örneğini açın:

  • Uygulamayı çalıştırın ve Departmanlar'ı seçin.
  • Test departmanı için Sil köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
  • Test departmanı için Düzenle köprüsüne tıklayın.

İki tarayıcı sekmesi aynı bilgileri görüntüler.

İlk tarayıcı sekmesinde bütçeyi değiştirin ve Kaydet'e tıklayın.

Tarayıcı, değiştirilen değeri ve güncelleştirilmiş rowVersion göstergesini içeren Dizin sayfasını gösterir. Güncelleştirilmiş rowVersion göstergesinin, diğer sekmedeki ikinci geri göndermede görüntülendiğine dikkat edin.

İkinci sekmeden test bölümünü silin. Veritabanındaki geçerli değerlerle eşzamanlılık hatası görüntülenir. Sil'e tıklanması, güncelleştirilmediği sürece RowVersion varlığı siler.

Ek kaynaklar

Sonraki adımlar

Bu, serideki son öğreticidir. Bu öğretici serisinin MVC sürümünde ek konular ele alınmıştır.

Bu öğreticide, birden çok kullanıcı bir varlığı eşzamanlı olarak güncelleştirdiğinde (aynı anda) çakışmaların nasıl işleneceği gösterilir. Çözemediğiniz sorunlarla karşılaşırsanız tamamlanmış uygulamayı indirin veya görüntüleyin. İndirme yönergeleri.

Eşzamanlılık çakışmaları

Eşzamanlılık çakışması şu durumlarda oluşur:

  • Kullanıcı bir varlığın düzenleme sayfasına gider.
  • İlk kullanıcının değişikliği DB'ye yazılmadan önce başka bir kullanıcı aynı varlığı güncelleştirir.

Eşzamanlılık algılama etkin değilse, eşzamanlı güncelleştirmeler gerçekleştiğinde:

  • Son güncelleştirme kazanır. Yani, son güncelleştirme değerleri db'ye kaydedilir.
  • Geçerli güncelleştirmelerin ilki kaybolur.

İyimser eşzamanlılık

İyimser eşzamanlılık eşzamanlılık çakışmalarının gerçekleşmesine olanak tanır ve bu çakışmalar gerçekleştiğinde uygun şekilde tepki verir. Örneğin, Jane Departman düzenleme sayfasını ziyaret ederek İngilizce bölümünün bütçesini 350.000,00 ABD doları olan 0,00 TL olarak değiştirir.

Bütçeyi 0 olarak değiştirme

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.

Başlangıç tarihini 2013 olarak değiştirme

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

Bütçe sıfır olarak değiştirildi

John, 350.000,00 TL'lik bütçeyi gösteren Düzenleme sayfasında Kaydet'e tıklar. Bundan sonra ne olacağı eşzamanlılık çakışmalarını nasıl işlediğinize göre belirlenir.

İyimser eşzamanlılık aşağıdaki seçenekleri içerir:

  • Kullanıcının hangi özelliği değiştirdiğini izleyebilir ve yalnızca db'deki ilgili sütunları güncelleştirebilirsiniz.

    Senaryoda hiçbir veri kaybolmaz. İki kullanıcı tarafından farklı özellikler güncelleştirildi. Bir dahaki sefere birisi İngilizce bölümüne göz atarsa hem Jane'in hem de John'un değişikliklerini görür. Bu güncelleştirme yöntemi, veri kaybına neden olabilecek çakışma sayısını azaltabilir. Bu yaklaşım:

    • Aynı özellikte rakip değişiklikler yapıldığında veri kaybı önlenemez.
    • Genellikle bir web uygulamasında pratik değildir. Getirilen tüm değerleri ve yeni değerleri izlemek için önemli bir durumun korunmasını gerektirir. Büyük miktarlarda durumun korunması uygulama performansını etkileyebilir.
    • Bir varlıkta eşzamanlılık algılamaya kıyasla uygulama karmaşıklığını artırabilir.
  • John'un değişiminin Jane'in değişikliğinin üzerine yazmasına izin vekleyebilirsiniz.

    Bir daha İngilizce bölümüne göz atıldığında, 1/9/2013 ve getirilen 350.000,00 ABD doları değeri görür. Bu yaklaşım İstemci Kazanır veya Wins senaryosunda Son olarak adlandırılır. (İstemcideki tüm değerler veri deposundakilerden önceliklidir.) Eşzamanlılık işleme için herhangi bir kodlama yapmazsanız, İstemci Kazançları otomatik olarak gerçekleşir.

  • John'un değişikliğinin db'de güncelleştirilmesini engelleyebilirsiniz. Genellikle uygulama şunları yapar:

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

    Buna Store Wins senaryosu adı verilir. (Veri deposu değerleri, istemci tarafından gönderilen değerlerden önceliklidir.) Bu öğreticide Store Wins senaryosunu uygulayacaksınız. Bu yöntem, kullanıcı uyarılmadan hiçbir değişikliğin üzerine yazılmamasını sağlar.

Eşzamanlılığı işleme

Bir özellik eşzamanlılık belirteci olarak yapılandırıldığında:

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 . Veri Ek Açıklamaları-Eşzamanlılık Denetimi.

[ConcurrencyCheck] Özniteliği bu öğreticide kullanılmaz.

Satırdaki eşzamanlılık çakışmalarını algılama

Eşzamanlılık çakışmalarını algılamak için modele bir rowversion izleme sütunu eklenir. rowversion :

  • SQL Server'a özgüdür. Diğer veritabanları benzer bir özellik sağlamayabilir.
  • Bir varlığın veritabanından getirildikten sonra değiştirilmediğini belirlemek için kullanılır.

Veritabanı, satır her güncelleştirildiğinde artırılan bir sıralı rowversion sayı oluşturur. veya Update Delete komutunda yan tümcesi Where getirilen değerini rowversioniç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 atılır.

içinde EF Core, bir Update veya Delete komutu tarafından hiçbir satır güncelleştirilmemişse eşzamanlılık özel durumu oluşturulur.

Departman varlığına izleme özelliği ekleme

içinde Models/Department.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 Where Update dahil olduğunu belirtir. SQL Server'ın önceki sürümleri SQL türü değiştirmeden önce bir SQL timestamp rowversion veri türü kullandığından özniteliği çağrılırTimestamp.

Akıcı API, izleme özelliğini de belirtebilir:

modelBuilder.Entity<Department>()
  .Property<byte[]>("RowVersion")
  .IsRowVersion();

Aşağıdaki kod, Bölüm adı güncelleştirildiğinde tarafından EF Core oluşturulan T-SQL'in bir bölümünü gösterir:

SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

Yukarıdaki vurgulanan kod, öğesini içeren 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.

T-SQL'in EF Core ürettiğini Visual Studio'nun çıkış penceresinde görebilirsiniz.

Db'yi güncelleştirme

özelliğinin RowVersion eklenmesi, geçiş gerektiren VERITABANı modelini değiştirir.

Projeyi derleyin. Komut penceresine aşağıdakileri girin:

dotnet ef migrations add RowVersion
dotnet ef database update

Önceki komutlar:

  • Migrations/{time stamp}_RowVersion.cs Geçiş dosyasını ekler.

  • Migrations/SchoolContextModelSnapshot.cs Dosyayı güncelleştirir. Güncelleştirme yöntemine aşağıdaki vurgulanmış 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 RowVersion son baytı görüntülenir. Son baysın benzersiz olması garanti değildir. Gerçek bir uygulama görüntülenmez veya son baytı RowVersiongörüntülenmezRowVersion.

Dizin sayfasını güncelleştirin:

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

Sayfa modelini düzenle'yi güncelleştirme

Aşağıdaki kodla güncelleştirin Pages/Departments/Edit.cshtml.cs :

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            // null means Department was deleted by another user.
            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            // Update the RowVersion to the value when this entity was
            // fetched. If the entity has been updated after it was
            // fetched, RowVersion won't match the DB RowVersion and
            // a DbUpdateConcurrencyException is thrown.
            // A second postback will make them match, unless a new 
            // concurrency issue happens.
            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Must clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task SetDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}

Eşzamanlılık sorununu algılamak için , OriginalValue getirildiği varlığın rowVersion değeriyle güncelleştirilir. EF Core özgün RowVersion değeri içeren WHERE yan tümcesine sahip bir SQL UPDATE komutu oluşturur. UPDATE komutundan hiçbir satır etkilenmezse (hiçbir satır özgün RowVersion değere sahip değilse), bir DbUpdateConcurrencyException özel durum oluşturulur.

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    // null means Department was deleted by another user.
    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Update the RowVersion to the value when this entity was
    // fetched. If the entity has been updated after it was
    // fetched, RowVersion won't match the DB RowVersion and
    // a DbUpdateConcurrencyException is thrown.
    // A second postback will make them match, unless a new 
    // concurrency issue happens.
    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;

Yukarıdaki kodda, Department.RowVersion varlık getirildiğinde değerdir. OriginalValue , bu yöntemde çağrıldığında FirstOrDefaultAsync db'deki değerdir.

Aşağıdaki kod istemci değerlerini (bu yönteme gönderilen değerler) ve VERITABANı değerlerini alır:

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await SetDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

Aşağıdaki kod, her sütun için gönderilenden farklı 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 DB'den alınan yeni değere ayarlar RowVersion . Kullanıcı Kaydet'e bir sonraki tıklayışında, yalnızca Düzenle sayfasının son görüntüsünden bu yana oluşan eşzamanlılık hataları yakalanacaktır.

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await SetDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

ModelState.Remove Eski RowVersion değere sahip olduğundan ModelState deyimi gereklidir. Razor Sayfada, ModelState her ikisi de mevcut olduğunda bir alanın değeri model özelliği değerlerinden önceliklidir.

Düzenle sayfasını güncelleştirme

Aşağıdaki işaretlemeyle güncelleştirin Pages/Departments/Edit.cshtml :

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Yukarıdaki işaretleme:

  • yönergesini page olarak @page @page "{id:int}"güncelleştirir.
  • Gizli satır sürümü ekler. RowVersion geri göndermenin değeri bağlaması için eklenmelidir.
  • Hata ayıklama amacıyla son baytını RowVersion görüntüler.
  • ViewData değerini, kesin olarak türü belirlenmiş InstructorNameSLolan ile değiştirir.

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

İngilizce bölümünde Düzenle'nin iki tarayıcı örneğini açın:

  • Uygulamayı çalıştırın ve Departmanlar'ı seçin.
  • İngilizce bölümü için Düzenle köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
  • İlk sekmede İngilizce bölümü için Düzenle köprüsüne tıklayın.

İki tarayıcı sekmesi aynı bilgileri görüntüler.

İlk tarayıcı sekmesinde adı değiştirin ve Kaydet'e tıklayın.

Değişiklik sonrasında Bölüm Düzenleme sayfası 1

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.

Değişiklik sonrasında Bölüm Düzenleme sayfası 2

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

Bölüm Düzenleme sayfası hata iletisi 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.

Bölüm Düzenleme sayfası hata iletisi 2

Yeniden Kaydet'e tıklayın. İkinci tarayıcı sekmesine girdiğiniz değer kaydedilir. Kaydedilen değerleri Dizin sayfasında görürsünüz.

Sil sayfasını güncelleştirme

Sayfayı sil modelini aşağıdaki kodla güncelleştirin:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

Sil sayfası, varlık getirildikten sonra değiştiğinde eşzamanlılık çakışmalarını algılar. Department.RowVersion varlık getirildiğinde satır sürümüdür. EF Core SQL DELETE komutunu oluşturduğunda, ile 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şturulur.
  • 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 olarak @page @page "{id:int}"güncelleştirir.
  • Bir hata iletisi ekler.
  • FirstMidName değerini Yönetici alanındaki FullName ile değiştirir.
  • Son bayta görüntülenecek değişiklikler RowVersion .
  • Gizli satır sürümü ekler. RowVersion geri göndermenin değeri bağlaması için eklenmelidir.

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

Bir test departmanı oluşturun.

Test departmanında delete'in iki tarayıcı örneğini açın:

  • Uygulamayı çalıştırın ve Departmanlar'ı seçin.
  • Test departmanı için Sil köprüsüne sağ tıklayın ve Yeni sekmede aç'ı seçin.
  • Test departmanı için Düzenle köprüsüne tıklayın.

İki tarayıcı sekmesi aynı bilgileri görüntüler.

İlk tarayıcı sekmesinde bütçeyi değiştirin ve Kaydet'e tıklayın.

Tarayıcı, değiştirilen değeri ve güncelleştirilmiş rowVersion göstergesini içeren Dizin sayfasını gösterir. Güncelleştirilmiş rowVersion göstergesinin, diğer sekmedeki ikinci geri göndermede görüntülendiğine dikkat edin.

İkinci sekmeden test bölümünü silin. Db'den geçerli değerlerle bir eşzamanlılık hatası görüntülenir. Sil'e tıklanması, güncelleştirilmediği sürece RowVersion varlığı siler.

Bkz . Veri modelini devralma hakkında devralma .

Ek kaynaklar