Bagikan melalui


Bagian 8, Razor Halaman dengan EF Core inti ASP.NET - Konkurensi

Tom Dykstra, dan Jon P Smith

Aplikasi web Contoso University menunjukkan cara membuat Razor aplikasi web Pages menggunakan EF Core dan Visual Studio. Untuk informasi tentang seri tutorial, lihat tutorial pertama.

Jika Anda mengalami masalah yang tidak dapat Anda selesaikan, unduh aplikasi yang telah selesai dan bandingkan kode tersebut dengan apa yang Anda buat dengan mengikuti tutorial.

Tutorial ini menunjukkan cara menangani konflik saat beberapa pengguna memperbarui entitas secara bersamaan.

Konflik konkurensi

Konflik konkurensi terjadi ketika:

  • Pengguna menavigasi ke halaman edit untuk entitas.
  • Pengguna lain memperbarui entitas yang sama sebelum perubahan pengguna pertama ditulis ke database.

Jika deteksi konkurensi tidak diaktifkan, siapa pun yang memperbarui database terakhir kali menimpa perubahan pengguna lain. Jika risiko ini dapat diterima, biaya pemrograman untuk konkurensi mungkin melebihi manfaatnya.

Konkurensi pesimis

Salah satu cara untuk mencegah konflik konkurensi adalah dengan menggunakan kunci database. Ini disebut konkurensi pesimis. Sebelum aplikasi membaca baris database yang ingin diperbarui, aplikasi meminta kunci. Setelah baris dikunci untuk akses pembaruan, tidak ada pengguna lain yang diizinkan untuk mengunci baris hingga kunci pertama dirilis.

Mengelola kunci memiliki kekurangan. Ini bisa rumit untuk diprogram dan dapat menyebabkan masalah performa saat jumlah pengguna meningkat. Entity Framework Core tidak menyediakan dukungan bawaan untuk konkurensi pesimis.

Konkurensi optimis

Konkurensi optimis memungkinkan konflik konkurensi terjadi, lalu bereaksi dengan tepat ketika terjadi. Misalnya, Jane mengunjungi halaman edit Departemen dan mengubah anggaran untuk departemen Bahasa Inggris dari $ 350.000,00 menjadi $ 0,00.

Changing budget to 0

Sebelum Klik Jane Simpan, John mengunjungi halaman yang sama dan mengubah bidang Tanggal Mulai dari 1/9/2007 menjadi 1/9/2013.

Changing start date to 2013

Jane mengklik Simpan terlebih dahulu dan melihat perubahannya berlaku, karena browser menampilkan halaman Indeks dengan nol sebagai jumlah Anggaran.

John mengklik Simpan pada halaman Edit yang masih menunjukkan anggaran $350.000,00. Apa yang terjadi selanjutnya ditentukan oleh cara Anda menangani konflik konkurensi:

  • Lacak properti mana yang telah dimodifikasi pengguna dan perbarui hanya kolom yang sesuai dalam database.

    Dalam skenario, tidak ada data yang akan hilang. Properti yang berbeda diperbarui oleh dua pengguna. Lain kali seseorang menelusuri departemen Inggris, mereka akan melihat perubahan Jane dan John. Metode pembaruan ini dapat mengurangi jumlah konflik yang dapat mengakibatkan hilangnya data. Pendekatan ini memiliki beberapa kelemahan:

    • Tidak dapat menghindari kehilangan data jika perubahan yang bersaing dilakukan pada properti yang sama.
    • Umumnya tidak praktis di aplikasi web. Ini membutuhkan mempertahankan status yang signifikan untuk melacak semua nilai yang diambil dan nilai baru. Mempertahankan status dalam jumlah besar dapat memengaruhi performa aplikasi.
    • Dapat meningkatkan kompleksitas aplikasi dibandingkan dengan deteksi konkurensi pada entitas.
  • Mari kita ubah menimpa perubahan Jane.

    Lain kali seseorang menelusuri departemen Bahasa Inggris, mereka akan melihat 9/1/2013 dan nilai $350,000.00 yang diambil. Pendekatan ini disebut skenario Client Wins atau Last in Wins . Semua nilai dari klien lebih diutamakan daripada apa yang ada di penyimpanan data. Kode perancah tidak melakukan penanganan konkurensi, Client Wins terjadi secara otomatis.

  • Mencegah perubahan John diperbarui dalam database. Biasanya, aplikasi akan:

    • Menampilkan pesan kesalahan.
    • Perlihatkan status data saat ini.
    • Izinkan pengguna untuk menerapkan kembali perubahan.

    Ini disebut skenario Store Wins . Nilai penyimpanan data lebih diutamakan daripada nilai yang dikirimkan oleh klien. Skenario Store Wins digunakan dalam tutorial ini. Metode ini memastikan bahwa tidak ada perubahan yang ditimpa tanpa pengguna diberi tahu.

Deteksi konflik dalam EF Core

Properti yang dikonfigurasi sebagai token konkurensi digunakan untuk menerapkan kontrol konkurensi optimis. Ketika operasi pembaruan atau penghapusan dipicu oleh SaveChanges atau SaveChangesAsync, nilai token konkurensi dalam database dibandingkan dengan nilai asli yang dibaca oleh EF Core:

  • Jika nilai cocok, operasi dapat diselesaikan.
  • Jika nilai tidak cocok, EF Core mengasumsikan bahwa pengguna lain telah melakukan operasi yang bertentangan, membatalkan transaksi saat ini, dan melempar DbUpdateConcurrencyException.

Pengguna atau proses lain yang melakukan operasi yang bertentangan dengan operasi saat ini dikenal sebagai konflik konkurensi.

Pada database relasional EF Core memeriksa nilai token konkurensi dalam WHERE klausul UPDATE dan DELETE pernyataan untuk mendeteksi konflik konkurensi.

Model data harus dikonfigurasi untuk mengaktifkan deteksi konflik dengan menyertakan kolom pelacakan yang dapat digunakan untuk menentukan kapan baris telah diubah. EF menyediakan dua pendekatan untuk token konkurensi:

Pendekatan SQL Server dan detail implementasi SQLite sedikit berbeda. File perbedaan ditampilkan nanti dalam tutorial yang mencantumkan perbedaan. Tab Visual Studio memperlihatkan pendekatan SQL Server. Tab Visual Studio Code memperlihatkan pendekatan untuk database non-SQL Server, seperti SQLite.

  • Dalam model, sertakan kolom pelacakan yang digunakan untuk menentukan kapan baris telah diubah.
  • Terapkan ke TimestampAttribute properti konkurensi.

Models/Department.cs Perbarui file dengan kode yang disorot berikut:

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; }
    }
}

inilah TimestampAttribute yang mengidentifikasi kolom sebagai kolom pelacakan konkurensi. API fasih adalah cara alternatif untuk menentukan properti pelacakan:

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

Atribut [Timestamp] pada properti entitas menghasilkan kode berikut dalam ModelBuilder metode :

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

Kode sebelumnya:

  • Mengatur jenis ConcurrencyToken properti ke array byte. byte[] adalah jenis yang diperlukan untuk SQL Server.
  • Panggilan IsConcurrencyToken. IsConcurrencyToken mengonfigurasi properti sebagai token konkurensi. Pada pembaruan, nilai token konkurensi dalam database dibandingkan dengan nilai asli untuk memastikannya tidak berubah sejak instans diambil dari database. Jika telah berubah, akan DbUpdateConcurrencyException dilemparkan dan perubahan tidak diterapkan.
  • ValueGeneratedOnAddOrUpdatePanggilan , yang mengonfigurasi ConcurrencyToken properti agar memiliki nilai yang dihasilkan secara otomatis saat menambahkan atau memperbarui entitas.
  • HasColumnType("rowversion") mengatur jenis kolom dalam database SQL Server ke rowversion.

Kode berikut menunjukkan sebagian T-SQL yang dihasilkan oleh EF Core saat nama diperbarui Department :

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

Kode yang disorot sebelumnya menunjukkan WHERE klausa yang berisi ConcurrencyToken. Jika database ConcurrencyToken tidak sama dengan ConcurrencyToken parameter @p2, tidak ada baris yang diperbarui.

Kode yang disorot berikut menunjukkan T-SQL yang memverifikasi persis satu baris diperbarui:

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 mengembalikan jumlah baris yang terpengaruh oleh pernyataan terakhir. Jika tidak ada baris yang diperbarui, EF Core melempar .DbUpdateConcurrencyException

Menambahkan migrasi

Menambahkan properti mengubah ConcurrencyToken model data, yang memerlukan migrasi.

Bangun proyek.

Jalankan perintah berikut di PMC:

Add-Migration RowVersion
Update-Database

Perintah sebelumnya:

  • Migrations/{time stamp}_RowVersion.cs Membuat file migrasi.
  • Migrations/SchoolContextModelSnapshot.cs Memperbarui file. Pembaruan menambahkan kode berikut ke BuildModel metode :
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");

Halaman Departemen Perancah

Ikuti instruksi di halaman Scaffold Student dengan pengecualian berikut:

  • Buat folder Halaman/Departemen .
  • Gunakan Department untuk kelas model.
  • Gunakan kelas konteks yang ada alih-alih membuat yang baru.

Menambahkan kelas utilitas

Di folder proyek, buat Utility kelas dengan kode berikut:

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

Kelas Utility menyediakan metode yang GetLastChars digunakan untuk menampilkan beberapa karakter terakhir dari token konkurensi. Kode berikut menunjukkan kode yang berfungsi dengan SQL Server iklan SQLite:

#if SQLiteVersion
using System;

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

Direktif #if SQLiteVersion praprosedur mengisolasi perbedaan dalam versi SQLite dan SQL Server dan membantu:

  • Penulis mempertahankan satu basis kode untuk kedua versi.
  • Pengembang SQLite menyebarkan aplikasi ke Azure dan menggunakan SQL Azure.

Bangun proyek.

Memperbarui halaman Indeks

Alat perancah membuat ConcurrencyToken kolom untuk halaman Indeks, tetapi bidang tersebut tidak akan ditampilkan di aplikasi produksi. Dalam tutorial ini, bagian terakhir ditampilkan ConcurrencyToken untuk membantu menunjukkan cara kerja penanganan konkurensi. Bagian terakhir tidak dijamin unik dengan sendirinya.

Perbarui halaman Pages\Departments\Index.cshtml :

  • Ganti Indeks dengan Departemen.
  • Ubah kode yang berisi ConcurrencyToken untuk menampilkan hanya beberapa karakter terakhir.
  • Ganti FirstMidName dengan FullName.

Kode berikut menunjukkan halaman yang diperbarui:

@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>

Memperbarui model halaman Edit

Perbarui Pages/Departments/Edit.cshtml.cs dengan kode berikut:

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.");
        }
    }
}

Pembaruan konkurensi

OriginalValue diperbarui dengan ConcurrencyToken nilai dari entitas ketika diambil dalam OnGetAsync metode . EF Core menghasilkan perintah dengan klausul SQL UPDATE yang WHERE berisi nilai asli ConcurrencyToken . Jika tidak ada baris yang dipengaruhi oleh UPDATE perintah, DbUpdateConcurrencyException pengecualian akan dilemparkan. Tidak ada baris yang dipengaruhi oleh UPDATE perintah ketika tidak ada baris yang memiliki nilai asli ConcurrencyToken .

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;

Dalam kode yang disorot sebelumnya:

  • Nilai di Department.ConcurrencyToken adalah nilai ketika entitas diambil dalam Get permintaan untuk Edit halaman. Nilai disediakan untuk OnPost metode oleh bidang tersembunyi di Razor halaman yang menampilkan entitas yang akan diedit. Nilai bidang tersembunyi disalin oleh pengikat Department.ConcurrencyToken model.
  • OriginalValue adalah apa yang EF Core digunakan dalam WHERE klausul. Sebelum baris kode yang disorot dijalankan:
    • OriginalValue memiliki nilai yang ada dalam database ketika FirstOrDefaultAsync dipanggil dalam metode ini.
    • Nilai ini mungkin berbeda dari apa yang ditampilkan di halaman Edit.
  • Kode yang disorot memastikan bahwa EF Core menggunakan nilai asli ConcurrencyToken dari entitas yang Department ditampilkan dalam klausa pernyataan WHERE SQLUPDATE.

Kode berikut menunjukkan Department model. Department diinisialisasi dalam:

  • OnGetAsync metode dengan kueri EF.
  • OnPostAsync metode dengan bidang tersembunyi di Razor halaman menggunakan pengikatan model:
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;

Kode sebelumnya menunjukkan ConcurrencyToken nilai entitas dari HTTP POST permintaan diatur ke ConcurrencyToken nilai dari HTTP GET permintaan.Department

Ketika kesalahan konkurensi terjadi, kode yang disorot berikut mendapatkan nilai klien (nilai yang diposting ke metode ini) dan nilai database.

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)}");
    }

Kode berikut menambahkan pesan kesalahan kustom untuk setiap kolom yang memiliki nilai database yang berbeda dari yang diposting ke OnPostAsync:

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

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

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

Kode yang disorot ConcurrencyToken berikut menetapkan nilai ke nilai baru yang diambil dari database. Saat berikutnya pengguna mengklik Simpan, hanya kesalahan konkurensi yang terjadi sejak tampilan terakhir halaman Edit yang akan tertangkap.

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)}");
    }

Pernyataan ModelState.Remove diperlukan karena ModelState memiliki nilai sebelumnya ConcurrencyToken . Razor Di Halaman, ModelState nilai untuk bidang lebih diutamakan daripada nilai properti model saat keduanya ada.

Perbedaan kode SQL Server vs SQLite

Berikut ini menunjukkan perbedaan antara versi SQL Server dan SQLite:

+ using System;    // For GUID on SQLite

+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

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

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

Memperbarui halaman Edit Razor

Perbarui Pages/Departments/Edit.cshtml dengan kode berikut:

@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");}
}

Kode sebelumnya:

  • Memperbarui direktif page dari @page ke @page "{id:int}".
  • Menambahkan versi baris tersembunyi. ConcurrencyToken harus ditambahkan sehingga postback mengikat nilai.
  • Menampilkan byte ConcurrencyToken terakhir untuk tujuan penelusuran kesalahan.
  • ViewData Mengganti dengan yang dititik InstructorNameSLdengan kuat .

Menguji konflik konkurensi dengan halaman Edit

Buka dua instans browser Edit di departemen bahasa Inggris:

  • Jalankan aplikasi dan pilih Departemen.
  • Klik kanan hyperlink Edit untuk departemen bahasa Inggris dan pilih Buka di tab baru.
  • Di tab pertama, klik edit hyperlink untuk departemen bahasa Inggris.

Dua tab browser menampilkan informasi yang sama.

Ubah nama di tab browser pertama dan klik Simpan.

Department Edit page 1 after change

Browser memperlihatkan halaman Indeks dengan nilai yang diubah dan indikator yang diperbarui ConcurrencyToken. Perhatikan indikator yang diperbarui ConcurrencyToken, indikator ditampilkan pada postback kedua di tab lain.

Ubah bidang lain di tab browser kedua.

Department Edit page 2 after change

Klik Simpan. Anda melihat pesan kesalahan untuk semua bidang yang tidak cocok dengan nilai database:

Department Edit page error message

Jendela browser ini tidak berniat mengubah bidang Nama. Salin dan tempel nilai saat ini (Bahasa) ke bidang Nama. Tab keluar. Validasi sisi klien menghapus pesan kesalahan.

Klik Simpan lagi. Nilai yang Anda masukkan di tab browser kedua disimpan. Anda melihat nilai yang disimpan di halaman Indeks.

Memperbarui model halaman Hapus

Perbarui Pages/Departments/Delete.cshtml.cs dengan kode berikut:

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 });
            }
        }
    }
}

Halaman Hapus mendeteksi konflik konkurensi ketika entitas telah berubah setelah diambil. Department.ConcurrencyToken adalah versi baris saat entitas diambil. Saat EF Core membuat SQL DELETE perintah, perintah tersebut menyertakan klausa WHERE dengan ConcurrencyToken. SQL DELETE Jika perintah menghasilkan nol baris yang terpengaruh:

  • ConcurrencyToken Dalam SQL DELETE perintah tidak cocok ConcurrencyToken dalam database.
  • Pengecualian DbUpdateConcurrencyException dilemparkan.
  • OnGetAsync dipanggil dengan concurrencyError.

Memperbarui halaman Hapus Razor

Perbarui Pages/Departments/Delete.cshtml dengan kode berikut:

@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>

Kode sebelumnya membuat perubahan berikut:

  • Memperbarui direktif page dari @page ke @page "{id:int}".
  • Menambahkan pesan kesalahan.
  • Mengganti FirstMidName dengan FullName di bidang Administrator .
  • Perubahan ConcurrencyToken untuk menampilkan byte terakhir.
  • Menambahkan versi baris tersembunyi. ConcurrencyToken harus ditambahkan sehingga postback mengikat nilai.

Menguji konflik konkurensi

Buat departemen pengujian.

Buka dua instans browser Hapus pada departemen pengujian:

  • Jalankan aplikasi dan pilih Departemen.
  • Klik kanan hyperlink Hapus untuk departemen pengujian dan pilih Buka di tab baru.
  • Klik edit hyperlink untuk departemen pengujian.

Dua tab browser menampilkan informasi yang sama.

Ubah anggaran di tab browser pertama dan klik Simpan.

Browser memperlihatkan halaman Indeks dengan nilai yang diubah dan indikator yang diperbarui ConcurrencyToken. Perhatikan indikator yang diperbarui ConcurrencyToken, indikator ditampilkan pada postback kedua di tab lain.

Hapus departemen pengujian dari tab kedua. Kesalahan konkurensi ditampilkan dengan nilai saat ini dari database. Mengklik Hapus akan menghapus entitas, kecuali ConcurrencyToken telah diperbarui.

Sumber Daya Tambahan:

Langkah berikutnya

Ini adalah tutorial terakhir dalam seri ini. Topik tambahan dibahas dalam versi MVC dari seri tutorial ini.

Tutorial ini menunjukkan cara menangani konflik ketika beberapa pengguna memperbarui entitas secara bersamaan (pada saat yang sama).

Konflik konkurensi

Konflik konkurensi terjadi ketika:

  • Pengguna menavigasi ke halaman edit untuk entitas.
  • Pengguna lain memperbarui entitas yang sama sebelum perubahan pengguna pertama ditulis ke database.

Jika deteksi konkurensi tidak diaktifkan, siapa pun yang memperbarui database terakhir kali menimpa perubahan pengguna lain. Jika risiko ini dapat diterima, biaya pemrograman untuk konkurensi mungkin melebihi manfaatnya.

Konkurensi pesimis (penguncian)

Salah satu cara untuk mencegah konflik konkurensi adalah dengan menggunakan kunci database. Ini disebut konkurensi pesimis. Sebelum aplikasi membaca baris database yang ingin diperbarui, aplikasi meminta kunci. Setelah baris dikunci untuk akses pembaruan, tidak ada pengguna lain yang diizinkan untuk mengunci baris hingga kunci pertama dirilis.

Mengelola kunci memiliki kekurangan. Ini bisa rumit untuk diprogram dan dapat menyebabkan masalah performa saat jumlah pengguna meningkat. Entity Framework Core tidak menyediakan dukungan bawaan untuk itu, dan tutorial ini tidak menunjukkan cara mengimplementasikannya.

Konkurensi optimis

Konkurensi optimis memungkinkan konflik konkurensi terjadi, lalu bereaksi dengan tepat ketika terjadi. Misalnya, Jane mengunjungi halaman edit Departemen dan mengubah anggaran untuk departemen Bahasa Inggris dari $ 350.000,00 menjadi $ 0,00.

Changing budget to 0

Sebelum Klik Jane Simpan, John mengunjungi halaman yang sama dan mengubah bidang Tanggal Mulai dari 1/9/2007 menjadi 1/9/2013.

Changing start date to 2013

Jane mengklik Simpan terlebih dahulu dan melihat perubahannya berlaku, karena browser menampilkan halaman Indeks dengan nol sebagai jumlah Anggaran.

John mengklik Simpan pada halaman Edit yang masih menunjukkan anggaran $350.000,00. Apa yang terjadi selanjutnya ditentukan oleh cara Anda menangani konflik konkurensi:

  • Anda dapat melacak properti mana yang telah dimodifikasi pengguna dan memperbarui hanya kolom yang sesuai dalam database.

    Dalam skenario, tidak ada data yang akan hilang. Properti yang berbeda diperbarui oleh dua pengguna. Lain kali seseorang menelusuri departemen Inggris, mereka akan melihat perubahan Jane dan John. Metode pembaruan ini dapat mengurangi jumlah konflik yang dapat mengakibatkan hilangnya data. Pendekatan ini memiliki beberapa kelemahan:

    • Tidak dapat menghindari kehilangan data jika perubahan yang bersaing dilakukan pada properti yang sama.
    • Umumnya tidak praktis di aplikasi web. Ini membutuhkan mempertahankan status yang signifikan untuk melacak semua nilai yang diambil dan nilai baru. Mempertahankan status dalam jumlah besar dapat memengaruhi performa aplikasi.
    • Dapat meningkatkan kompleksitas aplikasi dibandingkan dengan deteksi konkurensi pada entitas.
  • Kau bisa membiarkan perubahan John menimpa perubahan Jane.

    Lain kali seseorang menelusuri departemen Bahasa Inggris, mereka akan melihat 9/1/2013 dan nilai $350,000.00 yang diambil. Pendekatan ini disebut skenario Client Wins atau Last in Wins . (Semua nilai dari klien lebih diutamakan daripada apa yang ada di penyimpanan data.) Jika Anda tidak melakukan pengkodan apa pun untuk penanganan konkurensi, Client Wins terjadi secara otomatis.

  • Anda dapat mencegah perubahan John diperbarui dalam database. Biasanya, aplikasi akan:

    • Menampilkan pesan kesalahan.
    • Perlihatkan status data saat ini.
    • Izinkan pengguna untuk menerapkan kembali perubahan.

    Ini disebut skenario Store Wins . (Nilai penyimpanan data lebih diutamakan daripada nilai yang dikirimkan oleh klien.) Anda menerapkan skenario Store Wins dalam tutorial ini. Metode ini memastikan bahwa tidak ada perubahan yang ditimpa tanpa pengguna diberi tahu.

Deteksi konflik dalam EF Core

EF CoreDbConcurrencyException melempar pengecualian ketika mendeteksi konflik. Model data harus dikonfigurasi untuk mengaktifkan deteksi konflik. Opsi untuk mengaktifkan deteksi konflik meliputi yang berikut ini:

  • Konfigurasikan EF Core untuk menyertakan nilai asli kolom yang dikonfigurasi sebagai token konkurensi dalam klausa Di mana perintah Perbarui dan Hapus.

    Ketika SaveChanges dipanggil, klausa Where mencari nilai asli properti apa pun yang dianotasi dengan ConcurrencyCheckAttribute atribut . Pernyataan pembaruan tidak akan menemukan baris untuk diperbarui jika salah satu properti token konkurensi berubah sejak baris pertama kali dibaca. EF Core menafsirkan bahwa sebagai konflik konkurensi. Untuk tabel database yang memiliki banyak kolom, pendekatan ini dapat menghasilkan klausa Where yang sangat besar, dan dapat memerlukan status dalam jumlah besar. Oleh karena itu pendekatan ini umumnya tidak disarankan, dan bukan metode yang digunakan dalam tutorial ini.

  • Dalam tabel database, sertakan kolom pelacakan yang dapat digunakan untuk menentukan kapan baris telah diubah.

    Dalam database SQL Server, jenis data kolom pelacakan adalah rowversion. Nilainya rowversion adalah angka berurutan yang bertahap setiap kali baris diperbarui. Dalam perintah Perbarui atau Hapus, klausa Where menyertakan nilai asli kolom pelacakan (nomor versi baris asli). Jika baris yang diperbarui telah diubah oleh pengguna lain, nilai dalam rowversion kolom berbeda dari nilai aslinya. Dalam hal ini, pernyataan Perbarui atau Hapus tidak dapat menemukan baris untuk diperbarui karena klausa Where. EF Core melempar pengecualian konkurensi ketika tidak ada baris yang dipengaruhi oleh perintah Perbarui atau Hapus.

Menambahkan properti pelacakan

Di Models/Department.cs, tambahkan properti pelacakan bernama RowVersion:

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

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

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

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

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

        public int? InstructorID { get; set; }

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

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

Atribut TimestampAttribute inilah yang mengidentifikasi kolom sebagai kolom pelacakan konkurensi. API fasih adalah cara alternatif untuk menentukan properti pelacakan:

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

Untuk database SQL Server, [Timestamp] atribut pada properti entitas yang didefinisikan sebagai array byte:

  • Menyebabkan kolom disertakan dalam klausa DELETE dan UPDATE WHERE.
  • Mengatur jenis kolom dalam database ke rowversion.

Database menghasilkan nomor versi baris berurutan yang bertahap setiap kali baris diperbarui. Dalam perintah Update atau Delete , Where klausul menyertakan nilai versi baris yang diambil. Jika baris yang diperbarui telah berubah sejak diambil:

  • Nilai versi baris saat ini tidak cocok dengan nilai yang diambil.
  • Perintah Update atau Delete tidak menemukan baris karena Where klausul mencari nilai versi baris yang diambil.
  • A DbUpdateConcurrencyException dilemparkan.

Kode berikut menunjukkan bagian dari T-SQL yang dihasilkan oleh EF Core ketika nama Departemen diperbarui:

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

Kode yang disorot sebelumnya menunjukkan WHERE klausa yang berisi RowVersion. Jika database RowVersion tidak sama dengan RowVersion parameter (@p2), tidak ada baris yang diperbarui.

Kode yang disorot berikut menunjukkan T-SQL yang memverifikasi persis satu baris diperbarui:

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 mengembalikan jumlah baris yang terpengaruh oleh pernyataan terakhir. Jika tidak ada baris yang diperbarui, EF Core melempar .DbUpdateConcurrencyException

Memperbarui database

Menambahkan properti mengubah RowVersion model data, yang memerlukan migrasi.

Bangun proyek.

  • Jalankan perintah berikut di PMC:

    Add-Migration RowVersion
    

Perintah ini:

  • Migrations/{time stamp}_RowVersion.cs Membuat file migrasi.

  • Migrations/SchoolContextModelSnapshot.cs Memperbarui file. Pembaruan menambahkan kode yang disorot berikut ke BuildModel metode :

    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");
        });
    
  • Jalankan perintah berikut di PMC:

    Update-Database
    

Halaman Departemen Perancah

  • Ikuti instruksi di halaman Scaffold Student dengan pengecualian berikut:

  • Buat folder Halaman/Departemen .

  • Gunakan Department untuk kelas model.

    • Gunakan kelas konteks yang ada alih-alih membuat yang baru.

Bangun proyek.

Memperbarui halaman Indeks

Alat perancah membuat RowVersion kolom untuk halaman Indeks, tetapi bidang tersebut tidak akan ditampilkan di aplikasi produksi. Dalam tutorial ini, byte RowVersion terakhir ditampilkan untuk membantu menunjukkan cara kerja penanganan konkurensi. Byte terakhir tidak dijamin unik dengan sendirinya.

Perbarui halaman Pages\Departments\Index.cshtml :

  • Ganti Indeks dengan Departemen.
  • Ubah kode yang berisi RowVersion untuk memperlihatkan hanya byte terakhir dari array byte.
  • Ganti FirstMidName dengan FullName.

Kode berikut menunjukkan halaman yang diperbarui:

@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>

Memperbarui model halaman Edit

Perbarui Pages/Departments/Edit.cshtml.cs dengan kode berikut:

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.");
        }
    }
}

diperbarui OriginalValue dengan rowVersion nilai dari entitas ketika diambil dalam OnGetAsync metode . EF Core menghasilkan perintah SQL UPDATE dengan klausa WHERE yang berisi nilai asli RowVersion . Jika tidak ada baris yang dipengaruhi oleh perintah UPDATE (tidak ada baris yang memiliki nilai asli RowVersion ), DbUpdateConcurrencyException pengecualian akan dilemparkan.

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;

Dalam kode yang disorot sebelumnya:

  • Nilai di Department.RowVersion adalah apa yang ada di entitas ketika awalnya diambil di halaman Dapatkan permintaan untuk Edit. Nilai disediakan untuk OnPost metode oleh bidang tersembunyi di Razor halaman yang menampilkan entitas yang akan diedit. Nilai bidang tersembunyi disalin oleh pengikat Department.RowVersion model.
  • OriginalValue adalah apa yang EF Core akan digunakan dalam klausa Where. Sebelum baris kode yang disorot dijalankan, OriginalValue memiliki nilai yang ada dalam database ketika FirstOrDefaultAsync dipanggil dalam metode ini, yang mungkin berbeda dari apa yang ditampilkan di halaman Edit.
  • Kode yang disorot memastikan bahwa EF Core menggunakan nilai asli RowVersion dari entitas yang Department ditampilkan dalam klausa Where pernyataan SQL UPDATE.

Ketika kesalahan konkurensi terjadi, kode yang disorot berikut mendapatkan nilai klien (nilai yang diposting ke metode ini) dan nilai database.

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");
    }

Kode berikut menambahkan pesan kesalahan kustom untuk setiap kolom yang memiliki nilai database yang berbeda dari yang diposting ke OnPostAsync:

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

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

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

Kode yang disorot RowVersion berikut menetapkan nilai ke nilai baru yang diambil dari database. Saat berikutnya pengguna mengklik Simpan, hanya kesalahan konkurensi yang terjadi sejak tampilan terakhir halaman Edit yang akan tertangkap.

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");
    }

Pernyataan ModelState.Remove diperlukan karena ModelState memiliki nilai lama RowVersion . Razor Di Halaman, ModelState nilai untuk bidang lebih diutamakan daripada nilai properti model saat keduanya ada.

Memperbarui halaman Edit

Perbarui Pages/Departments/Edit.cshtml dengan kode berikut:

@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");}
}

Kode sebelumnya:

  • Memperbarui direktif page dari @page ke @page "{id:int}".
  • Menambahkan versi baris tersembunyi. RowVersion harus ditambahkan sehingga postback mengikat nilai.
  • Menampilkan byte RowVersion terakhir untuk tujuan penelusuran kesalahan.
  • ViewData Mengganti dengan yang dititik InstructorNameSLdengan kuat .

Menguji konflik konkurensi dengan halaman Edit

Buka dua instans browser Edit di departemen bahasa Inggris:

  • Jalankan aplikasi dan pilih Departemen.
  • Klik kanan hyperlink Edit untuk departemen bahasa Inggris dan pilih Buka di tab baru.
  • Di tab pertama, klik edit hyperlink untuk departemen bahasa Inggris.

Dua tab browser menampilkan informasi yang sama.

Ubah nama di tab browser pertama dan klik Simpan.

Department Edit page 1 after change

Browser memperlihatkan halaman Indeks dengan nilai yang diubah dan indikator rowVersion yang diperbarui. Perhatikan indikator rowVersion yang diperbarui, indikator ditampilkan pada postback kedua di tab lain.

Ubah bidang lain di tab browser kedua.

Department Edit page 2 after change

Klik Simpan. Anda melihat pesan kesalahan untuk semua bidang yang tidak cocok dengan nilai database:

Department Edit page error message

Jendela browser ini tidak berniat mengubah bidang Nama. Salin dan tempel nilai saat ini (Bahasa) ke bidang Nama. Tab keluar. Validasi sisi klien menghapus pesan kesalahan.

Klik Simpan lagi. Nilai yang Anda masukkan di tab browser kedua disimpan. Anda melihat nilai yang disimpan di halaman Indeks.

Memperbarui model halaman Hapus

Perbarui Pages/Departments/Delete.cshtml.cs dengan kode berikut:

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 });
            }
        }
    }
}

Halaman Hapus mendeteksi konflik konkurensi ketika entitas telah berubah setelah diambil. Department.RowVersion adalah versi baris saat entitas diambil. Saat EF Core membuat perintah SQL DELETE, perintah tersebut menyertakan klausa WHERE dengan RowVersion. Jika perintah SQL DELETE menghasilkan nol baris yang terpengaruh:

  • Dalam RowVersion perintah SQL DELETE tidak cocok RowVersion dalam database.
  • Pengecualian DbUpdateConcurrencyException dilemparkan.
  • OnGetAsync dipanggil dengan concurrencyError.

Memperbarui halaman Hapus

Perbarui Pages/Departments/Delete.cshtml dengan kode berikut:

@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>

Kode sebelumnya membuat perubahan berikut:

  • Memperbarui direktif page dari @page ke @page "{id:int}".
  • Menambahkan pesan kesalahan.
  • Mengganti FirstMidName dengan FullName di bidang Administrator .
  • Perubahan RowVersion untuk menampilkan byte terakhir.
  • Menambahkan versi baris tersembunyi. RowVersion harus ditambahkan sehingga postback mengikat nilai.

Menguji konflik konkurensi

Buat departemen pengujian.

Buka dua instans browser Hapus pada departemen pengujian:

  • Jalankan aplikasi dan pilih Departemen.
  • Klik kanan hyperlink Hapus untuk departemen pengujian dan pilih Buka di tab baru.
  • Klik edit hyperlink untuk departemen pengujian.

Dua tab browser menampilkan informasi yang sama.

Ubah anggaran di tab browser pertama dan klik Simpan.

Browser memperlihatkan halaman Indeks dengan nilai yang diubah dan indikator rowVersion yang diperbarui. Perhatikan indikator rowVersion yang diperbarui, indikator ditampilkan pada postback kedua di tab lain.

Hapus departemen pengujian dari tab kedua. Kesalahan konkurensi ditampilkan dengan nilai saat ini dari database. Mengklik Hapus akan menghapus entitas, kecuali RowVersion telah diperbarui.

Sumber Daya Tambahan:

Langkah berikutnya

Ini adalah tutorial terakhir dalam seri ini. Topik tambahan dibahas dalam versi MVC dari seri tutorial ini.

Tutorial ini menunjukkan cara menangani konflik ketika beberapa pengguna memperbarui entitas secara bersamaan (pada saat yang sama). Jika Mengalami masalah, Anda tidak dapat menyelesaikan, mengunduh, atau melihat aplikasi yang telah selesai.Unduh instruksi.

Konflik konkurensi

Konflik konkurensi terjadi ketika:

  • Pengguna menavigasi ke halaman edit untuk entitas.
  • Pengguna lain memperbarui entitas yang sama sebelum perubahan pengguna pertama ditulis ke DB.

Jika deteksi konkurensi tidak diaktifkan, saat pembaruan bersamaan terjadi:

  • Pembaruan terakhir menang. Artinya, nilai pembaruan terakhir disimpan ke DB.
  • Pembaruan pertama saat ini hilang.

Konkurensi optimis

Konkurensi optimis memungkinkan konflik konkurensi terjadi, lalu bereaksi dengan tepat ketika terjadi. Misalnya, Jane mengunjungi halaman edit Departemen dan mengubah anggaran untuk departemen Bahasa Inggris dari $ 350.000,00 menjadi $ 0,00.

Changing budget to 0

Sebelum Klik Jane Simpan, John mengunjungi halaman yang sama dan mengubah bidang Tanggal Mulai dari 1/9/2007 menjadi 1/9/2013.

Changing start date to 2013

Jane mengklik Simpan terlebih dahulu dan melihat perubahannya saat browser menampilkan halaman Indeks.

Budget changed to zero

John mengklik Simpan pada halaman Edit yang masih menunjukkan anggaran $350.000,00. Apa yang terjadi selanjutnya ditentukan oleh cara Anda menangani konflik konkurensi.

Konkurensi optimis mencakup opsi berikut:

  • Anda dapat melacak properti mana yang telah dimodifikasi pengguna dan memperbarui hanya kolom yang sesuai di DB.

    Dalam skenario, tidak ada data yang akan hilang. Properti yang berbeda diperbarui oleh dua pengguna. Lain kali seseorang menelusuri departemen Inggris, mereka akan melihat perubahan Jane dan John. Metode pembaruan ini dapat mengurangi jumlah konflik yang dapat mengakibatkan hilangnya data. Pendekatan ini:

    • Tidak dapat menghindari kehilangan data jika perubahan yang bersaing dilakukan pada properti yang sama.
    • Umumnya tidak praktis di aplikasi web. Ini membutuhkan mempertahankan status yang signifikan untuk melacak semua nilai yang diambil dan nilai baru. Mempertahankan status dalam jumlah besar dapat memengaruhi performa aplikasi.
    • Dapat meningkatkan kompleksitas aplikasi dibandingkan dengan deteksi konkurensi pada entitas.
  • Kau bisa membiarkan perubahan John menimpa perubahan Jane.

    Lain kali seseorang menelusuri departemen Bahasa Inggris, mereka akan melihat 9/1/2013 dan nilai $350,000.00 yang diambil. Pendekatan ini disebut skenario Client Wins atau Last in Wins . (Semua nilai dari klien lebih diutamakan daripada apa yang ada di penyimpanan data.) Jika Anda tidak melakukan pengkodan apa pun untuk penanganan konkurensi, Client Wins terjadi secara otomatis.

  • Anda dapat mencegah perubahan John diperbarui di DB. Biasanya, aplikasi akan:

    • Menampilkan pesan kesalahan.
    • Perlihatkan status data saat ini.
    • Izinkan pengguna untuk menerapkan kembali perubahan.

    Ini disebut skenario Store Wins . (Nilai penyimpanan data lebih diutamakan daripada nilai yang dikirimkan oleh klien.) Anda menerapkan skenario Store Wins dalam tutorial ini. Metode ini memastikan bahwa tidak ada perubahan yang ditimpa tanpa pengguna diberi tahu.

Menangani konkurensi

Saat properti dikonfigurasi sebagai token konkurensi:

Model DB dan data harus dikonfigurasi untuk mendukung pelemparan DbUpdateConcurrencyException.

Mendeteksi konflik konkurensi pada properti

Konflik konkurensi dapat dideteksi di tingkat properti dengan atribut ConcurrencyCheck . Atribut dapat diterapkan ke beberapa properti pada model. Untuk informasi selengkapnya, lihat Anotasi Data-ConcurrencyCheck.

Atribut [ConcurrencyCheck] tidak digunakan dalam tutorial ini.

Mendeteksi konflik konkurensi pada baris

Untuk mendeteksi konflik konkurensi, kolom pelacakan rowversion ditambahkan ke model. rowversion :

  • Apakah SQL Server spesifik. Database lain mungkin tidak menyediakan fitur serupa.
  • Digunakan untuk menentukan bahwa entitas belum diubah sejak diambil dari DB.

DB menghasilkan angka berurutan rowversion yang bertahap setiap kali baris diperbarui. Dalam perintah Update atau Delete , Where klausul menyertakan nilai yang diambil dari rowversion. Jika baris yang diperbarui telah berubah:

  • rowversion tidak cocok dengan nilai yang diambil.
  • Perintah Update atau Delete tidak menemukan baris karena Where klausul menyertakan yang diambil rowversion.
  • A DbUpdateConcurrencyException dilemparkan.

Dalam EF Core, ketika tidak ada baris yang diperbarui oleh perintah Update atau Delete , pengecualian konkurensi dilemparkan.

Menambahkan properti pelacakan ke entitas Departemen

Di Models/Department.cs, tambahkan properti pelacakan bernama RowVersion:

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

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

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

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

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

        public int? InstructorID { get; set; }

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

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

Atribut Tanda Waktu menentukan bahwa kolom ini disertakan dalam Where klausul Update dan Delete perintah. Atribut dipanggil Timestamp karena versi SQL Server sebelumnya menggunakan jenis data SQL timestamp sebelum jenis SQL rowversion menggantinya.

API fasih juga dapat menentukan properti pelacakan:

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

Kode berikut menunjukkan bagian dari T-SQL yang dihasilkan oleh EF Core ketika nama Departemen diperbarui:

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

Kode yang disorot sebelumnya menunjukkan WHERE klausa yang berisi RowVersion. Jika DB RowVersion tidak sama dengan RowVersion parameter (@p2), tidak ada baris yang diperbarui.

Kode yang disorot berikut menunjukkan T-SQL yang memverifikasi persis satu baris diperbarui:

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 mengembalikan jumlah baris yang terpengaruh oleh pernyataan terakhir. Dalam tidak ada baris yang diperbarui, EF Core melempar .DbUpdateConcurrencyException

Anda dapat melihat T-SQL EF Core yang dihasilkan di jendela output Visual Studio.

Memperbarui DB

Menambahkan properti mengubah RowVersion model DB, yang memerlukan migrasi.

Bangun proyek. Masukkan yang berikut ini di jendela perintah:

dotnet ef migrations add RowVersion
dotnet ef database update

Perintah sebelumnya:

  • Migrations/{time stamp}_RowVersion.cs Menambahkan file migrasi.

  • Migrations/SchoolContextModelSnapshot.cs Memperbarui file. Pembaruan menambahkan kode yang disorot berikut ke BuildModel metode :

  • Menjalankan migrasi untuk memperbarui DB.

Perancah model Departemen

Ikuti instruksi di Perancah model siswa dan gunakan Department untuk kelas model.

Perintah sebelumnya mengacak Department model. Buka proyek di Visual Studio.

Bangun proyek.

Memperbarui halaman Indeks Departemen

Mesin perancah membuat RowVersion kolom untuk halaman Indeks, tetapi bidang tersebut tidak boleh ditampilkan. Dalam tutorial ini, byte RowVersion terakhir ditampilkan untuk membantu memahami konkurensi. Byte terakhir tidak dijamin unik. Aplikasi nyata tidak akan ditampilkan RowVersion atau byte terakhir dari RowVersion.

Perbarui halaman Indeks:

  • Ganti Indeks dengan Departemen.
  • Ganti markup yang berisi RowVersion dengan byte terakhir .RowVersion
  • Ganti FirstMidName dengan FullName.

Markup berikut menunjukkan halaman yang diperbarui:

@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>

Memperbarui model halaman Edit

Perbarui Pages/Departments/Edit.cshtml.cs dengan kode berikut:

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.");
        }
    }
}

Untuk mendeteksi masalah konkurensi, diperbarui OriginalValue dengan rowVersion nilai dari entitas yang diambil. EF Core menghasilkan perintah SQL UPDATE dengan klausa WHERE yang berisi nilai asli RowVersion . Jika tidak ada baris yang dipengaruhi oleh perintah UPDATE (tidak ada baris yang memiliki nilai asli RowVersion ), DbUpdateConcurrencyException pengecualian akan dilemparkan.

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;

Dalam kode sebelumnya, Department.RowVersion adalah nilai ketika entitas diambil. OriginalValue adalah nilai dalam DB ketika FirstOrDefaultAsync dipanggil dalam metode ini.

Kode berikut mendapatkan nilai klien (nilai yang diposting ke metode ini) dan nilai DB:

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");
}

Kode berikut menambahkan pesan kesalahan kustom untuk setiap kolom yang memiliki nilai DB berbeda dari yang diposting ke OnPostAsync:

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

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

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

Kode yang disorot RowVersion berikut menetapkan nilai ke nilai baru yang diambil dari DB. Saat berikutnya pengguna mengklik Simpan, hanya kesalahan konkurensi yang terjadi sejak tampilan terakhir halaman Edit yang akan tertangkap.

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");
}

Pernyataan ModelState.Remove diperlukan karena ModelState memiliki nilai lama RowVersion . Razor Di Halaman, ModelState nilai untuk bidang lebih diutamakan daripada nilai properti model saat keduanya ada.

Memperbarui halaman Edit

Perbarui Pages/Departments/Edit.cshtml dengan markup berikut:

@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");}
}

Markup sebelumnya:

  • Memperbarui direktif page dari @page ke @page "{id:int}".
  • Menambahkan versi baris tersembunyi. RowVersion harus ditambahkan sehingga post back mengikat nilai.
  • Menampilkan byte RowVersion terakhir untuk tujuan penelusuran kesalahan.
  • ViewData Mengganti dengan yang dititik InstructorNameSLdengan kuat .

Menguji konflik konkurensi dengan halaman Edit

Buka dua instans browser Edit di departemen bahasa Inggris:

  • Jalankan aplikasi dan pilih Departemen.
  • Klik kanan hyperlink Edit untuk departemen bahasa Inggris dan pilih Buka di tab baru.
  • Di tab pertama, klik edit hyperlink untuk departemen bahasa Inggris.

Dua tab browser menampilkan informasi yang sama.

Ubah nama di tab browser pertama dan klik Simpan.

Department Edit page 1 after change

Browser memperlihatkan halaman Indeks dengan nilai yang diubah dan indikator rowVersion yang diperbarui. Perhatikan indikator rowVersion yang diperbarui, indikator ditampilkan pada postback kedua di tab lain.

Ubah bidang lain di tab browser kedua.

Department Edit page 2 after change

Klik Simpan. Anda melihat pesan kesalahan untuk semua bidang yang tidak cocok dengan nilai DB:

Department Edit page error message 1

Jendela browser ini tidak berniat mengubah bidang Nama. Salin dan tempel nilai saat ini (Bahasa) ke bidang Nama. Tab keluar. Validasi sisi klien menghapus pesan kesalahan.

Department Edit page error message 2

Klik Simpan lagi. Nilai yang Anda masukkan di tab browser kedua disimpan. Anda melihat nilai yang disimpan di halaman Indeks.

Memperbarui halaman Hapus

Perbarui model halaman Hapus dengan kode berikut:

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 });
            }
        }
    }
}

Halaman Hapus mendeteksi konflik konkurensi ketika entitas telah berubah setelah diambil. Department.RowVersion adalah versi baris saat entitas diambil. Saat EF Core membuat perintah SQL DELETE, perintah tersebut menyertakan klausa WHERE dengan RowVersion. Jika perintah SQL DELETE menghasilkan nol baris yang terpengaruh:

  • RowVersion dalam perintah SQL DELETE tidak cocok RowVersion di DB.
  • Pengecualian DbUpdateConcurrencyException dilemparkan.
  • OnGetAsync dipanggil dengan concurrencyError.

Memperbarui halaman Hapus

Perbarui Pages/Departments/Delete.cshtml dengan kode berikut:

@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>

Kode sebelumnya membuat perubahan berikut:

  • Memperbarui direktif page dari @page ke @page "{id:int}".
  • Menambahkan pesan kesalahan.
  • Mengganti FirstMidName dengan FullName di bidang Administrator .
  • Perubahan RowVersion untuk menampilkan byte terakhir.
  • Menambahkan versi baris tersembunyi. RowVersion harus ditambahkan sehingga post back mengikat nilai.

Menguji konflik konkurensi dengan halaman Hapus

Buat departemen pengujian.

Buka dua instans browser Hapus pada departemen pengujian:

  • Jalankan aplikasi dan pilih Departemen.
  • Klik kanan hyperlink Hapus untuk departemen pengujian dan pilih Buka di tab baru.
  • Klik edit hyperlink untuk departemen pengujian.

Dua tab browser menampilkan informasi yang sama.

Ubah anggaran di tab browser pertama dan klik Simpan.

Browser memperlihatkan halaman Indeks dengan nilai yang diubah dan indikator rowVersion yang diperbarui. Perhatikan indikator rowVersion yang diperbarui, indikator ditampilkan pada postback kedua di tab lain.

Hapus departemen pengujian dari tab kedua. Kesalahan konkurensi ditampilkan dengan nilai saat ini dari DB. Mengklik Hapus akan menghapus entitas, kecuali RowVersion telah diperbarui.

Lihat Pewarisan tentang cara mewarisi model data.

Sumber Daya Tambahan: