📜 ⬆️ ⬇️

Training course. Working with concurrency with the Entity Framework in an ASP.NET MVC application

This is the final part of a series of articles devoted to developing using the Entity Framework and ASP.NET MVC 3. You can find the first chapters at the following links:
In previous lessons you worked with related data. In this lesson we will look at simultaneous access issues. You will create pages that work with the Department entity, and pages for editing and deleting Department entities will also handle concurrency errors. The results of the work are shown in the illustrations.

clip_image001

clip_image002

Concurrent access conflicts
')
A concurrent access conflict occurs when one user views data about one entity and then edits it, and at the same time another user updates this same data before changes made by the first user are saved to the database. If the EF is not configured to detect such conflicts, the one who last updates the database will overwrite changes made earlier. In many applications, risk is not critical: if there are several users, or several updates, or rewriting changes is not very critical, the price of concurrency-oriented programming will be higher than the benefit from this. In this case, it is not necessary to set up applications to handle such situations.

Pessimistic Concurrency (Locking)

If an application needs to prevent accidental data loss due to concurrent access conflicts, one of the methods to solve the problem is to lock the tables. This is called pessimistic concurrency . For example, before loading a record from a database, you request a lock for read-only or update access. If you block change access in this way, no other user can block this record for read-only or change access, as they only get a copy of the data. If you block a record for read-only access, others can also block it for read-only access, but not for a change.

Managing locks has its drawbacks. Programming can be too complicated, locks need serious database resources, and the overhead of loading increases as the number of users of the application increases. In this regard, not all DBMSs support pessimistic parallelism. The Entity Framework does not provide a built-in mechanism for providing pessimistic concurrency, and this approach will not be considered in this lesson.

Optimistic concurrency

As an alternative to pessimistic concurrency (pessimistic concurrency), optimistic concurrency appears. Optimistic concurrency allows simultaneous access conflicts to occur, but allows you to adequately respond to such situations. For example, John opens the Departments Edit page, changes the Budget value for an English branch from $ 350,000.00 to $ 100,000.00.

clip_image003

Before John saves the Save button, Jane opens the same page and changes the Start Date value to 1/1/1999.

clip_image004

John presses the Save button first and sees his changes, and at that moment Jane presses the button. What follows from this depends on how you handle this kind of situation. They can be processed by the following methods:
This method can reduce the number of situations with data loss, but will not be able to help when editing a single entity property. However, the use of this method is rarely found in web applications due to the large amount of data that needs to be managed to keep track of old and new property values. Managing large amounts of data can affect application performance.
Concurrent Access Detection

You can resolve such conflicts with the exception handling of the OptimisticConcurrencyException thrown by the EF. In order to know when to throw out this exception, the EF must be able to determine the moment of the occurrence of the conflict. Therefore, it is necessary to properly configure the database and data model. You can use the following options for such a setting:
The data type of this column is usually timestamp, but in fact it does not store the date or time. Instead, the value is equal to the digit increasing by one each time data is updated (the same data type can be of rowversion type in recent versions of SQL Server). In Update or Delete requests, the Where clause includes the original value of the “witness” column. If the record is being edited by the user during the update process, the value in this column is different from the original one, so the Update and Delete queries will not be able to find this record. When EF detects that nothing has been updated with the Update or Delete request, it regards this as a concurrent access conflict.
As one of the options, if nothing in the record has changed since it was first loaded, the Where operator will not return the record for updating, which EF will perceive as a conflict. This option is as effective as the “tracking” column option. However, if there is a table with multiple columns, this approach can result in a large number of Where clauses and the need to manage large arrays of data and states. This approach is not recommended for use in most cases.

In this lesson, we will add a “tracking” column to the Department entity, create the controller and views, and test everything in conjunction.

Note If you implement concurrency without a “witness” column, you must mark all non-primary keys with the ConcurrencyCheck attribute, specifying for EF that all columns will be included in the Where statement of the Update request

Adding a “witness” column to a Department entity

In Models \ Department . cs add a “witness” column:

[Timestamp] public Byte[] Timestamp { get; set; } 

The Timestamp attribute specifies that this column will be included in the Where clause of Update and Delete requests.

Creating a controller

Create a Department controller and views:

clip_image005

In Controllers \ DepartmentController.cs, add using:

using System.Data.Entity.Infrastructure;

Change LastName to FullName in the entire file (four entries) so that the drop-down lists of faculty administrators display the full name instead of the last name.

Replace the code for the HttpPost Edit method with:

 [HttpPost] public ActionResult Edit(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 databaseValues = (Department)entry.GetDatabaseValues().ToObject(); var clientValues = (Department)entry.Entity; 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.Timestamp = databaseValues.Timestamp; } catch (DataException) { //Log the error (add a variable name after Exception) 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); } 

The view displays the original value of the “witness” column in a hidden field. When creating a department instance, this object will have no value in the Timestamp property. Then, after creating the EF Update request, the query will include a Where clause with the search condition for the record with the original Timestamp value.

If no records are updated by the Update request, EF will throw a DbUpdateConcurrencyException exception, and the code in the catch block will return the Department entity associated with the exception. This entity has original and new property values:

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

Next, the code adds an error message for each column that has values ​​in the database that differ from what the user entered on the Edit page:

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

If an error occurs, a detailed message is displayed:

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

Finally, the code sets the value of the Timestamp property for the Department object to a new value obtained from the database. This new value will be saved in a hidden field when the Edit page is refreshed, and the next time you click Save, only those parallelism errors that have occurred since the page was reloaded will be captured.

In Views \ Department \ Edit . cshtml add a hidden field to save the Timestamp value, immediately after the hidden field for the DepartmentID property:

Html .HiddenFor (model => model.Timestamp)

In Views \ Department \ Index . cshtml replace the code so that you move the links of the records to the left and change the page headers and columns:

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

Optimistic Concurrency Performance Review


Run the project and click on Departments :

clip_image001[1]

Click the Edit link and then in the new browser window open another Edit page. Windows should display identical information.

clip_image006

Change the field in the first browser window and click Save .

clip_image007

The Index page is displayed with the changed data.

clip_image008

Change the same field to a different value in the second browser window.

clip_image009

Click Save to see the error message:

clip_image002[1]

Click Save again. The value you entered in the second browser window is saved in the database and you will see that the changes appear on the Index page.

clip_image010

Add Delete Page

For the Delete page, concurrency issues are handled in a similar way. When the confirmation window is displayed by the HttpGet Delete method, the view includes the original Timestamp value in a hidden field. This value is available to the HttpPost Delete method, which is called when the user confirms the deletion. When EF creates a Delete request, this request includes a Where clause with the original Timestamp value. If the request returns nothing, a parallelism exception is thrown, and the HttpGet Delete method is called with the error parameter set to true to reload the confirmation page with an error message.

In the DepartmentController . cs replace the code of the HttpGet Delete method with:

 public ActionResult Delete(int id, bool? concurrencyError) { if (concurrencyError.GetValueOrDefault()) { 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."; } Department department = db.Departments.Find(id); return View(department); } 

The method accepts an optional parameter that determines whether the page needs to be reloaded after a parallel error. If the parameter is set to true, the error message is sent to the view in the ViewBag property.

Replace the code of the HttpPost Delete (DeleteConfirmed) method with:

 [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(Department department) { try { db.Entry(department).State = EntityState.Deleted; db.SaveChanges(); return RedirectToAction("Index"); } catch (DbUpdateConcurrencyException) { return RedirectToAction("Delete", new System.Web.Routing.RouteValueDictionary { { "concurrencyError", true } }); } catch (DataException) { //Log the error (add a variable name after Exception) ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator."); return View(department); } } 

Initially, the method accepted only the value of the record ID:

public ActionResult DeleteConfirmed (int id)


We changed this parameter to the Department entity, which gives us access to the Timestamp property.

public ActionResult DeleteConfirmed (Department department)

If a parallelism error is thrown, the code reloads the confirmation page with the error parameter set.

In Views \ Department \ Delete . cshtml replace the code with the code that provides the formatting and the field for the error message:

 @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.LabelFor(model => model.Name) </div> <div class="display-field"> @Html.DisplayFor(model => model.Name) </div> <div class="display-label"> @Html.LabelFor(model => model.Budget) </div> <div class="display-field"> @Html.DisplayFor(model => model.Budget) </div> <div class="display-label"> @Html.LabelFor(model => model.StartDate) </div> <div class="display-field"> @Html.DisplayFor(model => model.StartDate) </div> <div class="display-label"> @Html.LabelFor(model => model.InstructorID) </div> <div class="display-field"> @Html.DisplayFor(model => model.Administrator.FullName) </div> </fieldset> @using (Html.BeginForm()) { @Html.HiddenFor(model => model.DepartmentID) @Html.HiddenFor(model => model.Timestamp) <p> <input type="submit" value="Delete" /> | @Html.ActionLink("Back to List", "Index") </p> } 

This code adds an error message between the h2 and h3 headers:

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

It replaces LastName with FullName in the Administrator field:

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

And finally, hidden fields for DepartmentID and Timestamp are added:

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

Open the Departments Index page in different browser windows.

In the first window, click Edit and change one of the values, but do not click Save :

clip_image003[1]

In the second window, click Delete in the same department. A confirmation window will appear.

clip_image011

Click Save in the first browser window. Changes are confirmed.

clip_image012

Now click Delete in the second browser window to see the concurrency error message. Data will be updated.

clip_image013

If you click Delete again, the Index page will open, confirming that the faculty record has been deleted.

We have completed the entry into handling concurrent access conflicts. For more information, see Optimistic Concurrency Patterns and Working with Property Values . In the next lesson, we will show you how to implement inheritance for Instructor and Student entities.

Acknowledgments

Thanks for the help in the translation of Alexander Belotserkovsky.

Source: https://habr.com/ru/post/136328/


All Articles