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

Handling concurrency conflicts - EF Core with ASP.NET Core MVC tutorial (8 of 10)

By Tom Dykstra and Rick Anderson

The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web applications using Entity Framework Core and Visual Studio. 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 create web pages that work with the Department entity and handle concurrency errors. The following illustrations show the Edit and Delete pages, including some messages that are displayed if a concurrency conflict occurs.

Department Edit page
Department Edit page
Department Delete page
Department Delete page

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. Entity Framework Core 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, Jane visits the Department Edit page and changes the Budget amount for the English department from $350,000.00 to $0.00.

Changing budget to 0
Changing budget to 0

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

Changing start date to 2013
Changing start date to 2013

Jane clicks Save first and sees her change when the browser returns to the Index page.

Budget changed to zero
Budget changed to zero

Then John clicks Save on an Edit page that still shows a budget of $350,000.00. 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 DbConcurrencyException 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 a tracking property to the Department entity

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

[!code-csharpMain]

   1:  #define Final
   2:   
   3:  #if Begin
   4:  #region snippet_Begin
   5:  using System;
   6:  using System.Collections.Generic;
   7:  using System.ComponentModel.DataAnnotations;
   8:  using System.ComponentModel.DataAnnotations.Schema;
   9:   
  10:  namespace ContosoUniversity.Models
  11:  {
  12:      public class Department
  13:      {
  14:          public int DepartmentID { get; set; }
  15:   
  16:          [StringLength(50, MinimumLength = 3)]
  17:          public string Name { get; set; }
  18:   
  19:          [DataType(DataType.Currency)]
  20:          [Column(TypeName = "money")]
  21:          public decimal Budget { get; set; }
  22:   
  23:          [DataType(DataType.Date)]
  24:          [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
  25:          [Display(Name = "Start Date")]
  26:          public DateTime StartDate { get; set; }
  27:   
  28:          public int? InstructorID { get; set; }
  29:   
  30:          public Instructor Administrator { get; set; }
  31:          public ICollection<Course> Courses { get; set; }
  32:      }
  33:  }
  34:  #endregion
  35:   
  36:   
  37:  #elif Final
  38:  #region snippet_Final
  39:  using System;
  40:  using System.Collections.Generic;
  41:  using System.ComponentModel.DataAnnotations;
  42:  using System.ComponentModel.DataAnnotations.Schema;
  43:   
  44:  namespace ContosoUniversity.Models
  45:  {
  46:      public class Department
  47:      {
  48:          public int DepartmentID { get; set; }
  49:   
  50:          [StringLength(50, MinimumLength = 3)]
  51:          public string Name { get; set; }
  52:   
  53:          [DataType(DataType.Currency)]
  54:          [Column(TypeName = "money")]
  55:          public decimal Budget { get; set; }
  56:   
  57:          [DataType(DataType.Date)]
  58:          [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
  59:          [Display(Name = "Start Date")]
  60:          public DateTime StartDate { get; set; }
  61:   
  62:          public int? InstructorID { get; set; }
  63:   
  64:          [Timestamp]
  65:          public byte[] RowVersion { get; set; }
  66:   
  67:          public Instructor Administrator { get; set; }
  68:          public ICollection<Course> Courses { get; set; }
  69:      }
  70:  }
  71:  #endregion
  72:  #endif  

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 (in Data/SchoolContext.cs) to specify the tracking property, as shown in the following example:

By adding a property you changed the database model, so you need to do another migration.

Save your changes and build the project, and then enter the following commands in the command window:

dotnet ef migrations add RowVersion
dotnet ef database update

Create a Departments controller and views

Scaffold a Departments controller and views as you did earlier for Students, Courses, and Instructors.

Scaffold Department
Scaffold Department

In the DepartmentsController.cs file, change all four occurrences of “FirstMidName” 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:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

Update the Departments Index view

The scaffolding engine created a RowVersion column in the Index view, but that field shouldn’t be displayed.

Replace the code in Views/Departments/Index.cshtml with the following code.

[!code-htmlMain]

   1:  @model IEnumerable<ContosoUniversity.Models.Department>
   2:   
   3:  @{
   4:      ViewData["Title"] = "Departments";
   5:  }
   6:   
   7:  <h2>Departments</h2>
   8:   
   9:  <p>
  10:      <a asp-action="Create">Create New</a>
  11:  </p>
  12:  <table class="table">
  13:      <thead>
  14:          <tr>
  15:              <th>
  16:                  @Html.DisplayNameFor(model => model.Name)
  17:              </th>
  18:              <th>
  19:                  @Html.DisplayNameFor(model => model.Budget)
  20:              </th>
  21:              <th>
  22:                  @Html.DisplayNameFor(model => model.StartDate)
  23:              </th>
  24:              <th>
  25:                  @Html.DisplayNameFor(model => model.Administrator)
  26:              </th>
  27:              <th></th>
  28:          </tr>
  29:      </thead>
  30:      <tbody>
  31:          @foreach (var item in Model)
  32:          {
  33:              <tr>
  34:                  <td>
  35:                      @Html.DisplayFor(modelItem => item.Name)
  36:                  </td>
  37:                  <td>
  38:                      @Html.DisplayFor(modelItem => item.Budget)
  39:                  </td>
  40:                  <td>
  41:                      @Html.DisplayFor(modelItem => item.StartDate)
  42:                  </td>
  43:                  <td>
  44:                      @Html.DisplayFor(modelItem => item.Administrator.FullName)
  45:                  </td>
  46:                  <td>
  47:                      <a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
  48:                      <a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
  49:                      <a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
  50:                  </td>
  51:              </tr>
  52:          }
  53:      </tbody>
  54:  </table>

This changes the heading to “Departments”, deletes the RowVersion column, and shows full name instead of first name for the administrator.

Update the Edit methods in the Departments controller

In both the HttpGet Edit method and the Details method, add AsNoTracking. In the HttpGet Edit method, add eager loading for the Administrator.

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

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

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

The code begins by trying to read the department to be updated. If the SingleOrDefaultAsync method returns null, the department was deleted by another user. In that case the code 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 this method receives that value 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.

The code in the catch block for that exception gets the affected Department entity that has the updated values from the Entries property on the exception object.

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

The Entries collection will have just one EntityEntry object. You can use that object to get the new values entered by the user and the current database values.

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

The code adds a custom error message for each column that has database values different from what the user entered on the Edit page (only one field is shown here for brevity).

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

Finally, the code sets the RowVersion value of the departmentToUpdate 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.

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

The ModelState.Remove statement is required because ModelState has the old RowVersion value. In the view, the ModelState value for a field takes precedence over the model property values when both are present.

Update the Department Edit view

In Views/Departments/Edit.cshtml, make the following changes:

[!code-htmlMain]

   1:  @model ContosoUniversity.Models.Department
   2:   
   3:  @{
   4:      ViewData["Title"] = "Edit";
   5:  }
   6:   
   7:  <h2>Edit</h2>
   8:   
   9:  <h4>Department</h4>
  10:  <hr />
  11:  <div class="row">
  12:      <div class="col-md-4">
  13:          <form asp-action="Edit">
  14:              <div asp-validation-summary="ModelOnly" class="text-danger"></div>
  15:              <input type="hidden" asp-for="DepartmentID" />
  16:              <input type="hidden" asp-for="RowVersion" />
  17:              <div class="form-group">
  18:                  <label asp-for="Name" class="control-label"></label>
  19:                  <input asp-for="Name" class="form-control" />
  20:                  <span asp-validation-for="Name" class="text-danger"></span>
  21:              </div>
  22:              <div class="form-group">
  23:                  <label asp-for="Budget" class="control-label"></label>
  24:                  <input asp-for="Budget" class="form-control" />
  25:                  <span asp-validation-for="Budget" class="text-danger"></span>
  26:              </div>
  27:              <div class="form-group">
  28:                  <label asp-for="StartDate" class="control-label"></label>
  29:                  <input asp-for="StartDate" class="form-control" />
  30:                  <span asp-validation-for="StartDate" class="text-danger"></span>
  31:              </div>
  32:              <div class="form-group">
  33:                  <label asp-for="InstructorID" class="control-label"></label>
  34:                  <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
  35:                      <option value="">-- Select Administrator --</option>
  36:                  </select>
  37:                  <span asp-validation-for="InstructorID" class="text-danger"></span>
  38:              </div>
  39:              <div class="form-group">
  40:                  <input type="submit" value="Save" class="btn btn-default" />
  41:              </div>
  42:          </form>
  43:      </div>
  44:  </div>
  45:   
  46:  <div>
  47:      <a asp-action="Index">Back to List</a>
  48:  </div>
  49:   
  50:  @section Scripts {
  51:      @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
  52:  }

Test concurrency conflicts in the Edit page

Run the app and go to the Departments Index page. 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 browser tabs now display the same information.

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.

Change a field in the second browser tab.

Department Edit page 2 after change
Department Edit page 2 after change

Click Save. You see an error message:

Department Edit page error message
Department Edit page error message

Click Save again. The value you entered in the second browser tab is saved. You see the saved values when the Index page appears.

Update 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 no error message is displayed.

Update the Delete methods in the Departments controller

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

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

The method accepts an optional parameter that indicates whether the page is being redisplayed after a concurrency error. If this flag is true and the department specified no longer exists, it was deleted by another user. In that case, the code redirects to the Index page. If this flag is true and the Department does exist, it was changed by another user. In that case, the code sends an error message to the view using ViewData.

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

[!code-csharpMain]

   1:  //#define ScaffoldedCode
   2:  #define RawSQL
   3:   
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Threading.Tasks;
   8:  using Microsoft.AspNetCore.Mvc;
   9:  using Microsoft.AspNetCore.Mvc.Rendering;
  10:  using Microsoft.EntityFrameworkCore;
  11:  using ContosoUniversity.Data;
  12:  using ContosoUniversity.Models;
  13:   
  14:  namespace ContosoUniversity.Controllers
  15:  {
  16:      public class DepartmentsController : Controller
  17:      {
  18:          private readonly SchoolContext _context;
  19:   
  20:          public DepartmentsController(SchoolContext context)
  21:          {
  22:              _context = context;
  23:          }
  24:   
  25:          // GET: Departments
  26:          public async Task<IActionResult> Index()
  27:          {
  28:              var departments = _context.Departments
  29:                  .Include(d => d.Administrator)
  30:                  .AsNoTracking();
  31:              return View(await departments.ToListAsync());
  32:          }
  33:   
  34:          // GET: Departments/Details/5
  35:  #if ScaffoldedCode
  36:          public async Task<IActionResult> Details(int? id)
  37:          {
  38:              if (id == null)
  39:              {
  40:                  return NotFound();
  41:              }
  42:   
  43:              var department = await _context.Departments
  44:                  .AsNoTracking()
  45:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
  46:              if (department == null)
  47:              {
  48:                  return NotFound();
  49:              }
  50:   
  51:              return View(department);
  52:          }
  53:  #elif RawSQL
  54:          #region snippet_RawSQL
  55:          public async Task<IActionResult> Details(int? id)
  56:          {
  57:              if (id == null)
  58:              {
  59:                  return NotFound();
  60:              }
  61:   
  62:              string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
  63:              var department = await _context.Departments
  64:                  .FromSql(query, id)
  65:                  .Include(d => d.Administrator)
  66:                  .AsNoTracking()
  67:                  .SingleOrDefaultAsync();
  68:   
  69:              if (department == null)
  70:              {
  71:                  return NotFound();
  72:              }
  73:   
  74:              return View(department);
  75:          }
  76:          #endregion
  77:  #endif
  78:   
  79:          // GET: Departments/Create
  80:          public IActionResult Create()
  81:          {
  82:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName");
  83:              return View();
  84:          }
  85:   
  86:          // POST: Departments/Create
  87:          [HttpPost]
  88:          [ValidateAntiForgeryToken]
  89:          public async Task<IActionResult> Create([Bind("Budget,InstructorID,Name,StartDate")] Department department)
  90:          {
  91:              if (ModelState.IsValid)
  92:              {
  93:                  _context.Add(department);
  94:                  await _context.SaveChangesAsync();
  95:                  return RedirectToAction(nameof(Index));
  96:              }
  97:              #region snippet_Dropdown
  98:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
  99:              #endregion
 100:              return View(department);
 101:          }
 102:   
 103:          // GET: Departments/Edit/5
 104:          public async Task<IActionResult> Edit(int? id)
 105:          {
 106:              if (id == null)
 107:              {
 108:                  return NotFound();
 109:              }
 110:   
 111:              #region snippet_EagerLoading
 112:              var department = await _context.Departments
 113:                  .Include(i => i.Administrator)
 114:                  .AsNoTracking()
 115:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 116:              #endregion
 117:              if (department == null)
 118:              {
 119:                  return NotFound();
 120:              }
 121:              ViewData["InstructorID"] = new SelectList(_context.Instructors.AsNoTracking(), "ID", "FullName", department.InstructorID);
 122:              return View(department);
 123:          }
 124:   
 125:          // POST: Departments/Edit/5
 126:          // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 127:          // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 128:          #region snippet_EditPost
 129:          [HttpPost]
 130:          [ValidateAntiForgeryToken]
 131:          public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
 132:          {
 133:              if (id == null)
 134:              {
 135:                  return NotFound();
 136:              }
 137:   
 138:              var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
 139:   
 140:              if (departmentToUpdate == null)
 141:              {
 142:                  Department deletedDepartment = new Department();
 143:                  await TryUpdateModelAsync(deletedDepartment);
 144:                  ModelState.AddModelError(string.Empty,
 145:                      "Unable to save changes. The department was deleted by another user.");
 146:                  ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
 147:                  return View(deletedDepartment);
 148:              }
 149:   
 150:              _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
 151:   
 152:              if (await TryUpdateModelAsync<Department>(
 153:                  departmentToUpdate,
 154:                  "",
 155:                  s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
 156:              {
 157:                  try
 158:                  {
 159:                      await _context.SaveChangesAsync();
 160:                      return RedirectToAction(nameof(Index));
 161:                  }
 162:                  catch (DbUpdateConcurrencyException ex)
 163:                  {
 164:                      var exceptionEntry = ex.Entries.Single();
 165:                      var clientValues = (Department)exceptionEntry.Entity;
 166:                      var databaseEntry = exceptionEntry.GetDatabaseValues();
 167:                      if (databaseEntry == null)
 168:                      {
 169:                          ModelState.AddModelError(string.Empty,
 170:                              "Unable to save changes. The department was deleted by another user.");
 171:                      }
 172:                      else
 173:                      {
 174:                          var databaseValues = (Department)databaseEntry.ToObject();
 175:   
 176:                          if (databaseValues.Name != clientValues.Name)
 177:                          {
 178:                              ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
 179:                          }
 180:                          if (databaseValues.Budget != clientValues.Budget)
 181:                          {
 182:                              ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
 183:                          }
 184:                          if (databaseValues.StartDate != clientValues.StartDate)
 185:                          {
 186:                              ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
 187:                          }
 188:                          if (databaseValues.InstructorID != clientValues.InstructorID)
 189:                          {
 190:                              Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
 191:                              ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
 192:                          }
 193:   
 194:                          ModelState.AddModelError(string.Empty, "The record you attempted to edit "
 195:                                  + "was modified by another user after you got the original value. The "
 196:                                  + "edit operation was canceled and the current values in the database "
 197:                                  + "have been displayed. If you still want to edit this record, click "
 198:                                  + "the Save button again. Otherwise click the Back to List hyperlink.");
 199:                          departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
 200:                          ModelState.Remove("RowVersion");
 201:                      }
 202:                  }
 203:              }
 204:              ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
 205:              return View(departmentToUpdate);
 206:          }
 207:          #endregion
 208:   
 209:          // GET: Departments/Delete/5
 210:          #region snippet_DeleteGet
 211:          public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
 212:          {
 213:              if (id == null)
 214:              {
 215:                  return NotFound();
 216:              }
 217:   
 218:              var department = await _context.Departments
 219:                  .Include(d => d.Administrator)
 220:                  .AsNoTracking()
 221:                  .SingleOrDefaultAsync(m => m.DepartmentID == id);
 222:              if (department == null)
 223:              {
 224:                  if (concurrencyError.GetValueOrDefault())
 225:                  {
 226:                      return RedirectToAction(nameof(Index));
 227:                  }
 228:                  return NotFound();
 229:              }
 230:   
 231:              if (concurrencyError.GetValueOrDefault())
 232:              {
 233:                  ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
 234:                      + "was modified by another user after you got the original values. "
 235:                      + "The delete operation was canceled and the current values in the "
 236:                      + "database have been displayed. If you still want to delete this "
 237:                      + "record, click the Delete button again. Otherwise "
 238:                      + "click the Back to List hyperlink.";
 239:              }
 240:   
 241:              return View(department);
 242:          }
 243:          #endregion
 244:   
 245:          // POST: Departments/Delete/5
 246:          #region snippet_DeletePost
 247:          [HttpPost]
 248:          [ValidateAntiForgeryToken]
 249:          public async Task<IActionResult> Delete(Department department)
 250:          {
 251:              try
 252:              {
 253:                  if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
 254:                  {
 255:                      _context.Departments.Remove(department);
 256:                      await _context.SaveChangesAsync();
 257:                  }
 258:                  return RedirectToAction(nameof(Index));
 259:              }
 260:              catch (DbUpdateConcurrencyException /* ex */)
 261:              {
 262:                  //Log the error (uncomment ex variable name and write a log.)
 263:                  return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
 264:              }
 265:          }
 266:          #endregion
 267:      }
 268:  }

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

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

You have also changed the action method name from DeleteConfirmed to Delete. The scaffolded code used the name 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 the department is already deleted, the AnyAsync method returns false and the application just goes back to the Index method.

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.

Update the Delete view

In Views/Departments/Delete.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-htmlMain]

   1:  @model ContosoUniversity.Models.Department
   2:   
   3:  @{
   4:      ViewData["Title"] = "Delete";
   5:  }
   6:   
   7:  <h2>Delete</h2>
   8:   
   9:  <p class="text-danger">@ViewData["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:              @Html.DisplayNameFor(model => model.Name)
  18:          </dt>
  19:          <dd>
  20:              @Html.DisplayFor(model => model.Name)
  21:          </dd>
  22:          <dt>
  23:              @Html.DisplayNameFor(model => model.Budget)
  24:          </dt>
  25:          <dd>
  26:              @Html.DisplayFor(model => model.Budget)
  27:          </dd>
  28:          <dt>
  29:              @Html.DisplayNameFor(model => model.StartDate)
  30:          </dt>
  31:          <dd>
  32:              @Html.DisplayFor(model => model.StartDate)
  33:          </dd>
  34:          <dt>
  35:              @Html.DisplayNameFor(model => model.Administrator)
  36:          </dt>
  37:          <dd>
  38:              @Html.DisplayFor(model => model.Administrator.FullName)
  39:          </dd>
  40:      </dl>
  41:      
  42:      <form asp-action="Delete">
  43:          <input type="hidden" asp-for="DepartmentID" />
  44:          <input type="hidden" asp-for="RowVersion" />
  45:          <div class="form-actions no-color">
  46:              <input type="submit" value="Delete" class="btn btn-default" /> |
  47:              <a asp-action="Index">Back to List</a>
  48:          </div>
  49:      </form>
  50:  </div>

This makes the following changes:

Run the app and go to 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

In the second tab, click Delete. 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.

Update Details and Create views

You can optionally clean up scaffolded code in the Details and Create views.

Replace the code in Views/Departments/Details.cshtml to delete the RowVersion column and show the full name of the Administrator.

[!code-htmlMain]

   1:  @model ContosoUniversity.Models.Department
   2:   
   3:  @{
   4:      ViewData["Title"] = "Details";
   5:  }
   6:   
   7:  <h2>Details</h2>
   8:   
   9:  <div>
  10:      <h4>Department</h4>
  11:      <hr />
  12:      <dl class="dl-horizontal">
  13:          <dt>
  14:              @Html.DisplayNameFor(model => model.Name)
  15:          </dt>
  16:          <dd>
  17:              @Html.DisplayFor(model => model.Name)
  18:          </dd>
  19:          <dt>
  20:              @Html.DisplayNameFor(model => model.Budget)
  21:          </dt>
  22:          <dd>
  23:              @Html.DisplayFor(model => model.Budget)
  24:          </dd>
  25:          <dt>
  26:              @Html.DisplayNameFor(model => model.StartDate)
  27:          </dt>
  28:          <dd>
  29:              @Html.DisplayFor(model => model.StartDate)
  30:          </dd>
  31:          <dt>
  32:              @Html.DisplayNameFor(model => model.Administrator)
  33:          </dt>
  34:          <dd>
  35:              @Html.DisplayFor(model => model.Administrator.FullName)
  36:          </dd>
  37:      </dl>
  38:  </div>
  39:  <div>
  40:      <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
  41:      <a asp-action="Index">Back to List</a>
  42:  </div>

Replace the code in Views/Departments/Create.cshtml to add a Select option to the drop-down list.

[!code-htmlMain]

   1:  @model ContosoUniversity.Models.Department
   2:   
   3:  @{
   4:      ViewData["Title"] = "Create";
   5:  }
   6:   
   7:  <h2>Create</h2>
   8:   
   9:  <h4>Department</h4>
  10:  <hr />
  11:  <div class="row">
  12:      <div class="col-md-4">
  13:          <form asp-action="Create">
  14:              <div asp-validation-summary="ModelOnly" class="text-danger"></div>
  15:              <div class="form-group">
  16:                  <label asp-for="Name" class="control-label"></label>
  17:                  <input asp-for="Name" class="form-control" />
  18:                  <span asp-validation-for="Name" class="text-danger"></span>
  19:              </div>
  20:              <div class="form-group">
  21:                  <label asp-for="Budget" class="control-label"></label>
  22:                  <input asp-for="Budget" class="form-control" />
  23:                  <span asp-validation-for="Budget" class="text-danger"></span>
  24:              </div>
  25:              <div class="form-group">
  26:                  <label asp-for="StartDate" class="control-label"></label>
  27:                  <input asp-for="StartDate" class="form-control" />
  28:                  <span asp-validation-for="StartDate" class="text-danger"></span>
  29:              </div>
  30:              <div class="form-group">
  31:                  <label asp-for="InstructorID" class="control-label"></label>
  32:                  <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
  33:                      <option value="">-- Select Administrator --</option>
  34:                  </select>
  35:              </div>
  36:              <div class="form-group">
  37:                  <input type="submit" value="Create" class="btn btn-default" />
  38:              </div>
  39:          </form>
  40:      </div>
  41:  </div>
  42:   
  43:  <div>
  44:      <a asp-action="Index">Back to List</a>
  45:  </div>
  46:   
  47:  @section Scripts {
  48:      @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
  49:  }

Summary

This completes the introduction to handling concurrency conflicts. For more information about how to handle concurrency in EF Core, see Concurrency conflicts. The next tutorial shows how to implement table-per-hierarchy inheritance for the Instructor and Student entities.

Previous Next





Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/AspNet-DocAndSamples-2017/aspnetcore/data/ef-mvc/concurrency.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>