Udostępnij za pośrednictwem


Część 8, Razor strony z EF Core w ASP.NET Core — współbieżność

Tom Dykstra i Jon P Smith

Aplikacja internetowa Contoso University pokazuje, jak tworzyć Razor aplikacje internetowe stron przy użyciu programu EF Core Visual Studio. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek.

Jeśli napotkasz problemy, których nie możesz rozwiązać, pobierz ukończoną aplikację i porównaj ten kod z utworzonymi elementami, wykonując czynności opisane w samouczku.

W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników jednocześnie aktualizuje jednostkę.

Konflikty współbieżności

Konflikt współbieżności występuje, gdy:

  • Użytkownik przechodzi do strony edycji jednostki.
  • Inny użytkownik aktualizuje tę samą jednostkę przed zapisaniem zmiany pierwszego użytkownika w bazie danych.

Jeśli wykrywanie współbieżności nie jest włączone, kto ostatnio aktualizuje bazę danych, zastępuje zmiany innego użytkownika. Jeśli to ryzyko jest akceptowalne, koszt programowania współbieżności może przeważyć nad korzyścią.

Pesymistyczna współbieżność

Jednym ze sposobów zapobiegania konfliktom współbieżności jest użycie blokad bazy danych. Jest to nazywane pesymistyczną współbieżnością. Zanim aplikacja odczytuje wiersz bazy danych, który zamierza zaktualizować, żąda blokady. Po zablokowaniu wiersza dostępu do aktualizacji żaden inny użytkownik nie może zablokować wiersza do momentu zwolnienia pierwszej blokady.

Zarządzanie blokadami ma wady. Program może być złożony i może powodować problemy z wydajnością w miarę wzrostu liczby użytkowników. Platforma Entity Framework Core nie zapewnia wbudowanej obsługi pesymistycznej współbieżności.

Optymistyczna współbieżność

Optymistyczna współbieżność umożliwia wystąpieniu konfliktów współbieżności, a następnie odpowiednio reaguje podczas ich wykonywania. Na przykład Jane odwiedza stronę edycji Departamentu i zmienia budżet dla działu angielskiego z 350 000,000 USD do 0,00 USD.

Zmiana budżetu na 0

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

Zmiana daty rozpoczęcia na 2013

Jane najpierw klika pozycję Zapisz i widzi, że jej zmiana zostanie w życie, ponieważ w przeglądarce zostanie wyświetlona strona Indeks z wartością zero jako kwota budżetu.

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

  • Śledź, która właściwość użytkownika zmodyfikowała i zaktualizuj tylko odpowiednie kolumny w bazie danych.

    W scenariuszu żadne dane nie zostaną utracone. Różne właściwości zostały zaktualizowane przez dwóch użytkowników. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane's, jak i Johna. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych. Takie podejście ma pewne wady:

    • Nie można uniknąć utraty danych, jeśli konkurencyjne zmiany są wprowadzane do tej samej właściwości.
    • Zazwyczaj nie jest praktyczne w aplikacji internetowej. Wymaga to utrzymania znaczącego stanu w celu śledzenia wszystkich pobranych wartości i nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji.
    • Może zwiększyć złożoność aplikacji w porównaniu z wykrywaniem współbieżności w jednostce.
  • Zmieńmy zmianę Jane'a.

    Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i pobraną wartość $350,000.000. Takie podejście jest nazywane scenariuszem Wins klienta lub Last in Wins . Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych. Kod szkieletowy nie obsługuje współbieżności. Usługa Wins klienta odbywa się automatycznie.

  • Uniemożliwianie aktualizowania zmiany Johna w bazie danych. Zazwyczaj aplikacja:

    • Wyświetl komunikat o błędzie.
    • Pokaż bieżący stan danych.
    • Zezwalaj użytkownikowi na ponowne zastosowania zmian.

    Jest to nazywane scenariuszem Store Wins . Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta. Scenariusz Store Wins jest używany w tym samouczku. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez zgłaszania alertów przez użytkownika.

Wykrywanie konfliktów w programie EF Core

Właściwości skonfigurowane jako tokeny współbieżności są używane do implementowania optymistycznej kontroli współbieżności. Gdy operacja aktualizacji lub usuwania jest wyzwalana przez SaveChanges element lub SaveChangesAsync, wartość tokenu współbieżności w bazie danych jest porównywana z oryginalną wartością odczytaną przez EF Core:

  • Jeśli wartości są zgodne, operacja może zakończyć się.
  • Jeśli wartości nie są zgodne, zakłada, EF Core że inny użytkownik wykonał operację powodującą konflikt, przerywa bieżącą transakcję i zgłasza błąd DbUpdateConcurrencyException.

Inny użytkownik lub proces wykonujący operację, która powoduje konflikt z bieżącą operacją, jest nazywana konfliktem współbieżności.

W relacyjnych bazach danych EF Core sprawdza wartość tokenu współbieżności w WHERE klauzuli UPDATE i DELETE instrukcji w celu wykrycia konfliktu współbieżności.

Aby umożliwić wykrywanie konfliktów, należy skonfigurować model danych, uwzględniając kolumnę śledzenia, która może służyć do określenia, kiedy wiersz został zmieniony. Platforma EF oferuje dwa podejścia do tokenów współbieżności:

Podejście do programu SQL Server i szczegóły implementacji SQLite są nieco inne. W dalszej części samouczka przedstawiono plik różnic. Na karcie Programu Visual Studio przedstawiono podejście programu SQL Server. Na karcie Visual Studio Code przedstawiono podejście do baz danych innych niż SQL Server, takich jak SQLite.

  • W modelu dołącz kolumnę śledzenia, która służy do określania, kiedy wiersz został zmieniony.
  • Zastosuj właściwość TimestampAttribute do właściwości współbieżności.

Models/Department.cs Zaktualizuj plik przy użyciu następującego wyróżnionego kodu:

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

Element TimestampAttribute określa kolumnę jako kolumnę śledzenia współbieżności. Płynny interfejs API to alternatywny sposób określania właściwości śledzenia:

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

Atrybut [Timestamp] we właściwości jednostki generuje następujący kod w metodzie ModelBuilder :

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

Powyższy kod ma następujące działanie:

  • Ustawia typ ConcurrencyToken właściwości na tablicę bajtów. byte[] jest wymaganym typem programu SQL Server.
  • Wywołuje IsConcurrencyToken. IsConcurrencyToken Konfiguruje właściwość jako token współbieżności. W przypadku aktualizacji wartość tokenu współbieżności w bazie danych jest porównywana z oryginalną wartością, aby upewnić się, że nie uległa zmianie od czasu pobrania wystąpienia z bazy danych. W przypadku zmiany zostanie zgłoszony element , a DbUpdateConcurrencyException zmiany nie zostaną zastosowane.
  • Wywołuje ValueGeneratedOnAddOrUpdatemetodę , która konfiguruje ConcurrencyToken właściwość tak, aby wartość została wygenerowana automatycznie podczas dodawania lub aktualizowania jednostki.
  • HasColumnType("rowversion") Ustawia typ kolumny w bazie danych programu SQL Server na rowversion.

Poniższy kod przedstawia część języka T-SQL wygenerowaną przez EF Core po zaktualizowaniu Department nazwy:

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

Powyższy wyróżniony kod przedstawia klauzulę zawierającą WHERE ConcurrencyToken. Jeśli baza danych ConcurrencyToken nie jest równa parametrowi ConcurrencyToken @p2, nie są aktualizowane żadne wiersze.

Poniższy wyróżniony kod przedstawia język T-SQL, który weryfikuje dokładnie jeden wiersz został zaktualizowany:

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 zwraca liczbę wierszy, których dotyczy ostatnia instrukcja. Jeśli wiersze nie są aktualizowane, EF Core zwraca wartość DbUpdateConcurrencyException.

Dodawanie migracji

ConcurrencyToken Dodanie właściwości zmienia model danych, który wymaga migracji.

Skompiluj projekt.

Uruchom następujące polecenia w usłudze PMC:

Add-Migration RowVersion
Update-Database

Poprzednie polecenia:

  • Migrations/{time stamp}_RowVersion.cs Tworzy plik migracji.
  • Migrations/SchoolContextModelSnapshot.cs Aktualizuje plik. Aktualizacja dodaje następujący kod do BuildModel metody :
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");

Strony działu szkieletu

Postępuj zgodnie z instrukcjami na stronach Szkielet studenta z następującymi wyjątkami:

  • Utwórz folder Strony/Działy.
  • Użyj dla Department klasy modelu.
  • Użyj istniejącej klasy kontekstu zamiast utworzyć nową.

Dodawanie klasy narzędzi

W folderze projektu utwórz klasę Utility przy użyciu następującego kodu:

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

Klasa Utility udostępnia metodę GetLastChars używaną do wyświetlania kilku ostatnich znaków tokenu współbieżności. Poniższy kod przedstawia kod, który działa z programem SQL Server w obu usługach SQLite:

#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

Dyrektywa #if SQLiteVersion preprocesora izoluje różnice w wersjach SQLite i SQL Server i pomaga:

  • Autor utrzymuje jedną bazę kodu dla obu wersji.
  • Deweloperzy SQLite wdrażają aplikację na platformie Azure i używają Usługi SQL Azure.

Skompiluj projekt.

Aktualizowanie strony Indeks

Narzędzie do tworzenia szkieletu utworzyło kolumnę ConcurrencyToken dla strony Indeks, ale to pole nie będzie wyświetlane w aplikacji produkcyjnej. W tym samouczku zostanie wyświetlona ostatnia część elementu ConcurrencyToken , aby pokazać, jak działa obsługa współbieżności. Ostatnia część nie ma gwarancji, że jest unikatowa sama w sobie.

Zaktualizuj stronę Pages\Departments\Index.cshtml :

  • Zastąp indeks działem.
  • Zmień kod zawierający ConcurrencyToken , aby pokazać tylko kilka ostatnich znaków.
  • Zamień FirstMidName na FullName.

Poniższy kod przedstawia zaktualizowaną stronę:

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

Aktualizowanie modelu strony Edycji

Zaktualizuj Pages/Departments/Edit.cshtml.cs za pomocą następującego kodu:

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

Aktualizacje współbieżności

OriginalValue element jest aktualizowany przy użyciu ConcurrencyToken wartości z jednostki, która została pobrana w metodzie OnGetAsync . EF Core Generuje SQL UPDATE polecenie z klauzulą WHERE zawierającą oryginalną ConcurrencyToken wartość. Jeśli polecenie nie ma wpływu na UPDATE żadne wiersze, zgłaszany DbUpdateConcurrencyException jest wyjątek. Żadne wiersze nie mają wpływu na UPDATE polecenie, gdy żadne wiersze nie mają oryginalnej ConcurrencyToken wartości.

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;

W poprzednim wyróżnionym kodzie:

  • Wartość w Department.ConcurrencyToken pliku to wartość, gdy jednostka została pobrana w żądaniu Get Edit dla strony. Wartość jest udostępniana metodzie OnPost przez ukryte pole na Razor stronie, na których wyświetlana jest jednostka do edycji. Wartość pola ukrytego jest kopiowana przez Department.ConcurrencyToken powiązanie modelu.
  • OriginalValue to, co EF Core używa w klauzuli WHERE . Przed wykonaniem wyróżnionego wiersza kodu:
    • OriginalValue ma wartość, która znajdowała się w bazie danych, gdy FirstOrDefaultAsync została wywołana w tej metodzie.
    • Ta wartość może się różnić od tego, co zostało wyświetlone na stronie Edycja.
  • Wyróżniony kod zapewnia, że EF Core używa oryginalnej ConcurrencyToken wartości z wyświetlanej Department jednostki w klauzuli instrukcji WHERE SQLUPDATE.

Poniższy kod przedstawia Department model. Department element jest inicjowany w:

  • OnGetAsync metoda przez zapytanie EF.
  • OnPostAsync metoda według ukrytego pola na Razor stronie przy użyciu powiązania 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;

Powyższy kod pokazuje ConcurrencyToken wartość jednostki z HTTP POST żądania jest ustawiona ConcurrencyToken na wartość z HTTP GET Department żądania.

W przypadku wystąpienia błędu współbieżności wyróżniony kod pobiera wartości klienta (wartości opublikowane w tej metodzie) i wartości bazy danych.

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

Poniższy kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych inne niż to, co zostało opublikowane w pliku 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.");
}

Poniższy wyróżniony kod ustawia ConcurrencyToken wartość na nową wartość pobraną z bazy danych. Następnym razem, gdy użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu ostatniego wyświetlenia strony Edytuj.

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

Instrukcja jest wymagana ModelState.Remove , ponieważ ModelState ma poprzednią ConcurrencyToken wartość. Na stronie Razor ModelState wartość pola ma pierwszeństwo przed wartościami właściwości modelu, gdy oba te wartości są obecne.

Różnice w kodzie programu SQL Server a SQLite

Poniżej przedstawiono różnice między wersjami programu SQL Server i 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;

Aktualizowanie strony Edytuj Razor

Zaktualizuj Pages/Departments/Edit.cshtml za pomocą następującego kodu:

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

Powyższy kod ma następujące działanie:

  • Aktualizuje dyrektywę page z @page do @page "{id:int}".
  • Dodaje ukrytą wersję wiersza. ConcurrencyToken należy dodać wartość , więc postback wiąże wartość.
  • Wyświetla ostatni bajt ConcurrencyToken dla celów debugowania.
  • ViewData Zastępuje element silnie typizowane InstructorNameSL.

Testowanie konfliktów współbieżności ze stroną Edytuj

Otwórz dwa wystąpienia przeglądarki Edit w dziale angielskim:

  • Uruchom aplikację i wybierz pozycję Działy.
  • Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz polecenie Otwórz na nowej karcie.
  • Na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.

Dwie karty przeglądarki wyświetlają te same informacje.

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

Strona edycji działu 1 po zmianie

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym ConcurrencyTokenwskaźnikiem. Zwróć uwagę na zaktualizowany ConcurrencyTokenwskaźnik, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.

Zmień inne pole na drugiej karcie przeglądarki.

Edytowanie strony 2 działu po zmianie

Kliknij przycisk Zapisz. Zobaczysz komunikaty o błędach dla wszystkich pól, które nie są zgodne z wartościami bazy danych:

Komunikat o błędzie strony Edytowanie działu

To okno przeglądarki nie zamierzało zmienić pola Nazwa. Skopiuj i wklej bieżącą wartość (Języki) do pola Nazwa. Na karcie. Walidacja po stronie klienta usuwa komunikat o błędzie.

Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości zostaną wyświetlone na stronie Indeks.

Aktualizowanie modelu strony Usuń

Zaktualizuj Pages/Departments/Delete.cshtml.cs za pomocą następującego kodu:

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

Strona Usuń wykrywa konflikty współbieżności, gdy jednostka uległa zmianie po jej pobraniu. Department.ConcurrencyToken to wersja wiersza, gdy jednostka została pobrana. Podczas EF Core tworzenia SQL DELETE polecenia zawiera klauzulę WHERE z ConcurrencyToken. SQL DELETE Jeśli polecenie spowoduje, że nie ma to wpływu na wiersze zerowe:

  • Polecenie ConcurrencyToken w poleceniu SQL DELETE nie jest zgodne ConcurrencyToken z bazą danych.
  • Zgłaszany DbUpdateConcurrencyException jest wyjątek.
  • OnGetAsyncelement jest wywoływany za pomocą .concurrencyError

Aktualizowanie strony Usuwanie Razor

Zaktualizuj Pages/Departments/Delete.cshtml za pomocą następującego kodu:

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

Powyższy kod wprowadza następujące zmiany:

  • Aktualizuje dyrektywę page z @page do @page "{id:int}".
  • Dodaje komunikat o błędzie.
  • Zastępuje wartość FirstMidName wartością FullName w polu Administrator .
  • Zmiany ConcurrencyToken w celu wyświetlenia ostatniego bajtu.
  • Dodaje ukrytą wersję wiersza. ConcurrencyToken należy dodać wartość , więc postback wiąże wartość.

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

Utwórz dział testów.

Otwórz dwa wystąpienia przeglądarki Usuń w dziale testowym:

  • Uruchom aplikację i wybierz pozycję Działy.
  • Kliknij prawym przyciskiem myszy hiperlink Usuń dla działu testów i wybierz polecenie Otwórz na nowej karcie.
  • Kliknij hiperlink Edytuj dla działu testów.

Dwie karty przeglądarki wyświetlają te same informacje.

Zmień budżet na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym ConcurrencyTokenwskaźnikiem. Zwróć uwagę na zaktualizowany ConcurrencyTokenwskaźnik, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.

Usuń dział testów z drugiej karty. Zostanie wyświetlony błąd współbieżności z bieżącymi wartościami z bazy danych. Kliknięcie przycisku Usuń powoduje usunięcie jednostki, chyba że ConcurrencyToken została zaktualizowana.

Dodatkowe zasoby

Następne kroki

Jest to ostatni samouczek z serii. Dodatkowe tematy zostały omówione w wersji MVC tej serii samouczków.

W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników jednocześnie aktualizuje jednostkę (jednocześnie).

Konflikty współbieżności

Konflikt współbieżności występuje, gdy:

  • Użytkownik przechodzi do strony edycji jednostki.
  • Inny użytkownik aktualizuje tę samą jednostkę przed zapisaniem zmiany pierwszego użytkownika w bazie danych.

Jeśli wykrywanie współbieżności nie jest włączone, kto ostatnio aktualizuje bazę danych, zastępuje zmiany innego użytkownika. Jeśli to ryzyko jest akceptowalne, koszt programowania współbieżności może przeważyć nad korzyścią.

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

Jednym ze sposobów zapobiegania konfliktom współbieżności jest użycie blokad bazy danych. Jest to nazywane pesymistyczną współbieżnością. Zanim aplikacja odczytuje wiersz bazy danych, który zamierza zaktualizować, żąda blokady. Po zablokowaniu wiersza dostępu do aktualizacji żaden inny użytkownik nie może zablokować wiersza do momentu zwolnienia pierwszej blokady.

Zarządzanie blokadami ma wady. Program może być złożony i może powodować problemy z wydajnością w miarę wzrostu liczby użytkowników. Program Entity Framework Core nie zapewnia wbudowanej obsługi, a w tym samouczku nie pokazano, jak ją zaimplementować.

Optymistyczna współbieżność

Optymistyczna współbieżność umożliwia wystąpieniu konfliktów współbieżności, a następnie odpowiednio reaguje podczas ich wykonywania. Na przykład Jane odwiedza stronę edycji Departamentu i zmienia budżet dla działu angielskiego z 350 000,000 USD do 0,00 USD.

Zmiana budżetu na 0

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

Zmiana daty rozpoczęcia na 2013

Jane najpierw klika pozycję Zapisz i widzi, że jej zmiana zostanie w życie, ponieważ w przeglądarce zostanie wyświetlona strona Indeks z wartością zero jako kwota budżetu.

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

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

    W scenariuszu żadne dane nie zostaną utracone. Różne właściwości zostały zaktualizowane przez dwóch użytkowników. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane's, jak i Johna. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych. Takie podejście ma pewne wady:

    • Nie można uniknąć utraty danych, jeśli konkurencyjne zmiany są wprowadzane do tej samej właściwości.
    • Zazwyczaj nie jest praktyczne w aplikacji internetowej. Wymaga to utrzymania znaczącego stanu w celu śledzenia wszystkich pobranych wartości i nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji.
    • Może zwiększyć złożoność aplikacji w porównaniu z wykrywaniem współbieżności w jednostce.
  • Możesz pozwolić Jane's change zastąpić zmianę Jane.

    Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i pobraną wartość $350,000.000. Takie podejście jest nazywane scenariuszem Wins klienta lub Last in Wins . (Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych). Jeśli nie wykonasz żadnego kodowania na potrzeby obsługi współbieżności, usługa Wins klienta odbywa się automatycznie.

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

    • Wyświetl komunikat o błędzie.
    • Pokaż bieżący stan danych.
    • Zezwalaj użytkownikowi na ponowne zastosowania zmian.

    Jest to nazywane scenariuszem Store Wins . (Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta). W tym samouczku zaimplementujesz scenariusz Store Wins. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez zgłaszania alertów przez użytkownika.

Wykrywanie konfliktów w programie EF Core

EF CoreDbConcurrencyException zgłasza wyjątki podczas wykrywania konfliktów. Aby umożliwić wykrywanie konfliktów, należy skonfigurować model danych. Opcje włączania wykrywania konfliktów obejmują następujące elementy:

  • Skonfiguruj EF Core , aby uwzględnić oryginalne wartości kolumn skonfigurowanych jako tokeny współbieżności w klauzuli Where poleceń Update and Delete.

    Po SaveChanges wywołaniu klauzula Where wyszukuje oryginalne wartości wszystkich właściwości z adnotacjami z atrybutem ConcurrencyCheckAttribute . Instrukcja update nie znajdzie wiersza do zaktualizowania, jeśli którakolwiek z właściwości tokenu współbieżności została zmieniona, ponieważ wiersz został po raz pierwszy odczytany. EF Core interpretuje to jako konflikt współbieżności. W przypadku tabel baz danych, które mają wiele kolumn, takie podejście może spowodować bardzo duże klauzule Where i może wymagać dużej ilości stanu. W związku z tym takie podejście nie jest zwykle zalecane i nie jest to metoda używana w tym samouczku.

  • W tabeli bazy danych dołącz kolumnę śledzenia, która może służyć do określenia, kiedy wiersz został zmieniony.

    W bazie danych programu SQL Server typ danych kolumny śledzenia to rowversion. Wartość rowversion jest sekwencyjną liczbą, która jest zwiększana za każdym razem, gdy wiersz jest aktualizowany. W poleceniu Aktualizuj lub Usuń klauzula Where zawiera oryginalną wartość kolumny śledzenia (oryginalny numer wersji wiersza). Jeśli aktualizowany wiersz został zmieniony przez innego użytkownika, wartość w rowversion kolumnie różni się od oryginalnej wartości. W takim przypadku instrukcja Update or Delete nie może odnaleźć wiersza do zaktualizowania z powodu klauzuli Where. EF Core zgłasza wyjątek współbieżności, gdy żadne wiersze nie mają wpływu na polecenie Aktualizuj lub Usuń.

Dodawanie właściwości śledzenia

W Models/Department.cspliku dodaj właściwość śledzenia o nazwie RowVersion:

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

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

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

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

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

        public int? InstructorID { get; set; }

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

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

Atrybut TimestampAttribute określa kolumnę jako kolumnę śledzenia współbieżności. Płynny interfejs API to alternatywny sposób określania właściwości śledzenia:

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

W przypadku bazy danych [Timestamp] programu SQL Server atrybut właściwości jednostki zdefiniowany jako tablica bajtów:

  • Powoduje, że kolumna zostanie uwzględniona w klauzulach DELETE i UPDATE WHERE.
  • Ustawia typ kolumny w bazie danych na rowversion.

Baza danych generuje sekwencyjny numer wersji wiersza, który jest zwiększany za każdym razem, gdy wiersz jest aktualizowany. W poleceniu Update lub Delete klauzula Where zawiera pobraną wartość wersji wiersza. Jeśli aktualizowany wiersz uległ zmianie od czasu pobrania:

  • Bieżąca wartość wersji wiersza jest niezgodna z pobraną wartością.
  • Polecenia Update lub Delete nie znajdują wiersza, ponieważ Where klauzula szuka pobranej wartości wersji wiersza.
  • Jest DbUpdateConcurrencyException zgłaszany.

Poniższy kod przedstawia część języka T-SQL wygenerowaną przez EF Core po zaktualizowaniu nazwy działu:

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

Powyższy wyróżniony kod przedstawia klauzulę zawierającą WHERE RowVersion. Jeśli baza danych RowVersion nie jest równa parametrowi RowVersion (@p2), nie są aktualizowane żadne wiersze.

Poniższy wyróżniony kod przedstawia język T-SQL, który weryfikuje dokładnie jeden wiersz został zaktualizowany:

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 zwraca liczbę wierszy, których dotyczy ostatnia instrukcja. Jeśli wiersze nie są aktualizowane, EF Core zwraca wartość DbUpdateConcurrencyException.

Aktualizowanie bazy danych

RowVersion Dodanie właściwości zmienia model danych, który wymaga migracji.

Skompiluj projekt.

  • Uruchom następujące polecenie w usłudze PMC:

    Add-Migration RowVersion
    

To polecenie:

  • Migrations/{time stamp}_RowVersion.cs Tworzy plik migracji.

  • Migrations/SchoolContextModelSnapshot.cs Aktualizuje plik. Aktualizacja dodaje następujący wyróżniony kod do BuildModel metody :

    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");
        });
    
  • Uruchom następujące polecenie w usłudze PMC:

    Update-Database
    

Strony działu szkieletu

  • Postępuj zgodnie z instrukcjami na stronach Szkielet studenta z następującymi wyjątkami:

  • Utwórz folder Strony/Działy.

  • Użyj dla Department klasy modelu.

    • Użyj istniejącej klasy kontekstu zamiast utworzyć nową.

Skompiluj projekt.

Aktualizowanie strony Indeks

Narzędzie do tworzenia szkieletu utworzyło kolumnę RowVersion dla strony Indeks, ale to pole nie będzie wyświetlane w aplikacji produkcyjnej. W tym samouczku zostanie wyświetlony ostatni bajt, RowVersion aby pokazać, jak działa obsługa współbieżności. Ostatni bajt nie ma gwarancji, że sam jest unikatowy.

Zaktualizuj stronę Pages\Departments\Index.cshtml :

  • Zastąp indeks działem.
  • Zmień kod zawierający RowVersion , aby pokazać tylko ostatni bajt tablicy bajtów.
  • Zastąp ciąg FirstMidName wartością FullName.

Poniższy kod przedstawia zaktualizowaną stronę:

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

Aktualizowanie modelu strony Edycji

Zaktualizuj Pages/Departments/Edit.cshtml.cs za pomocą następującego kodu:

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

Element OriginalValue jest aktualizowany przy użyciu rowVersion wartości z jednostki, gdy został pobrany w metodzie OnGetAsync . EF Core Generuje polecenie SQL UPDATE z klauzulą WHERE zawierającą oryginalną RowVersion wartość. Jeśli polecenie UPDATE nie ma wpływu na żadne wiersze (żadne wiersze nie mają oryginalnej RowVersion wartości), zgłaszany DbUpdateConcurrencyException jest wyjątek.

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;

W poprzednim wyróżnionym kodzie:

  • Wartość w Department.RowVersion pliku to to, co znajdowało się w jednostce, gdy zostało pierwotnie pobrane w żądaniu Get dla strony Edycja. Wartość jest udostępniana metodzie OnPost przez ukryte pole na Razor stronie, na których wyświetlana jest jednostka do edycji. Wartość pola ukrytego jest kopiowana przez Department.RowVersion powiązanie modelu.
  • OriginalValue jest to, co EF Core będzie używane w klauzuli Where. Przed wykonaniem OriginalValue wyróżnionego wiersza kodu ma wartość, która znajdowała się w bazie danych, gdy FirstOrDefaultAsync została wywołana w tej metodzie, co może się różnić od tego, co było wyświetlane na stronie Edycja.
  • Wyróżniony kod zapewnia, że EF Core używa oryginalnej RowVersion wartości z wyświetlanej Department jednostki w klauzuli Where instrukcji SQL UPDATE.

W przypadku wystąpienia błędu współbieżności wyróżniony kod pobiera wartości klienta (wartości opublikowane w tej metodzie) i wartości bazy danych.

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

Poniższy kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych inne niż to, co zostało opublikowane w pliku 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.");
}

Poniższy wyróżniony kod ustawia RowVersion wartość na nową wartość pobraną z bazy danych. Następnym razem, gdy użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu ostatniego wyświetlenia strony Edytuj.

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

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

Aktualizowanie strony Edytuj

Zaktualizuj Pages/Departments/Edit.cshtml za pomocą następującego kodu:

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

Powyższy kod ma następujące działanie:

  • Aktualizuje dyrektywę page z @page do @page "{id:int}".
  • Dodaje ukrytą wersję wiersza. RowVersion należy dodać wartość , więc postback wiąże wartość.
  • Wyświetla ostatni bajt RowVersion dla celów debugowania.
  • ViewData Zastępuje element silnie typizowane InstructorNameSL.

Testowanie konfliktów współbieżności ze stroną Edytuj

Otwórz dwa wystąpienia przeglądarki Edit w dziale angielskim:

  • Uruchom aplikację i wybierz pozycję Działy.
  • Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz polecenie Otwórz na nowej karcie.
  • Na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.

Dwie karty przeglądarki wyświetlają te same informacje.

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

Strona edycji działu 1 po zmianie

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.

Zmień inne pole na drugiej karcie przeglądarki.

Edytowanie strony 2 działu po zmianie

Kliknij przycisk Zapisz. Zobaczysz komunikaty o błędach dla wszystkich pól, które nie są zgodne z wartościami bazy danych:

Komunikat o błędzie strony Edytowanie działu

To okno przeglądarki nie zamierzało zmienić pola Nazwa. Skopiuj i wklej bieżącą wartość (Języki) do pola Nazwa. Na karcie. Walidacja po stronie klienta usuwa komunikat o błędzie.

Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości zostaną wyświetlone na stronie Indeks.

Aktualizowanie modelu strony Usuń

Zaktualizuj Pages/Departments/Delete.cshtml.cs za pomocą następującego kodu:

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

Strona Usuń wykrywa konflikty współbieżności, gdy jednostka uległa zmianie po jej pobraniu. Department.RowVersion to wersja wiersza, gdy jednostka została pobrana. Podczas EF Core tworzenia polecenia SQL DELETE zawiera klauzulę WHERE z RowVersion. Jeśli polecenie SQL DELETE powoduje, że nie ma to wpływu na wiersze:

  • Polecenie RowVersion w poleceniu SQL DELETE nie jest zgodne RowVersion z bazą danych.
  • Zgłaszany jest wyjątek DbUpdateConcurrencyException.
  • OnGetAsyncelement jest wywoływany za pomocą .concurrencyError

Aktualizowanie strony Usuwanie

Zaktualizuj Pages/Departments/Delete.cshtml za pomocą następującego kodu:

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

Powyższy kod wprowadza następujące zmiany:

  • Aktualizuje dyrektywę page z @page do @page "{id:int}".
  • Dodaje komunikat o błędzie.
  • Zastępuje wartość FirstMidName wartością FullName w polu Administrator .
  • Zmiany RowVersion w celu wyświetlenia ostatniego bajtu.
  • Dodaje ukrytą wersję wiersza. RowVersion należy dodać wartość , więc postback wiąże wartość.

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

Utwórz dział testów.

Otwórz dwa wystąpienia przeglądarki Usuń w dziale testowym:

  • Uruchom aplikację i wybierz pozycję Działy.
  • Kliknij prawym przyciskiem myszy hiperlink Usuń dla działu testów i wybierz polecenie Otwórz na nowej karcie.
  • Kliknij hiperlink Edytuj dla działu testów.

Dwie karty przeglądarki wyświetlają te same informacje.

Zmień budżet na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.

Usuń dział testów z drugiej karty. Zostanie wyświetlony błąd współbieżności z bieżącymi wartościami z bazy danych. Kliknięcie przycisku Usuń powoduje usunięcie jednostki, chyba że RowVersion została zaktualizowana.

Dodatkowe zasoby

Następne kroki

Jest to ostatni samouczek z serii. Dodatkowe tematy zostały omówione w wersji MVC tej serii samouczków.

W tym samouczku pokazano, jak obsługiwać konflikty, gdy wielu użytkowników jednocześnie aktualizuje jednostkę (jednocześnie). Jeśli napotkasz problemy, nie możesz rozwiązać, pobierz lub wyświetl ukończoną aplikację. Pobierz instrukcje.

Konflikty współbieżności

Konflikt współbieżności występuje, gdy:

  • Użytkownik przechodzi do strony edycji jednostki.
  • Inny użytkownik aktualizuje tę samą jednostkę przed zapisaniem zmiany pierwszego użytkownika w bazie danych.

Jeśli wykrywanie współbieżności nie jest włączone, po wystąpieniu współbieżnych aktualizacji:

  • Ostatnia aktualizacja wygrywa. Oznacza to, że ostatnie wartości aktualizacji są zapisywane w bazie danych.
  • Pierwsze z bieżących aktualizacji zostaną utracone.

Optymistyczna współbieżność

Optymistyczna współbieżność umożliwia wystąpieniu konfliktów współbieżności, a następnie odpowiednio reaguje podczas ich wykonywania. Na przykład Jane odwiedza stronę edycji Departamentu i zmienia budżet dla działu angielskiego z 350 000,000 USD do 0,00 USD.

Zmiana budżetu na 0

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

Zmiana daty rozpoczęcia na 2013

Jane klika pozycję Zapisz jako pierwszy i widzi jej zmianę po wyświetleniu strony Indeks w przeglądarce.

Budżet został zmieniony na zero

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

Optymistyczna współbieżność obejmuje następujące opcje:

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

    W scenariuszu żadne dane nie zostaną utracone. Różne właściwości zostały zaktualizowane przez dwóch użytkowników. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy zarówno zmiany Jane's, jak i Johna. Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych. Takie podejście:

    • Nie można uniknąć utraty danych, jeśli konkurencyjne zmiany są wprowadzane do tej samej właściwości.
    • Zazwyczaj nie jest praktyczne w aplikacji internetowej. Wymaga to utrzymania znaczącego stanu w celu śledzenia wszystkich pobranych wartości i nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji.
    • Może zwiększyć złożoność aplikacji w porównaniu z wykrywaniem współbieżności w jednostce.
  • Możesz pozwolić Jane's change zastąpić zmianę Jane.

    Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 9/1/2013 i pobraną wartość $350,000.000. Takie podejście jest nazywane scenariuszem Wins klienta lub Last in Wins . (Wszystkie wartości z klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych). Jeśli nie wykonasz żadnego kodowania na potrzeby obsługi współbieżności, usługa Wins klienta odbywa się automatycznie.

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

    • Wyświetl komunikat o błędzie.
    • Pokaż bieżący stan danych.
    • Zezwalaj użytkownikowi na ponowne zastosowania zmian.

    Jest to nazywane scenariuszem Store Wins . (Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta). W tym samouczku zaimplementujesz scenariusz Store Wins. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez zgłaszania alertów przez użytkownika.

Obsługa współbieżności

Gdy właściwość jest skonfigurowana jako token współbieżności:

  • EF Core Sprawdza, czy właściwość nie została zmodyfikowana po pobraniu. Sprawdzanie występuje, gdy SaveChanges wywoływana jest funkcja lub SaveChangesAsync .
  • Jeśli właściwość została zmieniona po pobraniu, zostanie zwrócona DbUpdateConcurrencyException wartość .

Aby można było zgłaszać wyjątek DbUpdateConcurrencyException, należy skonfigurować bazę danych i model danych.

Wykrywanie konfliktów współbieżności we właściwości

Konflikty współbieżności można wykryć na poziomie właściwości za pomocą atrybutu ConcurrencyCheck . Atrybut można zastosować do wielu właściwości w modelu. Aby uzyskać więcej informacji, zobacz Data Annotations-ConcurrencyCheck.

Atrybut [ConcurrencyCheck] nie jest używany w tym samouczku.

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

Aby wykryć konflikty współbieżności, do modelu zostanie dodana kolumna śledzenia rowversion . rowversion :

  • Jest specyficzny dla programu SQL Server. Inne bazy danych mogą nie zapewniać podobnej funkcji.
  • Służy do określania, że jednostka nie została zmieniona, ponieważ została pobrana z bazy danych.

Baza danych generuje sekwencyjną rowversion liczbę, która jest zwiększana za każdym razem, gdy wiersz jest aktualizowany. W poleceniu lub klauzula Where zawiera pobraną wartość rowversion.Delete Update Jeśli aktualizowany wiersz uległ zmianie:

  • rowversion nie jest zgodna z pobraną wartością.
  • Polecenia Update lub Delete nie znajdują wiersza, ponieważ klauzula Where zawiera pobrany rowversionelement .
  • Jest DbUpdateConcurrencyException zgłaszany.

W EF Coresystemie, gdy nie zaktualizowano wierszy za pomocą Update polecenia lub Delete , zgłaszany jest wyjątek współbieżności.

Dodawanie właściwości śledzenia do jednostki Department

W Models/Department.cspliku dodaj właściwość śledzenia o nazwie RowVersion:

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

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

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

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

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

        public int? InstructorID { get; set; }

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

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

Atrybut Znacznik czasu określa, że ta kolumna jest zawarta w Where klauzuli Update i Delete polecenia. Atrybut jest wywoływany Timestamp , ponieważ poprzednie wersje programu SQL Server używały typu danych SQL timestamp przed zastąpieniem go typem SQL rowversion .

Płynny interfejs API może również określać właściwość śledzenia:

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

Poniższy kod przedstawia część języka T-SQL wygenerowaną przez EF Core po zaktualizowaniu nazwy działu:

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

Powyższy wyróżniony kod przedstawia klauzulę zawierającą WHERE RowVersion. Jeśli baza danych RowVersion nie jest równa parametrowi RowVersion (@p2), nie są aktualizowane żadne wiersze.

Poniższy wyróżniony kod przedstawia język T-SQL, który weryfikuje dokładnie jeden wiersz został zaktualizowany:

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 zwraca liczbę wierszy, których dotyczy ostatnia instrukcja. W żadnym wierszu nie są aktualizowane, EF Core zgłasza element DbUpdateConcurrencyException.

W oknie danych wyjściowych programu Visual Studio można zobaczyć generowanie kodu T-SQL EF Core .

Aktualizowanie bazy danych

RowVersion Dodanie właściwości zmienia model bazy danych, który wymaga migracji.

Skompiluj projekt. Wprowadź następujące polecenie w oknie polecenia:

dotnet ef migrations add RowVersion
dotnet ef database update

Poprzednie polecenia:

  • Migrations/{time stamp}_RowVersion.cs Dodaje plik migracji.

  • Migrations/SchoolContextModelSnapshot.cs Aktualizuje plik. Aktualizacja dodaje następujący wyróżniony kod do BuildModel metody :

  • Uruchamia migracje w celu zaktualizowania bazy danych.

Tworzenie szkieletu modelu Działy

Postępuj zgodnie z instrukcjami w artykule Tworzenie szkieletu modelu ucznia i używanie go Department do klasy modelu.

Poprzednie polecenie szkieletuje Department model. Otwórz projekt w programie Visual Studio.

Skompiluj projekt.

Aktualizowanie strony Indeks działów

Aparat tworzenia szkieletów utworzył kolumnę RowVersion dla strony Indeks, ale to pole nie powinno być wyświetlane. W tym samouczku zostanie wyświetlony ostatni bajt, RowVersion aby ułatwić zrozumienie współbieżności. Ostatni bajt nie ma gwarancji, że będzie unikatowy. Rzeczywista aplikacja nie będzie wyświetlana RowVersion ani ostatnia bajt .RowVersion

Zaktualizuj stronę Indeks:

  • Zastąp indeks działem.
  • Zastąp znaczniki zawierające RowVersion ostatni bajt .RowVersion
  • Zastąp ciąg FirstMidName wartością FullName.

Na poniższej adiustacji jest wyświetlana zaktualizowana strona:

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

Aktualizowanie modelu strony Edycji

Zaktualizuj Pages/Departments/Edit.cshtml.cs za pomocą następującego kodu:

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

Aby wykryć problem z współbieżnością, OriginalValue element zostanie zaktualizowany o rowVersion wartość z jednostki, która została pobrana. EF Core Generuje polecenie SQL UPDATE z klauzulą WHERE zawierającą oryginalną RowVersion wartość. Jeśli polecenie UPDATE nie ma wpływu na żadne wiersze (żadne wiersze nie mają oryginalnej RowVersion wartości), zgłaszany DbUpdateConcurrencyException jest wyjątek.

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;

W poprzednim kodzie jest wartością, Department.RowVersion gdy jednostka została pobrana. OriginalValue jest wartością w bazie danych, gdy FirstOrDefaultAsync została wywołana w tej metodzie.

Poniższy kod pobiera wartości klienta (wartości opublikowane w tej metodzie) i wartości bazy danych:

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

Poniższy kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych różniące się od tego, co zostało opublikowane w pliku 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.");
}

Poniższy wyróżniony kod ustawia RowVersion wartość na nową wartość pobraną z bazy danych. Następnym razem, gdy użytkownik kliknie pozycję Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu ostatniego wyświetlenia strony Edytuj.

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

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

Aktualizowanie strony Edytuj

Zaktualizuj Pages/Departments/Edit.cshtml za pomocą następującego znacznika:

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

Powyższy znacznik:

  • Aktualizuje dyrektywę page z @page do @page "{id:int}".
  • Dodaje ukrytą wersję wiersza. RowVersion należy dodać element , aby po powrocie powiązać wartość.
  • Wyświetla ostatni bajt RowVersion dla celów debugowania.
  • ViewData Zastępuje element silnie typizowane InstructorNameSL.

Testowanie konfliktów współbieżności ze stroną Edytuj

Otwórz dwa wystąpienia przeglądarki Edit w dziale angielskim:

  • Uruchom aplikację i wybierz pozycję Działy.
  • Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz polecenie Otwórz na nowej karcie.
  • Na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.

Dwie karty przeglądarki wyświetlają te same informacje.

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

Strona edycji działu 1 po zmianie

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.

Zmień inne pole na drugiej karcie przeglądarki.

Edytowanie strony 2 działu po zmianie

Kliknij przycisk Zapisz. Zobaczysz komunikaty o błędach dla wszystkich pól, które nie są zgodne z wartościami bazy danych:

Komunikat o błędzie strony Edycji działu 1

To okno przeglądarki nie zamierzało zmienić pola Nazwa. Skopiuj i wklej bieżącą wartość (Języki) do pola Nazwa. Na karcie. Walidacja po stronie klienta usuwa komunikat o błędzie.

Komunikat o błędzie strony Edycji działu 2

Kliknij ponownie przycisk Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana. Zapisane wartości zostaną wyświetlone na stronie Indeks.

Aktualizowanie strony Usuwanie

Zaktualizuj model strony Usuń przy użyciu następującego kodu:

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

Strona Usuń wykrywa konflikty współbieżności, gdy jednostka uległa zmianie po jej pobraniu. Department.RowVersion to wersja wiersza, gdy jednostka została pobrana. Podczas EF Core tworzenia polecenia SQL DELETE zawiera klauzulę WHERE z RowVersion. Jeśli polecenie SQL DELETE powoduje, że nie ma to wpływu na wiersze:

  • Polecenie RowVersion w poleceniu SQL DELETE nie jest zgodne RowVersion z bazą danych.
  • Zgłaszany jest wyjątek DbUpdateConcurrencyException.
  • OnGetAsyncelement jest wywoływany za pomocą .concurrencyError

Aktualizowanie strony Usuwanie

Zaktualizuj Pages/Departments/Delete.cshtml za pomocą następującego kodu:

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

Powyższy kod wprowadza następujące zmiany:

  • Aktualizuje dyrektywę page z @page do @page "{id:int}".
  • Dodaje komunikat o błędzie.
  • Zastępuje wartość FirstMidName wartością FullName w polu Administrator .
  • Zmiany RowVersion w celu wyświetlenia ostatniego bajtu.
  • Dodaje ukrytą wersję wiersza. RowVersion należy dodać element , aby po powrocie powiązać wartość.

Testowanie konfliktów współbieżności ze stroną Usuwanie

Utwórz dział testów.

Otwórz dwa wystąpienia przeglądarki Usuń w dziale testowym:

  • Uruchom aplikację i wybierz pozycję Działy.
  • Kliknij prawym przyciskiem myszy hiperlink Usuń dla działu testów i wybierz polecenie Otwórz na nowej karcie.
  • Kliknij hiperlink Edytuj dla działu testów.

Dwie karty przeglądarki wyświetlają te same informacje.

Zmień budżet na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością i zaktualizowanym wskaźnikiem rowVersion. Zwróć uwagę na zaktualizowany wskaźnik rowVersion, który jest wyświetlany na drugiej poście zwrotnym na drugiej karcie.

Usuń dział testów z drugiej karty. Zostanie wyświetlony błąd współbieżności z bieżącymi wartościami z bazy danych. Kliknięcie przycisku Usuń powoduje usunięcie jednostki, chyba że RowVersion została zaktualizowana.

Zobacz Dziedziczenie dotyczące dziedziczenia modelu danych.

Dodatkowe zasoby