Training course. Concurrency with Entity Framework in ASP.NET MVC

Published on January 16, 2012

Training course. Concurrency with Entity Framework in ASP.NET MVC

Original author: ASP.NET Team
  • Transfer
This is the final part of a series of articles on development using 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 address concurrent 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 are shown in the illustrations.

clip_image001

clip_image002

Conflicts of simultaneous access A

conflict of simultaneous access arises when one user views data about one entity and then edits them, while at the same time another user updates the 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 last one to update the database will overwrite the changes made earlier. In many applications, the risk is not critical: if there are several users, or several updates, or rewriting changes is not very critical, then the cost of concurrency-oriented programming will be higher than the benefit of this. In this case, it is not necessary to configure applications to handle such situations

Pessimistic Concurrency (Locking)

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

Lock management has its drawbacks. Programming can be too complicated, locks require serious database resources, and download overheads increase as the number of users of the application increases. In this regard, not all DBMSs support pessimistic concurrency. The Entity Framework does not provide a built-in mechanism for pessimistic concurrency, and this approach will not be considered in this lesson.

Optimistic Concurrency

An optimistic concurrency is an alternative to pessimistic concurrency.. Optimistic concurrency allows concurrent access conflicts to happen, but allows you to adequately respond to such situations. For example, John opens the Departments Edit page, changes the Budget value for the English branch from $ 350,000.00 to $ 100,000.00.

clip_image003

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

clip_image004

John pushes the Save button first and sees his changes, and at that moment Jane presses the button. What follows is dependent on how you handle this kind of situation. They can be processed using the following methods:
  • You can keep a record of which property has been edited and only update the corresponding columns in the database. In the example, the data will not be lost, since different properties have been edited by different users. Next time, when viewing the data on the English branch, the user will see the changes made by both John and Jane.
This method can reduce the number of situations with data loss, but cannot help when editing a single property of an entity. However, the use of this method is rarely seen in web applications due to the large amount of data that must be managed to track old and new property values. Managing large amounts of data can affect application performance.
  • You can let Jane change overwrite John change. Then the next user will see 1/1/1999 and the old value is $ 350,000.00. This script is called Client Wins or Last in Wins . This happens automatically if you do not change the behavior of the application.
  • You can not update the database for Jane’s changes: throw an error, show the current state of the data and let it refresh the page and enter its data again. This script is called Store Wins . We will use this scenario in this tutorial. It guarantees that the changes will not be lost, and that the user is timely notified of the simultaneous access conflict.
Concurrent Access Conflict Detection

You can resolve similar conflicts by handling the OptimisticConcurrencyException thrown by EF. In order to know when to throw this exception, EF must be able to determine when a conflict occurs. Therefore, you must properly configure the database and data model. You can use the following options for this setup:
  • In the table in the database, include a "tracking" column, which can be used to determine when the record changed. You can then include this column in the Where clause of an Update or Delete query.
The data type of this column is usually a timestamp, but it does not actually store a date or time. Instead, the value is equal to a digit increasing by one each time the data is updated (the same data type may be of the rowversion type in recent versions of SQL Server). In Update or Delete queries, the Where statement includes the original value of the "tracking" column. If a record is edited by the user during the update process, the value in this column is different from the original one, therefore Update and Delete queries cannot find this record. When the EF detects that nothing has been updated with the Update or Delete request, it sees this as a concurrent access conflict.
  • Configure EF to include the source data of each column in the Where statement of the Update and Delete queries.
As one option, if nothing in the record has changed since it was first loaded, the Where statement will not return the record for updating, which EF will perceive as a conflict. This option is as effective as the follow-up column option. However, if there is a table with many columns, the use of this approach may result in a large number of Where statements 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 a controller and views, and test everything together.

Note if you implement parallelism without a “tracking” column, you must mark all non-primary keys with the ConcurrencyCheck attribute, defining for EF that in the Where statement of the Update queries all columns will be

added Adding a “tracking” column to the Department entity

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

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

The Timestamp attribute determines that this column will be included in the Where statement of the Update and Delete queries.

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 full name instead of the last name is displayed in the drop-down lists of faculty administrators.

Replace the HttpPost Edit method code 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 "tracking" column in a hidden field. When creating a department instance, this object will not have a value in the Timestamp property. Then, after creating the EF Update request, the request will include a Where statement with a search condition for the record with the original Timestamp value.

If no records are updated with 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;

Further, 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); 
    // ...

On error, 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 retrieved from the database. This new value will be stored in a hidden field when the Edit page is refreshed, and the next time you click Save, only those concurrency errors that have occurred since the page was reloaded will be caught.

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 to shift the links of the entries 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>

Testing Optimistic Concurrency


Launch 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 with the changed data is displayed.

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 that you entered in the second browser window is saved in the database and you will see that the changes appeared on the Index page.

clip_image010

Adding a Delete Page

For the Delete page, concurrency issues are handled similarly. When the confirmation window is displayed by the HttpGet Delete method, the view includes the original Timestamp value in the 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 statement with the original Timestamp value. If the request does not return anything, concurrency exceptions are thrown, and the HttpGet Delete method is called with the error parameter set to true to reload the confirmation page with the error message.

In DepartmentController . cs replace the HttpGet Delete method code 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 to reload the page after a concurrency error. If set to true, the error message is sent to the view in the ViewBag property.

Replace the HttpPost Delete (DeleteConfirmed) method code 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 concurrency 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 on the same faculty. A confirmation window will appear.

clip_image011

Click Save in the first browser window. Changes will be 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 with confirmation that the faculty entry has been deleted.

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

Acknowledgments

Thank you for your help in translating Alexander Belotserkovsky.