Samouczek: obsługa współbieżności — ASP.NET MVC za pomocą polecenia EF Core

W poprzednich samouczkach przedstawiono sposób aktualizowania danych. W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników aktualizuje tę samą jednostkę w tym samym czasie.

Utworzysz strony internetowe, które współpracują z jednostką Department i będą obsługiwać błędy współbieżności. Na poniższych ilustracjach przedstawiono strony Edytowanie i usuwanie, w tym niektóre komunikaty wyświetlane w przypadku wystąpienia konfliktu współbieżności.

Department Edit page

Department Delete page

W tym samouczku zostały wykonane następujące czynności:

  • Dowiedz się więcej o konfliktach współbieżności
  • Dodawanie właściwości śledzenia
  • Tworzenie kontrolera i widoków działów
  • Aktualizowanie widoku indeksu
  • Aktualizowanie metod edycji
  • Aktualizowanie widoku edycji
  • Testowanie konfliktów współbieżności
  • Aktualizowanie strony Usuwanie
  • Aktualizowanie szczegółów i tworzenie widoków

Wymagania wstępne

Konflikty współbieżności

Konflikt współbieżności występuje, gdy jeden użytkownik wyświetla dane jednostki, aby je edytować, a następnie inny użytkownik aktualizuje dane tej samej jednostki przed zapisaniem zmiany pierwszego użytkownika w bazie danych. Jeśli nie włączysz wykrywania takich konfliktów, kto ostatnio aktualizuje bazę danych, zastępuje zmiany innego użytkownika. W wielu aplikacjach to ryzyko jest dopuszczalne: jeśli istnieje niewielu użytkowników lub kilka aktualizacji, lub jeśli nie jest naprawdę krytyczne, jeśli niektóre zmiany zostaną zastąpione, koszt programowania współbieżności może przeważyć nad korzyścią. W takim przypadku nie trzeba konfigurować aplikacji do obsługi konfliktów współbieżności.

Pesymistyczna współbieżność (blokowanie)

Jeśli aplikacja musi zapobiec przypadkowej utracie danych w scenariuszach współbieżności, jednym ze sposobów jest użycie blokad bazy danych. Jest to nazywane pesymistyczną współbieżnością. Na przykład przed odczytaniem wiersza z bazy danych należy zażądać blokady tylko do odczytu lub dostępu do aktualizacji. Jeśli zablokujesz wiersz dostępu do aktualizacji, żaden inny użytkownik nie może zablokować wiersza w celu uzyskania dostępu tylko do odczytu lub aktualizacji, ponieważ otrzymają kopię danych w procesie zmiany. Jeśli zablokujesz wiersz dostępu tylko do odczytu, inne osoby mogą również zablokować go na potrzeby dostępu tylko do odczytu, ale nie do aktualizacji.

Zarządzanie blokadami ma wady. Może to być skomplikowane do programowania. Wymaga to znaczących zasobów zarządzania bazami danych i może powodować problemy z wydajnością w miarę wzrostu liczby użytkowników aplikacji. Z tych powodów nie wszystkie systemy zarządzania bazami danych obsługują pesymistyczną współbieżność. Program Entity Framework Core nie zapewnia wbudowanej obsługi, a ten samouczek nie pokazuje, jak ją zaimplementować.

Optymistyczna współbieżność

Alternatywą dla pesymistycznej współbieżności jest optymistyczna współbieżność. Optymistyczna współbieżność oznacza umożliwienie wystąpienia konfliktów współbieżności, a następnie odpowiednie reagowanie, jeśli tak. Na przykład Jane odwiedza stronę Edycja Departamentu i zmienia kwotę budżetu dla działu angielskiego z 350 000,000 USD do 0,00 USD.

Changing budget to 0

Zanim Jane kliknie pozycję Zapisz, Jan odwiedzi tę samą stronę i zmieni pole Data rozpoczęcia z 2007-09-1.2013.

Changing start date to 2013

Jane najpierw klika pozycję Zapisz i widzi jej zmianę po powrocie przeglądarki na stronę Indeks.

Budget changed to zero

Następnie Jan klika pozycję Zapisz na stronie Edycja, która nadal pokazuje budżet w wysokości 350 000,000 USD. To, co się stanie dalej, zależy od sposobu obsługi konfliktów współbieżności.

Niektóre opcje obejmują następujące opcje:

  • Możesz śledzić, która właściwość użytkownika zmodyfikowała i zaktualizować tylko odpowiednie kolumny w bazie danych.

    W przykładowym scenariuszu żadne dane nie zostaną utracone, ponieważ dwaj użytkownicy zaktualizowali różne właściwości. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane' i Johna - datę rozpoczęcia 9/1/2013 i budżet zerowych dolarów. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych, ale nie może uniknąć utraty danych, jeśli konkurencyjne zmiany zostaną wprowadzone do tej samej właściwości jednostki. To, czy program Entity Framework działa w ten sposób, zależy od sposobu implementacji kodu aktualizacji. Często nie jest to praktyczne w aplikacji internetowej, ponieważ może wymagać utrzymania dużych ilości stanu w celu śledzenia wszystkich oryginalnych wartości właściwości dla jednostki, a także nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji, ponieważ wymaga zasobów serwera lub musi zostać uwzględnione na samej stronie internetowej (na przykład w ukrytych polach) lub w cookieobiekcie .

  • Możesz pozwolić Jane's change zastąpić zmianę Jane.

    Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i przywróconą wartość $350,000.000. Jest to nazywane scenariuszem wins klienta lub Last in Wins . (Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych). Jak wspomniano we wprowadzeniu do tej sekcji, jeśli nie zrobisz żadnego kodowania na potrzeby obsługi współbieżności, nastąpi to automatycznie.

  • Możesz uniemożliwić aktualizację johna w bazie danych.

    Zazwyczaj wyświetlany jest komunikat o błędzie, pokazywanie mu bieżącego stanu danych i umożliwianie mu ponownego wprowadzania zmian, jeśli nadal chce je wprowadzić. Jest to nazywane scenariuszem Store Wins . (Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta). W tym samouczku zaimplementujesz scenariusz Store Wins. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez powiadamiania użytkownika o tym, co się dzieje.

Wykrywanie konfliktów współbieżności

Konflikty można rozwiązać, obsługując DbConcurrencyException wyjątki zgłaszane przez program Entity Framework. Aby wiedzieć, kiedy zgłaszać te wyjątki, program Entity Framework musi mieć możliwość wykrywania konfliktów. W związku z tym należy odpowiednio skonfigurować bazę danych i model danych. Niektóre opcje włączania wykrywania konfliktów obejmują następujące elementy:

  • W tabeli bazy danych dołącz kolumnę śledzenia, która może służyć do określenia, kiedy wiersz został zmieniony. Następnie można skonfigurować program Entity Framework tak, aby zawierał kolumnę w klauzuli Where poleceń SQL Update lub Delete.

    Typ danych kolumny śledzenia to zazwyczaj rowversion. Wartość rowversion jest sekwencyjną liczbą, która jest zwiększana za każdym razem, gdy wiersz jest aktualizowany. W poleceniu Aktualizuj lub Usuń klauzula Where zawiera oryginalną wartość kolumny śledzenia (oryginalna wersja wiersza). Jeśli aktualizowany wiersz został zmieniony przez innego użytkownika, wartość w rowversion kolumnie różni się od oryginalnej wartości, więc instrukcja Update or Delete nie może znaleźć wiersza do zaktualizowania ze względu na klauzulę Where. Gdy program Entity Framework wykryje, że żadne wiersze nie zostały zaktualizowane przez polecenie Aktualizuj lub Usuń (czyli gdy liczba wierszy, których dotyczy problem, wynosi zero), interpretuje to jako konflikt współbieżności.

  • Skonfiguruj program Entity Framework tak, aby zawierał oryginalne wartości każdej kolumny w tabeli w klauzuli Where poleceń Update and Delete.

    Podobnie jak w pierwszej opcji, jeśli coś w wierszu uległo zmianie od czasu pierwszego odczytania wiersza, klauzula Where nie zwróci wiersza do zaktualizowania, który program Entity Framework interpretuje jako konflikt współbieżności. W przypadku tabel baz danych, które mają wiele kolumn, takie podejście może spowodować bardzo duże klauzule Where i może wymagać zachowania dużej ilości stanu. Jak wspomniano wcześniej, utrzymanie dużych ilości stanu może mieć wpływ na wydajność aplikacji. W związku z tym takie podejście nie jest zwykle zalecane i nie jest to metoda używana w tym samouczku.

    Jeśli chcesz zaimplementować to podejście do współbieżności, musisz oznaczyć wszystkie właściwości inne niż klucz podstawowy w jednostce, dla której chcesz śledzić współbieżność, dodając ConcurrencyCheck do nich atrybut. Ta zmiana umożliwia programowi Entity Framework uwzględnienie wszystkich kolumn w klauzuli SQL Where instrukcji Update and Delete.

W pozostałej części tego samouczka dodasz rowversion właściwość śledzenia do jednostki Dział, utworzysz kontroler i widoki i przetestujesz, aby sprawdzić, czy wszystko działa poprawnie.

Dodawanie właściwości śledzenia

W Models/Department.cspliku dodaj właściwość śledzenia o nazwie 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; }
    }
}

Atrybut określa, że ta kolumna Timestamp zostanie uwzględniona w klauzuli Where poleceń Update and Delete wysyłanych do bazy danych. Atrybut jest wywoływany Timestamp , ponieważ poprzednie wersje programu SQL Server używały typu danych SQL timestamp przed zastąpieniem go przez program SQL rowversion . Typ platformy .NET dla rowversion jest tablicą bajtów.

Jeśli wolisz używać płynnego interfejsu API, możesz użyć IsConcurrencyToken metody (w Data/SchoolContext.cs), aby określić właściwość śledzenia, jak pokazano w poniższym przykładzie:

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

Dodając właściwość, którą zmieniono model bazy danych, należy więc przeprowadzić inną migrację.

Zapisz zmiany i skompiluj projekt, a następnie wprowadź następujące polecenia w oknie polecenia:

dotnet ef migrations add RowVersion
dotnet ef database update

Tworzenie kontrolera i widoków działów

Tworzenie szkieletu administratora i widoków działów, tak jak wcześniej dla studentów, kursów i instruktorów.

Scaffold Department

DepartmentsController.cs W pliku zmień wszystkie cztery wystąpienia elementu "FirstMidName" na "FullName", aby listy rozwijane administratora działu zawierały pełną nazwę instruktora, a nie tylko nazwisko.

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

Aktualizowanie widoku indeksu

Aparat tworzenia szkieletów utworzył kolumnę RowVersion w widoku Indeks, ale to pole nie powinno być wyświetlane.

Zamień kod w pliku Views/Departments/Index.cshtml na następujący kod.

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

Spowoduje to zmianę nagłówka na "Działy", usunięcie RowVersion kolumny i wyświetlenie pełnej nazwy zamiast imienia administratora.

Aktualizowanie metod edycji

W metodzie HttpGet Edit i metodzie Details dodaj AsNoTrackingelement . W metodzie HttpGet Edit dodaj chętne ładowanie dla Administracja istratora.

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

Zastąp istniejący kod metody HttpPost Edit następującym kodem:

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

Kod rozpoczyna się od próby odczytania działu, który ma zostać zaktualizowany. FirstOrDefaultAsync Jeśli metoda zwraca wartość null, dział został usunięty przez innego użytkownika. W takim przypadku kod używa wartości opublikowanych formularzy do utworzenia Department jednostki, aby można było ponownie edytować stronę edycji z komunikatem o błędzie. Alternatywnie nie byłoby konieczne ponowne utworzenie Department jednostki, jeśli zostanie wyświetlony tylko komunikat o błędzie bez ponownego tworzenia pól działu.

Widok przechowuje oryginalną RowVersion wartość w ukrytym polu, a ta metoda odbiera tę wartość w parametrze rowVersion . Przed wywołaniem SaveChangesmetody należy umieścić tę oryginalną RowVersion wartość właściwości w OriginalValues kolekcji dla jednostki.

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

Następnie, gdy program Entity Framework utworzy polecenie SQL UPDATE, to polecenie będzie zawierać klauzulę WHERE, która wyszukuje wiersz zawierający oryginalną RowVersion wartość. Jeśli polecenie UPDATE nie ma wpływu na żadne wiersze (żadne wiersze DbUpdateConcurrencyException nie mają oryginalnej RowVersion wartości), program Entity Framework zgłasza wyjątek.

Kod w bloku catch dla tego wyjątku pobiera jednostkę działu, której dotyczy problem, zawierającą zaktualizowane wartości z Entries właściwości obiektu wyjątku.

var exceptionEntry = ex.Entries.Single();

Kolekcja Entries będzie miała tylko jeden EntityEntry obiekt. Możesz użyć tego obiektu, aby uzyskać nowe wartości wprowadzone przez użytkownika i bieżące wartości bazy danych.

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

Kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych inne niż to, co użytkownik wprowadził na stronie Edycja (tylko jedno pole jest wyświetlane tutaj w celu zwięzłości).

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

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

Na koniec kod ustawia RowVersion wartość departmentToUpdate elementu na nową wartość pobraną z bazy danych. Ta nowa RowVersion wartość będzie przechowywana w ukrytym polu, gdy strona Edytuj zostanie ponownie odtworzona, a następnym razem użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności występujące od czasu ponownego redysponowania strony Edycja.

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

Instrukcja jest wymagana ModelState.Remove , ponieważ ModelState ma starą RowVersion wartość. W widoku ModelState wartość pola ma pierwszeństwo przed wartościami właściwości modelu, gdy oba te wartości są obecne.

Aktualizowanie widoku edycji

W Views/Departments/Edit.cshtmlpliku wprowadź następujące zmiany:

  • Dodaj ukryte pole, aby zapisać RowVersion wartość właściwości bezpośrednio po ukrytym polu właściwości DepartmentID .

  • Dodaj opcję "Wybierz Administracja istrator" do listy rozwijanej.

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

Testowanie konfliktów współbieżności

Uruchom aplikację i przejdź do strony Indeks działów. Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz pozycję Otwórz w nowej karcie, a następnie kliknij hiperlink Edytuj dla działu angielskiego. Dwie karty przeglądarki teraz wyświetlają te same informacje.

Zmień pole na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.

Department Edit page 1 after change

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością.

Zmień pole na drugiej karcie przeglądarki.

Department Edit page 2 after change

Kliknij przycisk Zapisz. Zostanie wyświetlony komunikat o błędzie:

Department Edit page error message

Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości są widoczne po wyświetleniu strony Indeks.

Aktualizowanie strony Usuwanie

Na stronie Usuwanie platforma Entity Framework wykrywa konflikty współbieżności spowodowane przez inną osobę edytując dział w podobny sposób. Gdy metoda HttpGet Delete wyświetla widok potwierdzenia, widok zawiera oryginalną RowVersion wartość w ukrytym polu. Ta wartość jest następnie dostępna dla metody HttpPost Delete , która jest wywoływana, gdy użytkownik potwierdzi usunięcie. Gdy program Entity Framework tworzy polecenie SQL DELETE, zawiera klauzulę WHERE z oryginalną RowVersion wartością. Jeśli polecenie spowoduje, że nie ma to wpływu na wiersze (co oznacza, że wiersz został zmieniony po wyświetleniu strony potwierdzenia Usuń), zostanie zgłoszony wyjątek współbieżności, a metoda HttpGet Delete jest wywoływana z flagą błędu ustawioną na true w celu ponownego redysponowania strony potwierdzenia z komunikatem o błędzie. Istnieje również możliwość, że nie ma to wpływu na wiersze, ponieważ wiersz został usunięty przez innego użytkownika, więc w takim przypadku nie jest wyświetlany żaden komunikat o błędzie.

Aktualizowanie metod Usuwania w kontrolerze działów

W DepartmentsController.cspliku zastąp metodę HttpGet Delete następującym kodem:

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

Metoda akceptuje opcjonalny parametr wskazujący, czy strona jest odtwarzana ponownie po błędzie współbieżności. Jeśli ta flaga ma wartość true, a określony dział już nie istnieje, został usunięty przez innego użytkownika. W takim przypadku kod przekierowuje do strony Indeks. Jeśli ta flaga ma wartość true, a dział już istnieje, został zmieniony przez innego użytkownika. W takim przypadku kod wysyła komunikat o błędzie do widoku przy użyciu polecenia ViewData.

Zastąp kod w metodzie HttpPost Delete (o nazwie DeleteConfirmed) następującym kodem:

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

W kodzie szkieletowym, który właśnie zamieniono, ta metoda zaakceptowała tylko identyfikator rekordu:

public async Task<IActionResult> DeleteConfirmed(int id)

Ten parametr został zmieniony na Department wystąpienie jednostki utworzone przez powiązanie modelu. Zapewnia to programOWI EF dostęp do wartości właściwości RowVers'ion oprócz klucza rekordu.

public async Task<IActionResult> Delete(Department department)

Zmieniono również nazwę metody akcji z DeleteConfirmed na Delete. Kod szkieletowy użył nazwy DeleteConfirmed w celu nadania metodzie HttpPost unikatowego podpisu. (ClR wymaga przeciążonych metod, aby mieć różne parametry metody). Teraz, gdy podpisy są unikatowe, możesz trzymać się konwencji MVC i używać tej samej nazwy dla metod usuwania HttpPost i HttpGet.

Jeśli dział został już usunięty, metoda zwraca wartość false, AnyAsync a aplikacja wraca do metody Index.

Jeśli zostanie przechwycony błąd współbieżności, kod ponownie wyświetla stronę potwierdzenia Usuń i udostępnia flagę wskazującą, że powinien wyświetlić komunikat o błędzie współbieżności.

Aktualizowanie widoku Usuwania

W Views/Departments/Delete.cshtmlpliku zastąp kod szkieletowy następującym kodem, który dodaje pole komunikatu o błędzie i ukryte pola właściwości DepartmentID i RowVersion. Zmiany są wyróżnione.

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

Spowoduje to następujące zmiany:

  • Dodaje komunikat o błędzie między h2 nagłówkami i h3 .

  • Zastępuje wartość FirstMidName wartością FullName w polu Administracja istrator.

  • Usuwa pole RowVersion.

  • Dodaje ukryte pole dla RowVersion właściwości .

Uruchom aplikację i przejdź do strony Indeks działów. Kliknij prawym przyciskiem myszy hiperłącze Usuń dla działu angielskiego i wybierz pozycję Otwórz w nowej karcie, a następnie na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.

W pierwszym oknie zmień jedną z wartości, a następnie kliknij przycisk Zapisz:

Department Edit page after change before delete

Na drugiej karcie kliknij pozycję Usuń. Zostanie wyświetlony komunikat o błędzie współbieżności, a wartości Dział są odświeżane z bieżącymi elementami w bazie danych.

Department Delete confirmation page with concurrency error

Po ponownym kliknięciu przycisku Usuń nastąpi przekierowanie do strony Indeks, która pokazuje, że dział został usunięty.

Aktualizowanie szczegółów i tworzenie widoków

Opcjonalnie możesz wyczyścić szkielet kodu w widokach Szczegóły i Tworzenie.

Zastąp kod , Views/Departments/Details.cshtml aby usunąć kolumnę RowVersion i wyświetlić pełną nazwę Administracja istratora.

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

Zastąp kod, Views/Departments/Create.cshtml aby dodać opcję Wybierz do listy rozwijanej.

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

Uzyskiwanie kodu

Pobierz lub wyświetl ukończoną aplikację.

Dodatkowe zasoby

Aby uzyskać więcej informacji na temat obsługi współbieżności w programie EF Core, zobacz Konflikty współbieżności.

Następne kroki

W tym samouczku zostały wykonane następujące czynności:

  • Informacje o konfliktach współbieżności
  • Dodano właściwość śledzenia
  • Utworzony kontroler i widoki działów
  • Zaktualizowany widok indeksu
  • Zaktualizowane metody edycji
  • Zaktualizowany widok edycji
  • Przetestowane konflikty współbieżności
  • Zaktualizowano stronę Usuwanie
  • Zaktualizowane szczegóły i tworzenie widoków

Przejdź do następnego samouczka, aby dowiedzieć się, jak zaimplementować dziedziczenie tabeli na hierarchię dla jednostek Instruktor i Student.