Partager via


Gestion de l’accès concurrentiel avec Entity Framework dans une application MVC ASP.NET (7 sur 10)

par Tom Dykstra

L’exemple d’application web Contoso University montre comment créer ASP.NET applications MVC 4 à l’aide d’Entity Framework 5 Code First et de Visual Studio 2012. Pour obtenir des informations sur la série de didacticiels, consultez le premier didacticiel de la série.

Remarque

Si vous rencontrez un problème, téléchargez le chapitre terminé et essayez de reproduire votre problème. Vous pouvez généralement trouver la solution au problème en comparant votre code au code terminé. Pour certaines erreurs courantes et comment les résoudre, consultez Erreurs et solutions de contournement.

Dans les deux didacticiels précédents, vous avez travaillé avec des données associées. Ce tutoriel montre comment gérer l’accès concurrentiel. Vous allez créer des pages web qui fonctionnent avec l’entité Department , et les pages qui modifient et suppriment Department des entités gèrent les erreurs d’accès concurrentiel. Les illustrations suivantes montrent les pages Index et Suppression, y compris certains messages affichés si un conflit d’accès concurrentiel se produit.

Capture d’écran montrant la page Services de l’université Contoso avant les modifications.

Capture d’écran montrant la page Université avec un message expliquant que l’opération est annulée, car la valeur a été modifiée par un autre utilisateur.

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.

Concurrence 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. C’est ce qu’on appelle la concurrence 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 importantes de gestion de base de données et peut entraîner des problèmes de performances au fur et à mesure que le nombre d’utilisateurs d’une application augmente (autrement dit, il n’est pas correctement mis à l’échelle). 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 ne fournit aucune prise en charge intégrée, et ce didacticiel ne vous montre pas comment l’implémenter.

Accès concurrentiel optimiste

L’alternative à la concurrence 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, John exécute la page De modification des services, modifie le montant du budget du service anglais de 350 000,000 $ à 0,00 $.

Changing_English_dept_budget_to_100000

Avant que John clique sur Enregistrer, Jane exécute la même page et modifie le champ Date de début du 1er/1/2007 au 8/8/2013.

Changing_English_dept_start_date_to_1999

John clique d’abord sur Enregistrer et voit sa modification lorsque le navigateur revient à la page Index, puis Jane clique sur Enregistrer. 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 navigue dans le département anglais, ils verront à la fois les modifications de John et Jane , une date de début du 8/8/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’état 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 les champs masqués).

  • Vous pouvez laisser Jane changer le changement de John. La prochaine fois que quelqu’un navigue dans le département anglais, il verra 8/8/2013 et la valeur restaurée de 350 000,000 $. Ceci s’appelle un scénario Priorité au client ou Priorité au dernier entré (Last in Wins). (Les valeurs du client sont prioritaires sur ce qui se trouve dans le magasin de données.) Comme indiqué dans l’introduction à cette section, si vous ne codez pas pour la gestion de la concurrence, cela se produit automatiquement.

  • Vous pouvez empêcher la mise à jour de Jane dans la base de données. En règle générale, vous affichez un message d’erreur, affichez-lui l’état actuel des données et lui permet de réappliquer ses modifications si elle souhaite toujours les apporter. 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 OptimisticConcurrencyException 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 Where clause sql Update ou Delete les commandes.

    Le type de données de la colonne de suivi est généralement rowversion. La valeur rowversion est un nombre séquentiel incrémenté chaque fois que la ligne est mise à jour. Dans une Update ou Delete commande, la Where clause inclut la valeur d’origine de la colonne de suivi (version de ligne d’origine). Si la ligne mise à jour a été modifiée par un autre utilisateur, la valeur de la rowversion colonne est différente de la valeur d’origine, de sorte que l’instruction ou Delete l’instruction Update ne trouve pas la ligne à mettre à jour en raison de la Where clause. Lorsque Entity Framework détecte qu’aucune ligne n’a été mise à jour par la ou Delete la Update commande (autrement dit, lorsque le nombre de lignes affectées est égal à zéro), il interprète qu’il s’agit d’un conflit d’accès concurrentiel.

  • Configurez Entity Framework pour inclure les valeurs d’origine de chaque colonne de la table dans la Where clause des Update commandes et Delete des commandes.

    Comme dans la première option, si quelque chose dans la ligne a changé depuis la première lecture de la ligne, la Where clause ne retourne pas de ligne à mettre à jour, que Entity Framework interprète comme un conflit d’accès concurrentiel. Pour les tables de base de données qui ont de nombreuses colonnes, cette approche peut entraîner des clauses très volumineuses Where et peut nécessiter que vous mainteniez de grandes quantités d’état. Comme indiqué précédemment, la gestion de grandes quantités d’état 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 conséquent, cette approche n’est généralement pas recommandée et elle n’est pas la méthode utilisée dans ce didacticiel.

    Si vous souhaitez implémenter cette approche de concurrence, vous devez marquer toutes les propriétés de clé non primaire dans l’entité pour laquelle vous souhaitez suivre la concurrence en ajoutant l’attribut ConcurrencyCheck à ceux-ci. Cette modification permet à Entity Framework d’inclure toutes les colonnes dans la clause SQL WHERE des UPDATE instructions.

Dans le reste de ce tutoriel, vous allez ajouter une propriété de suivi rowversion à l’entité Department , créer un contrôleur et des vues et tester pour vérifier que tout fonctionne correctement.

Ajouter une propriété d’accès concurrentiel optimiste à l’entité Department

Dans Models\Department.cs, ajoutez une propriété de suivi nommée RowVersion:

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)]
    public DateTime StartDate { get; set; }

    [Display(Name = "Administrator")]
    public int? InstructorID { get; set; }

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

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

L’attribut Timestamp spécifie que cette colonne sera incluse dans la Where clause des commandes envoyées Delete à la base de Update données. L’attribut est appelé Timestamp, car les versions précédentes de SQL Server utilisaient un type de données d’horodatage SQL avant que la version de ligne SQL ne la remplace. Le type .Net pour rowversion est un tableau d’octets. Si vous préférez utiliser l’API Fluent, vous pouvez utiliser la méthode IsConcurrencyToken pour spécifier la propriété de suivi, comme illustré dans l’exemple suivant :

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

Consultez le problème GitHub Remplacer IsConcurrencyToken par IsRowVersion.

En ajoutant une propriété, vous avez changé le modèle de base de données et vous devez donc effectuer une autre migration. Dans la console du Gestionnaire de package, entrez les commandes suivantes :

Add-Migration RowVersion
Update-Database

Créer un contrôleur de service

Créez un Department contrôleur et affichez la même façon que les autres contrôleurs à l’aide des paramètres suivants :

Add_Controller_dialog_box_for_Department_controller

Dans Controllers\DepartmentController.cs, ajoutez une using instruction :

using System.Data.Entity.Infrastructure;

Remplacez « LastName » par « FullName » partout dans ce fichier (quatre occurrences) afin que les listes déroulantes d’administrateur de service contiennent le nom complet du formateur plutôt que le nom de famille.

ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
    Department department)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(department).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DbUpdateConcurrencyException ex)
   {
      var entry = ex.Entries.Single();
      var clientValues = (Department)entry.Entity;
      var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

      if (databaseValues.Name != clientValues.Name)
         ModelState.AddModelError("Name", "Current value: "
             + databaseValues.Name);
      if (databaseValues.Budget != clientValues.Budget)
         ModelState.AddModelError("Budget", "Current value: "
             + String.Format("{0:c}", databaseValues.Budget));
      if (databaseValues.StartDate != clientValues.StartDate)
         ModelState.AddModelError("StartDate", "Current value: "
             + String.Format("{0:d}", databaseValues.StartDate));
      if (databaseValues.InstructorID != clientValues.InstructorID)
         ModelState.AddModelError("InstructorID", "Current value: "
             + db.Instructors.Find(databaseValues.InstructorID).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.");
      department.RowVersion = databaseValues.RowVersion;
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
      ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
   }

   ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID);
   return View(department);
}

La vue stocke la valeur d’origine RowVersion dans un champ masqué. Lorsque le classeur de modèles crée l’instance department , cet objet aura la valeur de propriété d’origine RowVersion et les nouvelles valeurs des autres propriétés, comme entré par l’utilisateur dans la page Modifier. Ensuite, lorsque Entity Framework crée une commande SQL UPDATE , cette commande inclut une clause qui recherche une WHERE ligne qui a la valeur d’origine RowVersion .

Si aucune ligne n’est affectée par la UPDATE commande (aucune ligne n’a la valeur d’origine RowVersion ), Entity Framework lève une DbUpdateConcurrencyException exception et le code du catch bloc obtient l’entité affectée Department de l’objet exception. Cette entité contient à la fois les valeurs lues à partir de la base de données et les nouvelles valeurs entrées par l’utilisateur :

var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

Ensuite, le code ajoute un message d’erreur personnalisé pour chaque colonne qui a des valeurs de base de données différentes de ce que l’utilisateur a entré dans la page Modifier :

if (databaseValues.Name != currentValues.Name)
    ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
    // ...

Un message d’erreur plus long explique ce qui s’est passé et ce qu’il faut faire à ce sujet :

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

Enfin, le code définit la RowVersion valeur de l’objet Department sur la nouvelle valeur récupérée à partir de la base de données. 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.

Dans Views\Department\Edit.cshtml, ajoutez un champ masqué pour enregistrer la valeur de la RowVersion propriété, immédiatement après le champ masqué de la DepartmentID propriété :

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Department</legend>

        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>

Dans Views\Department\Index.cshtml, remplacez le code existant par le code suivant pour déplacer les liens de ligne vers la gauche et modifier le titre de la page et les en-têtes de colonne à afficher FullName au lieu de LastName la colonne Administrateur :

@model IEnumerable<ContosoUniversity.Models.Department>

@{
    ViewBag.Title = "Departments";
}

<h2>Departments</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Name</th>
        <th>Budget</th>
        <th>Start Date</th>
        <th>Administrator</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
            @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
        </td>
        <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>
    </tr>
}

</table>

Test de la gestion de l’accès concurrentiel optimiste

Exécutez le site, puis cliquez sur Services :

Capture d’écran montrant la page Services de l’université Contoso.

Cliquez avec le bouton droit sur le lien hypertexte Modifier pour Kim Abercrombie, puis sélectionnez Ouvrir dans un nouvel onglet, puis cliquez sur le lien hypertexte Modifier pour Kim Abercrombie. Les deux fenêtres affichent les mêmes informations.

Department_Edit_page_before_changes

Modifiez un champ dans la première fenêtre du navigateur, puis cliquez sur Enregistrer.

Department_Edit_page_1_after_change

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

Departments_Index_page_after_first_budget_edit

Modifiez le champ n’importe quel champ dans la deuxième fenêtre du navigateur, puis cliquez sur Enregistrer.

Department_Edit_page_2_after_change

Cliquez sur Enregistrer dans la deuxième fenêtre du navigateur. Vous voyez un message d’erreur :

Capture d’écran montrant la page Université avec un message d’erreur, prêt pour que l’utilisateur sélectionne Enregistrer à nouveau.

Cliquez à nouveau sur Enregistrer. La valeur que vous avez entrée dans le deuxième navigateur est enregistrée avec la valeur d’origine des données que vous modifiez dans le premier navigateur. Vous voyez les valeurs enregistrées quand la page Index apparaît.

Department_Index_page_with_change_from_second_browser

Mise à jour de la page Supprimer

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. Lorsque la HttpGet Delete méthode affiche la vue de confirmation, la vue inclut la valeur d’origine RowVersion dans un champ masqué. Cette valeur est ensuite disponible pour la HttpPost Delete méthode appelée lorsque l’utilisateur confirme la suppression. Lorsque Entity Framework crée la commande SQL DELETE , elle inclut une WHERE clause avec la valeur d’origine RowVersion . Si la commande entraîne l’incidence de zéro ligne (ce qui signifie que la ligne a été modifiée après l’affichage de la page de confirmation Delete), une exception d’accès concurrentiel est levée et la HttpGet Delete méthode est appelée avec un indicateur d’erreur défini true pour réinscrire la page de confirmation avec un message d’erreur. Il est également possible que zéro ligne ait été affectée parce que la ligne a été supprimée par un autre utilisateur. Dans ce cas, un autre message d’erreur s’affiche.

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

public ActionResult Delete(int id, bool? concurrencyError)
{
    Department department = db.Departments.Find(id);

    if (concurrencyError.GetValueOrDefault())
    {
        if (department == null)
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was deleted by another user after you got the original values. "
                + "Click the Back to List hyperlink.";
        }
        else
        {
            ViewBag.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 est true, un message d’erreur est envoyé à la vue à l’aide d’une ViewBag propriété.

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError=true } );
    }
    catch (DataException /* dex */)
    {
        //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
        ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
        return View(department);
    }
}

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 ActionResult DeleteConfirmed(int id)

Vous avez changé ce paramètre en une instance d’entité Department créée par le classeur de modèles. Cela vous donne accès à la valeur de propriété RowVersion en plus de la clé d’enregistrement.

public ActionResult Delete(Department department)

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

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.

Dans Views\Department\Delete.cshtml, remplacez le code généré par le code généré par le code suivant qui apporte des modifications de mise en forme et ajoute un champ de message d’erreur. Les modifications sont mises en surbrillance.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
    <legend>Department</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Name)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Budget)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Budget)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.StartDate)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.StartDate)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Administrator.FullName)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Administrator.FullName)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
   @Html.HiddenFor(model => model.DepartmentID)
    @Html.HiddenFor(model => model.RowVersion)
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "Index")
    </p>
}

Ce code ajoute un message d’erreur entre les titres et h3 les h2 en-têtes :

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

Elle se remplace LastName FullName par dans le Administrator champ :

<div class="display-label">
    @Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
    @Html.DisplayFor(model => model.Administrator.FullName)
</div>

Enfin, il ajoute des champs masqués pour les propriétés et RowVersion les DepartmentID propriétés après l’instruction Html.BeginForm :

@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)

Exécutez la page d’index des services. Cliquez avec le bouton droit sur le lien hypertexte Supprimer pour le service anglais, puis sélectionnez Ouvrir dans la nouvelle fenêtre, puis dans la première fenêtre, cliquez sur le lien hypertexte Modifier pour le service anglais.

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

Department_Edit_page_after_change_before_delete

La page Index confirme la modification.

Departments_Index_page_after_budget_edit_before_delete

Dans la deuxième fenêtre, cliquez sur Supprimer.

Department_Delete_confirmation_page_before_concurrency_error

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é.

Résumé

Ceci termine l’introduction à la gestion des conflits d’accès concurrentiel. Pour plus d’informations sur d’autres façons de gérer différents scénarios d’accès concurrentiel, consultez Les modèles d’accès concurrentiel optimiste et utilisation des valeurs de propriété sur le blog de l’équipe Entity Framework. Le tutoriel suivant montre comment implémenter l’héritage table par hiérarchie pour les entités et Student les Instructor entités.

Vous trouverez des liens vers d’autres ressources Entity Framework dans la carte de contenu d’accès aux données ASP.NET.