Část 8, Razor Stránky s EF Core ASP.NET Core – souběžnost

Tom Dykstra a Jon P Smith

Webová aplikace Contoso University ukazuje, jak vytvářet Razor webové aplikace Pages pomocí EF Core sady Visual Studio. Informace o sérii kurzů najdete v prvním kurzu.

Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte si dokončenou aplikaci a porovnejte tento kód s tím, co jste vytvořili podle kurzu.

V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně.

Konflikty souběžnosti

Ke konfliktu souběžnosti dochází v případě, že:

  • Uživatel přejde na stránku pro úpravy entity.
  • Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.

Pokud detekce souběžnosti není povolená, přepíše změny ostatních uživatelů každý, kdo aktualizuje databázi. Pokud je toto riziko přijatelné, náklady na programování pro souběžnost můžou převažovat nad výhodou.

Pesimistické souběžnosti

Jedním zezpůsobůch Tomu se říká pesimistické souběžnost. Než aplikace přečte řádek databáze, který má v úmyslu aktualizovat, požádá o zámek. Jakmile je řádek uzamčen pro přístup k aktualizacím, nebudou moct ostatní uživatelé řádek uzamknout, dokud nebude vydán první zámek.

Správa zámků má nevýhody. Program může být složitý a může způsobovat problémy s výkonem při nárůstu počtu uživatelů. Entity Framework Core neposkytuje žádnou integrovanou podporu pesimistické souběžnosti.

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

Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet 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 uvidí, že se její změna projeví, protože v prohlížeči se jako částka rozpočtu zobrazí stránka Index s nulou.

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, určuje způsob zpracování konfliktů souběžnosti:

  • Sledujte, kterou vlastnost uživatel upravil, a aktualizujte pouze odpovídající sloupce v databázi.

    V tomto scénáři nedojde ke ztrátě žádných dat. 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. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup má určité nevýhody:

    • Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
    • Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
    • Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
  • Přepíše janovu změnu.

    Když někdo příště přejde do anglického oddělení, uvidí 1. 9. 2013 a načte hodnotu 350 000,00 USD. Tento přístup se označuje 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. Vygenerovaný kód nezpracovává žádné zpracování souběžnosti, klient wins se provádí automaticky.

  • Znemožnit aktualizaci Janovy změny v databázi. Aplikace obvykle:

    • Zobrazí chybovou zprávu.
    • Zobrazí aktuální stav dat.
    • Umožňuje uživateli znovu použít změny.

    Tomu se říká scénář wins ve Storu. Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem. Scénář Wins pro Store se používá v tomto kurzu. Tato metoda zajišťuje, že se bez upozornění uživatele nepřepíší žádné změny.

Detekce konfliktů v EF Core

Vlastnosti nakonfigurované jako tokeny souběžnosti se používají k implementaci optimistického řízení souběžnosti. Pokud je operace aktualizace nebo odstranění aktivována SaveChanges nebo SaveChangesAsync, hodnota tokenu souběžnosti v databázi se porovná s původní hodnotou přečtenou EF Core:

  • Pokud se hodnoty shodují, operace se může dokončit.
  • Pokud se hodnoty neshodují, předpokládá, EF Core že jiný uživatel provedl konfliktní operaci, přeruší aktuální transakci a vyvolá DbUpdateConcurrencyException.

Jiný uživatel nebo proces provádějící operaci, která je v konfliktu s aktuální operací, se označuje jako konflikt souběžnosti.

V relačních databázích EF Core kontroluje hodnotu tokenu souběžnosti v WHERE klauzuli UPDATE a DELETE příkazech za účelem zjištění konfliktu souběžnosti.

Datový model musí být nakonfigurovaný tak, aby umožňoval detekci konfliktů zahrnutím sloupce sledování, který lze použít k určení, kdy byl řádek změněn. EF poskytuje dva přístupy pro tokeny souběžnosti:

Podrobnosti o implementaci SQL Serveru a SQLite se mírně liší. Soubor rozdílů se zobrazí později v kurzu se seznamem rozdílů. Na kartě Visual Studio se zobrazuje přístup k SQL Serveru. Na kartě Visual Studio Code se zobrazuje přístup k databázím, které nejsou sql Serverem, jako je například SQLite.

  • Do modelu zahrňte sledovací sloupec, který slouží k určení, kdy byl řádek změněn.
  • TimestampAttribute Použijte vlastnost souběžnosti.

Models/Department.cs Aktualizujte soubor následujícím zvýrazněným kódem:

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

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

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

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

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

        public int? InstructorID { get; set; }

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

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

Jedná se o TimestampAttribute to, co identifikuje sloupec jako sloupec sledování souběžnosti. Fluent API je alternativní způsob, jak určit vlastnost sledování:

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

Atribut [Timestamp] vlastnosti entity vygeneruje následující kód v ModelBuilder metodě:

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

Předchozí kód:

  • Nastaví typ ConcurrencyToken vlastnosti na pole bajtů. byte[] je požadovaný typ pro SQL Server.
  • Zavolá metodu IsConcurrencyToken. IsConcurrencyToken nakonfiguruje vlastnost jako token souběžnosti. Při aktualizacích se hodnota tokenu souběžnosti v databázi porovnává s původní hodnotou, aby se od načtení instance z databáze nezměnila. Pokud došlo ke změně, DbUpdateConcurrencyException vyvolá se a změny se nepoužijí.
  • Volání ValueGeneratedOnAddOrUpdate, která konfiguruje vlastnost tak, aby měla hodnotu automaticky vygenerovanou ConcurrencyToken při přidání nebo aktualizaci entity.
  • HasColumnType("rowversion") nastaví typ sloupce v databázi SQL Serveru na rowversion.

Následující kód ukazuje část T-SQL vygenerovanou při EF CoreDepartment aktualizaci názvu:

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

Předchozí zvýrazněný kód zobrazuje klauzuli WHERE obsahující ConcurrencyToken. Pokud se databáze ConcurrencyToken nerovná parametru@p2, nebudou ConcurrencyToken aktualizovány žádné řádky.

Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:

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

@@ROWCOUNT vrátí počet řádků ovlivněných posledním příkazem. Pokud nejsou aktualizovány žádné řádkyDbUpdateConcurrencyException, EF Core vyvolá chybu .

Přidání migrace

ConcurrencyToken Přidání vlastnosti změní datový model, který vyžaduje migraci.

Sestavte projekt.

V PMC spusťte následující příkazy:

Add-Migration RowVersion
Update-Database

Předchozí příkazy:

  • Migrations/{time stamp}_RowVersion.cs Vytvoří soubor migrace.
  • Migrations/SchoolContextModelSnapshot.cs Aktualizace soubor. Aktualizace přidá do BuildModel metody následující kód:
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");

Stránky oddělení uživatelského rozhraní

Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:

  • Vytvořte složku Pages/Departments .
  • Používá Department se pro třídu modelu.
  • Místo vytvoření nové třídy použijte existující třídu kontextu.

Přidání třídy utility

Ve složce projektu vytvořte Utility třídu s následujícím kódem:

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

Třída Utility poskytuje metodu GetLastChars použitou k zobrazení posledních několika znaků tokenu souběžnosti. Následující kód ukazuje kód, který funguje s oběma službami SQLite ad SQL Server:

#if SQLiteVersion
using System;

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

Direktiva #if SQLiteVersion preprocesoru izoluje rozdíly ve verzích SQLite a SQL Server a pomáhá:

  • Autor udržuje pro obě verze jeden základ kódu.
  • Vývojáři SQLite nasazují aplikaci do Azure a používají SQL Azure.

Sestavte projekt.

Aktualizace indexové stránky

Nástroj pro generování uživatelského rozhraní vytvořil ConcurrencyToken sloupec pro indexovou stránku, ale toto pole se v produkční aplikaci nezobrazí. V tomto kurzu se zobrazí poslední část tohoto ConcurrencyToken kurzu, která vám pomůže ukázat, jak funguje zpracování souběžnosti. Poslední část není zaručená, že bude jedinečná sama o sobě.

Aktualizovat stránky\Departments\Index.cshtml :

  • Nahraďte index odděleními.
  • Změňte kód obsahující ConcurrencyToken jenom několik posledních znaků.
  • Nahraďte FirstMidNameFullName.

Aktualizovaná stránka se zobrazí v následujícím kódu:

@page
@model ContosoUniversity.Pages.Departments.IndexModel

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

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                Token
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Department)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    @Utility.GetLastChars(item.ConcurrencyToken)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Aktualizace modelu upravit stránku

Aktualizujte Pages/Departments/Edit.cshtml.cs následujícím kódem:

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            // Fetch current department from DB.
            // ConcurrencyToken may have changed.
            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            // Set ConcurrencyToken to value read in OnGetAsync
            _context.Entry(departmentToUpdate).Property(
                 d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current ConcurrencyToken so next postback
                    // matches unless an new concurrency issue happens.
                    Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
                    // Clear the model error for the next postback.
                    ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error
            // and overides the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

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

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

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

Aktualizace souběžnosti

OriginalValue se aktualizuje o ConcurrencyToken hodnotu z entity, když byla načtena v OnGetAsync metodě. EF Core vygeneruje SQL UPDATE příkaz s WHERE klauzulí obsahující původní ConcurrencyToken hodnotu. Pokud příkaz UPDATE neobsahuje žádné řádky, DbUpdateConcurrencyException vyvolá se výjimka. Příkaz nemá vliv na UPDATE žádné řádky, pokud žádné řádky nemají původní ConcurrencyToken hodnotu.

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // Fetch current department from DB.
    // ConcurrencyToken may have changed.
    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Set ConcurrencyToken to value read in OnGetAsync
    _context.Entry(departmentToUpdate).Property(
         d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

V předchozím zvýrazněném kódu:

  • Hodnota je Department.ConcurrencyToken hodnota, když byla entita načtena v Get požadavku na Edit stránku. Tato hodnota je pro metodu OnPost poskytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje do Department.ConcurrencyToken pořadače modelu.
  • OriginalValue je to, co EF Core se používá v klauzuli WHERE . Před spuštěním zvýrazněného řádku kódu:
    • OriginalValue má hodnotu, která byla v databázi, když FirstOrDefaultAsync byla volána v této metodě.
    • Tato hodnota se může lišit od toho, co se zobrazilo na stránce Upravit.
  • Zvýrazněný kód zajišťuje, že EF Core používá původní ConcurrencyToken hodnotu ze zobrazené Department entity v klauzuli příkazu WHERE SQLUPDATE.

Následující kód ukazuje Department model. Department inicializuje se v:

  • OnGetAsync metodou podle dotazu EF.
  • OnPostAsync metoda podle skrytého pole na Razor stránce pomocí vazby modelu:
public class EditModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public EditModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Department Department { get; set; }
    // Replace ViewData["InstructorID"] 
    public SelectList InstructorNameSL { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Department = await _context.Departments
            .Include(d => d.Administrator)  // eager loading
            .AsNoTracking()                 // tracking not required
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (Department == null)
        {
            return NotFound();
        }

        // Use strongly typed data rather than ViewData.
        InstructorNameSL = new SelectList(_context.Instructors,
            "ID", "FirstMidName");

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch current department from DB.
        // ConcurrencyToken may have changed.
        var departmentToUpdate = await _context.Departments
            .Include(i => i.Administrator)
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (departmentToUpdate == null)
        {
            return HandleDeletedDepartment();
        }

        // Set ConcurrencyToken to value read in OnGetAsync
        _context.Entry(departmentToUpdate).Property(
             d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

Předchozí kód ukazuje ConcurrencyToken hodnotu entity z HTTP POST požadavku je nastavena na ConcurrencyToken hodnotu z HTTP GET požadavku.Department

Když dojde k chybě souběžnosti, získá následující zvýrazněný kód hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await SetDbErrorMessage(dbValues, clientValues, _context);

        // Save the current ConcurrencyToken so next postback
        // matches unless an new concurrency issue happens.
        Department.ConcurrencyToken = dbValues.ConcurrencyToken;
        // Clear the model error for the next postback.
        ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
    }

Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno:OnPostAsync

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

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

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

Následující zvýrazněný kód nastaví ConcurrencyToken hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await SetDbErrorMessage(dbValues, clientValues, _context);

        // Save the current ConcurrencyToken so next postback
        // matches unless an new concurrency issue happens.
        Department.ConcurrencyToken = dbValues.ConcurrencyToken;
        // Clear the model error for the next postback.
        ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
    }

Příkaz ModelState.Remove je povinný, protože ModelState má předchozí ConcurrencyToken hodnotu. Razor Na stránce ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.

Rozdíly v kódu SQL Serveru vs. SQLite

Následující informace ukazují rozdíly mezi verzemi SQL Serveru a SQLite:

+ using System;    // For GUID on SQLite

+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

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

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

Aktualizace stránky Upravit Razor

Aktualizujte Pages/Departments/Edit.cshtml následujícím kódem:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.ConcurrencyToken" />
            <div class="form-group">
                <label>Version</label>
                @Utility.GetLastChars(Model.Department.ConcurrencyToken)
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Předchozí kód:

  • page Aktualizace směrnice od @page@page "{id:int}"
  • Přidá skrytou verzi řádku. ConcurrencyToken musí být přidán, takže postback sváže hodnotu.
  • Zobrazí poslední bajt ConcurrencyToken pro účely ladění.
  • ViewData Nahrazuje silným typem InstructorNameSL.

Konflikty souběžnosti testů se stránkou Upravit

Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:

  • Spusťte aplikaci a vyberte Oddělení.
  • Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
  • Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.

Na dvou kartách prohlížeče se zobrazují stejné informace.

Změňte název 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 a aktualizovaným ConcurrencyTokenindikátorem. Všimněte si aktualizovaného ConcurrencyTokenindikátoru, který se zobrazí na druhé po zpětném odeslání na druhé kartě.

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

Department Edit page 2 after change

Klikněte na možnost Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:

Department Edit page error message

Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.

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

Aktualizace modelu odstranění stránky

Aktualizujte Pages/Departments/Delete.cshtml.cs následujícím kódem:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.ConcurrencyToken value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila. Department.ConcurrencyToken je verze řádku, když byla entita načtena. Při EF Core vytváření SQL DELETE příkazu obsahuje klauzuli WHERE s ConcurrencyToken. Pokud má SQL DELETE příkaz za následek nulový počet ovlivněných řádků:

  • Příkaz ConcurrencyToken se SQL DELETE v databázi neshoduje ConcurrencyToken .
  • Vyvolá DbUpdateConcurrencyException se výjimka.
  • OnGetAsync je volána s concurrencyError.

Aktualizace stránky Odstranit Razor

Aktualizujte Pages/Departments/Delete.cshtml následujícím kódem:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
        </dt>
        <dd class="col-sm-10">
            @Utility.GetLastChars(Model.Department.ConcurrencyToken)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.ConcurrencyToken" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Předchozí kód provede následující změny:

  • page Aktualizace směrnice od @page@page "{id:int}"
  • Přidá chybovou zprávu.
  • Nahradí FirstMidName úplným názvem v poli Správa istrator.
  • Změny ConcurrencyToken pro zobrazení posledního bajtu
  • Přidá skrytou verzi řádku. ConcurrencyToken musí být přidán, takže postback sváže hodnotu.

Konflikty souběžnosti testů

Vytvoření testovacího oddělení

Otevřete dvě instance odstranění v testovacím oddělení:

  • Spusťte aplikaci a vyberte Oddělení.
  • Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
  • Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.

Na dvou kartách prohlížeče se zobrazují stejné informace.

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

V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným ConcurrencyTokenindikátorem. Všimněte si aktualizovaného ConcurrencyTokenindikátoru, který se zobrazí na druhé po zpětném odeslání na druhé kartě.

Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud ConcurrencyToken nebyla aktualizována.

Další prostředky

Další kroky

Toto je poslední kurz série. Další témata jsou popsána ve verzi MVC této série kurzů.

V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně (ve stejnou dobu).

Konflikty souběžnosti

Ke konfliktu souběžnosti dochází v případě, že:

  • Uživatel přejde na stránku pro úpravy entity.
  • Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.

Pokud detekce souběžnosti není povolená, přepíše změny ostatních uživatelů každý, kdo aktualizuje databázi. Pokud je toto riziko přijatelné, náklady na programování pro souběžnost můžou převažovat nad výhodou.

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

Jedním zezpůsobůch Tomu se říká pesimistické souběžnost. Než aplikace přečte řádek databáze, který má v úmyslu aktualizovat, požádá o zámek. Jakmile je řádek uzamčen pro přístup k aktualizacím, nebudou moct ostatní uživatelé řádek uzamknout, dokud nebude vydán první zámek.

Správa zámků má nevýhody. Program může být složitý a může způsobovat problémy s výkonem při nárůstu počtu uživatelů. Entity Framework Core neposkytuje žádnou integrovanou podporu a tento kurz neukazuje, jak ho implementovat.

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

Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet 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 uvidí, že se její změna projeví, protože v prohlížeči se jako částka rozpočtu zobrazí stránka Index s nulou.

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, určuje způsob zpracování konfliktů souběžnosti:

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

    V tomto scénáři nedojde ke ztrátě žádných dat. 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. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup má určité nevýhody:

    • Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
    • Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
    • Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
  • 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 načte hodnotu 350 000,00 USD. Tento přístup se označuje 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.) Pokud neprovádíte žádné kódování pro zpracování souběžnosti, dojde k automatickému zpracování služby Client Wins.

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

    • Zobrazí chybovou zprávu.
    • Zobrazí aktuální stav dat.
    • Umožňuje uživateli znovu použít změny.

    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, že se bez upozornění uživatele nepřepíší žádné změny.

Detekce konfliktů v EF Core

EF CoreDbConcurrencyException vyvolá výjimky, když zjistí konflikty. Datový model musí být nakonfigurovaný tak, aby umožňoval detekci konfliktů. Mezi možnosti povolení detekce konfliktů patří:

  • Nakonfigurujte EF Core tak, aby zahrnovaly původní hodnoty sloupců nakonfigurovaných jako tokeny souběžnosti v klauzuli Where příkazů Update a Delete.

    Při SaveChanges zavolání klauzule Where vyhledá původní hodnoty všech vlastností anotovaných atributem ConcurrencyCheckAttribute . Příkaz update nenajde řádek, který se má aktualizovat, pokud se od prvního čtení řádku změnily některé vlastnosti tokenu souběžnosti. EF Core interpretuje to 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 velké množství stavu. Proto se tento přístup obecně nedoporučuje a není to metoda použitá v tomto kurzu.

  • V tabulce databáze zahrňte sledovací sloupec, který lze použít k určení, kdy byl řádek změněn.

    V databázi SQL Serveru je datový typ sloupce rowversionsledování . 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í (číslo původní verze řádku). Pokud byl řádek aktualizovaný jiným uživatelem změněn, hodnota ve rowversion sloupci se liší od původní hodnoty. V takovém případě příkaz Update nebo Delete nemůže najít řádek, který se má aktualizovat kvůli klauzuli Where. EF Core vyvolá výjimku souběžnosti, pokud příkaz Update nebo Delete neovlivní žádné řádky.

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 TimestampAttribute je to, co identifikuje sloupec jako sloupec sledování souběžnosti. Fluent API je alternativní způsob, jak určit vlastnost sledování:

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

U databáze [Timestamp] SQL Serveru atribut vlastnosti entity definované jako pole bajtů:

  • Způsobí zahrnutí sloupce do klauzulí DELETE a UPDATE WHERE.
  • Nastaví typ sloupce v databázi na rowversion.

Databáze vygeneruje sekvenční číslo verze řádku, které se zvýší při každé aktualizaci řádku. Update V klauzuli nebo Delete příkaz Where obsahuje načtenou hodnotu verze řádku. Pokud se od načtení řádku změnil řádek:

  • Hodnota aktuální verze řádku neodpovídá načtené hodnotě.
  • Delete Příkazy Update nenaleznou řádek, protože Where klauzule hledá hodnotu verze načteného řádku.
  • A DbUpdateConcurrencyException je vyhozen.

Následující kód ukazuje část T-SQL vygenerovanou při EF Core aktualizaci názvu oddělení:

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

Předchozí zvýrazněný kód zobrazuje klauzuli WHERE obsahující RowVersion. Pokud se databáze RowVersion nerovná parametru (@p2), nebudou RowVersion aktualizovány žádné řádky.

Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:

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

@@ROWCOUNT vrátí počet řádků ovlivněných posledním příkazem. Pokud nejsou aktualizovány žádné řádkyDbUpdateConcurrencyException, EF Core vyvolá chybu .

Aktualizace databáze

RowVersion Přidání vlastnosti změní datový model, který vyžaduje migraci.

Sestavte projekt.

  • V PMC spusťte následující příkaz:

    Add-Migration RowVersion
    

Tento příkaz:

  • Migrations/{time stamp}_RowVersion.cs Vytvoří soubor migrace.

  • Migrations/SchoolContextModelSnapshot.cs Aktualizace soubor. Aktualizace přidá do BuildModel metody následující zvýrazněný kód:

    modelBuilder.Entity("ContosoUniversity.Models.Department", b =>
        {
            b.Property<int>("DepartmentID")
                .ValueGeneratedOnAdd()
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
    
            b.Property<decimal>("Budget")
                .HasColumnType("money");
    
            b.Property<int?>("InstructorID");
    
            b.Property<string>("Name")
                .HasMaxLength(50);
    
            b.Property<byte[]>("RowVersion")
                .IsConcurrencyToken()
                .ValueGeneratedOnAddOrUpdate();
    
            b.Property<DateTime>("StartDate");
    
            b.HasKey("DepartmentID");
    
            b.HasIndex("InstructorID");
    
            b.ToTable("Department");
        });
    
  • V PMC spusťte následující příkaz:

    Update-Database
    

Stránky oddělení uživatelského rozhraní

  • Postupujte podle pokynů na stránkách studentů uživatelského rozhraní s následujícími výjimkami:

  • Vytvořte složku Pages/Departments .

  • Používá Department se pro třídu modelu.

    • Místo vytvoření nové třídy použijte existující třídu kontextu.

Sestavte projekt.

Aktualizace indexové stránky

Nástroj pro generování uživatelského rozhraní vytvořil RowVersion sloupec pro indexovou stránku, ale toto pole se v produkční aplikaci nezobrazí. V tomto kurzu se zobrazí poslední bajt RowVersion bajtů, který vám pomůže ukázat, jak funguje zpracování souběžnosti. Poslední bajt není zaručený, že bude jedinečný sám.

Aktualizovat stránky\Departments\Index.cshtml :

  • Nahraďte index odděleními.
  • Změňte kód obsahující RowVersion tak, aby zobrazoval pouze poslední bajt pole bajtů.
  • Nahraďte FirstMidName úplným názvem.

Aktualizovaná stránka se zobrazí v následujícím kódu:

@page
@model ContosoUniversity.Pages.Departments.IndexModel

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

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Department)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    @item.RowVersion[7]
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Aktualizace modelu upravit stránku

Aktualizujte Pages/Departments/Edit.cshtml.cs následujícím kódem:

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

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

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

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

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await setDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            var deletedDepartment = new Department();
            // ModelState contains the posted data because of the deletion error
            // and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

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

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

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

Aktualizuje se OriginalValue o rowVersion hodnotu z entity, když byla načtena v OnGetAsync metodě. EF Core vygeneruje příkaz SQL UPDATE s klauzulí WHERE obsahující původní RowVersion hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion hodnotu), DbUpdateConcurrencyException vyvolá se výjimka.

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

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

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

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

V předchozím zvýrazněném kódu:

  • Hodnota je Department.RowVersion to, co byla v entitě, když byla původně načtena v požadavku Získat pro stránku Upravit. Tato hodnota je pro metodu OnPost poskytována skrytým polem na Razor stránce, která zobrazuje entitu, která se má upravit. Skrytá hodnota pole se zkopíruje do Department.RowVersion pořadače modelu.
  • OriginalValue je to, co EF Core se použije v klauzuli Where. Před spuštěním zvýrazněného řádku kódu má hodnotu, OriginalValue která byla v databázi, když FirstOrDefaultAsync byla volána v této metodě, což se může lišit od toho, co se zobrazilo na stránce Upravit.
  • Zvýrazněný kód zajišťuje, že EF Core používá původní RowVersion hodnotu ze zobrazené Department entity v klauzuli Where příkazu SQL UPDATE.

Když dojde k chybě souběžnosti, získá následující zvýrazněný kód hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await setDbErrorMessage(dbValues, clientValues, _context);

        // Save the current RowVersion so next postback
        // matches unless an new concurrency issue happens.
        Department.RowVersion = (byte[])dbValues.RowVersion;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }

Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno:OnPostAsync

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

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

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

Následující zvýrazněný kód nastaví RowVersion hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.

if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await setDbErrorMessage(dbValues, clientValues, _context);

        // Save the current RowVersion so next postback
        // matches unless an new concurrency issue happens.
        Department.RowVersion = (byte[])dbValues.RowVersion;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }

Příkaz ModelState.Remove je povinný, protože ModelState má starou RowVersion hodnotu. Razor Na stránce ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.

Aktualizace stránky Upravit

Aktualizujte Pages/Departments/Edit.cshtml následujícím kódem:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Předchozí kód:

  • page Aktualizace směrnice od @page@page "{id:int}"
  • Přidá skrytou verzi řádku. RowVersion musí být přidán, takže postback sváže hodnotu.
  • Zobrazí poslední bajt RowVersion pro účely ladění.
  • ViewData Nahrazuje silným typem InstructorNameSL.

Konflikty souběžnosti testů se stránkou Upravit

Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:

  • Spusťte aplikaci a vyberte Oddělení.
  • Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
  • Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.

Na dvou kartách prohlížeče se zobrazují stejné informace.

Změňte název 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 a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.

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

Department Edit page 2 after change

Klikněte na možnost Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:

Department Edit page error message

Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.

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

Aktualizace modelu odstranění stránky

Aktualizujte Pages/Departments/Delete.cshtml.cs následujícím kódem:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila. Department.RowVersion je verze řádku, když byla entita načtena. Při EF Core vytváření příkazu SQL DELETE obsahuje klauzuli WHERE s RowVersion. Pokud výsledkem příkazu SQL DELETE je nula řádků:

  • Příkaz RowVersion SQL DELETE se v databázi neshoduje RowVersion .
  • Vyvolá se výjimka DbUpdateConcurrencyException.
  • OnGetAsync je volána s concurrencyError.

Aktualizace stránky Odstranit

Aktualizujte Pages/Departments/Delete.cshtml následujícím kódem:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

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

<h2>Delete</h2>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.RowVersion)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.RowVersion[7])
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-danger" /> |
            <a asp-page="./Index">Back to List</a>
        </div>
</form>
</div>

Předchozí kód provede následující změny:

  • page Aktualizace směrnice od @page@page "{id:int}"
  • Přidá chybovou zprávu.
  • Nahradí FirstMidName úplným názvem v poli Správa istrator.
  • Změny RowVersion pro zobrazení posledního bajtu
  • Přidá skrytou verzi řádku. RowVersion musí být přidán, takže postback sváže hodnotu.

Konflikty souběžnosti testů

Vytvoření testovacího oddělení

Otevřete dvě instance odstranění v testovacím oddělení:

  • Spusťte aplikaci a vyberte Oddělení.
  • Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
  • Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.

Na dvou kartách prohlížeče se zobrazují stejné informace.

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

V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.

Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud RowVersion nebyla aktualizována.

Další prostředky

Další kroky

Toto je poslední kurz série. Další témata jsou popsána ve verzi MVC této série kurzů.

V tomto kurzu se dozvíte, jak řešit konflikty, když více uživatelů aktualizuje entitu souběžně (ve stejnou dobu). Pokud narazíte na problémy, které nemůžete vyřešit, stáhněte nebo zobrazte dokončenou aplikaci.Pokyny ke stažení

Konflikty souběžnosti

Ke konfliktu souběžnosti dochází v případě, že:

  • Uživatel přejde na stránku pro úpravy entity.
  • Jiný uživatel aktualizuje stejnou entitu před zápisem změny prvního uživatele do databáze.

Pokud detekce souběžnosti není povolená, když dojde k souběžné aktualizaci:

  • Poslední aktualizace vyhrává. To znamená, že poslední hodnoty aktualizace se uloží do databáze.
  • První zaktuálních

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

Optimistická souběžnost umožňuje, aby došlo ke konfliktům souběžnosti, a pak odpovídajícím způsobem reaguje, když ano. Jane například navštíví stránku pro úpravy oddělení a změní rozpočet 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ž prohlížeč zobrazí stránku Rejstřík.

Budget changed to zero

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, určuje způsob zpracování konfliktů souběžnosti.

Optimistická souběžnost zahrnuje následující možnosti:

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

    V tomto scénáři nedojde ke ztrátě žádných dat. 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. Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat. Tento přístup:

    • Nelze se vyhnout ztrátě dat, pokud jsou u stejné vlastnosti provedeny konkurenční změny.
    • Obecně není praktické ve webové aplikaci. Vyžaduje udržování významného stavu, aby bylo možné sledovat všechny načtené hodnoty a nové hodnoty. Udržování velkého objemu stavu může ovlivnit výkon aplikace.
    • Může zvýšit složitost aplikace v porovnání s detekcí souběžnosti u entity.
  • 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 načte hodnotu 350 000,00 USD. Tento přístup se označuje 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.) Pokud neprovádíte žádné kódování pro zpracování souběžnosti, dojde k automatickému zpracování služby Client Wins.

  • Můžete zabránit tomu, aby se v databázi aktualizovala změna Johna. Aplikace obvykle:

    • Zobrazí chybovou zprávu.
    • Zobrazí aktuální stav dat.
    • Umožňuje uživateli znovu použít změny.

    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, že se bez upozornění uživatele nepřepíší žádné změny.

Zpracování souběžnosti

Když je vlastnost nakonfigurovaná jako token souběžnosti:

Databáze a datový model musí být nakonfigurovány tak, aby podporovaly vyvolání DbUpdateConcurrencyException.

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

Konflikty souběžnosti lze zjistit na úrovni vlastnosti pomocí atributu ConcurrencyCheck . Atribut lze použít na více vlastností modelu. Další informace naleznete v tématu Datové poznámky- SouběžnostCheck.

Tento [ConcurrencyCheck] atribut se v tomto kurzu nepoužívá.

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

Pokud chcete zjistit konflikty souběžnosti, přidá se do modelu sloupec sledování rowversion . rowversion :

  • Je specifický pro SQL Server. Jiné databáze nemusí poskytovat podobnou funkci.
  • Slouží k určení, že se entita od načtení z databáze nezměnila.

Databáze vygeneruje sekvenční rowversion číslo, které se při každé aktualizaci řádku zvýší. Update V příkazu nebo DeleteWhere příkaz obsahuje klauzule načtenou hodnotu rowversion. Pokud se řádek, který se aktualizuje, změnil:

  • rowversion neodpovídá načtené hodnotě.
  • Delete Příkazy Update nenaleznou řádek, protože Where klauzule obsahuje načtený rowversion.
  • A DbUpdateConcurrencyException je vyhozen.

Pokud EF Corepříkaz nebo Delete příkaz neaktualizoval Update žádné řádky, vyvolá se výjimka souběžnosti.

Přidání vlastnosti sledování do entity Oddělení

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 časového razítka určuje, že tento sloupec je součástí Where klauzule Update a Delete příkazů. Atribut je volán Timestamp , protože předchozí verze SQL Serveru používaly datový typ SQL timestamp před tím, než ho typ SQL rowversion nahradil.

Fluent API může také zadat vlastnost sledování:

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

Následující kód ukazuje část T-SQL vygenerovanou při EF Core aktualizaci názvu oddělení:

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

Předchozí zvýrazněný kód zobrazuje klauzuli WHERE obsahující RowVersion. Pokud se databáze RowVersion nerovná parametru (@p2), nebudou RowVersion aktualizovány žádné řádky.

Následující zvýrazněný kód ukazuje T-SQL, který ověřuje, že byl aktualizován přesně jeden řádek:

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

@@ROWCOUNT vrátí počet řádků ovlivněných posledním příkazem. V žádném řádku nejsou aktualizoványDbUpdateConcurrencyException, EF Core vyvolá chybu .

T-SQL EF Core se zobrazí v okně výstupu sady Visual Studio.

Aktualizace databáze

Přidáním vlastnosti se RowVersion změní model databáze, který vyžaduje migraci.

Sestavte projekt. Do příkazového okna zadejte následující:

dotnet ef migrations add RowVersion
dotnet ef database update

Předchozí příkazy:

  • Migrations/{time stamp}_RowVersion.cs Přidá soubor migrace.

  • Migrations/SchoolContextModelSnapshot.cs Aktualizace soubor. Aktualizace přidá do BuildModel metody následující zvýrazněný kód:

  • Spustí migrace pro aktualizaci databáze.

Generování modelu oddělení

Postupujte podle pokynů vygenerování modelu studenta a použijte Department ho pro třídu modelu.

Předchozí příkaz vygeneruje Department model. Otevřete projekt v sadě Visual Studio.

Sestavte projekt.

Aktualizace indexové stránky Oddělení

Modul generování uživatelského rozhraní vytvořil RowVersion sloupec pro indexovou stránku, ale toto pole by se nemělo zobrazovat. V tomto kurzu se zobrazí poslední bajt, RowVersion který vám pomůže porozumět souběžnosti. Poslední bajt není zaručený jako jedinečný. Skutečná aplikace by se nezobrazila RowVersion ani poslední bajt RowVersion.

Aktualizujte indexovou stránku:

  • Nahraďte index odděleními.
  • Nahraďte značky obsahující RowVersion poslední bajt .RowVersion
  • Nahraďte FirstMidName úplným názvem.

Aktualizovaná stránka zobrazuje následující kód:

@page
@model ContosoUniversity.Pages.Departments.IndexModel

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

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Department) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Budget)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.StartDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Administrator.FullName)
            </td>
            <td>
                @item.RowVersion[7]
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Aktualizace modelu upravit stránku

Aktualizujte Pages/Departments/Edit.cshtml.cs následujícím kódem:

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

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

            // null means Department was deleted by another user.
            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            // Update the RowVersion to the value when this entity was
            // fetched. If the entity has been updated after it was
            // fetched, RowVersion won't match the DB RowVersion and
            // a DbUpdateConcurrencyException is thrown.
            // A second postback will make them match, unless a new 
            // concurrency issue happens.
            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Must clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

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

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

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

Pokud chcete zjistit problém se souběžností, aktualizuje rowVersion se OriginalValue hodnota z entity, kterou byla načtena. EF Core vygeneruje příkaz SQL UPDATE s klauzulí WHERE obsahující původní RowVersion hodnotu. Pokud příkaz UPDATE neovlivní žádné řádky (žádné řádky nemají původní RowVersion hodnotu), DbUpdateConcurrencyException vyvolá se výjimka.

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

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

    // null means Department was deleted by another user.
    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Update the RowVersion to the value when this entity was
    // fetched. If the entity has been updated after it was
    // fetched, RowVersion won't match the DB RowVersion and
    // a DbUpdateConcurrencyException is thrown.
    // A second postback will make them match, unless a new 
    // concurrency issue happens.
    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;

V předchozím kódu je hodnota při Department.RowVersion načtení entity. OriginalValue je hodnota v databázi, když FirstOrDefaultAsync byla volána v této metodě.

Následující kód získá hodnoty klienta (hodnoty publikované do této metody) a hodnoty databáze:

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await SetDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

Následující kód přidá vlastní chybovou zprávu pro každý sloupec, který má hodnoty databáze odlišné od toho, co bylo publikováno:OnPostAsync

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

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

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

Následující zvýrazněný kód nastaví RowVersion hodnotu na novou hodnotu načtenou z databáze. Když uživatel příště klikne na Uložit, budou zachyceny pouze chyby souběžnosti, ke kterým dochází od posledního zobrazení stránky Upravit.

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await SetDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

Příkaz ModelState.Remove je povinný, protože ModelState má starou RowVersion hodnotu. Razor Na stránce ModelState má hodnota pole přednost před hodnotami vlastností modelu, pokud jsou k dispozici obě hodnoty.

Aktualizace stránky Upravit

Aktualizujte Pages/Departments/Edit.cshtml následujícím kódem:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Předchozí kód:

  • page Aktualizace směrnice od @page@page "{id:int}"
  • Přidá skrytou verzi řádku. RowVersion musí být přidán, takže post back binds hodnotu.
  • Zobrazí poslední bajt RowVersion pro účely ladění.
  • ViewData Nahrazuje silným typem InstructorNameSL.

Konflikty souběžnosti testů se stránkou Upravit

Otevřete dvě instance prohlížeče pro úpravy v anglickém oddělení:

  • Spusťte aplikaci a vyberte Oddělení.
  • Klikněte pravým tlačítkem myši na hypertextový odkaz Pro anglické oddělení a vyberte Otevřít v nové kartě.
  • Na první kartě klikněte na hypertextový odkaz Upravit pro anglické oddělení.

Na dvou kartách prohlížeče se zobrazují stejné informace.

Změňte název 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 a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.

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

Department Edit page 2 after change

Klikněte na možnost Uložit. Zobrazí se chybové zprávy pro všechna pole, která neodpovídají hodnotám databáze:

Department Edit page error message 1

Toto okno prohlížeče nemělo v úmyslu změnit pole Název. Zkopírujte a vložte aktuální hodnotu (Jazyky) do pole Název. Vysouvte tabulátor. Ověření na straně klienta odebere chybovou zprávu.

Department Edit page error message 2

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

Aktualizace stránky Odstranit

Aktualizujte model stránky Delete následujícím kódem:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

Stránka Delete detekuje konflikty souběžnosti, když se entita po načtení změnila. Department.RowVersion je verze řádku, když byla entita načtena. Při EF Core vytváření příkazu SQL DELETE obsahuje klauzuli WHERE s RowVersion. Pokud výsledkem příkazu SQL DELETE je nula řádků:

  • Příkaz RowVersion SQL DELETE se v databázi neshoduje RowVersion .
  • Vyvolá se výjimka DbUpdateConcurrencyException.
  • OnGetAsync je volána s concurrencyError.

Aktualizace stránky Odstranit

Aktualizujte Pages/Departments/Delete.cshtml následujícím kódem:

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

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

<h2>Delete</h2>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.RowVersion)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.RowVersion[7])
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-page="./Index">Back to List</a>
        </div>
</form>
</div>

Předchozí kód provede následující změny:

  • page Aktualizace směrnice od @page@page "{id:int}"
  • Přidá chybovou zprávu.
  • Nahradí FirstMidName úplným názvem v poli Správa istrator.
  • Změny RowVersion pro zobrazení posledního bajtu
  • Přidá skrytou verzi řádku. RowVersion musí být přidán, takže post back binds hodnotu.

Test souběžnosti koliduje se stránkou Odstranit

Vytvoření testovacího oddělení

Otevřete dvě instance odstranění v testovacím oddělení:

  • Spusťte aplikaci a vyberte Oddělení.
  • Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro testovací oddělení a vyberte Otevřít na nové kartě.
  • Klikněte na odkaz Upravit hypertextový odkaz pro testovací oddělení.

Na dvou kartách prohlížeče se zobrazují stejné informace.

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

V prohlížeči se zobrazí stránka Index se změněnou hodnotou a aktualizovaným indikátorem rowVersion. Všimněte si aktualizovaného indikátoru rowVersion, který se zobrazí na druhé po zpětném odeslání na druhé kartě.

Odstraňte testovací oddělení z druhé karty. Zobrazí se chyba souběžnosti s aktuálními hodnotami z databáze. Kliknutím na Odstranit odstraníte entitu, pokud RowVersion nebyla aktualizována.

Přečtěte si téma Dědičnost , jak dědit datový model.

Další prostředky