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(20, ErrorMessage = "Name cannot be longer than 20 characters.")]
public string FirstName { get; set; }
[StringLength(20, ErrorMessage = "Name cannot be longer than 20 characters.")]
public string LastName { get; set; }
[NotMapped]
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 async Task<IActionResult> EditPost(int? id, byte[] rowVersion)
{
string[] fieldsToBind = new string[] { "FirstName", "LastName", "BirthDate" };
if (id == null)
{
return new NotFoundResult();
}
var authorToUpdate = await _context.Authors.FirstOrDefaultAsync(s => s.AuthorId == id);
if (authorToUpdate == null)
{
Author deletedAuthor = new Author();
await TryUpdateModelAsync(deletedAuthor);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The author was deleted by another user.");
return View(deletedAuthor);
}
_context.Entry(authorToUpdate).Property("RowVersion").OriginalValue = rowVersion;
if (await TryUpdateModelAsync<Author>(
authorToUpdate,
"",
s => s.FirstName, s => s.LastName, s => s.BirthDate))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Author)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty,
"Unable to save changes. The author 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: {databaseValues.LastName}");
}
if (databaseValues.BirthDate != clientValues.BirthDate)
{
ModelState.AddModelError("BirthDate", $"Current value: {databaseValues.BirthDate:d}");
}
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 = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
}
}
}
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.
@model MvcWithEFCoreDemo.Models.Author
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Author</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="AuthorId" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="FirstName" class="control-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LastName" class="control-label"></label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="BirthDate" class="control-label"></label>
<input asp-for="BirthDate" class="form-control" />
<span asp-validation-for="BirthDate" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
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.