Parte 8, Razor Pagine con EF Core in ASP.NET Core - Concorrenza

Tom Dykstra e Jon P Smith

L'app Web Contoso University illustra come creare Razor app Web Pages usando EF Core e Visual Studio. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione.

Se si verificano problemi che non è possibile risolvere, scaricare l'app completata e confrontare tale codice con quello creato seguendo questa esercitazione.

Questa esercitazione illustra come gestire i conflitti quando più utenti aggiornano un'entità contemporaneamente.

Conflitti di concorrenza

Un conflitto di concorrenza si verifica quando:

  • Un utente passa alla pagina di modifica di un'entità.
  • Un altro utente aggiorna la stessa entità prima che la modifica del primo utente venga scritta nel database.

Se il rilevamento della concorrenza non è abilitato, chiunque aggiorni il database per ultimo sovrascrive le modifiche apportate dall'altro utente. Se questo rischio è accettabile, il costo della programmazione per la concorrenza potrebbe essere superiore ai vantaggi.

Concorrenza pessimistica

Un modo per impedire i conflitti di concorrenza consiste nell'usare blocchi di database. Questo approccio è denominato concorrenza pessimistica. Prima che l'app legga una riga del database che intende aggiornare, richiede un blocco. Una volta bloccata una riga per l'accesso per gli aggiornamenti, nessun altro utente potrà bloccare la riga fino a quando non viene rilasciato il primo blocco.

La gestione dei blocchi presenta svantaggi. La programmazione può essere complessa e può causare problemi di prestazioni con l'aumentare del numero di utenti. Entity Framework Core non offre supporto predefinito per la concorrenza pessimistica.

Concorrenza ottimistica

La concorrenza ottimistica consente che si verifichino conflitti di concorrenza, quindi attiva le misure necessarie. Ad esempio Jane visita la pagina Department Edit (Modifica - Reparto) e cambia il budget per il reparto English (Inglese) da $ 350.000,00 a $ 0,00.

Changing budget to 0

Prima che Jane faccia clic su Salva John visita la stessa pagina e cambia il valore del campo Start Date (Data inizio) da 9/1/2007 a 9/1/2013.

Changing start date to 2013

Jane fa prima di tutto clic su Save e visualizza l'effetto della modifica, dato che il browser visualizza la pagina Index con zero come importo del budget.

John fa clic su Salva in una pagina Edit (Modifica) che visualizza ancora un budget pari a $ 350.000,00. Le operazioni successive dipendono da come si decide di gestire i conflitti di concorrenza:

  • Tenere traccia della proprietà modificata da un utente e aggiornare solo le colonne corrispondenti nel database.

    Con questo scenario non si registra la perdita di dati. I due utenti hanno aggiornato proprietà diverse. Quando un utente torna a visualizzare il reparto English (Inglese), visualizza sia le modifiche di Jane sia quelle di John. Questo metodo di aggiornamento riduce il numero di conflitti che possono comportare la perdita di dati. Questo approccio presenta alcuni svantaggi:

    • Non evita la perdita di dati se vengono apportate modifiche concorrenti alla stessa proprietà.
    • Risulta in genere poco pratico in un'app Web. Richiede la manutenzione di un volume importante di codice statico per tenere traccia di tutti i valori recuperati e i nuovi valori. La gestione di grandi quantità di codice statico può ridurre le prestazioni dell'applicazione.
    • Può rendere più complesse le app rispetto al rilevamento della concorrenza in un'entità.
  • Riscrivere il cambiamento di Jane.

    Quando un utente torna a visualizzare il reparto English (Inglese), visualizza 9/1/2013 e il valore $ 350.000,00 recuperato. Questo scenario è detto Priorità client o Last in Wins (Priorità ultimo accesso). Tutti i valori del client hanno la precedenza su ciò che si trova nell'archivio dati. Il codice con scaffolding non gestisce la concorrenza, il client wins viene eseguito automaticamente.

  • Impedire che la modifica di John venga aggiornata nel database. In genere, l'app:

    • Visualizza un messaggio di errore.
    • Visualizza lo stato corrente dei dati.
    • Consente all'utente di riapplicare le modifiche.

    Questo scenario è detto Store Wins (Priorità archivio). I valori dell'archivio dati hanno la precedenza sui valori inviati dal client. Lo scenario Store Wins viene usato in questa esercitazione. Questo metodo garantisce che nessuna modifica venga sovrascritta senza che un utente riceva un avviso.

Rilevamento dei conflitti in EF Core

Le proprietà configurate come token di concorrenza vengono usate per implementare il controllo della concorrenza ottimistica. Quando un'operazione di aggiornamento o eliminazione viene attivata da SaveChanges o SaveChangesAsync, il valore del token di concorrenza nel database viene confrontato con il valore originale letto da EF Core:

  • Se i valori corrispondono, l'operazione può essere completata.
  • Se i valori non corrispondono, EF Core si presuppone che un altro utente abbia eseguito un'operazione in conflitto, interrompe la transazione corrente e genera un'eccezione DbUpdateConcurrencyException.

Un altro utente o processo che esegue un'operazione in conflitto con l'operazione corrente è noto come conflitto di concorrenza.

Nei database EF Core relazionali controlla il valore del token di concorrenza nella WHERE clausola delle UPDATE istruzioni e DELETE per rilevare un conflitto di concorrenza.

Il modello di dati deve essere configurato per abilitare il rilevamento dei conflitti includendo una colonna di rilevamento che può essere utilizzata per determinare quando una riga è stata modificata. Ef offre due approcci per i token di concorrenza:

L'approccio di SQL Server e i dettagli dell'implementazione di SQLite sono leggermente diversi. Un file di differenza viene mostrato più avanti nell'esercitazione che elenca le differenze. La scheda Visual Studio mostra l'approccio di SQL Server. La scheda Visual Studio Code mostra l'approccio per i database non SQL Server, ad esempio SQLite.

  • Nel modello includere una colonna di rilevamento utilizzata per determinare quando una riga è stata modificata.
  • Applicare l'oggetto TimestampAttribute alla proprietà di concorrenza.

Aggiornare il Models/Department.cs file con il codice evidenziato seguente:

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

TimestampAttribute è ciò che identifica la colonna come colonna di rilevamento della concorrenza. L'API Fluent è un modo alternativo per specificare la proprietà di rilevamento:

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

L'attributo [Timestamp] in una proprietà di entità genera il codice seguente nel ModelBuilder metodo :

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

Il codice precedente:

  • Imposta il tipo di ConcurrencyToken proprietà sulla matrice di byte. byte[] è il tipo obbligatorio per SQL Server.
  • Chiama IsConcurrencyToken. IsConcurrencyToken configura la proprietà come token di concorrenza. Negli aggiornamenti, il valore del token di concorrenza nel database viene confrontato con il valore originale per assicurarsi che non sia stato modificato dopo che l'istanza è stata recuperata dal database. Se è stata modificata, viene generata un'eccezione DbUpdateConcurrencyException e le modifiche non vengono applicate.
  • Chiama ValueGeneratedOnAddOrUpdate, che configura la ConcurrencyToken proprietà in modo che venga generato automaticamente un valore durante l'aggiunta o l'aggiornamento di un'entità.
  • HasColumnType("rowversion") imposta il tipo di colonna nel database di SQL Server su rowversion.

Il codice seguente mostra una parte del T-SQL generato da EF Core quando il Department nome viene aggiornato:

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

Il codice evidenziato precedente visualizza la clausola WHERE contenente ConcurrencyToken. Se il database ConcurrencyToken non è uguale al ConcurrencyToken parametro @p2, non vengono aggiornate righe.

Il codice evidenziato seguente visualizza la notazione T-SQL che verifica che è stata aggiornata esattamente una riga:

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 restituisce il numero di righe interessate dall'ultima istruzione. Se non vengono aggiornate righe, EF Core genera un'eccezione DbUpdateConcurrencyException.

Aggiungere una migrazione

L'aggiunta della proprietà ConcurrencyToken cambia il modello di dati e ciò richiede una migrazione.

Compilare il progetto.

Eseguire i comandi seguenti nella console di Gestione pacchetti:

Add-Migration RowVersion
Update-Database

I comandi precedenti:

  • Crea il file di Migrations/{time stamp}_RowVersion.cs migrazione.
  • Aggiornamenti il Migrations/SchoolContextModelSnapshot.cs file. L'aggiornamento aggiunge il codice seguente al BuildModel metodo :
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");

Scaffolding delle pagine Department

Seguire le istruzioni in Scaffolding delle pagine Student con le eccezioni seguenti:

  • Creare una cartella Pages/Departments.
  • Usare Department per la classe del modello.
  • Usare la classe di contesto esistente anziché crearne una nuova.

Aggiungere una classe di utilità

Nella cartella del progetto creare la Utility classe con il codice seguente:

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

La Utility classe fornisce il GetLastChars metodo usato per visualizzare gli ultimi caratteri del token di concorrenza. Il codice seguente illustra il codice che funziona con SQL Server ad 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

La #if SQLiteVersiondirettiva del preprocessore isola le differenze nelle versioni di SQLite e SQL Server e consente di:

  • L'autore mantiene una codebase per entrambe le versioni.
  • Gli sviluppatori SQLite distribuiscono l'app in Azure e usano SQL Azure.

Compilare il progetto.

Aggiornare la pagina Index

Lo strumento di scaffolding crea una colonna ConcurrencyToken per la pagina Index, ma questo campo non verrebbe visualizzato in un'app in produzione. In questa esercitazione viene visualizzata l'ultima parte di per illustrare il funzionamento della ConcurrencyToken gestione della concorrenza. L'ultima parte non è garantita che sia univoca da sola.

Aggiornare la pagina Pages\Departments\Index.cshtml:

  • Sostituire Index con Departments.
  • Modificare il codice contenente ConcurrencyToken per visualizzare solo gli ultimi caratteri.
  • Sostituisci FirstMidName con FullName.

Il codice seguente mostra la pagina aggiornata:

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

Aggiornare il modello di pagina Edit

Eseguire l'aggiornamento Pages/Departments/Edit.cshtml.cs con il codice seguente:

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

Aggiornamenti della concorrenza

OriginalValue viene aggiornato con il ConcurrencyToken valore dell'entità quando è stato recuperato nel OnGetAsync metodo . EF Core genera un SQL UPDATE comando con una WHERE clausola contenente il valore originale ConcurrencyToken . Se nessuna riga è interessata dal UPDATE comando, viene generata un'eccezione DbUpdateConcurrencyException . Nessuna riga interessata dal UPDATE comando quando nessuna riga ha il valore originale ConcurrencyToken .

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;

Nel codice evidenziato precedente:

  • Il valore in Department.ConcurrencyToken è il valore quando l'entità è stata recuperata nella Get richiesta per la Edit pagina. Il valore viene fornito al OnPost metodo da un campo nascosto nella Razor pagina che visualizza l'entità da modificare. Il valore del campo nascosto viene copiato in Department.ConcurrencyToken dallo strumento di associazione di modelli.
  • OriginalValue è ciò che EF Core viene usato nella WHERE clausola . Prima dell'esecuzione della riga di codice evidenziata:
    • OriginalValue ha il valore presente nel database quando FirstOrDefaultAsync è stato chiamato in questo metodo.
    • Questo valore potrebbe essere diverso da quello visualizzato nella pagina Modifica.
  • Il codice evidenziato assicura che EF Core usi il valore originale ConcurrencyToken dell'entità visualizzata Department nella clausola dell'istruzione WHERE SQLUPDATE.

Il codice seguente illustra il Department modello. Department viene inizializzato in:

  • OnGetAsync metodo dalla query ef.
  • OnPostAsyncmetodo in base al campo nascosto nella pagina usando l'associazione Razordi modelli:
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;

Il codice precedente mostra il ConcurrencyToken valore dell'entità Department dalla HTTP POST richiesta viene impostato sul ConcurrencyToken valore della HTTP GET richiesta.

Quando si verifica un errore di concorrenza, il codice evidenziato seguente ottiene i valori client (i valori inseriti in questo metodo) e i valori del database.

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

Il codice seguente aggiunge un messaggio di errore personalizzato per ogni colonna che ha valori del database diversi da quelli inseriti in 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.");
}

Il codice evidenziato seguente imposta il valore ConcurrencyToken sul nuovo valore recuperato dal database. Quando l'utente fa di nuovo clic su Salva vengono rilevati solo gli errori di concorrenza che si verificano dopo l'ultima visualizzazione della pagina Edit (Modifica).

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

L'istruzione ModelState.Remove è obbligatoria perché ModelState ha il valore precedente ConcurrencyToken . Razor Nella pagina il ModelState valore di un campo ha la precedenza sui valori delle proprietà del modello quando sono presenti entrambi.

Differenze tra codice SQL Server e SQLite

Di seguito sono illustrate le differenze tra le versioni di SQL Server e 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;

Aggiornare la pagina Modifica Razor

Eseguire l'aggiornamento Pages/Departments/Edit.cshtml con il codice seguente:

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

Il codice precedente:

  • Aggiorna la direttiva page da @page a @page "{id:int}".
  • Aggiunge una versione di riga nascosta. ConcurrencyToken deve essere aggiunto in modo che il postback associa il valore.
  • Visualizza l'ultimo byte di ConcurrencyToken a scopo di debug.
  • Sostituisce ViewData con l'elemento InstructorNameSL fortemente tipizzato.

Eseguire il test dei conflitti di concorrenza con la pagina Edit (Modifica)

Aprire due istanze di browser con la pagina Edit (Modifica) e il reparto English (Inglese):

  • Eseguire l'app e selezionare Departments (Reparti).
  • Fare clic con il pulsante destro del mouse sul collegamento ipertestuale Edit (Modifica) per il reparto English (Inglese) e selezionare Apri in una nuova scheda.
  • Nella prima scheda fare clic sul collegamento ipertestuale Edit (Modifica) per il reparto English (Inglese).

Le due schede del browser visualizzano le stesse informazioni.

Modificare il nome nella prima scheda del browser e fare clic su Salva.

Department Edit page 1 after change

Il browser mostra la pagina Indice con il valore modificato e l'indicatore aggiornato ConcurrencyToken. Si noti che l'indicatore aggiornato ConcurrencyTokenviene visualizzato nel secondo postback nell'altra scheda.

Modificare un altro campo nella seconda scheda del browser.

Department Edit page 2 after change

Fare clic su Salva. Vengono visualizzati messaggi di errore per tutti i campi che non corrispondono ai valori del database:

Department Edit page error message

Questa finestra del browser non prevedeva la modifica del campo Name (Nome). Copiare e incollare il valore corrente Languages (Lingue) nel campo Name (Nome). Esci. La convalida lato client rimuove il messaggio di errore.

Fare di nuovo clic su Salva. Il valore immesso nella seconda scheda del browser viene salvato. I valori salvati vengono visualizzati nella pagina Index.

Aggiornare il modello di pagina Elimina

Eseguire l'aggiornamento Pages/Departments/Delete.cshtml.cs con il codice seguente:

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

La pagina Delete (Elimina) rileva i conflitti di concorrenza quando l'entità è stata modificata dopo il recupero. Department.ConcurrencyToken è la versione di riga quando l'entità è stata recuperata. Quando EF Core crea il SQL DELETE comando, include una clausola WHERE con ConcurrencyToken. Se il SQL DELETE comando restituisce zero righe interessate:

  • L'oggetto ConcurrencyTokenSQL DELETE nel comando non corrisponde ConcurrencyToken al database.
  • Viene generata un'eccezione DbUpdateConcurrencyException .
  • OnGetAsync viene chiamata con concurrencyError.

Aggiornare la pagina Elimina Razor

Eseguire l'aggiornamento Pages/Departments/Delete.cshtml con il codice seguente:

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

Il codice precedente apporta le modifiche seguenti:

  • Aggiorna la direttiva page da @page a @page "{id:int}".
  • Aggiunge un messaggio di errore.
  • Sostituisce FirstMidName con FullName nel campo Administrator (Amministratore).
  • Modifica ConcurrencyToken per visualizzare l'ultimo byte.
  • Aggiunge una versione di riga nascosta. ConcurrencyToken deve essere aggiunto in modo che il postback associa il valore.

Testare i conflitti di concorrenza

Creare un reparto di test.

Aprire due istanze del browser con la pagina Delete (Elimina):

  • Eseguire l'app e selezionare Departments (Reparti).
  • Fare clic con il pulsante destro del mouse sul collegamento ipertestuale Delete (Elimina) per il reparto di test e selezionare Apri in una nuova scheda.
  • Fare clic sul collegamento ipertestuale Edit (Modifica) per il reparto di test.

Le due schede del browser visualizzano le stesse informazioni.

Modificare il budget nella prima scheda del browser e fare clic su Salva.

Il browser mostra la pagina Indice con il valore modificato e l'indicatore aggiornato ConcurrencyToken. Si noti che l'indicatore aggiornato ConcurrencyTokenviene visualizzato nel secondo postback nell'altra scheda.

Eliminare il reparto di test dalla seconda scheda. Viene visualizzato un errore di concorrenza con i valori correnti del database. Facendo clic su Elimina viene eliminata l'entità, a meno che non ConcurrencyToken sia stato aggiornato.

Risorse aggiuntive

Passaggi successivi

Questa è l'ultima esercitazione nella serie. Ulteriori argomenti sono trattati nella versione MVC di questa serie di esercitazioni.

Questa esercitazione descrive la gestione dei conflitti quando più utenti aggiornano la stessa entità contemporaneamente.

Conflitti di concorrenza

Un conflitto di concorrenza si verifica quando:

  • Un utente passa alla pagina di modifica di un'entità.
  • Un altro utente aggiorna la stessa entità prima che la modifica del primo utente venga scritta nel database.

Se il rilevamento della concorrenza non è abilitato, chiunque aggiorni il database per ultimo sovrascrive le modifiche apportate dall'altro utente. Se questo rischio è accettabile, il costo della programmazione per la concorrenza potrebbe essere superiore ai vantaggi.

Concorrenza pessimistica (blocco)

Un modo per impedire i conflitti di concorrenza consiste nell'usare blocchi di database. Questo approccio è denominato concorrenza pessimistica. Prima che l'app legga una riga del database che intende aggiornare, richiede un blocco. Una volta bloccata una riga per l'accesso per gli aggiornamenti, nessun altro utente potrà bloccare la riga fino a quando non viene rilasciato il primo blocco.

La gestione dei blocchi presenta svantaggi. La programmazione può essere complessa e può causare problemi di prestazioni con l'aumentare del numero di utenti. Entity Framework Core non offre supporto predefinito per questa modalità e la presente esercitazione non indica come implementarla.

Concorrenza ottimistica

La concorrenza ottimistica consente che si verifichino conflitti di concorrenza, quindi attiva le misure necessarie. Ad esempio Jane visita la pagina Department Edit (Modifica - Reparto) e cambia il budget per il reparto English (Inglese) da $ 350.000,00 a $ 0,00.

Changing budget to 0

Prima che Jane faccia clic su Salva John visita la stessa pagina e cambia il valore del campo Start Date (Data inizio) da 9/1/2007 a 9/1/2013.

Changing start date to 2013

Jane fa prima di tutto clic su Save e visualizza l'effetto della modifica, dato che il browser visualizza la pagina Index con zero come importo del budget.

John fa clic su Salva in una pagina Edit (Modifica) che visualizza ancora un budget pari a $ 350.000,00. Le operazioni successive dipendono da come si decide di gestire i conflitti di concorrenza:

  • È possibile tenere traccia della proprietà che un utente ha modificato e aggiornare solo le colonne corrispondenti nel database.

    Con questo scenario non si registra la perdita di dati. I due utenti hanno aggiornato proprietà diverse. Quando un utente torna a visualizzare il reparto English (Inglese), visualizza sia le modifiche di Jane sia quelle di John. Questo metodo di aggiornamento riduce il numero di conflitti che possono comportare la perdita di dati. Questo approccio presenta alcuni svantaggi:

    • Non evita la perdita di dati se vengono apportate modifiche concorrenti alla stessa proprietà.
    • Risulta in genere poco pratico in un'app Web. Richiede la manutenzione di un volume importante di codice statico per tenere traccia di tutti i valori recuperati e i nuovi valori. La gestione di grandi quantità di codice statico può ridurre le prestazioni dell'applicazione.
    • Può rendere più complesse le app rispetto al rilevamento della concorrenza in un'entità.
  • È possibile consentire che la modifica di John sovrascriva la modifica di Jane.

    Quando un utente torna a visualizzare il reparto English (Inglese), visualizza 9/1/2013 e il valore $ 350.000,00 recuperato. Questo scenario è detto Priorità client o Last in Wins (Priorità ultimo accesso). Tutti i valori del client hanno la precedenza su ciò che si trova nell'archivio dati. Se non si esegue alcuna codifica per la gestione della concorrenza, il client vince automaticamente.

  • È possibile impedire l'aggiornamento del database con la modifica di John. In genere, l'app:

    • Visualizza un messaggio di errore.
    • Visualizza lo stato corrente dei dati.
    • Consente all'utente di riapplicare le modifiche.

    Questo scenario è detto Store Wins (Priorità archivio). I valori dell'archivio dati hanno la precedenza sui valori inviati dal client. In questa esercitazione si implementa lo scenario Delle vittorie nello Store. Questo metodo garantisce che nessuna modifica venga sovrascritta senza che un utente riceva un avviso.

Rilevamento dei conflitti in EF Core

EF Core genera DbConcurrencyException eccezioni quando rileva conflitti. Il modello di dati deve essere configurato per abilitare il rilevamento dei conflitti. Di seguito sono elencate alcune opzioni per abilitare il rilevamento dei conflitti:

  • Configurare EF Core per includere i valori originali delle colonne configurati come token di concorrenza nella clausola Where dei comandi Update e Delete.

    Quando SaveChanges viene chiamato , la clausola Where cerca i valori originali di qualsiasi proprietà annotata con l'attributo ConcurrencyCheckAttribute . L'istruzione Update non troverà una riga da aggiornare se una delle proprietà del token di concorrenza è cambiata dopo la prima lettura della riga. EF Core interpreta come un conflitto di concorrenza. Per le tabelle di database con molte colonne, questo approccio può risultare in clausole Where molto grandi e richiedere grandi quantità di stato. Pertanto questo approccio è in genere sconsigliato e non è il metodo usato in questa esercitazione.

  • Nella tabella del database, includere una colonna di rilevamento che può essere usata per determinare quando è stata modificata una riga.

    In un database di SQL Server il tipo di dati della colonna di rilevamento è rowversion. Il valore rowversion è un numero sequenziale che viene incrementato ogni volta che la riga viene aggiornata. In un comando Update o Delete, la clausola Where include il valore originale della colonna di rilevamento (il numero di versione originale della riga). Se la riga da aggiornare è stata modificata da un altro utente, il valore nella colonna rowversion è diverso dal valore originale. In tal caso, l'istruzione Update o Delete non riesce a trovare la riga da aggiornare a causa della clausola Where. EF Core genera un'eccezione di concorrenza quando nessuna riga è interessata da un comando Update o Delete.

Aggiungere una proprietà di rilevamento modifiche

In Models/Department.csaggiungere una proprietà di rilevamento denominata 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; }
    }
}

L'attributo TimestampAttribute identifica la colonna come colonna di rilevamento della concorrenza. L'API Fluent è un modo alternativo per specificare la proprietà di rilevamento:

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

Per un database di SQL Server l'attributo [Timestamp] per una proprietà dell'entità definita come matrice di byte:

  • Causa l'inclusione della colonna nelle clausole Where per Delete e Update.
  • Imposta il tipo di colonna nel database su rowversion.

Il database genera un numero di versione di riga sequenziale che viene incrementato ogni volta che la riga viene aggiornata. In un comando Update o Delete la clausola Where include il valore della versione della riga recuperato. Se la riga da aggiornare è stata modificata dopo il recupero:

  • Il valore della versione della riga corrente non corrisponde al valore recuperato.
  • I comandi Update o Delete non trovano una riga perché la clausola Where cerca il valore della versione della riga recuperata.
  • Viene generata un'eccezione DbUpdateConcurrencyException.

Il codice seguente mostra una parte del T-SQL generato da EF Core quando viene aggiornato il nome del reparto:

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

Il codice evidenziato precedente visualizza la clausola WHERE contenente RowVersion. Se il valore RowVersion del database non è uguale al parametro RowVersion (@p2) non viene aggiornata alcuna riga.

Il codice evidenziato seguente visualizza la notazione T-SQL che verifica che è stata aggiornata esattamente una riga:

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 restituisce il numero di righe interessate dall'ultima istruzione. Se non vengono aggiornate righe, EF Core genera un'eccezione DbUpdateConcurrencyException.

Aggiornare il database

L'aggiunta della proprietà RowVersion cambia il modello di dati e ciò richiede una migrazione.

Compilare il progetto.

  • Eseguire il comando seguente nella console di Gestione pacchetti:

    Add-Migration RowVersion
    

Questo comando:

  • Crea il file di Migrations/{time stamp}_RowVersion.cs migrazione.

  • Aggiornamenti il Migrations/SchoolContextModelSnapshot.cs file. L'aggiornamento aggiunge al metodo BuildModel il codice evidenziato seguente:

    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");
        });
    
  • Eseguire il comando seguente nella console di Gestione pacchetti:

    Update-Database
    

Scaffolding delle pagine Department

  • Seguire le istruzioni in Scaffolding delle pagine Student con le eccezioni seguenti:

  • Creare una cartella Pages/Departments.

  • Usare Department per la classe del modello.

    • Usare la classe di contesto esistente anziché crearne una nuova.

Compilare il progetto.

Aggiornare la pagina Index

Lo strumento di scaffolding crea una colonna RowVersion per la pagina Index, ma questo campo non verrebbe visualizzato in un'app in produzione. In questa esercitazione, l'ultimo byte di RowVersion viene visualizzato per illustrare in modo più chiaro come funziona la gestione della concorrenza. L'univocità dell'ultimo byte non è garantita.

Aggiornare la pagina Pages\Departments\Index.cshtml:

  • Sostituire Index con Departments.
  • Modificare il codice che contiene RowVersion per visualizzare solo l'ultimo byte della matrice di byte.
  • Sostituire FirstMidName con FullName.

Il codice seguente mostra la pagina aggiornata:

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

Aggiornare il modello di pagina Edit

Eseguire l'aggiornamento Pages/Departments/Edit.cshtml.cs con il codice seguente:

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

L'oggetto OriginalValue viene aggiornato con il rowVersion valore dell'entità quando è stato recuperato nel OnGetAsync metodo . EF Core genera un comando SQL UPDATE con una clausola WHERE contenente il valore originale RowVersion . Se il comando UPDATE non ha effetto su nessuna riga (nessuna riga ha il valore originale RowVersion), viene generata un'eccezione DbUpdateConcurrencyException.

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;

Nel codice evidenziato precedente:

  • Il valore in Department.RowVersion corrisponde a quello presente nell'entità quando è stato recuperato in origine nella richiesta Get per la pagina Edit. Il valore viene fornito al OnPost metodo da un campo nascosto nella Razor pagina che visualizza l'entità da modificare. Il valore del campo nascosto viene copiato in Department.RowVersion dallo strumento di associazione di modelli.
  • OriginalValue è ciò che EF Core verrà usato nella clausola Where. Prima dell'esecuzione della riga di codice evidenziata, OriginalValue ha il valore che era presente nel database al momento della chiamata di FirstOrDefaultAsync in questo metodo, che potrebbe essere diverso da quello visualizzato nella pagina Edit.
  • Il codice evidenziato assicura che EF Core usi il valore originale RowVersion dell'entità visualizzata Department nella clausola Where dell'istruzione SQL UPDATE.

Quando si verifica un errore di concorrenza, il codice evidenziato seguente ottiene i valori client (i valori inseriti in questo metodo) e i valori del database.

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

Il codice seguente aggiunge un messaggio di errore personalizzato per ogni colonna che ha valori del database diversi da quelli inseriti in 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.");
}

Il codice evidenziato seguente imposta il valore RowVersion sul nuovo valore recuperato dal database. Quando l'utente fa di nuovo clic su Salva vengono rilevati solo gli errori di concorrenza che si verificano dopo l'ultima visualizzazione della pagina Edit (Modifica).

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

L'istruzione ModelState.Remove è necessaria perché ModelState presenta il valore obsoleto RowVersion. Razor Nella pagina il ModelState valore di un campo ha la precedenza sui valori delle proprietà del modello quando sono presenti entrambi.

Aggiornare la pagina Edit

Eseguire l'aggiornamento Pages/Departments/Edit.cshtml con il codice seguente:

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

Il codice precedente:

  • Aggiorna la direttiva page da @page a @page "{id:int}".
  • Aggiunge una versione di riga nascosta. RowVersion deve essere aggiunto in modo che il postback associa il valore.
  • Visualizza l'ultimo byte di RowVersion a scopo di debug.
  • Sostituisce ViewData con l'elemento InstructorNameSL fortemente tipizzato.

Eseguire il test dei conflitti di concorrenza con la pagina Edit (Modifica)

Aprire due istanze di browser con la pagina Edit (Modifica) e il reparto English (Inglese):

  • Eseguire l'app e selezionare Departments (Reparti).
  • Fare clic con il pulsante destro del mouse sul collegamento ipertestuale Edit (Modifica) per il reparto English (Inglese) e selezionare Apri in una nuova scheda.
  • Nella prima scheda fare clic sul collegamento ipertestuale Edit (Modifica) per il reparto English (Inglese).

Le due schede del browser visualizzano le stesse informazioni.

Modificare il nome nella prima scheda del browser e fare clic su Salva.

Department Edit page 1 after change

Il browser visualizza la pagina Index con il valore modificato e l'indicatore rowVersion aggiornato. Si noti l'indicatore rowVersion aggiornato, che è visualizzato sul secondo postback nell'altra scheda.

Modificare un altro campo nella seconda scheda del browser.

Department Edit page 2 after change

Fare clic su Salva. Vengono visualizzati messaggi di errore per tutti i campi che non corrispondono ai valori del database:

Department Edit page error message

Questa finestra del browser non prevedeva la modifica del campo Name (Nome). Copiare e incollare il valore corrente Languages (Lingue) nel campo Name (Nome). Esci. La convalida lato client rimuove il messaggio di errore.

Fare di nuovo clic su Salva. Il valore immesso nella seconda scheda del browser viene salvato. I valori salvati vengono visualizzati nella pagina Index.

Aggiornare il modello di pagina Elimina

Eseguire l'aggiornamento Pages/Departments/Delete.cshtml.cs con il codice seguente:

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

La pagina Delete (Elimina) rileva i conflitti di concorrenza quando l'entità è stata modificata dopo il recupero. Department.RowVersion è la versione di riga quando l'entità è stata recuperata. Quando EF Core crea il comando SQL DELETE, include una clausola WHERE con RowVersion. Se il comando SQL DELETE non ha effetto su nessuna riga:

  • RowVersion nel comando SQL DELETE non corrisponde a RowVersion nel database.
  • Viene generata un'eccezione DbUpdateConcurrencyException.
  • OnGetAsync viene chiamata con concurrencyError.

Aggiornare la pagina Delete (Elimina)

Eseguire l'aggiornamento Pages/Departments/Delete.cshtml con il codice seguente:

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

Il codice precedente apporta le modifiche seguenti:

  • Aggiorna la direttiva page da @page a @page "{id:int}".
  • Aggiunge un messaggio di errore.
  • Sostituisce FirstMidName con FullName nel campo Administrator (Amministratore).
  • Modifica RowVersion per visualizzare l'ultimo byte.
  • Aggiunge una versione di riga nascosta. RowVersion deve essere aggiunto in modo che il postback associa il valore.

Testare i conflitti di concorrenza

Creare un reparto di test.

Aprire due istanze del browser con la pagina Delete (Elimina):

  • Eseguire l'app e selezionare Departments (Reparti).
  • Fare clic con il pulsante destro del mouse sul collegamento ipertestuale Delete (Elimina) per il reparto di test e selezionare Apri in una nuova scheda.
  • Fare clic sul collegamento ipertestuale Edit (Modifica) per il reparto di test.

Le due schede del browser visualizzano le stesse informazioni.

Modificare il budget nella prima scheda del browser e fare clic su Salva.

Il browser visualizza la pagina Index con il valore modificato e l'indicatore rowVersion aggiornato. Si noti l'indicatore rowVersion aggiornato, che è visualizzato sul secondo postback nell'altra scheda.

Eliminare il reparto di test dalla seconda scheda. Viene visualizzato un errore di concorrenza con i valori correnti del database. Facendo clic su Elimina viene eliminata l'entità, a meno che non RowVersion sia stato aggiornato.

Risorse aggiuntive

Passaggi successivi

Questa è l'ultima esercitazione nella serie. Ulteriori argomenti sono trattati nella versione MVC di questa serie di esercitazioni.

Questa esercitazione descrive la gestione dei conflitti quando più utenti aggiornano la stessa entità contemporaneamente. Se si verificano problemi non è possibile risolvere, scaricare o visualizzare l'app completata.Istruzioni per il download.

Conflitti di concorrenza

Un conflitto di concorrenza si verifica quando:

  • Un utente passa alla pagina di modifica di un'entità.
  • Un altro utente aggiorna la stessa entità prima che la modifica del primo utente venga scritta nel database.

Se non è abilitato il rilevamento della concorrenza, quando si verificano aggiornamenti concorrenti:

  • L'ultimo aggiornamento è quello valido. In altri termini, nel database vengono salvati i valori dell'ultimo aggiornamento.
  • I dati del primo aggiornamento vengono ignorati.

Concorrenza ottimistica

La concorrenza ottimistica consente che si verifichino conflitti di concorrenza, quindi attiva le misure necessarie. Ad esempio Jane visita la pagina Department Edit (Modifica - Reparto) e cambia il budget per il reparto English (Inglese) da $ 350.000,00 a $ 0,00.

Changing budget to 0

Prima che Jane faccia clic su Salva John visita la stessa pagina e cambia il valore del campo Start Date (Data inizio) da 9/1/2007 a 9/1/2013.

Changing start date to 2013

Jane fa clic su Salva per prima e vede la sua modifica quando il browser torna alla pagina di indice.

Budget changed to zero

John fa clic su Salva in una pagina Edit (Modifica) che visualizza ancora un budget pari a $ 350.000,00. Le operazioni successive dipendono da come si decide di gestire i conflitti di concorrenza.

La concorrenza ottimistica include le opzioni seguenti:

  • È possibile tenere traccia della proprietà che un utente ha modificato e aggiornare solo le colonne corrispondenti nel database.

    Con questo scenario non si registra la perdita di dati. I due utenti hanno aggiornato proprietà diverse. Quando un utente torna a visualizzare il reparto English (Inglese), visualizza sia le modifiche di Jane sia quelle di John. Questo metodo di aggiornamento riduce il numero di conflitti che possono comportare la perdita di dati. Questo approccio:

    • Non evita la perdita di dati se vengono apportate modifiche concorrenti alla stessa proprietà.
    • Risulta in genere poco pratico in un'app Web. Richiede la manutenzione di un volume importante di codice statico per tenere traccia di tutti i valori recuperati e i nuovi valori. La gestione di grandi quantità di codice statico può ridurre le prestazioni dell'applicazione.
    • Può rendere più complesse le app rispetto al rilevamento della concorrenza in un'entità.
  • È possibile consentire che la modifica di John sovrascriva la modifica di Jane.

    Quando un utente torna a visualizzare il reparto English (Inglese), visualizza 9/1/2013 e il valore $ 350.000,00 recuperato. Questo scenario è detto Priorità client o Last in Wins (Priorità ultimo accesso). Tutti i valori del client hanno la precedenza su ciò che si trova nell'archivio dati. Se non si esegue alcuna codifica per la gestione della concorrenza, il client vince automaticamente.

  • È possibile impedire che la modifica di John venga implementata nel database. In genere, l'app:

    • Visualizza un messaggio di errore.
    • Visualizza lo stato corrente dei dati.
    • Consente all'utente di riapplicare le modifiche.

    Questo scenario è detto Store Wins (Priorità archivio). I valori dell'archivio dati hanno la precedenza sui valori inviati dal client. In questa esercitazione si implementa lo scenario Delle vittorie nello Store. Questo metodo garantisce che nessuna modifica venga sovrascritta senza che un utente riceva un avviso.

Gestione della concorrenza

Quando una proprietà è configurata come token di concorrenza:

Il database e il modello di dati devono essere configurati per supportare la generazione di DbUpdateConcurrencyException.

Rilevamento dei conflitti di concorrenza per una proprietà

È possibile rilevare i conflitti di concorrenza a livello delle proprietà con l'attributo ConcurrencyCheck. L'attributo può essere applicato a più proprietà del modello. Per altre informazioni, vedere Data Annotations-ConcurrencyCheck (Annotazioni dei dati - ConcurrencyCheck).

L'attributo [ConcurrencyCheck] non viene usato in questa esercitazione.

Rilevamento dei conflitti di concorrenza per una riga

Per rilevare i conflitti di concorrenza si aggiunge al modello una colonna di rilevamento rowversion. rowversion :

  • È specifica per SQL Server. È possibile che altri database non dispongano di una funzionalità simile.
  • Viene usata per determinare che un'entità non è stata modificata dopo il suo recupero dal database.

Il database genera un numero rowversion sequenziale che viene incrementato ogni volta che la riga viene aggiornata. In un comando Update o Delete la clausola Where include il valore recuperato di rowversion. Se la riga che viene aggiornata è stata modificata:

  • rowversion non corrisponde al valore recuperato.
  • I comandi Update o Delete non trovano una riga perché la clausola Where include il valore rowversion recuperato.
  • Viene generata un'eccezione DbUpdateConcurrencyException.

In EF Core, quando nessuna riga è stata aggiornata da un Update comando o Delete , viene generata un'eccezione di concorrenza.

Aggiungere una proprietà di rilevamento all'entità Department

In Models/Department.csaggiungere una proprietà di rilevamento denominata 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; }
    }
}

L'attributo Timestamp specifica che questa colonna è inclusa nella clausola Where dei comandi Update e Delete. L'attributo viene chiamato Timestamp perché le versioni precedenti di SQL Server usavano un tipo di dati SQL timestamp prima che questo fosse sostituito dal tipo SQL rowversion.

L'API Fluent può anche specificare la proprietà di rilevamento:

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

Il codice seguente mostra una parte del T-SQL generato da EF Core quando viene aggiornato il nome del reparto:

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

Il codice evidenziato precedente visualizza la clausola WHERE contenente RowVersion. Se nel database RowVersion non è uguale al parametro RowVersion (@p2) non viene aggiornata nessuna riga.

Il codice evidenziato seguente visualizza la notazione T-SQL che verifica che è stata aggiornata esattamente una riga:

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 restituisce il numero di righe interessate dall'ultima istruzione. In nessuna riga viene aggiornata, EF Core genera un'eccezione DbUpdateConcurrencyException.

È possibile visualizzare l'errore T-SQL EF Core generato nella finestra di output di Visual Studio.

Aggiornare il database

L'aggiunta della proprietà RowVersion cambia il modello di database e ciò richiede una migrazione.

Compilare il progetto. Digitare quanto segue in una finestra di comando:

dotnet ef migrations add RowVersion
dotnet ef database update

I comandi precedenti:

  • Aggiunge il file di Migrations/{time stamp}_RowVersion.cs migrazione.

  • Aggiornamenti il Migrations/SchoolContextModelSnapshot.cs file. L'aggiornamento aggiunge al metodo BuildModel il codice evidenziato seguente:

  • Eseguono migrations per aggiornare il database.

Scaffolding del modello Departments (Reparti)

Seguire le istruzioni in Eseguire lo scaffolding del modello Student (Studente) e usare Department per la classe modello.

Il comando precedente esegue lo scaffolding del modello Department. Aprire il progetto in Visual Studio.

Compilare il progetto.

Aggiornare la pagina Departments Index (Indice reparti)

Il motore di scaffolding crea una colonna RowVersion per la pagina Index, ma questo campo non deve essere visualizzato. In questa esercitazione, l'ultimo byte di RowVersion viene visualizzato per facilitare la comprensione della concorrenza. L'univocità dell'ultimo byte non è garantita. Un'app reale non visualizza RowVersion o l'ultimo byte di RowVersion.

Aggiornare la pagina Index:

  • Sostituire Index con Departments.
  • Sostituire il markup che contiene RowVersion con l'ultimo byte di RowVersion.
  • Sostituire FirstMidName con FullName.

Il markup seguente visualizza la pagina aggiornata:

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

Aggiornare il modello di pagina Edit

Eseguire l'aggiornamento Pages/Departments/Edit.cshtml.cs con il codice seguente:

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

Per rilevare un problema di concorrenza, viene OriginalValue aggiornato con il rowVersion valore dell'entità recuperata. EF Core genera un comando SQL UPDATE con una clausola WHERE contenente il valore originale RowVersion . Se il comando UPDATE non ha effetto su nessuna riga (nessuna riga ha il valore originale RowVersion), viene generata un'eccezione DbUpdateConcurrencyException.

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;

Nel codice precedente, Department.RowVersion è il valore al momento del recupero dell'entità. OriginalValue è il valore presente nel database quando in questo metodo è stato chiamato FirstOrDefaultAsync.

Il codice seguente ottiene i valori del client (i valori inseriti in questo metodo) e i valori del database:

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

Il codice seguente aggiunge un messaggio di errore personalizzato per ogni colonna che ha valori del database diversi da quelli inseriti in 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.");
}

Il codice evidenziato seguente imposta il valore RowVersion sul nuovo valore recuperato dal database. Quando l'utente fa di nuovo clic su Salva vengono rilevati solo gli errori di concorrenza che si verificano dopo l'ultima visualizzazione della pagina Edit (Modifica).

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

L'istruzione ModelState.Remove è necessaria perché ModelState presenta il valore obsoleto RowVersion. Razor Nella pagina il ModelState valore di un campo ha la precedenza sui valori delle proprietà del modello quando sono presenti entrambi.

Aggiornare la pagina Edit

Eseguire l'aggiornamento Pages/Departments/Edit.cshtml con il markup seguente:

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

Il markup precedente:

  • Aggiorna la direttiva page da @page a @page "{id:int}".
  • Aggiunge una versione di riga nascosta. L'aggiunta di RowVersion è necessaria per far sì che il postback associ il valore.
  • Visualizza l'ultimo byte di RowVersion a scopo di debug.
  • Sostituisce ViewData con l'elemento InstructorNameSL fortemente tipizzato.

Eseguire il test dei conflitti di concorrenza con la pagina Edit (Modifica)

Aprire due istanze di browser con la pagina Edit (Modifica) e il reparto English (Inglese):

  • Eseguire l'app e selezionare Departments (Reparti).
  • Fare clic con il pulsante destro del mouse sul collegamento ipertestuale Edit (Modifica) per il reparto English (Inglese) e selezionare Apri in una nuova scheda.
  • Nella prima scheda fare clic sul collegamento ipertestuale Edit (Modifica) per il reparto English (Inglese).

Le due schede del browser visualizzano le stesse informazioni.

Modificare il nome nella prima scheda del browser e fare clic su Salva.

Department Edit page 1 after change

Il browser visualizza la pagina Index con il valore modificato e l'indicatore rowVersion aggiornato. Si noti l'indicatore rowVersion aggiornato, che è visualizzato sul secondo postback nell'altra scheda.

Modificare un altro campo nella seconda scheda del browser.

Department Edit page 2 after change

Fare clic su Salva. Vengono visualizzati messaggi di errore per tutti i campi che non corrispondono ai valori del database:

Department Edit page error message 1

Questa finestra del browser non prevedeva la modifica del campo Name (Nome). Copiare e incollare il valore corrente Languages (Lingue) nel campo Name (Nome). Esci. La convalida lato client rimuove il messaggio di errore.

Department Edit page error message 2

Fare di nuovo clic su Salva. Il valore immesso nella seconda scheda del browser viene salvato. I valori salvati vengono visualizzati nella pagina Index.

Aggiornare la pagina Delete (Elimina)

Aggiornare il modello di pagina Delete (Elimina) con il codice seguente:

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

La pagina Delete (Elimina) rileva i conflitti di concorrenza quando l'entità è stata modificata dopo il recupero. Department.RowVersion è la versione di riga quando l'entità è stata recuperata. Quando EF Core crea il comando SQL DELETE, include una clausola WHERE con RowVersion. Se il comando SQL DELETE non ha effetto su nessuna riga:

  • RowVersion nel comando SQL DELETE non corrisponde a RowVersion nel database.
  • Viene generata un'eccezione DbUpdateConcurrencyException.
  • OnGetAsync viene chiamata con concurrencyError.

Aggiornare la pagina Delete (Elimina)

Eseguire l'aggiornamento Pages/Departments/Delete.cshtml con il codice seguente:

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

Il codice precedente apporta le modifiche seguenti:

  • Aggiorna la direttiva page da @page a @page "{id:int}".
  • Aggiunge un messaggio di errore.
  • Sostituisce FirstMidName con FullName nel campo Administrator (Amministratore).
  • Modifica RowVersion per visualizzare l'ultimo byte.
  • Aggiunge una versione di riga nascosta. L'aggiunta di RowVersion è necessaria per far sì che il postback associ il valore.

Eseguire il test dei conflitti di concorrenza con la pagina Delete (Elimina)

Creare un reparto di test.

Aprire due istanze del browser con la pagina Delete (Elimina):

  • Eseguire l'app e selezionare Departments (Reparti).
  • Fare clic con il pulsante destro del mouse sul collegamento ipertestuale Delete (Elimina) per il reparto di test e selezionare Apri in una nuova scheda.
  • Fare clic sul collegamento ipertestuale Edit (Modifica) per il reparto di test.

Le due schede del browser visualizzano le stesse informazioni.

Modificare il budget nella prima scheda del browser e fare clic su Salva.

Il browser visualizza la pagina Index con il valore modificato e l'indicatore rowVersion aggiornato. Si noti l'indicatore rowVersion aggiornato, che è visualizzato sul secondo postback nell'altra scheda.

Eliminare il reparto di test dalla seconda scheda. Viene visualizzato un errore di concorrenza con i valori correnti del database. Facendo clic su Elimina viene eliminata l'entità, a meno che non RowVersion sia stato aggiornato.

Per informazioni su come ereditare un modello di dati, vedere Ereditarietà.

Risorse aggiuntive