Translation: Getting Started with Entity Framework 6 of MVC5 (10)-Handling Concurrency for ASP.NET MVC Applications

Handling concurrency for ASP.NET MVC applications

This is a translation of Microsoft's official tutorial Getting Started with Entity Framework 6 Code First using MVC 5 series. Here is the tenth article: Handling Concurrency for ASP.NET MVC Applications

原文: Handling Concurrency with the Entity Framework 6 in an ASP.NET MVC 5 Application


In the previous tutorial, you have learned how to update data. In this tutorial, we will show how to handle conflicts when multiple users update the same entity at the same time.

You will modify web pages to handle Department entities so that they can handle concurrent errors. The screenshot below shows the index and delete pages, as well as some concurrency conflict error messages.

departments

editdepartment

Concurrency conflict

When one user displays the entity's data and edits it, and then another user updates the same entity's data before the first user's changes are written to the database, a concurrent conflict will occur. If you do not enable this conflict detection, the user who last updated the database will overwrite the changes made by other users to the database. In most applications, this risk is acceptable: if there are only a few users or few updates, or the problem of data update coverage is really not very important, the cost of implementing concurrent conflicts may be greater than it brings benefit. In this case, you do not need to configure the application to handle concurrency conflicts.

Pessimistic concurrency (locked)

If your application needs to prevent accidental data loss due to concurrency, one way to do this is to use database locks. This is called pessimistic concurrency. For example, before you read a row from the database, you request a read-only or updated access lock. If you lock update access to a row, no other user can lock the row, whether it is read-only or updated. Because the data they get is just a copy of the change process. If you lock read-only access to a row, others can also lock it for read-only access, but you cannot update it.

Management locks also have disadvantages. It will make programming more complicated. And it requires database management resources-a large number, and it may cause performance problems such as the increase in the number of users of the application. For these reasons, not all database management systems support pessimistic concurrency. The Entity Framework has built-in support for pessimistic concurrency, and this tutorial will not discuss how to implement it.

Optimistic concurrency

The alternative to pessimistic concurrency is optimistic concurrency. Optimistic concurrency means that running concurrency conflicts occur, and then respond appropriately to the changes that occur. For example, Passerby A changed the English budget from 350,000.00 to 0.00 on the department edit page.

departments

Before the passerby saved the change, the passerby B also opened the page and changed the start date field to May 2, 2014.

editdepartment

Passerby first clicked Save, he saw the changes he made on the index page, and Passerby B also clicked Save. What happens next depends on how you handle concurrency conflicts. Here are some options:

  • You can track the user's modified attributes and update only the corresponding columns in the database. In the example, no data will be lost, because two different attributes are updated by two different users. Passerby C will see the changes made by A and B at the same time when browsing the page-the start date of 8/8/2013 and the budget of 0 yuan.
    This method of updating can reduce conflicts, but it can still cause data loss if you make changes to the same attribute. Whether to use this way to make Entity Framework work depends on how you implement your update code. This is often not the best practice in actual web applications. Because it will require maintaining a large amount of state in order to track all the original attributes and new values ​​of the entity. Maintaining a large amount of state can affect application performance. Because this requires more server resources.

  • You can let B's changes override A's changes. When C browses the page, he will see the start date of 8/8/2013 and the restored budget of 350,000.00 yuan. This is called the client-all-takes or last-take-all. (The value from the client takes precedence over the value saved first, overwriting all data).

  • You can also prevent B's changes from being saved to the database. Usually an error message is displayed showing the difference between the data being overwritten to allow the user to resubmit the changes if the user wants to do so. This is called storage-take-all. (The saved value takes precedence over the value submitted by the client) You will implement this solution in this tutorial to ensure that the changes of other users are not overwritten before prompting the user.

Detect concurrent conflicts

You can resolve the conflict by handling the OptimisticConcurrencyException exception raised by the Entity Framework. In order to know when and where these exceptions are raised, the Entity Framework must be able to detect conflicts. Therefore, you must properly configure the database and data model, including the following:

  • In the data table, contains a column for tracking modifications. You can then configure the Entity Framework to include this column when it is updated or deleted for detection.
    The data type of the tracking column is usually rowversion. The value of the row version is a sequential number that is incremented each time it is updated. In the update or delete command, the Where statement will contain the original value of the tracking column. If another user changes the row being updated, the value in the row version will be inconsistent with the original. Therefore, the update and delete statements cannot find the row to be updated. When no rows are updated at the time of updating or deleting, the Entity Framework will recognize the command as a concurrent conflict.

  • Configure the Entity Framework to include the original value of each column of the data table in the Where clause of the update and delete commands.
    Similar to the first method, if the data row is changed after being read for the first time, the Where clause will not find the row to be updated, and the entity box will be interpreted as a concurrent conflict. For a database table with multiple columns, this method may result in a very large Where sentence and require you to maintain a large number of states. As mentioned earlier, maintaining a large number of states may affect application performance. Therefore, this method is generally not recommended and will not be used in this tutorial.
    If you want to implement this method to achieve concurrency, you must add the ConcurrencyCheck feature to all non-primary key attributes of the entity. This change allows the Entity Framework to include all marked columns in the Where clause of the update statement.

In the remainder of this tutorial, you will add a row version to track the attributes of the Department entity.

Add required attributes for optimistic concurrency to the Department entity

In Models \ Department.cs, add a tracking attribute named RowCersion:

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)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Start 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; }
}

The Timestamp attribute specifies that the column will be included in the Where clause of the update or delete command sent to the database. This attribute is called a timestamp because the previous version of SQL Server used the SQL Timestamp data type. The line version of the .Net type is a byte array.

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

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

Now that you have changed the database model, you need to do another migration. In the package manager console, enter the following command:

Add-Migration RowVersion
Update-Database

Modify Department Controller

In DepartmentController.cs, add the using statement:

using System.Data.Entity.Infrastructure;

Change all "LastName" in the file to "FullName" so that the drop-down list uses the teacher's full name instead of the last name.

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

Replace the Edit method of HttpPost with the following code:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int? id, byte[] rowVersion)
{
    string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" };

    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    var departmentToUpdate = await db.Departments.FindAsync(id);
    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        TryUpdateModel(deletedDepartment, fieldsToBind);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    if (TryUpdateModel(departmentToUpdate, fieldsToBind))
    {
        try
        {
            db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion;
            await db.SaveChangesAsync();

            return RedirectToAction("Index");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.Single();
            var clientValues = (Department)entry.Entity;
            var databaseEntry = entry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.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.");
                departmentToUpdate.RowVersion = databaseValues.RowVersion;
            }
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
        }
    }
    ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

The view stores the original RowVersion value in a hidden field. When the model binder creates an instance of the system, the object will have the original RowVersion attribute value and the new value of other attributes, such as the user entered on the edit page. Then the Entity Framework creates an update command, which will include the RowVersion value in the Where clause for query.

If no rows are updated (no rows are found that match the original RowVersion value), the Entity Framework will throw a DbUpdateConcurrencyException exception and obtain the affected Department entity from the exception object in the catch code block.

var entry = ex.Entries.Single();

The Entity property of the object has the new value entered by the user. You can also call the GetDatabaseValues ​​method to read the original value from the database.

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

If someone deletes the row from the database, the GetDataBaseValue method will return null, otherwise, you must cast the returned object to the Department class to access the properties in the Department.

if (databaseEntry == null)
{
    ModelState.AddModelError(string.Empty,
        "Unable to save changes. The department was deleted by another user.");
}
else
{
    var databaseValues = (Department)databaseEntry.ToObject();

Next, the code will add a custom error message for each column of database and user input different values:

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

A long error message explains to the user what happened:

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 RowVersion value of the Department object to the new value retrieved from the database. The new value is stored in a hidden field when the editing page is redisplayed. The next time the user clicks Save, the redisplayed editing page will continue to catch concurrent errors.

In Views \ Department \ Edit.cshtml, add a RowVersion hidden field after the DepartmentID hidden field.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

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

    <div class="form-horizontal">
        <h4>Department</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

Test optimistic concurrent processing

Run the application, click the Department tab and copy a tab, and open the two Department pages repeatedly.

departments

Open the edit page of the same department in two windows at the same time, edit one of the pages and save.

editdepartment

editdepartment

You will see the value has been saved to the database.

departments

Modify and save the fields in the second window.

editdepartment

You will see the concurrency error message:

editdepartment

Click Save again, and your database value in the second browser will overwrite the save in the first window to the database.

departments

Update delete page

For deleting pages, the Entity Framework uses a similar method to detect concurrent conflicts. When the Delete method of HttpGet displays the confirmation view, the original RowVersion value is included in the hidden field of the view. When the user confirms the deletion, the value is enough to be passed and called in the Delete method of HttpPost. When the Entity Framework creates the SQL Delete command, the original RowVersion value will be included in the Where clause. If no lines are affected after the command is executed, a concurrent exception will be thrown. The Delete method of HttpGet is called and the flag will be set to true to redisplay the confirmation page and display the error. But at the same time, consider that if another user happens to delete the row, it will also result in a 0 row affected. In this case, we will display a different error message.

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

public async Task<ActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Department department = await db.Departments.FindAsync(id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction("Index");
        }
        return HttpNotFound();
    }

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

    return View(department);
}

This method accepts an optional parameter indicating whether the page will be redisplayed when a concurrent conflict error occurs. If this flag is true, an error will be sent to the view using ViewBag.

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

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError = true, id=department.DepartmentID });
    }
    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);
    }
}

In your unmodified scaffolding code, this method receives a record ID

public async Task<ActionResult> DeleteConfirmed(int id)

You changed this parameter and used the model binder to create a Department entity, which gives you access to the RowVersion property value.

public async Task<ActionResult> Delete(Department department)

At the same time you changed the method name from DeleteConfirmed to Delete. The scaffolding code for the HttpPost Delete method uses the Delete name, because this will give the HttpPost method a unique signature. (The CLR requires methods to have different parameters for overloading. Now the signature is unique. You can keep the MVC convention and use the same method name on HttpPost and HttpGet methods.)

If a concurrent error is caught, the code redisplays the delete confirmation page and provides a flag to indicate that a concurrent error message is displayed.

In Views \ Department \ Delete.cshtml, add error message fields and hidden fields to the view. Replace the scaffolding code with the following:

@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>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            Administrator
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Budget)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Budget)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.StartDate)
        </dd>

    </dl>

    @using (Html.BeginForm()) {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            @Html.ActionLink("Back to List", "Index")
        </div>
    }
</div>

The code adds an error message between h2 and h3:

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

Replace LastName with FullName:

<dt>
  Administrator
</dt>
<dd>
  @Html.DisplayFor(model => model.Administrator.FullName)
</dd>

Finally, it adds hidden fields after the Html.BeginForm statement:

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

Run the application, open the system index page, right-click the delete hyperlink of natural science, and choose to open in a new window. Then click Edit on the first window to modify the budget and save it.

editdepartment

The changes have been saved to the database.

departments

Click the delete button in the second window and you will see a concurrent error message.

deletedepartment

If you click Delete again at this time, the entity will be deleted and you will be redirected to the index page.

deletedepartment

to sum up

In this section we introduced how to deal with concurrent conflicts. For more information on handling concurrent conflicts, please refer to MSDN. In the next section we will introduce how to implement the Instructor and Student entity table-each level of inheritance.

author information

tom-dykstra Tom Dykstra -Tom Dykstra is a senior programmer and writer on the Microsoft Web Platform and Tools team.

Published 40 original articles · 25 praises · 100,000+ views

Guess you like

Origin blog.csdn.net/yym373872996/article/details/69486444