Bagikan melalui


Tutorial: Menangani konkurensi - ASP.NET MVC dengan EF Core

Dalam tutorial sebelumnya, Anda mempelajari cara memperbarui data. Tutorial ini menunjukkan cara menangani konflik ketika beberapa pengguna memperbarui entitas yang sama secara bersamaan.

Anda akan membuat halaman web yang berfungsi dengan Department entitas dan menangani kesalahan konkurensi. Ilustrasi berikut ini memperlihatkan halaman Edit dan Hapus, termasuk beberapa pesan yang ditampilkan jika terjadi konflik konkurensi.

Department Edit page

Department Delete page

Di tutorial ini, Anda akan:

  • Pelajari tentang konflik konkurensi
  • Menambahkan properti pelacakan
  • Membuat pengontrol dan tampilan Departemen
  • Perbarui tampilan Indeks
  • Perbarui metode Edit
  • Perbarui tampilan Edit
  • Menguji konflik konkurensi
  • Memperbarui halaman Hapus
  • Perbarui Detail dan Buat tampilan

Prasyarat

Konflik konkurensi

Konflik konkurensi terjadi ketika satu pengguna menampilkan data entitas untuk mengeditnya, lalu pengguna lain memperbarui data entitas yang sama sebelum perubahan pengguna pertama ditulis ke database. Jika Anda tidak mengaktifkan deteksi konflik tersebut, siapa pun yang memperbarui database terakhir kali menimpa perubahan pengguna lain. Dalam banyak aplikasi, risiko ini dapat diterima: jika ada beberapa pengguna, atau beberapa pembaruan, atau jika tidak terlalu penting jika beberapa perubahan ditimpa, biaya pemrograman untuk konkurensi mungkin melebihi manfaatnya. Dalam hal ini, Anda tidak perlu mengonfigurasi aplikasi untuk menangani konflik konkurensi.

Konkurensi pesimis (penguncian)

Jika aplikasi Anda memang perlu mencegah kehilangan data yang tidak disengaja dalam skenario konkurensi, salah satu cara untuk melakukannya adalah dengan menggunakan kunci database. Ini disebut konkurensi pesimis. Misalnya, sebelum Membaca baris dari database, Anda meminta kunci untuk akses baca-saja atau untuk pembaruan. Jika Anda mengunci baris untuk akses pembaruan, tidak ada pengguna lain yang diizinkan untuk mengunci baris baik untuk akses baca-saja atau perbarui, karena mereka akan mendapatkan salinan data yang sedang dalam proses diubah. Jika Anda mengunci baris untuk akses baca-saja, yang lain juga dapat menguncinya untuk akses baca-saja tetapi tidak untuk pembaruan.

Mengelola kunci memiliki kekurangan. Ini bisa menjadi kompleks untuk diprogram. Ini membutuhkan sumber daya manajemen database yang signifikan, dan dapat menyebabkan masalah performa saat jumlah pengguna aplikasi meningkat. Untuk alasan ini, tidak semua sistem manajemen database mendukung konkurensi pesimis. Entity Framework Core tidak menyediakan dukungan bawaan untuk itu, dan tutorial ini tidak menunjukkan kepada Anda cara menerapkannya.

Konkurensi optimis

Alternatif untuk konkurensi pesimis adalah konkurensi optimis. Konkurensi optimis berarti memungkinkan konflik konkurensi terjadi, lalu bereaksi dengan tepat jika terjadi. Misalnya, Jane mengunjungi halaman Edit Departemen dan mengubah jumlah 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 kembali ke halaman Indeks.

Budget changed to zero

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

Beberapa opsi termasuk yang berikut ini:

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

    Dalam skenario contoh, tidak ada data yang akan hilang, karena properti yang berbeda diperbarui oleh dua pengguna. Lain kali seseorang menelusuri departemen Inggris, mereka akan melihat perubahan Jane dan John -- tanggal mulai 9/1/2013 dan anggaran nol dolar. Metode pembaruan ini dapat mengurangi jumlah konflik yang dapat mengakibatkan kehilangan data, tetapi tidak dapat menghindari kehilangan data jika perubahan yang bersaing dilakukan pada properti entitas yang sama. Apakah Kerangka Kerja Entitas berfungsi dengan cara ini tergantung pada cara Anda menerapkan kode pembaruan Anda. Seringkali tidak praktis dalam aplikasi web, karena dapat mengharuskan Anda mempertahankan status dalam jumlah besar untuk melacak semua nilai properti asli untuk entitas serta nilai baru. Mempertahankan status dalam jumlah besar dapat memengaruhi performa aplikasi karena memerlukan sumber daya server atau harus disertakan dalam halaman web itu sendiri (misalnya, di bidang tersembunyi) atau di cookie.

  • 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 dipulihkan. Ini disebut skenario Client Wins atau Last in Wins . (Semua nilai dari klien lebih diutamakan daripada apa yang ada di penyimpanan data.) Seperti yang disebutkan dalam pengenalan bagian ini, jika Anda tidak melakukan pengkodan apa pun untuk penanganan konkurensi, ini akan terjadi secara otomatis.

  • Anda dapat mencegah perubahan John diperbarui dalam database.

    Biasanya, Anda akan menampilkan pesan kesalahan, menunjukkan status data saat ini, dan memungkinkannya untuk menerapkan kembali perubahannya jika dia masih ingin membuatnya. Ini disebut skenario Store Wins . (Nilai penyimpanan data lebih diutamakan daripada nilai yang dikirimkan oleh klien.) Anda akan menerapkan skenario Store Wins dalam tutorial ini. Metode ini memastikan bahwa tidak ada perubahan yang ditimpa tanpa pengguna diberitahu tentang apa yang terjadi.

Mendeteksi konflik konkurensi

Anda dapat mengatasi konflik dengan menangani DbConcurrencyException pengecualian yang dilemparkan Kerangka Kerja Entitas. Untuk mengetahui kapan harus melemparkan pengecualian ini, Kerangka Kerja Entitas harus dapat mendeteksi konflik. Oleh karena itu, Anda harus mengonfigurasi database dan model data dengan tepat. Beberapa opsi untuk mengaktifkan deteksi konflik meliputi yang berikut ini:

  • Dalam tabel database, sertakan kolom pelacakan yang dapat digunakan untuk menentukan kapan baris telah diubah. Anda kemudian dapat mengonfigurasi Kerangka Kerja Entitas untuk menyertakan kolom tersebut dalam klausa Where SQL Update atau Delete.

    Jenis data kolom pelacakan biasanya rowversion. Nilainya rowversion adalah angka berurutan yang bertahap setiap kali baris diperbarui. Dalam perintah Perbarui atau Hapus, klausa Where menyertakan nilai asli kolom pelacakan (versi baris asli) . Jika baris yang diperbarui telah diubah oleh pengguna lain, nilai dalam rowversion kolom berbeda dari nilai asli, sehingga pernyataan Perbarui atau Hapus tidak dapat menemukan baris untuk diperbarui karena klausa Where. Ketika Kerangka Kerja Entitas menemukan bahwa tidak ada baris yang telah diperbarui oleh perintah Perbarui atau Hapus (yaitu, ketika jumlah baris yang terpengaruh adalah nol), itu menafsirkan bahwa sebagai konflik konkurensi.

  • Konfigurasikan Kerangka Kerja Entitas untuk menyertakan nilai asli setiap kolom dalam tabel dalam klausa Di mana perintah Perbarui dan Hapus.

    Seperti pada opsi pertama, jika apa pun dalam baris telah berubah sejak baris pertama kali dibaca, klausa Where tidak akan mengembalikan baris untuk diperbarui, yang ditafsirkan oleh Kerangka Kerja Entitas sebagai konflik konkurensi. Untuk tabel database yang memiliki banyak kolom, pendekatan ini dapat menghasilkan klausa Where yang sangat besar, dan dapat mengharuskan Anda mempertahankan status dalam jumlah besar. Seperti disebutkan sebelumnya, mempertahankan status dalam jumlah besar dapat memengaruhi performa aplikasi. Oleh karena itu pendekatan ini umumnya tidak disarankan, dan bukan metode yang digunakan dalam tutorial ini.

    Jika Anda ingin menerapkan pendekatan ini ke konkurensi, Anda harus menandai semua properti kunci non-primer di entitas yang ingin Anda lacak konkurensinya dengan menambahkan atribut ke properti tersebut ConcurrencyCheck . Perubahan itu memungkinkan Kerangka Kerja Entitas untuk menyertakan semua kolom dalam klausa SQL Di mana pernyataan Perbarui dan Hapus.

Di sisa tutorial ini, Anda akan menambahkan rowversion properti pelacakan ke entitas Departemen, membuat pengontrol dan tampilan, dan menguji untuk memverifikasi bahwa semuanya berfungsi dengan benar.

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 Timestamp menentukan bahwa kolom ini akan disertakan dalam perintah Where clause of Update and Delete yang dikirim ke database. Atribut dipanggil Timestamp karena versi SQL Server sebelumnya menggunakan jenis data SQL timestamp sebelum SQL rowversion menggantinya. Jenis .NET untuk rowversion adalah array byte.

Jika Anda lebih suka menggunakan API yang fasih, Anda dapat menggunakan IsConcurrencyToken metode (dalam Data/SchoolContext.cs) untuk menentukan properti pelacakan, seperti yang ditunjukkan dalam contoh berikut:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

Dengan menambahkan properti, Anda mengubah model database, jadi Anda perlu melakukan migrasi lain.

Simpan perubahan Anda dan buat proyek, lalu masukkan perintah berikut di jendela perintah:

dotnet ef migrations add RowVersion
dotnet ef database update

Membuat pengontrol dan tampilan Departemen

Perancah pengontrol dan pandangan Departemen seperti yang Anda lakukan sebelumnya untuk Siswa, Kursus, dan Instruktur.

Scaffold Department

DepartmentsController.cs Dalam file, ubah keempat kemunculan "FirstMidName" menjadi "FullName" sehingga daftar drop-down administrator departemen akan berisi nama lengkap instruktur daripada hanya nama belakang.

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);

Perbarui tampilan Indeks

Mesin perancah membuat RowVersion kolom dalam tampilan Indeks, tetapi bidang tersebut tidak boleh ditampilkan.

Ganti kode dengan Views/Departments/Index.cshtml kode berikut.

@model IEnumerable<ContosoUniversity.Models.Department>

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

<h2>Departments</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Administrator)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <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>
                    <a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Ini mengubah judul ke "Departemen", menghapus RowVersion kolom, dan menampilkan nama lengkap alih-alih nama depan untuk administrator.

Perbarui metode Edit

Dalam metode HttpGet Edit dan Details metode , tambahkan AsNoTracking. Dalam metode HttpGet Edit , tambahkan pemuatan yang bersemangat untuk Administrator.

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

Ganti kode yang ada untuk metode HttpPost Edit dengan kode berikut:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
    if (id == null)
    {
        return NotFound();
    }

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

    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        await TryUpdateModelAsync(deletedDepartment);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

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

    if (await TryUpdateModelAsync<Department>(
        departmentToUpdate,
        "",
        s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(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 changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                {
                    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
                }
                if (databaseValues.Budget != clientValues.Budget)
                {
                    ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
                }
                if (databaseValues.StartDate != clientValues.StartDate)
                {
                    ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
                }
                if (databaseValues.InstructorID != clientValues.InstructorID)
                {
                    Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
                    ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
                }

                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. 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. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
                ModelState.Remove("RowVersion");
            }
        }
    }
    ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

Kode dimulai dengan mencoba membaca departemen yang akan diperbarui. FirstOrDefaultAsync Jika metode mengembalikan null, departemen dihapus oleh pengguna lain. Dalam hal ini kode menggunakan nilai formulir yang diposting untuk membuat Department entitas sehingga halaman Edit dapat diputar ulang dengan pesan kesalahan. Sebagai alternatif, Anda tidak perlu membuat Department ulang entitas jika Hanya menampilkan pesan kesalahan tanpa memutar ulang bidang departemen.

Tampilan menyimpan nilai asli RowVersion di bidang tersembunyi, dan metode ini menerima nilai tersebut rowVersion dalam parameter . Sebelum memanggil SaveChanges, Anda harus menempatkan nilai properti asli RowVersion tersebut OriginalValues dalam koleksi untuk entitas.

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

Kemudian ketika Kerangka Kerja Entitas membuat perintah PEMBARUAN SQL, perintah tersebut akan menyertakan klausa WHERE yang mencari baris yang memiliki nilai asli RowVersion . Jika tidak ada baris yang dipengaruhi oleh perintah UPDATE (tidak ada baris yang memiliki nilai asli RowVersion ), Kerangka Kerja Entitas akan memberikan DbUpdateConcurrencyException pengecualian.

Kode dalam blok tangkapan untuk pengecualian tersebut mendapatkan entitas Departemen yang terpengaruh yang memiliki nilai yang diperbarui dari Entries properti pada objek pengecualian.

var exceptionEntry = ex.Entries.Single();

Koleksi Entries hanya akan memiliki satu EntityEntry objek. Anda bisa menggunakan objek tersebut untuk mendapatkan nilai baru yang dimasukkan oleh pengguna dan nilai database saat ini.

var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();

Kode menambahkan pesan kesalahan kustom untuk setiap kolom yang memiliki nilai database yang berbeda dari apa yang dimasukkan pengguna di halaman Edit (hanya satu bidang yang ditampilkan di sini untuk brevity).

var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");

Terakhir, kode menetapkan RowVersion nilai ke nilai baru yang departmentToUpdate diambil dari database. Nilai baru RowVersion ini akan disimpan di bidang tersembunyi ketika halaman Edit diputar ulang, dan saat berikutnya pengguna mengklik Simpan, hanya kesalahan konkurensi yang terjadi karena pemutaran ulang halaman Edit akan tertangkap.

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

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

Perbarui tampilan Edit

Pada Views/Departments/Edit.cshtml, buat perubahan berikut:

  • Tambahkan bidang tersembunyi untuk menyimpan RowVersion nilai properti, segera mengikuti bidang tersembunyi untuk DepartmentID properti .

  • Tambahkan opsi "Pilih Administrator" ke daftar drop-down.

@model ContosoUniversity.Models.Department

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

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="DepartmentID" />
            <input type="hidden" asp-for="RowVersion" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
                <span asp-validation-for="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-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Menguji konflik konkurensi

Jalankan aplikasi dan buka halaman Indeks Departemen. Klik kanan hyperlink Edit untuk departemen bahasa Inggris dan pilih Buka di tab baru, lalu klik edit hyperlink untuk departemen bahasa Inggris. Dua tab browser sekarang menampilkan informasi yang sama.

Ubah bidang di tab browser pertama dan klik Simpan.

Department Edit page 1 after change

Browser memperlihatkan halaman Indeks dengan nilai yang diubah.

Ubah bidang di tab browser kedua.

Department Edit page 2 after change

Klik Simpan. Anda melihat pesan kesalahan:

Department Edit page error message

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

Memperbarui halaman Hapus

Untuk halaman Hapus, Kerangka Kerja Entitas mendeteksi konflik konkurensi yang disebabkan oleh orang lain yang mengedit departemen dengan cara yang sama. Saat metode HttpGet Delete menampilkan tampilan konfirmasi, tampilan menyertakan nilai asli RowVersion dalam bidang tersembunyi. Nilai tersebut kemudian tersedia untuk metode HttpPost Delete yang dipanggil ketika pengguna mengonfirmasi penghapusan. Saat Kerangka Kerja Entitas membuat perintah HAPUS SQL, ini menyertakan klausa WHERE dengan nilai asli RowVersion . Jika perintah menghasilkan nol baris yang terpengaruh (artinya baris diubah setelah halaman Konfirmasi penghapusan ditampilkan), pengecualian konkurensi dilemparkan, dan metode HttpGet Delete dipanggil dengan bendera kesalahan yang diatur ke true untuk memutar ulang halaman konfirmasi dengan pesan kesalahan. Ada kemungkinan juga bahwa nol baris terpengaruh karena baris dihapus oleh pengguna lain, jadi dalam hal ini tidak ada pesan kesalahan yang ditampilkan.

Memperbarui metode Hapus di pengontrol Departemen

Di DepartmentsController.cs, ganti metode HttpGet Delete dengan kode berikut:

public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return NotFound();
    }

    var department = await _context.Departments
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.DepartmentID == id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction(nameof(Index));
        }
        return NotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "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. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

Metode ini menerima parameter opsional yang menunjukkan apakah halaman sedang diputar ulang setelah kesalahan konkurensi. Jika bendera ini benar dan departemen yang ditentukan tidak ada lagi, bendera ini dihapus oleh pengguna lain. Dalam hal ini, kode mengalihkan ke halaman Indeks. Jika bendera ini benar dan departemen memang ada, bendera tersebut diubah oleh pengguna lain. Dalam hal ini, kode mengirimkan pesan kesalahan ke tampilan menggunakan ViewData.

Ganti kode dalam metode HttpPost Delete (bernama DeleteConfirmed) dengan kode berikut:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
        {
            _context.Departments.Remove(department);
            await _context.SaveChangesAsync();
        }
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateConcurrencyException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
    }
}

Dalam kode perancah yang baru saja Anda ganti, metode ini hanya menerima ID rekaman:

public async Task<IActionResult> DeleteConfirmed(int id)

Anda telah mengubah parameter ini menjadi instans entitas yang Department dibuat oleh pengikat model. Ini memberi EF akses ke nilai properti RowVers'ion selain kunci rekaman.

public async Task<IActionResult> Delete(Department department)

Anda juga telah mengubah nama metode tindakan dari DeleteConfirmed menjadi Delete. Kode perancah menggunakan nama DeleteConfirmed untuk memberi metode HttpPost tanda tangan unik. (CLR memerlukan metode kelebihan beban untuk memiliki parameter metode yang berbeda.) Sekarang setelah tanda tangan unik, Anda dapat tetap dengan konvensi MVC dan menggunakan nama yang sama untuk metode penghapusan HttpPost dan HttpGet.

Jika departemen sudah dihapus, AnyAsync metode mengembalikan false dan aplikasi hanya kembali ke metode Indeks.

Jika kesalahan konkurensi tertangkap, kode akan menampilkan kembali halaman Konfirmasi penghapusan dan menyediakan bendera yang menunjukkan bahwa kode harus menampilkan pesan kesalahan konkurensi.

Memperbarui tampilan Hapus

Di Views/Departments/Delete.cshtml, ganti kode perancah dengan kode berikut yang menambahkan bidang pesan kesalahan dan bidang tersembunyi untuk properti DepartmentID dan RowVersion. Perubahan disorot.

@model ContosoUniversity.Models.Department

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

<h2>Delete</h2>

<p class="text-danger">@ViewData["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.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="DepartmentID" />
        <input type="hidden" asp-for="RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

Ini membuat perubahan berikut:

  • Menambahkan pesan kesalahan antara h2 judul dan h3 .

  • Mengganti FirstMidName dengan FullName di bidang Administrator .

  • Menghapus bidang RowVersion.

  • Menambahkan bidang tersembunyi untuk RowVersion properti .

Jalankan aplikasi dan buka halaman Indeks Departemen. Klik kanan hyperlink Hapus untuk departemen bahasa Inggris dan pilih Buka di tab baru, lalu di tab pertama klik hyperlink Edit untuk departemen bahasa Inggris.

Di jendela pertama, ubah salah satu nilai, dan klik Simpan:

Department Edit page after change before delete

Di tab kedua, klik Hapus. Anda melihat pesan kesalahan konkurensi, dan nilai Departemen di-refresh dengan apa yang saat ini ada di database.

Department Delete confirmation page with concurrency error

Jika Anda mengklik Hapus lagi, Anda dialihkan ke halaman Indeks, yang menunjukkan bahwa departemen telah dihapus.

Perbarui Detail dan Buat tampilan

Anda dapat secara opsional membersihkan kode perancah di tampilan Detail dan Buat.

Ganti kode di Views/Departments/Details.cshtml untuk menghapus kolom RowVersion dan tampilkan nama lengkap Administrator.

@model ContosoUniversity.Models.Department

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

<h2>Details</h2>

<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

Ganti kode di Views/Departments/Create.cshtml untuk menambahkan opsi Pilih ke daftar drop-down.

@model ContosoUniversity.Models.Department

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

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Mendapatkan kode

Unduh atau lihat aplikasi yang telah selesai.

Sumber Daya Tambahan:

Untuk informasi selengkapnya tentang cara menangani konkurensi di EF Core, lihat Konflik konkurensi.

Langkah berikutnya

Di tutorial ini, Anda akan:

  • Dipelajari tentang konflik konkurensi
  • Menambahkan properti pelacakan
  • Pengontrol dan tampilan Departemen yang dibuat
  • Tampilan Indeks Yang Diperbarui
  • Metode Edit yang Diperbarui
  • Tampilan Edit Yang Diperbarui
  • Konflik konkurensi yang diuji
  • Memperbarui halaman Hapus
  • Detail Yang Diperbarui dan Buat tampilan

Lanjutkan ke tutorial berikutnya untuk mempelajari cara menerapkan warisan tabel per hierarki untuk entitas Instruktur dan Siswa.