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.
Pessimistic concurrency control is when a record is locked at the time the user begins his or her edit process.
The alternative to pessimistic concurrency is optimistic concurrency. Optimistic concurrency means allowing concurrency conflicts to happen, and then reacting appropriately if they do.
By default, Entity Framework supports optimistic concurrency.
In order to check concurrency for the Author
entity, the Authors table must have a rowversion column. So, add a tracking property named RowVersion
to the Author
class.
public class Author
{
public int AuthorId { get; set; }
[StringLength(50)]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[NotMapped]
[Display(Name = "Full Name")]
public string FullName
{
get
{
return FirstName + " " + LastName;
}
}
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime BirthDate { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual ICollection<Book> Books { get; set; }
}
The Timestamp
attribute specifies that this column will be included in the Where
clause of Update
and Delete
commands sent to the database.
If you prefer to use the fluent API, you can use the IsConcurrencyToken
method to specify the tracking property, as shown below.
modelBuilder.Entity<Author>()
.Property(p => p.RowVersion).IsConcurrencyToken();
The database has been changed, so we need to do another migration. In the Package Manager Console, run the following commands.
Add-Migration RowVersion
Update-Database
Replace the existing code for the HttpPost
Edit
method with the following code.
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id, byte[] rowVersion)
{
string[] fieldsToBind = new string[] { "FirstName", "LastName", "BirthDate" };
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var authorToUpdate = db.Authors.Find(id);
if (authorToUpdate == null)
{
Author deletedAuthor = new Author();
TryUpdateModel(deletedAuthor, fieldsToBind);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
return View(deletedAuthor);
}
if (TryUpdateModel(authorToUpdate, fieldsToBind))
{
try
{
db.Entry(authorToUpdate).OriginalValues["RowVersion"] = rowVersion;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Author)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 = (Author)databaseEntry.ToObject();
if (databaseValues.FirstName != clientValues.FirstName)
ModelState.AddModelError("FirstName", "Current value: " + databaseValues.FirstName);
if (databaseValues.LastName != clientValues.LastName)
ModelState.AddModelError("LastName", "Current value: " + String.Format("{0:c}", databaseValues.LastName));
if (databaseValues.BirthDate != clientValues.BirthDate)
ModelState.AddModelError("BirthDate", "Current value: " + String.Format("{0:d}", databaseValues.BirthDate));
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.");
authorToUpdate.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.");
}
}
return View(authorToUpdate);
}
RowVersion
value of the Author
object to the new value retrieved from the database.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\Author\Edit.cshtml, add a hidden field to save the RowVersion
property value, immediately following the hidden field for the AuthorId
property.
<h4>Author</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.AuthorId)
@Html.HiddenFor(model => model.RowVersion)
Let's run your application and click Authors tab, open the same author for editing in two different tabs.
Save
.You can see an error message, if you 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.