"
ASP.NET (snapshot 2017) Microsoft documentation and samples

Handling Concurrency with the Entity Framework 6 in an ASP.NET MVC 5 Application (10 of 12)

by Tom Dykstra

Download Completed Project or Download PDF

The Contoso University sample web application demonstrates how to create ASP.NET MVC 5 applications using the Entity Framework 6 Code First and Visual Studio 2013. For information about the tutorial series, see the first tutorial in the series.

In earlier tutorials you learned how to update data. This tutorial shows how to handle conflicts when multiple users update the same entity at the same time.

You’ll change the web pages that work with the Department entity so that they handle concurrency errors. The following illustrations show the Index and Delete pages, including some messages that are displayed if a concurrency conflict occurs.

Department_Index_page_before_edits
Department_Index_page_before_edits
Department_Edit_page_2_after_clicking_Save
Department_Edit_page_2_after_clicking_Save

Concurrency Conflicts

A concurrency conflict occurs when one user displays an entity’s data in order to edit it, and then another user updates the same entity’s data before the first user’s change is written to the database. If you don’t enable the detection of such conflicts, whoever updates the database last overwrites the other user’s changes. In many applications, this risk is acceptable: if there are few users, or few updates, or if isn’t really critical if some changes are overwritten, the cost of programming for concurrency might outweigh the benefit. In that case, you don’t have to configure the application to handle concurrency conflicts.

Pessimistic Concurrency (Locking)

If your application does need to prevent accidental data loss in concurrency scenarios, one way to do that is to use database locks. This is called pessimistic concurrency. For example, before you read a row from a database, you request a lock for read-only or for update access. If you lock a row for update access, no other users are allowed to lock the row either for read-only or update access, because they would get a copy of data that’s in the process of being changed. If you lock a row for read-only access, others can also lock it for read-only access but not for update.

Managing locks has disadvantages. It can be complex to program. It requires significant database management resources, and it can cause performance problems as the number of users of an application increases. For these reasons, not all database management systems support pessimistic concurrency. The Entity Framework provides no built-in support for it, and this tutorial doesn’t show you how to implement it.

Optimistic Concurrency

The alternative to pessimistic concurrency is optimistic concurrency. Optimistic concurrency means allowing concurrency conflicts to happen, and then reacting appropriately if they do. For example, John runs the Departments Edit page, changes the Budget amount for the English department from $350,000.00 to $0.00.

Changing_English_dept_budget_to_100000
Changing_English_dept_budget_to_100000

Before John clicks Save, Jane runs the same page and changes the Start Date field from 9/1/2007 to 8/8/2013.

Changing_English_dept_start_date_to_1999
Changing_English_dept_start_date_to_1999

John clicks Save first and sees his change when the browser returns to the Index page, then Jane clicks Save. What happens next is determined by how you handle concurrency conflicts. Some of the options include the following:

Detecting Concurrency Conflicts

You can resolve conflicts by handling OptimisticConcurrencyException exceptions that the Entity Framework throws. In order to know when to throw these exceptions, the Entity Framework must be able to detect conflicts. Therefore, you must configure the database and the data model appropriately. Some options for enabling conflict detection include the following:

In the remainder of this tutorial you’ll add a rowversion tracking property to the Department entity, create a controller and views, and test to verify that everything works correctly.

Add an Optimistic Concurrency Property to the Department Entity

In Models.cs, add a tracking property named RowVersion:

[!code-csharpMain]

   1:  public class Department
   2:  {
   3:      public int DepartmentID { get; set; }
   4:   
   5:      [StringLength(50, MinimumLength = 3)]
   6:      public string Name { get; set; }
   7:   
   8:      [DataType(DataType.Currency)]
   9:      [Column(TypeName = "money")]
  10:      public decimal Budget { get; set; }
  11:   
  12:      [DataType(DataType.Date)]
  13:      [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
  14:      [Display(Name = "Start Date")]
  15:      public DateTime StartDate { get; set; }
  16:   
  17:      [Display(Name = "Administrator")]
  18:      public int? InstructorID { get; set; }
  19:   
  20:      [Timestamp]
  21:      public byte[] RowVersion { get; set; }
  22:   
  23:      public virtual Instructor Administrator { get; set; }
  24:      public virtual ICollection<Course> Courses { get; set; }
  25:  }

The Timestamp attribute specifies that this column will be included in the Where clause of Update and Delete commands sent to the database. The attribute is called Timestamp because previous versions of SQL Server used a SQL timestamp data type before the SQL rowversion replaced it. The .Net type for rowversion is a byte array.

If you prefer to use the fluent API, you can use the IsConcurrencyToken method to specify the tracking property, as shown in the following example:

[!code-csharpMain]

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

By adding a property you changed the database model, so you need to do another migration. In the Package Manager Console (PMC), enter the following commands:

[!code-consoleMain]

   1:  Add-Migration RowVersion
   2:  Update-Database

Modify the Department Controller

In Controllers.cs, add a using statement:

[!code-csharpMain]

   1:  using System.Data.Entity.Infrastructure;

In the DepartmentController.cs file, change all four occurrences of “LastName” to “FullName” so that the department administrator drop-down lists will contain the full name of the instructor rather than just the last name.

[!code-csharpMain]

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

Replace the existing code for the HttpPost Edit method with the following code:

[!code-csharpMain]

   1:  [HttpPost]
   2:  [ValidateAntiForgeryToken]
   3:  public async Task<ActionResult> Edit(int? id, byte[] rowVersion)
   4:  {
   5:      string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" };
   6:   
   7:      if (id == null)
   8:      {
   9:          return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  10:      }
  11:   
  12:      var departmentToUpdate = await db.Departments.FindAsync(id);
  13:      if (departmentToUpdate == null)
  14:      {
  15:          Department deletedDepartment = new Department();
  16:          TryUpdateModel(deletedDepartment, fieldsToBind);
  17:          ModelState.AddModelError(string.Empty,
  18:              "Unable to save changes. The department was deleted by another user.");
  19:          ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
  20:          return View(deletedDepartment);
  21:      }
  22:   
  23:      if (TryUpdateModel(departmentToUpdate, fieldsToBind))
  24:      {
  25:          try
  26:          {
  27:              db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion;
  28:              await db.SaveChangesAsync();
  29:   
  30:              return RedirectToAction("Index");
  31:          }
  32:          catch (DbUpdateConcurrencyException ex)
  33:          {
  34:              var entry = ex.Entries.Single();
  35:              var clientValues = (Department)entry.Entity;
  36:              var databaseEntry = entry.GetDatabaseValues();
  37:              if (databaseEntry == null)
  38:              {
  39:                  ModelState.AddModelError(string.Empty,
  40:                      "Unable to save changes. The department was deleted by another user.");
  41:              }
  42:              else
  43:              {
  44:                  var databaseValues = (Department)databaseEntry.ToObject();
  45:   
  46:                  if (databaseValues.Name != clientValues.Name)
  47:                      ModelState.AddModelError("Name", "Current value: "
  48:                          + databaseValues.Name);
  49:                  if (databaseValues.Budget != clientValues.Budget)
  50:                      ModelState.AddModelError("Budget", "Current value: "
  51:                          + String.Format("{0:c}", databaseValues.Budget));
  52:                  if (databaseValues.StartDate != clientValues.StartDate)
  53:                      ModelState.AddModelError("StartDate", "Current value: "
  54:                          + String.Format("{0:d}", databaseValues.StartDate));
  55:                  if (databaseValues.InstructorID != clientValues.InstructorID)
  56:                      ModelState.AddModelError("InstructorID", "Current value: "
  57:                          + db.Instructors.Find(databaseValues.InstructorID).FullName);
  58:                  ModelState.AddModelError(string.Empty, "The record you attempted to edit "
  59:                      + "was modified by another user after you got the original value. The "
  60:                      + "edit operation was canceled and the current values in the database "
  61:                      + "have been displayed. If you still want to edit this record, click "
  62:                      + "the Save button again. Otherwise click the Back to List hyperlink.");
  63:                  departmentToUpdate.RowVersion = databaseValues.RowVersion;
  64:              }
  65:          }
  66:          catch (RetryLimitExceededException /* dex */)
  67:          {
  68:              //Log the error (uncomment dex variable name and add a line here to write a log.
  69:              ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
  70:          }
  71:      }
  72:      ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
  73:      return View(departmentToUpdate);
  74:  }

If the FindAsync method returns null, the department was deleted by another user. The code shown uses the posted form values to create a department entity so that the Edit page can be redisplayed with an error message. As an alternative, you wouldn’t have to re-create the department entity if you display only an error message without redisplaying the department fields.

The view stores the original RowVersion value in a hidden field, and the method receives it in the rowVersion parameter. Before you call SaveChanges, you have to put that original RowVersion property value in the OriginalValues collection for the entity. Then when the Entity Framework creates a SQL UPDATE command, that command will include a WHERE clause that looks for a row that has the original RowVersion value.

If no rows are affected by the UPDATE command (no rows have the original RowVersion value), the Entity Framework throws a DbUpdateConcurrencyException exception, and the code in the catch block gets the affected Department entity from the exception object.

[!code-csharpMain]

   1:  var entry = ex.Entries.Single();

This object has the new values entered by the user in its Entity property, and you can get the values read from the database by calling the GetDatabaseValues method.

[!code-csharpMain]

   1:  var clientValues = (Department)entry.Entity;
   2:  var databaseEntry = entry.GetDatabaseValues();

The GetDatabaseValues method returns null if someone has deleted the row from the database; otherwise, you have to cast the returned object to the Department class in order to access the Department properties. (Because you already checked for deletion, databaseEntry would be null only if the department was deleted after FindAsync executes and before SaveChanges executes.)

[!code-csharpMain]

   1:  if (databaseEntry == null)
   2:  {
   3:      ModelState.AddModelError(string.Empty,
   4:          "Unable to save changes. The department was deleted by another user.");
   5:  }
   6:  else
   7:  {
   8:      var databaseValues = (Department)databaseEntry.ToObject();

Next, the code adds a custom error message for each column that has database values different from what the user entered on the Edit page:

[!code-csharpMain]

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

A longer error message explains what happened and what to do about it:

[!code-csharpMain]

   1:  ModelState.AddModelError(string.Empty, "The record you attempted to edit "
   2:      + "was modified by another user after you got the original value. The"
   3:      + "edit operation was canceled and the current values in the database "
   4:      + "have been displayed. If you still want to edit this record, click "
   5:      + "the Save button again. Otherwise click the Back to List hyperlink.");

Finally, the code sets the RowVersion value of the Department object to the new value retrieved from the database. This new RowVersion value will be stored in the hidden field when the Edit page is redisplayed, and the next time the user clicks Save, only concurrency errors that happen since the redisplay of the Edit page will be caught.

In Views.cshtml, add a hidden field to save the RowVersion property value, immediately following the hidden field for the DepartmentID property:

[!code-cshtmlMain]

   1:  @model ContosoUniversity.Models.Department
   2:   
   3:  @{
   4:      ViewBag.Title = "Edit";
   5:  }
   6:   
   7:  <h2>Edit</h2>
   8:   
   9:  @using (Html.BeginForm())
  10:  {
  11:      @Html.AntiForgeryToken()
  12:      
  13:      <div class="form-horizontal">
  14:          <h4>Department</h4>
  15:          <hr />
  16:          @Html.ValidationSummary(true)
  17:          @Html.HiddenFor(model => model.DepartmentID)
  18:          @Html.HiddenFor(model => model.RowVersion)

Testing Optimistic Concurrency Handling

Run the site and click Departments:

Department_Index_page_before_edits
Department_Index_page_before_edits

Right click the Edit hyperlink for the English department and select Open in new tab, then click the Edit hyperlink for the English department. The two tabs display the same information.

Department_Edit_page_before_changes
Department_Edit_page_before_changes

Change a field in the first browser tab and click Save.

Department_Edit_page_1_after_change
Department_Edit_page_1_after_change

The browser shows the Index page with the changed value.

Departments_Index_page_after_first_budget_edit
Departments_Index_page_after_first_budget_edit

Change a field in the second browser tab and click Save.

Department_Edit_page_2_after_change
Department_Edit_page_2_after_change

Click Save in the second browser tab. You see an error message:

Department_Edit_page_2_after_clicking_Save
Department_Edit_page_2_after_clicking_Save

Click Save again. The value you entered in the second browser tab is saved along with the original value of the data you changed in the first browser. You see the saved values when the Index page appears.

Department_Index_page_with_change_from_second_browser
Department_Index_page_with_change_from_second_browser

Updating the Delete Page

For the Delete page, the Entity Framework detects concurrency conflicts caused by someone else editing the department in a similar manner. When the HttpGet Delete method displays the confirmation view, the view includes the original RowVersion value in a hidden field. That value is then available to the HttpPost Delete method that’s called when the user confirms the deletion. When the Entity Framework creates the SQL DELETE command, it includes a WHERE clause with the original RowVersion value. If the command results in zero rows affected (meaning the row was changed after the Delete confirmation page was displayed), a concurrency exception is thrown, and the HttpGet Delete method is called with an error flag set to true in order to redisplay the confirmation page with an error message. It’s also possible that zero rows were affected because the row was deleted by another user, so in that case a different error message is displayed.

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

[!code-csharpMain]

   1:  public async Task<ActionResult> Delete(int? id, bool? concurrencyError)
   2:  {
   3:      if (id == null)
   4:      {
   5:          return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
   6:      }
   7:      Department department = await db.Departments.FindAsync(id);
   8:      if (department == null)
   9:      {
  10:          if (concurrencyError.GetValueOrDefault())
  11:          {
  12:              return RedirectToAction("Index");
  13:          }
  14:          return HttpNotFound();
  15:      }
  16:   
  17:      if (concurrencyError.GetValueOrDefault())
  18:      {
  19:          ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
  20:              + "was modified by another user after you got the original values. "
  21:              + "The delete operation was canceled and the current values in the "
  22:              + "database have been displayed. If you still want to delete this "
  23:              + "record, click the Delete button again. Otherwise "
  24:              + "click the Back to List hyperlink.";
  25:      }
  26:   
  27:      return View(department);
  28:  }

The method accepts an optional parameter that indicates whether the page is being redisplayed after a concurrency error. If this flag is true, an error message is sent to the view using a ViewBag property.

Replace the code in the HttpPost Delete method (named DeleteConfirmed) with the following code:

[!code-csharpMain]

   1:  [HttpPost]
   2:  [ValidateAntiForgeryToken]
   3:  public async Task<ActionResult> Delete(Department department)
   4:  {
   5:      try
   6:      {
   7:          db.Entry(department).State = EntityState.Deleted;
   8:          await db.SaveChangesAsync();
   9:          return RedirectToAction("Index");
  10:      }
  11:      catch (DbUpdateConcurrencyException)
  12:      {
  13:          return RedirectToAction("Delete", new { concurrencyError = true, id=department.DepartmentID });
  14:      }
  15:      catch (DataException /* dex */)
  16:      {
  17:          //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
  18:          ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
  19:          return View(department);
  20:      }
  21:  }

In the scaffolded code that you just replaced, this method accepted only a record ID:

[!code-csharpMain]

   1:  public async Task<ActionResult> DeleteConfirmed(int id)

You’ve changed this parameter to a Department entity instance created by the model binder. This gives you access to the RowVersion property value in addition to the record key.

[!code-csharpMain]

   1:  public async Task<ActionResult> Delete(Department department)

You have also changed the action method name from DeleteConfirmed to Delete. The scaffolded code named the HttpPost Delete method DeleteConfirmed to give the HttpPost method a unique signature. ( The CLR requires overloaded methods to have different method parameters.) Now that the signatures are unique, you can stick with the MVC convention and use the same name for the HttpPost and HttpGet delete methods.

If a concurrency error is caught, the code redisplays the Delete confirmation page and provides a flag that indicates it should display a concurrency error message.

In Views.cshtml, replace the scaffolded code with the following code that adds an error message field and hidden fields for the DepartmentID and RowVersion properties. The changes are highlighted.

[!code-cshtmlMain]

   1:  @model ContosoUniversity.Models.Department
   2:   
   3:  @{
   4:      ViewBag.Title = "Delete";
   5:  }
   6:   
   7:  <h2>Delete</h2>
   8:   
   9:  <p class="error">@ViewBag.ConcurrencyErrorMessage</p>
  10:   
  11:  <h3>Are you sure you want to delete this?</h3>
  12:  <div>
  13:      <h4>Department</h4>
  14:      <hr />
  15:      <dl class="dl-horizontal">
  16:          <dt>
  17:              Administrator
  18:          </dt>
  19:   
  20:          <dd>
  21:              @Html.DisplayFor(model => model.Administrator.FullName)
  22:          </dd>
  23:   
  24:          <dt>
  25:              @Html.DisplayNameFor(model => model.Name)
  26:          </dt>
  27:   
  28:          <dd>
  29:              @Html.DisplayFor(model => model.Name)
  30:          </dd>
  31:   
  32:          <dt>
  33:              @Html.DisplayNameFor(model => model.Budget)
  34:          </dt>
  35:   
  36:          <dd>
  37:              @Html.DisplayFor(model => model.Budget)
  38:          </dd>
  39:   
  40:          <dt>
  41:              @Html.DisplayNameFor(model => model.StartDate)
  42:          </dt>
  43:   
  44:          <dd>
  45:              @Html.DisplayFor(model => model.StartDate)
  46:          </dd>
  47:   
  48:      </dl>
  49:   
  50:      @using (Html.BeginForm()) {
  51:          @Html.AntiForgeryToken()
  52:          @Html.HiddenFor(model => model.DepartmentID)
  53:          @Html.HiddenFor(model => model.RowVersion)
  54:   
  55:          <div class="form-actions no-color">
  56:              <input type="submit" value="Delete" class="btn btn-default" /> |
  57:              @Html.ActionLink("Back to List", "Index")
  58:          </div>
  59:      }
  60:  </div>

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

[!code-cshtmlMain]

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

It replaces LastName with FullName in the Administrator field:

[!code-cshtmlMain]

   1:  <dt>
   2:    Administrator
   3:  </dt>
   4:  <dd>
   5:    @Html.DisplayFor(model => model.Administrator.FullName)
   6:  </dd>

Finally, it adds hidden fields for the DepartmentID and RowVersion properties after the Html.BeginForm statement:

[!code-cshtmlMain]

   1:  @Html.HiddenFor(model => model.DepartmentID)
   2:  @Html.HiddenFor(model => model.RowVersion)

Run the Departments Index page. Right click the Delete hyperlink for the English department and select Open in new tab, then in the first tab click the Edit hyperlink for the English department.

In the first window, change one of the values, and click Save :

Department_Edit_page_after_change_before_delete
Department_Edit_page_after_change_before_delete

The Index page confirms the change.

Departments_Index_page_after_budget_edit_before_delete
Departments_Index_page_after_budget_edit_before_delete

In the second tab, click Delete.

Department_Delete_confirmation_page_before_concurrency_error
Department_Delete_confirmation_page_before_concurrency_error

You see the concurrency error message, and the Department values are refreshed with what’s currently in the database.

Department_Delete_confirmation_page_with_concurrency_error
Department_Delete_confirmation_page_with_concurrency_error

If you click Delete again, you’re redirected to the Index page, which shows that the department has been deleted.

Summary

This completes the introduction to handling concurrency conflicts. For information about other ways to handle various concurrency scenarios, see Optimistic Concurrency Patterns and Working with Property Values on MSDN. The next tutorial shows how to implement table-per-hierarchy inheritance for the Instructor and Student entities.

Links to other Entity Framework resources can be found in the ASP.NET Data Access - Recommended Resources.

Previous Next



Comments ( )
Link to this page: //www.vb-net.com/AspNet-DocAndSamples-2017/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application.htm
< THANKS ME>