Kurz: Zpracování souběžnosti – ASP.NET MVC s EF Core

V předchozích kurzech jste zjistili, jak aktualizovat data. V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje stejnou entitu současně.

Vytvoříte webové stránky, které pracují s entitou Department a zpracovávají chyby souběžnosti. Následující ilustrace ukazují stránky Pro úpravy a odstranění, včetně některých zpráv, které se zobrazují, pokud dojde ke konfliktu souběžnosti.

Stránka Upravit oddělení

Stránka Odstranění oddělení

V tomto kurzu jste:

  • Další informace o konfliktech souběžnosti
  • Přidání vlastnosti sledování
  • Vytvoření kontroleru a zobrazení oddělení
  • Aktualizovat zobrazení indexu
  • Aktualizace metod úprav
  • Aktualizovat zobrazení pro úpravy
  • Konflikty souběžnosti testů
  • Aktualizace stránky Odstranit
  • Aktualizace podrobností a vytváření zobrazení

Požadavky

Konflikty souběžnosti

Konflikt souběžnosti nastane, když jeden uživatel zobrazí data entity, aby je mohl upravit, a pak jiný uživatel aktualizuje data stejné entity před zápisem první změny uživatele do databáze. Pokud nepovolíte detekci takových konfliktů, každý, kdo databázi naposledy přepíše změny ostatních uživatelů. V mnoha aplikacích je toto riziko přijatelné: pokud existuje několik uživatelů nebo několik aktualizací nebo pokud není opravdu důležité, pokud jsou některé změny přepsány, náklady na programování pro souběžnost by mohly výhodu převážit. V takovém případě nemusíte aplikaci konfigurovat tak, aby zpracovávala konflikty souběžnosti.

Pesimistické souběžnosti (uzamykání)

Pokud vaše aplikace potřebuje zabránit náhodné ztrátě dat ve scénářích souběžnosti, jedním ze způsobů, jak to udělat, je použít zámky databáze. Říká se tomu pesimistické souběžnost. Například před čtením řádku z databáze si vyžádáte zámek jen pro čtení nebo pro aktualizaci přístupu. Pokud uzamknete řádek pro přístup k aktualizacím, nebudou moct ostatní uživatelé zamknout řádek pro přístup jen pro čtení nebo aktualizaci, protože by získali kopii dat, která se mění. Pokud uzamknete řádek pro přístup jen pro čtení, ostatní ho můžou také uzamknout pro přístup jen pro čtení, ale ne pro aktualizaci.

Správa zámků má nevýhody. Program může být složitý. Vyžaduje významné prostředky pro správu databází a může způsobit problémy s výkonem při zvýšení počtu uživatelů aplikace. Z těchto důvodů nepodporují všechny systémy správy databází pesimistické souběžnosti. Entity Framework Core neposkytuje žádnou integrovanou podporu a tento kurz vám neukazuje, jak ho implementovat.

Optimistická metoda souběžného zpracování

Alternativou k pesimistické souběžnosti je optimistická souběžnost. Optimistická souběžnost znamená, že umožňuje konflikty souběžnosti proběhnout a pak odpovídajícím způsobem reagovat, pokud ano. Jane například navštíví stránku Upravit oddělení a změní částku rozpočtu pro anglické oddělení z 350 000,00 USD na 0,00 USD.

Změna rozpočtu na 0

Před kliknutím na tlačítko Uložit jan navštíví stejnou stránku a změní pole Počáteční datum od 9. 1. 2007 do 9. 1. 2013.

Změna data zahájení na 2013

Jane nejprve klikne na Uložit a uvidí její změnu, když se prohlížeč vrátí na stránku indexu.

Rozpočet se změnil na nulu

Pak Jan klikne na uložit na stránce pro úpravy, která stále zobrazuje rozpočet 350 000,00 USD. Co se stane dál, je určeno tím, jak zpracováváte konflikty souběžnosti.

Mezi tyto možnosti patří:

  • Můžete sledovat, kterou vlastnost uživatel změnil, a aktualizovat pouze odpovídající sloupce v databázi.

    V ukázkovém scénáři by se neztratila žádná data, protože dva uživatelé aktualizovali různé vlastnosti. Když někdo příště přejde do anglického oddělení, uvidí změny Jane i Johna – počáteční datum 9. 1. 2013 a rozpočet nulových dolarů. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat, ale nemůže se vyhnout ztrátě dat, pokud jsou konkurenční změny provedeny ve stejné vlastnosti entity. To, jestli entity Framework funguje tímto způsobem, závisí na tom, jak implementujete kód aktualizace. Často to není praktické ve webové aplikaci, protože může vyžadovat, abyste zachovali velké množství stavu, abyste měli přehled o všech původních hodnotách vlastností pro entitu i nové hodnoty. Udržování velkého množství stavu může ovlivnit výkon aplikace, protože buď vyžaduje prostředky serveru, nebo musí být součástí samotné webové stránky (například ve skrytých polích) nebo v cookiesouboru .

  • Janovi můžete změnit přepsání janovy změny.

    Při příštím procházení anglického oddělení uvidí 9. 1. 2013 a obnovenou hodnotu 350 000,00 USD. Označuje se jako klient wins nebo last ve scénáři Wins . (Všechny hodnoty z klienta mají přednost před tím, co je v úložišti dat.) Jak je uvedeno v úvodu do této části, pokud neprovedou žádné kódování pro zpracování souběžnosti, dojde k tomu automaticky.

  • V databázi můžete zabránit aktualizaci změny Johna.

    Obvykle byste zobrazili chybovou zprávu, zobrazili by mu aktuální stav dat a umožnili mu znovu použít změny, pokud je přesto chce udělat. Říká se tomu scénář wins ve Storu . (Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem.) Scénář Wins pro Store implementujete v tomto kurzu. Tato metoda zajišťuje, aby se nepřepsaly žádné změny, aniž by se uživatel upozorňoval na to, co se děje.

Zjišťování konfliktů souběžnosti

Konflikty můžete vyřešit zpracováním DbConcurrencyException výjimek, které entity Framework vyvolá. Aby bylo možné zjistit, kdy tyto výjimky vyvolat, musí být Entity Framework schopen rozpoznat konflikty. Proto je nutné databázi a datový model odpovídajícím způsobem nakonfigurovat. Mezi možnosti povolení detekce konfliktů patří následující:

  • V tabulce databáze uveďte sloupec sledování, který lze použít k určení, kdy byl řádek změněn. Pak můžete nakonfigurovat Entity Framework tak, aby zahrnoval tento sloupec do klauzule Where příkazů SQL Update nebo Delete.

    Datový typ sloupce sledování je obvykle rowversion. Hodnota rowversion je pořadové číslo, které se při každé aktualizaci řádku zvýší. V příkazu Update nebo Delete klauzule Where obsahuje původní hodnotu sloupce sledování (původní verze řádku). Pokud se aktualizace řádku změnila jiným uživatelem, hodnota ve rowversion sloupci se liší od původní hodnoty, takže příkaz Update nebo Delete nemůže řádek aktualizovat kvůli klauzuli Where. Když Entity Framework zjistí, že příkaz Update nebo Delete neaktualizoval žádné řádky (to znamená, že počet ovlivněných řádků je nula), interpretuje to jako konflikt souběžnosti.

  • Nakonfigurujte Entity Framework tak, aby obsahovala původní hodnoty každého sloupce v tabulce v klauzuli Where příkazů Update a Delete.

    Stejně jako v první možnosti, pokud se od prvního čtení řádku něco v řádku změnilo, klauzule Where nevrátí řádek pro aktualizaci, který Entity Framework interpretuje jako konflikt souběžnosti. U databázových tabulek, které mají mnoho sloupců, může tento přístup vést k velmi velkým klauzulem Where a může vyžadovat, abyste zachovali velké objemy stavu. Jak bylo uvedeno výše, udržování velkých objemů stavu může ovlivnit výkon aplikace. Proto se tento přístup obecně nedoporučuje a není to metoda použitá v tomto kurzu.

    Pokud chcete tento přístup implementovat ke souběžnosti, musíte označit všechny vlastnosti jiného než primárního klíče v entitě, pro kterou chcete sledovat souběžnost, přidáním atributu ConcurrencyCheck do nich. Tato změna umožňuje rozhraní Entity Framework zahrnout všechny sloupce do klauzule SQL Where příkazů Update a Delete.

Ve zbývající části tohoto kurzu přidáte rowversion vlastnost sledování do entity Oddělení, vytvoříte kontroler a zobrazení a otestujete, jestli všechno funguje správně.

Přidání vlastnosti sledování

V Models/Department.cssouboru přidejte vlastnost sledování s názvem 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 určuje, že tento sloupec bude zahrnut do klauzule Where příkazu Update a Delete odeslané do databáze. Atribut se voláTimestamp, protože předchozí verze SQL Server použily datový typ SQL timestamp před nahrazením SQLrowversion. Typ .NET je rowversion bajtové pole.

Pokud dáváte přednost používání fluent api, můžete použít metodu IsConcurrencyToken (in Data/SchoolContext.cs) k určení vlastnosti sledování, jak je znázorněno v následujícím příkladu:

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

Přidáním vlastnosti, kterou jste změnili model databáze, takže potřebujete provést další migraci.

Uložte změny a sestavte projekt a potom do příkazového okna zadejte následující příkazy:

dotnet ef migrations add RowVersion
dotnet ef database update

Vytvoření kontroleru a zobrazení oddělení

Vygenerování kontroleru a zobrazení oddělení, jak jste to udělali dříve pro studenty, kurzy a instruktory.

Oddělení generování uživatelského rozhraní

DepartmentsController.cs V souboru změňte všechny čtyři výskyty "FirstMidName" na "FullName" tak, aby rozevírací seznamy správce oddělení obsahovaly celé jméno instruktora, nikoli jenom příjmení.

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

Aktualizovat zobrazení indexu

Modul generování uživatelského rozhraní vytvořil RowVersion sloupec v zobrazení Index, ale toto pole by se nemělo zobrazit.

Nahraďte kód v souboru Views/Departments/Index.cshtml následujícím kódem.

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

Tím se záhlaví změní na Oddělení, odstraní RowVersion sloupec a místo křestního jména správce se zobrazí celé jméno.

Aktualizace metod úprav

Do metody HttpGet Edit i Details metody přidejte AsNoTracking. V metodě HttpGet Edit přidejte dychtivé načítání správce.

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

Nahraďte existující kód pro metodu HttpPost Edit následujícím kódem:

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

Kód začíná pokusem o přečtení oddělení, které se má aktualizovat. FirstOrDefaultAsync Pokud metoda vrátí hodnotu null, oddělení bylo odstraněno jiným uživatelem. V takovém případě kód používá publikované hodnoty formuláře k vytvoření Department entity, aby se stránka Upravit znovu zobrazila s chybovou zprávou. Alternativně byste entitu Department nemuseli znovu vytvořit, pokud se zobrazí jenom chybová zpráva bez opětovného zobrazení polí oddělení.

Zobrazení ukládá původní RowVersion hodnotu do skrytého pole a tato metoda obdrží tuto hodnotu v parametru rowVersion . Před voláním SaveChangesmusíte tuto původní RowVersion hodnotu vlastnosti vložit do OriginalValues kolekce pro entitu.

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

Když pak Entity Framework vytvoří příkaz SQL UPDATE, tento příkaz bude obsahovat klauzuli WHERE, která hledá řádek, který má původní RowVersion hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion hodnotu), entity Framework vyvolá DbUpdateConcurrencyException výjimku.

Kód v bloku catch pro danou výjimku získá ovlivněnou entitu Oddělení, která má aktualizované hodnoty z Entries vlastnosti objektu výjimky.

var exceptionEntry = ex.Entries.Single();

Kolekce Entries bude mít pouze jeden EntityEntry objekt. Tento objekt můžete použít k získání nových hodnot zadaných uživatelem a aktuálními hodnotami databáze.

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

Kód přidá vlastní chybovou zprávu pro každý sloupec s hodnotami databáze, které se liší od toho, co uživatel zadal na stránce Upravit (pro stručnost se zobrazí jenom jedno pole).

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

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

Nakonec kód nastaví RowVersion hodnotu departmentToUpdate na novou hodnotu načtenou z databáze. Tato nová RowVersion hodnota bude uložena ve skrytém poli při opětovném zobrazení stránky Upravit a při příštím kliknutí uživatele na Uložit se zobrazí pouze chyby souběžnosti, ke kterým dochází, protože se zachytí opětovné zobrazení stránky Pro úpravy.

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

Příkaz ModelState.Remove je povinný, protože ModelState má starou RowVersion hodnotu. V zobrazení ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici oba.

Aktualizovat zobrazení pro úpravy

V souboru Views/Departments/Edit.cshtml proveďte následující změny:

  • Přidejte skryté pole pro uložení RowVersion hodnoty vlastnosti bezprostředně za skrytým polem DepartmentID vlastnosti.

  • Do rozevíracího seznamu přidejte možnost Vybrat správce.

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

Konflikty souběžnosti testů

Spusťte aplikaci a přejděte na stránku Index oddělení. Klikněte pravým tlačítkem myši na hypertextový odkaz pro anglické oddělení a vyberte Otevřít v nové kartě a potom klikněte na hypertextový odkaz Upravit pro anglické oddělení. Na dvou kartách prohlížeče se teď zobrazují stejné informace.

Změňte pole na první kartě prohlížeče a klikněte na Uložit.

Stránka Upravit oddělení 1 po změně

Prohlížeč zobrazuje stránku Index se změněnou hodnotou.

Změňte pole na druhé kartě prohlížeče.

Stránka Upravit oddělení 2 po změně

Klikněte na Uložit. Zobrazí se chybová zpráva:

Chybová zpráva o úpravě stránky oddělení

Znovu klikněte na Uložit . Hodnota zadaná na druhé kartě prohlížeče se uloží. Uložené hodnoty se zobrazí, když se zobrazí stránka Index.

Aktualizace stránky Odstranit

Na stránce Odstranit služba Entity Framework detekuje konflikty souběžnosti způsobené jiným uživatelem, který upravuje oddělení podobným způsobem. Když metoda HttpGet Delete zobrazí potvrzovací zobrazení, zobrazení obsahuje původní RowVersion hodnotu ve skrytém poli. Tato hodnota je pak dostupná pro metodu HttpPost Delete , která se volá, když uživatel potvrdí odstranění. Když Entity Framework vytvoří příkaz SQL DELETE, obsahuje klauzuli WHERE s původní RowVersion hodnotou. Pokud má příkaz vliv na nula řádků (to znamená, že řádek byl změněn po zobrazení stránky potvrzení odstranění), vyvolá se výjimka souběžnosti a metoda HttpGet Delete se volá s příznakem chyby nastaveným na true, aby se znovu zobrazila potvrzovací stránka s chybovou zprávou. Je také možné, že byly ovlivněny nulové řádky, protože řádek odstranil jiný uživatel, takže v takovém případě se nezobrazí žádná chybová zpráva.

Aktualizace metod delete v kontroleru oddělení

Nahraďte DepartmentsController.csmetodu HttpGet Delete následujícím kódem:

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 přijímá volitelný parametr, který označuje, zda se stránka znovu zobrazuje po chybě souběžnosti. Pokud je tento příznak pravdivý a zadané oddělení již neexistuje, byl odstraněn jiným uživatelem. V takovém případě se kód přesměruje na stránku indexu. Pokud je tento příznak pravdivý a oddělení existuje, změnil ho jiný uživatel. V takovém případě kód odešle do zobrazení chybovou zprávu pomocí ViewData.

Nahraďte kód v metodě HttpPost Delete (pojmenované DeleteConfirmed) následujícím kódem:

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

V vygenerovaný kód, který jste právě nahradili, tato metoda akceptovala pouze ID záznamu:

public async Task<IActionResult> DeleteConfirmed(int id)

Tento parametr jste změnili na Department instanci entity vytvořenou pořadačem modelu. Tím získáte přístup EF k hodnotě vlastnosti RowVers kromě klíče záznamu.

public async Task<IActionResult> Delete(Department department)

Změnili jste také název metody akce z DeleteConfirmed na Delete. Vygenerovaný kód použil název DeleteConfirmed k poskytnutí jedinečného podpisu metody HttpPost. (CLR vyžaduje přetížené metody, aby měly různé parametry metody.) Teď, když jsou podpisy jedinečné, můžete držet konvence MVC a použít stejný název pro metody HttpPost a HttpGet delete.

Pokud je oddělení již odstraněno, AnyAsync metoda vrátí hodnotu false a aplikace se jednoduše vrátí do metody Index.

Pokud dojde k chybě souběžnosti, kód znovu zobrazí stránku potvrzení odstranění a zobrazí příznak, který označuje, že by se měla zobrazit chybová zpráva souběžnosti.

Aktualizace zobrazení Pro odstranění

Nahraďte Views/Departments/Delete.cshtmlvygenerovaný kód následujícím kódem, který přidá pole chybové zprávy a skrytá pole pro vlastnosti DepartmentID a RowVersion. Změny jsou zvýrazněné.

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

To provede následující změny:

  • Přidá mezi nadpisy chybovou h2h3 zprávu.

  • Nahradí Jméno FirstMidName úplným jménem v poli Správce .

  • Odebere pole RowVersion.

  • Přidá skryté pole vlastnosti RowVersion .

Spusťte aplikaci a přejděte na stránku Index oddělení. Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro anglické oddělení a vyberte Otevřít v nové kartě a pak na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.

V prvním okně změňte jednu z hodnot a klikněte na Uložit:

Stránka Upravit oddělení po změně před odstraněním

Na druhé kartě klikněte na Odstranit. Zobrazí se chybová zpráva souběžnosti a hodnoty oddělení se aktualizují s informacemi o tom, co je aktuálně v databázi.

Stránka potvrzení odstranění oddělení s chybou souběžnosti

Pokud znovu kliknete na Odstranit , budete přesměrováni na stránku Index, která ukazuje, že oddělení bylo odstraněno.

Aktualizace podrobností a vytváření zobrazení

Volitelně můžete vyčistit vygenerovaný kód v zobrazení Podrobnosti a Vytvořit.

Nahraďte kód pro Views/Departments/Details.cshtml odstranění sloupce RowVersion a zobrazte úplný název správce.

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

Nahraďte kód Views/Departments/Create.cshtml pro přidání možnosti Vybrat do rozevíracího seznamu.

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

Získání kódu

Stáhněte nebo zobrazte dokončenou aplikaci.

Další materiály

Další informace o zpracování souběžnosti v EF Core najdete v tématu Konflikty souběžnosti.

Další kroky

V tomto kurzu jste:

  • Informace o konfliktech souběžnosti
  • Přidání vlastnosti sledování
  • Vytvořený kontroler oddělení a zobrazení
  • Aktualizované zobrazení indexu
  • Aktualizované metody úprav
  • Aktualizované zobrazení pro úpravy
  • Testované konflikty souběžnosti
  • Aktualizace stránky Odstranit
  • Aktualizované podrobnosti a vytváření zobrazení

V dalším kurzu se dozvíte, jak implementovat dědičnost tabulek na hierarchii pro entity instruktora a studenta.