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.
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 $.
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.
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 sqlUpdate
ouDelete
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
ouDelete
commande, laWhere
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 larowversion
colonne est différente de la valeur d’origine, de sorte que l’instruction ouDelete
l’instructionUpdate
ne trouve pas la ligne à mettre à jour en raison de laWhere
clause. Lorsque Entity Framework détecte qu’aucune ligne n’a été mise à jour par la ouDelete
laUpdate
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 desUpdate
commandes etDelete
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 volumineusesWhere
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
desUPDATE
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 :
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 :
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.
Modifiez un champ dans la première fenêtre du navigateur, puis cliquez sur Enregistrer.
Le navigateur affiche la page Index avec la valeur modifiée.
Modifiez le champ n’importe quel champ dans la deuxième fenêtre du navigateur, puis cliquez sur Enregistrer.
Cliquez sur Enregistrer dans la deuxième fenêtre du navigateur. Vous voyez un message d’erreur :
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.
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 :
La page Index confirme la modification.
Dans la deuxième fenêtre, cliquez sur Supprimer.
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.
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.