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

V předchozích kurzech jste se naučili 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 znázorňují stránky Upravit a odstranit, včetně některých zpráv, které se zobrazí, pokud dojde ke konfliktu souběžnosti.

Department Edit page

Department Delete page

V tomto kurzu se naučíte:

  • 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 vytvoření zobrazení

Požadavky

Konflikty souběžnosti

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

Pesimistické souběžnost (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. Tomu se říká pesimistické souběžnost. Například před čtením řádku z databáze si vyžádáte zámek jen pro čtení nebo pro přístup k aktualizaci. Pokud uzamknete řádek pro přístup k aktualizacím, nebudou moct ostatní uživatelé zamknout řádek pro přístup jen pro čtení nebo pro aktualizaci, protože by získali kopii dat, která se právě 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 správy databází a může způsobit problémy s výkonem při nárůstu počtu uživatelů aplikace. Z těchto důvodů ne všechny systémy pro správu databází podporují 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, aby došlo ke konfliktům souběžnosti, 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.

Changing budget to 0

Než Jane klikne na Uložit, jan navštíví stejnou stránku a změní pole Počáteční datum od 1. 9. 2007 do 1. 9. 2013.

Changing start date to 2013

Jane nejprve klikne na Uložit a zobrazí se její změna, když se prohlížeč vrátí na stránku Rejstříku.

Budget changed to zero

Pak Jan klikne na uložit na stránce Pro úpravy, která stále zobrazuje rozpočet ve výši 350 000,00 USD. Co se stane dál, určuje způsob zpracování konfliktů souběžnosti.

Mezi tyto možnosti patří:

  • Můžete sledovat, kterou vlastnost uživatel upravil, 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 Janeho i Johna – počáteční datum 1. 9. 2013 a rozpočet s nulovými dolary. Tato metoda aktualizace může snížit počet konfliktů, které můžou vést ke ztrátě dat, ale nemůže se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti entity provedeny konkurenční změny. To, jestli Entity Framework funguje tímto způsobem, závisí na tom, jak implementujete aktualizační kód. Často to není praktické ve webové aplikaci, protože může vyžadovat, abyste zachovali velké množství stavu, abyste mohli sledovat všechny původní hodnoty vlastností pro entitu i nové hodnoty. Udržování velkého množství stavu může mít vliv na výkon aplikace, protože vyžaduje prostředky serveru nebo musí být součástí samotné webové stránky (například ve skrytých polích) nebo ve skrytá cookiepole .

  • Johnovi můžete dát možnost přepsat Janovu změnu.

    Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a obnovenou hodnotu 350 000,00 USD. To se nazývá 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 neproděláte žá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 se zobrazí chybová zpráva, zobrazí se mu aktuální stav dat a umožníte mu znovu použít změny, pokud je stále chce provést. Tomu se říká scénář wins ve Storu. (Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem.) V tomto kurzu implementujete scénář Wins pro Store. Tato metoda zajišťuje, aby se nepřepsaly žádné změny, aniž by 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ává. Aby bylo možné zjistit, kdy tyto výjimky vyvolat, musí být Entity Framework schopen detekovat konflikty. Proto je nutné správně nakonfigurovat databázi a datový model. Mezi možnosti povolení detekce konfliktů patří:

  • V tabulce databáze zahrňte sledovací sloupec, který lze použít k určení, kdy byl řádek změněn. Potom můžete rozhraní Entity Framework nakonfigurovat tak, aby zahrnovalo tento sloupec v klauzuli Where příkazu 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 aktualizaci řádku změnil jiný uživatel, hodnota ve rowversion sloupci se liší od původní hodnoty, takže příkaz Update nebo Delete nemůže najít řádek, který se má 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 zahrnovaly 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 s mnoha sloupci může tento přístup vést k velmi velkým klauzulem Where a může vyžadovat, abyste zachovali velké množství stavu. Jak jsme uvedli dříve, udržování velkého objemu 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.csaplikaci přidejte sledovací vlastnost 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 je volán Timestamp , protože předchozí verze SQL Serveru používaly datový typ SQL timestamp před nahrazením SQL rowversion . Typ .NET je rowversion bajtové pole.

Pokud raději používáte fluent API, můžete metodu IsConcurrencyToken (in Data/SchoolContext.cs) použít 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 je potřeba 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.

Scaffold Department

DepartmentsController.cs V souboru změňte všechny čtyři výskyty "FirstMidName" na "FullName", aby rozevírací seznamy správců oddělení obsahovaly celé jméno instruktora, a ne 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 zobrazovat.

Nahraďte kód v 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 změní nadpis na Oddělení, odstraní RowVersion se 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í pro Správa istrator.

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. Jako alternativu byste entitu Department nemuseli znovu vytvořit, pokud se zobrazí jenom chybová zpráva bez opětovného zobrazení polí oddělení.

Zobrazení uloží původní RowVersion hodnotu do skrytého pole a tato metoda obdrží tuto hodnotu v parametru rowVersion . Než zavoláte SaveChanges, musí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, bude tento příkaz obsahovat klauzuli WHERE, která hledá řádek s původní RowVersion hodnotou. 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, který má hodnoty databáze odlišné 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 se uloží do skrytého pole při opětovném zobrazení stránky Pro úpravy a při příštím kliknutí uživatele na Uložit dojde pouze k chybám souběžnosti, ke kterým dochází, protože se při přehrání stránky Pro úpravy zachytí.

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

Proveďte Views/Departments/Edit.cshtmlná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áva istrator.

@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 upravit hypertextový odkaz 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.

Department Edit page 1 after change

V prohlížeči se zobrazí stránka Index se změněnou hodnotou.

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

Department Edit page 2 after change

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

Department Edit page error message

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

Aktualizace stránky Odstranit

U stránky 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 k dispozici pro metodu HttpPost Delete , která je volána, 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 za následek nulový dopad na řádky (to znamená, že se řádek po zobrazení stránky potvrzení delete změnil), vyvolá se výjimka souběžnosti a volá se metoda HttpGet Delete s příznakem chyby nastaveným na true, aby se znovu zobrazila potvrzovací stránka s chybovou zprávou. Je také možné, že nula řádků byla ovlivněna, 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í

V DepartmentsController.cspříkazu nahraďte metodu 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, odstranil ho jiný uživatel. V takovém případě se kód přesměruje na indexovou stránku. 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 chybovou zprávu do zobrazení 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 přijala 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. To dává EF přístup k hodnotě vlastnosti RowVers'ion 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í metody HttpPost jedinečný podpis. (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 právě vrátí k metodě Index.

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

Aktualizace zobrazení Odstranit

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>

Provede se následující změny:

  • Přidá chybovou h2 zprávu mezi nadpisy a h3 nadpisy.

  • Nahradí FirstMidName úplným názvem v poli Správa istrator.

  • Odebere pole RowVersion.

  • Přidá skryté pole pro RowVersion vlastnost.

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 potom 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:

Department Edit page after change before delete

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

Department Delete confirmation page with concurrency error

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 vytvoření zobrazení

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

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

@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ší prostředky

Další informace o tom, jak zpracovat souběžnost v EF Coretématu Konflikty souběžnosti.

Další kroky

V tomto kurzu se naučíte:

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

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