2

I'm wanting to do a simple edit form for our Issue Tracking app. For simplicity, the HttpGet Edit action looks something like this:

    // Issues/Edit/12
    public ActionResult Edit(int id)
    {
        var thisIssue = edmx.Issues.First(i => i.IssueID == id);
        return View(thisIssue);
    }

and then the HttpPost action looks something like this:

    [HttpPost]
    public ActionResult Edit(int id, FormCollection form)
    {
        // this is the dumb part where I grab the object before I update it.
        // concurrency is sidestepped here.
        var thisIssue = edmx.Issues.Single(c => c.IssueID == id);

        TryUpdateModel(thisIssue);
        if (ModelState.IsValid)
        {
            edmx.SaveChanges();

            TempData["message"] = string.Format("Issue #{0} successfully modified.", id);
            return RedirectToAction("Index");
        }

        return View(thisIssue);
    }

Which works wonderfully. However, the concurrency check doesn't work because in the Post, I'm re-retreiving the current entity right before I attempt to update it. However, with EF, I don't know how to use the fanciness of SaveChanges() but attach my thisIssue to the context. I tried to call edmx.Issues.Attach(thisIssue) but I get

The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.

How do I handle concurrency in MVC with EF and/or how do I properly Attach my edited object to the context?

Thanks in advance

Jorin
  • 1,652
  • 1
  • 19
  • 25

2 Answers2

6

What you are doing is tricky, but can be made to work. Let's presume your timestamp field is called ConcurrencyToken. Obviously, you must include this value in the View and submit it with your form. But you can't simply assign that to the value to thisIssue.ConcurrencyToken in the POST because the EF will remember both the "old" value (the value you fetched from the DB with your call to Single() as well as the "new" value (from your form) and use the "old" value in the WHERE clause. So you need to lie to the EF and assign the correct value. Try this:

    var thisIssue = edmx.Issues.Single(c => c.IssueID == id);
    TryUpdateModel(thisIssue); // assign ConcurrencyToken
    var ose = Context.ObjectStateManager.GetObjectStateEntry(entityToUpdate);
    ose.AcceptChanges();       // pretend object is unchanged
    TryUpdateModel(thisIssue); // assign rest of properties

You can optimize this by binding only ConcurrencyToken instead of calling TryUpdateModel twice, but this should get you started.

Craig Stuntz
  • 125,891
  • 12
  • 252
  • 273
  • 1
    well, that sure works, but it definitely feels kludgy. Is there not a way to just say this is my issue object; attach this issue object to issue #12 and update issue #12 with the values stored here? That way, it puts it on the database to make sure that issue #12 still has the same timestamp... Also, can you elaborate further on how I'd only bind the ConcurrencyToken field? – Jorin May 05 '10 at 19:46
  • You bind only one field by passing a whitelist; there's an overload for that. To do what you ask regarding updating, you'd create a stub object, attach it to the context, and update it. This prevents fetching the newer `ConcurrencyToken` from the DB. – Craig Stuntz May 05 '10 at 19:52
  • Sorry, I'm sure I'm just being dense, but can you spell out something on how to stub something out and attach it. I tried this and it didnt work [HttpPost] public ActionResult Edit(int id, Issue issue) { Issue newIssue = new Issue(); newIssue.IssueID = id; newIssue.EntityKey = new System.Data.EntityKey("MayflyEntities.Issues", "IssueID", id); edmx.Issues.Attach(newIssue); newIssue = issue; edmx.SaveChanges(); } – Jorin May 05 '10 at 20:21
  • Here's an example: http://blogs.msdn.com/alexj/archive/2009/06/19/tip-26-how-to-avoid-database-queries-using-stub-entities.aspx – Craig Stuntz May 05 '10 at 20:41
5

An example of performing optimistic concurrency compatible updates using EF5 follows (the method comes from a repository.) It is assumed that the Entity defines Timestamp with [ConcurrencyCheck]. The concurrency exception occurs upon calling DbContext.SaveChanges().

public TEntity Update(TEntity entity)
{
    var attached = this.GetById(entity.Id);
    if (attached == null)
    {
        throw new MvcBootstrapDataException("{0} with Id = {1} does not exist.".F(typeof(TEntity).Description(), entity.Id));
    }

    var entry = this.Context.Entry(attached);

    // The Timestamp must be in the original values for optimistic concurrency checking to occur.
    // Otherwise the context knows that the Timestamp has been modified in the context
    entry.OriginalValues["Timestamp"] = entity.Timestamp;

    entry.CurrentValues.SetValues(entity);

    attached.Modified = DateTime.Now;

    return attached;
}
Carl G
  • 17,394
  • 14
  • 91
  • 115
  • Calr, your solution is better than the one posted by Craig, because it goes straight to updating the property we want. Since I am working with an abstract and generic implementation of an Entity list, I created a simpler function that let me update any property of any entity (I just make sure I only use it on retrieved entities from DB): `public virtual void UpdateEntityOriginalValue(T entity, string property, object value) { var entry = _entities.Entry(entity); entry.OriginalValues[property] = value; }` – Max Vargas Jan 22 '19 at 22:40