Tutoriel : Gérer l’accès concurrentiel - ASP.NET MVC avec EF Core

Dans les didacticiels précédents, vous avez découvert comment mettre à jour des données. Ce didacticiel montre comment gérer les conflits quand plusieurs utilisateurs mettent à jour la même entité en même temps.

Vous allez créer des pages web qui utilisent l’entité Department et gérer les erreurs d’accès concurrentiel. Les illustrations suivantes montrent les pages Edit et Delete, notamment certains messages qui sont affichés si un conflit d’accès concurrentiel se produit.

Department Edit page

Department Delete page

Dans ce tutoriel, vous allez :

  • En savoir plus sur les conflits d’accès concurrentiel
  • Ajouter une propriété de suivi
  • Créer un contrôleur Departments et des vues
  • Mettre à jour la vue Index
  • Mettre à jour les méthodes de modification
  • Mettre à jour la vue Edit
  • Tester les conflits d'accès concurrentiel
  • Mettre à jour la page Delete
  • Mettre à jour les vues Details et Create

Prérequis

Conflits d’accès concurrentiel

Un conflit d’accès concurrentiel se produit quand un utilisateur affiche les données d’une entité pour la modifier, puis qu’un autre utilisateur met à jour les données de la même entité avant que les modifications du premier utilisateur soient écrites dans la base de données. Si vous n’activez pas la détection de ces conflits, la personne qui met à jour la base de données en dernier remplace les modifications de l’autre utilisateur. Dans de nombreuses applications, ce risque est acceptable : s’il n’y a que quelques utilisateurs ou quelques mises à jour, ou s’il n’est pas réellement critique que des modifications soient remplacées, le coût de la programmation nécessaire à la gestion des accès concurrentiels peut être supérieur au bénéfice qu’elle apporte. Dans ce cas, vous ne devez pas configurer l’application pour gérer les conflits d’accès concurrentiel.

Accès concurrentiel pessimiste (verrouillage)

Si votre application doit éviter la perte accidentelle de données dans des scénarios d’accès concurrentiel, une manière de le faire consiste à utiliser des verrous de base de données. Ceci est appelé « accès concurrentiel pessimiste ». Par exemple, avant de lire une ligne d’une base de données, vous demandez un verrou pour lecture seule ou pour accès avec mise à jour. Si vous verrouillez une ligne pour accès avec mise à jour, aucun autre utilisateur n’est autorisé à verrouiller la ligne pour lecture seule ou pour accès avec mise à jour, car ils obtiendraient ainsi une copie de données qui sont en cours de modification. Si vous verrouillez une ligne pour accès en lecture seule, d’autres utilisateurs peuvent également la verrouiller pour accès en lecture seule, mais pas pour accès avec mise à jour.

La gestion des verrous présente des inconvénients. Elle peut être complexe à programmer. Elle nécessite des ressources de gestion de base de données importantes, et peut provoquer des problèmes de performances au fil de l’augmentation du nombre d’utilisateurs d’une application. Pour ces raisons, certains systèmes de gestion de base de données ne prennent pas en charge l’accès concurrentiel pessimiste. Entity Framework Core n’en fournit pas de prise en charge intégrée et ce didacticiel ne vous montre comment l’implémenter.

Accès concurrentiel optimiste

La solution alternative à l’accès concurrentiel pessimiste est l’accès concurrentiel optimiste. L’accès concurrentiel optimiste signifie autoriser la survenance des conflits d’accès concurrentiel, puis de réagir correctement quand ils surviennent. Par exemple, Jane consulte la page Department Edit et change le montant de « Budget » pour le département « English » en le passant de $350 000,00 à $0,00.

Changing budget to 0

Avant que Jane clique sur Save, John consulte la même page et change le champ Start Date de 01/09/2007 en 01/09/2013.

Changing start date to 2013

Jane clique la première sur Save et voit sa modification quand le navigateur revient à la page Index.

Budget changed to zero

John clique à son tour sur Save sur une page Edit qui affiche toujours un budget de $350 000,00. Ce qui se passe ensuite est déterminé par la façon dont vous gérez les conflits d’accès concurrentiel.

Voici quelques-unes des options :

  • Vous pouvez effectuer le suivi des propriétés modifiées par un utilisateur et mettre à jour seulement les colonnes correspondantes dans la base de données.

    Dans l’exemple de scénario, aucune donnée ne serait perdue, car des propriétés différentes ont été mises à jour par chacun des deux utilisateurs. La prochaine fois que quelqu’un examine le département « English », il voit à la fois les modifications de Jane et de John : une date de début au 01/09/2013 et un budget de zéro dollars. Cette méthode de mise à jour peut réduire le nombre de conflits qui peuvent entraîner des pertes de données, mais elle ne peut pas éviter la perte de données si des modifications concurrentes sont apportées à la même propriété d’une entité. Un tel fonctionnement d’Entity Framework dépend de la façon dont vous implémentez votre code de mise à jour. Il n’est pas souvent pratique dans une application web, car il peut nécessiter la gestion de grandes quantités d’états pour effectuer le suivi de toutes les valeurs de propriété d’origine d’une entité, ainsi que des nouvelles valeurs. La gestion de grandes quantités d’états peut affecter les performances de l’application, car elle nécessite des ressources serveur, ou doit être incluse dans la page web elle-même (par exemple dans des champs masqués) ou dans un cookie.

  • Vous pouvez laisser les modifications de John remplacer les modifications de Jane.

    La prochaine fois que quelqu’un consultera le département « English », il verra la date du 01/09/2013 et la valeur $350 000,00 restaurée. Ceci s’appelle un scénario Priorité au client ou Priorité au dernier entré (Last in Wins). (Toutes les valeurs provenant du client sont prioritaires par rapport à ce qui se trouve dans le magasin de données.) Comme indiqué dans l’introduction de cette section, si vous ne codez rien pour la gestion des accès concurrentiels, ceci se produit automatiquement.

  • Vous pouvez empêcher les modifications de John de faire l’objet d’une mise à jour dans la base de données.

    En règle générale, vous affichez un message d’erreur, vous lui montrez l’état actuel des données et vous lui permettez de réappliquer ses modifications s’il veut toujours les faire. Il s’agit alors d’un scénario Priorité au magasin. (Les valeurs du magasin de données sont prioritaires par rapport à celles soumises par le client.) Dans ce tutoriel, vous allez implémenter le scénario Priorité au magasin. Cette méthode garantit qu’aucune modification n’est remplacée sans qu’un utilisateur soit averti de ce qui se passe.

Détection des conflits d’accès concurrentiel

Vous pouvez résoudre les conflits en gérant les exceptions DbConcurrencyException levées par Entity Framework. Pour savoir quand lever ces exceptions, Entity Framework doit être en mesure de détecter les conflits. Par conséquent, vous devez configurer de façon appropriée la base de données et le modèle de données. Voici quelques options pour l’activation de la détection des conflits :

  • Dans la table de base de données, incluez une colonne de suivi qui peut être utilisée pour déterminer quand une ligne a été modifiée. Vous pouvez ensuite configurer Entity Framework pour inclure cette colonne dans la clause WHERE des commandes SQL UPDATE et DELETE.

    Le type de données de la colonne de suivi est généralement rowversion. La valeur de rowversion est un nombre séquentiel qui est incrémenté chaque fois que la ligne est mise à jour. Dans une commande UPDATE ou DELETE, la clause WHERE inclut la valeur d’origine de la colonne de suivi (la version d’origine de la ligne). Si la ligne à mettre à jour a été changée par un autre utilisateur, la valeur de la colonne rowversion est différente de la valeur d’origine : l’instruction UPDATE ou DELETE ne peut donc pas trouver la ligne à mettre à jour en raison de la clause WHERE. Quand Entity Framework trouve qu’aucune ligne n’a été mise à jour par la commande UPDATE ou DELETE (c’est-à-dire quand le nombre de lignes affectées est égal à zéro), il interprète ceci comme étant un conflit d’accès concurrentiel.

  • Configurez Entity Framework de façon à inclure les valeurs d’origine de chaque colonne dans la table de la clause WHERE des commandes UPDATE et DELETE.

    Comme dans la première option, si quelque chose dans la ligne a changé depuis la première lecture de la ligne, la clause WHERE ne retourne pas de ligne à mettre à jour, ce qui est interprété par Entity Framework comme un conflit d’accès concurrentiel. Pour les tables de base de données qui ont beaucoup de colonnes, cette approche peut aboutir à des clauses WHERE de très grande taille et nécessiter la gestion de grandes quantités d’états. Comme indiqué précédemment, la gestion de grandes quantités d’états peut affecter les performances de l’application. Par conséquent, cette approche n’est généralement pas recommandée et n’est pas la méthode utilisée dans ce didacticiel.

    Si vous ne voulez pas implémenter cette approche de l’accès concurrentiel, vous devez marquer toutes les propriétés qui ne sont pas des clés primaires de l’entité dont vous voulez suivre les accès concurrentiels en leur ajoutant l’attribut ConcurrencyCheck. Cette modification permet à Entity Framework d’inclure toutes les colonnes dans la clause SQL WHERE des instructions UPDATE et DELETE.

Dans le reste de ce didacticiel, vous ajoutez une propriété de suivi rowversion à l’entité Department, vous créez un contrôleur et des vues, et vous testez pour vérifier que tout fonctionne correctement.

Ajouter une propriété de suivi

Dans Models/Department.cs, ajoutez une propriété de suivi nommée 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’attribut Timestamp spécifie que cette colonne sera incluse dans la clause WHERE des commandes UPDATE et DELETE envoyées à la base de données. L’attribut est nommé Timestamp, car les versions précédentes de SQL Server utilisaient un type de données SQL timestamp avant son remplacement par le type SQL rowversion. Le type .NET pour rowversion est un tableau d’octets.

Si vous préférez utiliser l’API actuelle, vous pouvez utiliser la méthode IsConcurrencyToken (dans Data/SchoolContext.cs) pour spécifier la propriété de suivi, comme indiqué dans l’exemple suivant :

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

En ajoutant une propriété, vous avez changé le modèle de base de données et vous devez donc effectuer une autre migration.

Enregistrez vos modifications et générez le projet, puis entrez les commandes suivantes dans la fenêtre Commande :

dotnet ef migrations add RowVersion
dotnet ef database update

Créer un contrôleur Departments et des vues

Générez automatiquement un modèle de contrôleur Departments et des vues, comme vous l’avez fait précédemment pour les étudiants, les cours et les enseignants.

Scaffold Department

Dans le fichier DepartmentsController.cs, changez les quatre occurrences de « FirstMidName » en « FullName », de façon que les listes déroulantes de l’administrateur du département contiennent le nom complet de l’enseignant et non pas simplement son nom de famille.

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

Mettre à jour la vue Index

Le moteur de génération de modèles automatique a créé une colonne RowVersion pour la vue Index, mais ce champ ne doit pas être affiché.

Remplacez le code dans Views/Departments/Index.cshtml par le code suivant.

@model IEnumerable<ContosoUniversity.Models.Department>

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

<h2>Departments</h2>

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

Ceci change l’en-tête en « Departments », supprime la colonne RowVersion et montre à l’administrateur le nom complet au lieu du prénom.

Mettre à jour les méthodes de modification

Dans la méthode HttpGet Edit et la méthode Details, ajoutez AsNoTracking. Dans la méthode HttpGet Edit, ajoutez un chargement hâtif pour l’administrateur.

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

Remplacez le code existant pour la méthode HttpPost Edit méthode par le code suivant :

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
    if (id == null)
    {
        return NotFound();
    }

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

    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        await TryUpdateModelAsync(deletedDepartment);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

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

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

                if (databaseValues.Name != clientValues.Name)
                {
                    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
                }
                if (databaseValues.Budget != clientValues.Budget)
                {
                    ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
                }
                if (databaseValues.StartDate != clientValues.StartDate)
                {
                    ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
                }
                if (databaseValues.InstructorID != clientValues.InstructorID)
                {
                    Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
                    ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
                }

                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
                ModelState.Remove("RowVersion");
            }
        }
    }
    ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

Le code commence par essayer de lire le département à mettre à jour. Si la méthode FirstOrDefaultAsync retourne null, c’est que le département a été supprimé par un autre utilisateur. Dans ce cas, le code utilise les valeurs du formulaire envoyé pour créer une entité Department de façon que la page Edit puisse être réaffichée avec un message d’erreur. Vous pouvez aussi ne pas recréer l’entité Department si vous affichez seulement un message d’erreur sans réafficher les champs du département.

La vue stocke la valeur d’origine de RowVersion dans un champ masqué, et cette méthode reçoit cette valeur dans le paramètre rowVersion. Avant d’appeler SaveChanges, vous devez placer la valeur d’origine de la propriété RowVersion dans la collection OriginalValues pour l’entité.

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

Ensuite, quand Entity Framework crée une commande SQL UPDATE, cette commande inclut une clause WHERE qui recherche une ligne contenant la valeur d’origine de RowVersion. Si aucune ligne n’est affectée par la commande UPDATE (aucune ligne ne contient la valeur RowVersion d’origine), Entity Framework lève une exception DbUpdateConcurrencyException.

Le code du bloc catch pour cette exception obtient l’entité Department affectée qui a les valeurs mises à jour de la propriété Entries sur l’objet d’exception.

var exceptionEntry = ex.Entries.Single();

La collection Entries n’a qu’un objet EntityEntry. Vous pouvez utiliser cet objet pour obtenir les nouvelles valeurs entrées par l’utilisateur et les valeurs actuelles de la base de données.

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

Le code ajoute un message d’erreur personnalisé pour chaque colonne dont les valeurs dans la base de données diffèrent de ce que l’utilisateur a entré dans la page Edit (un seul champ est montré ici par souci de concision).

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

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

Enfin, le code affecte la nouvelle valeur récupérée auprès de la base de données à RowVersion pour departmentToUpdate. Cette nouvelle valeur de RowVersion est stockée dans le champ masqué quand la page Edit est réaffichée et, la prochaine fois que l’utilisateur clique sur Save, seules les erreurs d’accès concurrentiel qui se produisent depuis le réaffichage de la page Edit sont interceptées.

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

L’instruction ModelState.Remove est nécessaire car ModelState contient l’ancienne valeur RowVersion. Dans la vue, la valeur ModelState d’un champ est prioritaire par rapport aux valeurs de propriétés du modèle quand les deux sont présentes.

Mettre à jour la vue Edit

Dans Views/Departments/Edit.cshtml, apportez les changements suivants :

  • Ajoutez un champ masqué pour enregistrer la valeur de la propriété RowVersion, immédiatement après le champ masqué pour la propriété DepartmentID.

  • Ajoutez une option « Select Administrator » à la liste déroulante.

@model ContosoUniversity.Models.Department

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

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="DepartmentID" />
            <input type="hidden" asp-for="RowVersion" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
                <span asp-validation-for="InstructorID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Tester les conflits d'accès concurrentiel

Exécutez l’application et accédez à la page Index des départements. Cliquez avec le bouton droit sur le lien hypertexte Edit pour le département « English », sélectionnez Ouvrir dans un nouvel onglet, puis cliquez sur le lien hypertexte Edit pour le département « English ». Les deux onglets du navigateur affichent maintenant les mêmes informations.

Changez un champ sous le premier onglet du navigateur, puis cliquez sur Save.

Department Edit page 1 after change

Le navigateur affiche la page Index avec la valeur modifiée.

Changez un champ sous le deuxième onglet du navigateur.

Department Edit page 2 after change

Cliquez sur Enregistrer. Vous voyez un message d’erreur :

Department Edit page error message

Cliquez à nouveau sur Enregistrer. La valeur que vous avez entrée sous le deuxième onglet du navigateur est enregistrée. Vous voyez les valeurs enregistrées quand la page Index apparaît.

Mettre à jour la page Delete

Pour la page Delete, Entity Framework détecte les conflits d’accès concurrentiel provoqués par un autre utilisateur qui modifie le service de façon similaire. Quand la méthode HttpGet Delete affiche la vue de confirmation, la vue inclut la version d’origine de RowVersion dans un champ masqué. Cette valeur est ensuite disponible pour la méthode HttpPost Delete qui est appelée quand l’utilisateur confirme la suppression. Quand Entity Framework crée la commande SQL DELETE, il inclut une clause WHERE avec la valeur d’origine de RowVersion. Si la commande a pour résultat qu’aucune ligne n’est affectée (ce qui signifie que la ligne a été modifiée après l’affichage de la page de confirmation de la suppression), une exception d’accès concurrentiel est levée et la méthode HttpGet Delete est appelée avec un indicateur d’erreur défini sur true pour réafficher la page de confirmation avec un message d’erreur. Il est également possible qu’aucune ligne ne soit affectée en raison du fait que la ligne a été supprimée par un autre utilisateur : dans ce cas, aucun message d’erreur n’est donc affiché.

Mettre à jour les méthodes Delete dans le contrôleur Departments

Dans DepartmentsController.cs, remplacez la méthode Delete HttpGet par le code suivant :

public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return NotFound();
    }

    var department = await _context.Departments
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.DepartmentID == id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction(nameof(Index));
        }
        return NotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

La méthode accepte un paramètre facultatif qui indique si la page est réaffichée après une erreur d’accès concurrentiel. Si cet indicateur a la valeur true et que le département spécifié n’existe plus, c’est qu’il a été supprimé par un autre utilisateur. Dans ce cas, le code redirige vers la page Index. Si cet indicateur a la valeur true et que le département existe, c’est qu’il a été modifié par un autre utilisateur. Dans ce cas, le code envoie un message d’erreur à la vue en utilisant ViewData.

Remplacez le code de la méthode HttpPost Delete (nommée DeleteConfirmed) par le code suivant :

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
        {
            _context.Departments.Remove(department);
            await _context.SaveChangesAsync();
        }
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateConcurrencyException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
    }
}

Dans le code du modèle généré automatiquement que vous venez de remplacer, cette méthode n’acceptait qu’un seul ID d’enregistrement :

public async Task<IActionResult> DeleteConfirmed(int id)

Vous avez changé ce paramètre en une instance d’entité Department créée par le classeur de modèles. Ceci permet à Entity Framework d’accéder à la valeur de la propriété RowVersion en plus de la clé d’enregistrement.

public async Task<IActionResult> Delete(Department department)

Vous avez également changé le nom de la méthode d’action de DeleteConfirmed en Delete. Le code du modèle généré automatiquement utilisait le nom DeleteConfirmed pour donner à la méthode HttpPost une signature unique. (Pour le CLR, les méthodes surchargées doivent avoir des paramètres de méthode différents.) Maintenant que les signatures sont uniques, vous pouvez appliquer la convention MVC et utiliser le même nom pour les méthodes delete HttpPost et HttpGet.

Si le département est déjà supprimé, la méthode AnyAsync retourne la valeur false et l’application revient simplement à la méthode Index.

Si une erreur d’accès concurrentiel est interceptée, le code réaffiche la page de confirmation de suppression et fournit un indicateur indiquant qu’elle doit afficher un message d’erreur d’accès concurrentiel.

Mettre à jour la vue Delete

Dans Views/Departments/Delete.cshtml, remplacez le code du modèle généré automatiquement par le code suivant, qui ajoute un champ de message d’erreur et des champs masqués pour les propriétés DepartmentID et RowVersion. Les modifications sont mises en surbrillance.

@model ContosoUniversity.Models.Department

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

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

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

Ceci apporte les modifications suivantes :

  • Ajoute un message d’erreur entre les titres h2 et h3.

  • Il remplace FirstMidName par FullName dans le champ Administrator.

  • Supprime le champ RowVersion.

  • Ajoute un champ masqué pour la propriété RowVersion.

Exécutez l’application et accédez à la page Index des départements. Cliquez avec le bouton droit sur le lien hypertexte Delete pour le département « English », sélectionnez Ouvrir dans un nouvel onglet puis, sous le premier onglet, cliquez sur le lien hypertexte Edit pour le département « English ».

Dans la première fenêtre, changez une des valeurs, puis cliquez sur Save :

Department Edit page after change before delete

Sous le deuxième onglet, cliquez sur Delete. Vous voyez le message d’erreur d’accès concurrentiel et les valeurs du département sont actualisées avec ce qui est actuellement dans la base de données.

Department Delete confirmation page with concurrency error

Si vous recliquez sur Delete, vous êtes redirigé vers la page Index, qui montre que le département a été supprimé.

Mettre à jour les vues Details et Create

Vous pouvez éventuellement nettoyer le code du modèle généré automatiquement dans les vues Details et Create.

Remplacez le code de Views/Departments/Details.cshtml pour supprimer la colonne RowVersion et afficher le nom complet de l’administrateur.

@model ContosoUniversity.Models.Department

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

<h2>Details</h2>

<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

Remplacez le code de Views/Departments/Create.cshtml pour ajouter une option de sélection à la liste déroulante.

@model ContosoUniversity.Models.Department

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

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Obtenir le code

Télécharger ou afficher l’application complète.

Ressources supplémentaires

Pour plus d’informations sur la gestion de l’accès concurrentiel dans EF Core, consultez Conflits d’accès concurrentiel.

Étapes suivantes

Dans ce tutoriel, vous allez :

  • Découvrez les conflits d’accès concurrentiel
  • Propriété de suivi ajoutée
  • Créez un contrôleur Departments et des vues
  • Vue Index mise à jour
  • Méthodes de modification mises à jour
  • Vue Edit mise à jour
  • Conflits d’accès concurrentiel testés
  • Mettre à jour la page Delete
  • Vues Details et Create mises à jour

Passez au tutoriel suivant pour découvrir comment implémenter l’héritage table par hiérarchie pour les entités Instructor et Student.