简体   繁体   中英

How to fix OptimisticConcurrencyException in an MVC application

We have an application that is giving the following error message (from time to time, not often):

System.Data.Entity.Core.OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries

I looked at other answers and tried them (primary key id being zero), but I'm not able to reproduce.

The code that saves records is implemented as a command invoker. The Execute method receives the command object to save, and calls the Handle method of the handler:

public class CommandInvoker : ICommandInvoker
{
    private readonly IKernel _kernel;
    private readonly IArhomaContext _context;

    public CommandInvoker(IKernel kernel, IArhomaContext context)
    {
        _kernel = kernel;
        _context = context;
    }

    public void Execute<T>(T command)
    {
        var handler = _kernel.TryGet<ICommandHandler<T>>();
        handler.Handle(command);
        _context.SaveChanges();
    }
}

Can someone guide me as to how we can fix this problem? Should we put the SaveChanges inside a try catch and catch an OptimisticConcurrencyException? I'm unable to reproduce it, but we get this in production.

Let's get to some understanding of what is happening before solutioning. Optimistic concurrency means you are grabbing the record assuming nobody else will change it before you change back. Pessimistic concurrency is when you lock the record, ensuring nobody else can change anything. You rarely use pessimistic in a disconnected application, like a website. Instead, you capture the error and determine what to do next.

You have a lot of options in what to do if you catch errors. The first is to alert the user of what is in the database now and what they were going to save and let them decide. As you have grabbed the record again, you can save it back (and if someone changes again before you save (very rare?), overwrite). The second is to simply overwrite, assuming last in wins. You can also use an approach where you use different methods depending on what change, as an example:

User A (your user) wants to change telephone number on a denormalized record that user B changed the address. You can combine the two, behind the scenes, and save.

With EF, the easiest method is to repull the record and overwrite fields and then save, as you will then have a copy the context recognizes as unchanged in the database. How much "safety" you add, or user interaction you add, is your choice, and depends on business requirements.

Here is a great article on handling concurrency with EF in a MVC application.

http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application

The article is for all intents and purposes showing an example of a single-tier appplication. However, it illustrates the approach pretty well. It basically catches DbUpdateConcurrencyException and displays the original values to the user, allowing them to determine whether or not to continue with their action. It also provides for handling of RetryLimitExceededException .

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

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

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

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

            return RedirectToAction("Index");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.Single();
            var clientValues = (Department)entry.Entity;
            var databaseEntry = entry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                    ModelState.AddModelError("Name", "Current value: "
                        + databaseValues.Name);
                if (databaseValues.Budget != clientValues.Budget)
                    ModelState.AddModelError("Budget", "Current value: "
                        + String.Format("{0:c}", databaseValues.Budget));
                if (databaseValues.StartDate != clientValues.StartDate)
                    ModelState.AddModelError("StartDate", "Current value: "
                        + String.Format("{0:d}", databaseValues.StartDate));
                if (databaseValues.InstructorID != clientValues.InstructorID)
                    ModelState.AddModelError("InstructorID", "Current value: "
                        + db.Instructors.Find(databaseValues.InstructorID).FullName);
                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                    + "was modified by another user after you got the original value. The "
                    + "edit operation was canceled and the current values in the database "
                    + "have been displayed. If you still want to edit this record, click "
                    + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = databaseValues.RowVersion;
            }
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
        }
    }
    ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM