views:

114

answers:

3

I have the following update code in the ASP.NET MVC controller:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Person(int id, FormCollection form)
{
  var ctx = new DB_Entities(); // ObjectContext
  var person = ctx.Persons.Where(s => s.Id == id).FirstOrDefault(); 
  TryUpdateModel(person, form.ToValueProvider()); 
  ctx.SaveChanges(); 
  return RedirectToAction("Person", id);
}

However, this update code is Last-Writer-Wins. Now I want to add some concurrency control. The Person table already has the SQL timestamp column. Do I have to send the timestamp value to the client as the hidden value and process it manually in the post back? Or is there a a standard pattern in Entity Framework to do this?

Thanks.

+1  A: 

First you need to define which property or properties will be used to perform the concurrency check, because concurrency is defined on a property-by-property basis in the Entity Framework. ConcurrencyMode is used to flag a property for concurrency checking and can be found in the Entity Object Properties window (just right click on Person entity in your model). Its options are None, which is the default, and Fixed.

During a call to SaveChanges, if a field has been changed in the DB since the row was retrieved, EF will cancel the Save and throw an OptimisticConcurrencyException if we set that field's ConcurrencyMode to Fixed.

Under the hood, EF includes that field's value in the Update or Delete SQL statement that is being Submitted to the data store as a WHERE clause.

If you want to have Optimistic Concurrency on all properties, just set TimeStamp property ConcurrencyMode to Fixed you will get an OptimisticConcurrencyException if any field's value within the table get changed (instead of setting it to Fixed on every single property).

EDIT
As per Craig comment below, you need to persist the TimeStamp in the view and read it back into Person object and the rest will be taken care of by EF if you set the ConcurrencyMode to fixed on the TimeStamp property. You can of course try to handle OptimisticConcurrencyException that could be thrown by EF and there are ways to recover from this exception, if you are interested.

Morteza Manavi
He does need to include the concurrency token in the view, because he wants to know if anyone has updated the entity since the GET. This is only possible if you know what the value was during the GET. However, re-fetching the entity during the POST complicates things, as it defeats the EF's built-in concurrency control. The EF will use the value you fetched during the POST, not the value from the GET.
Craig Stuntz
Yes, you are right, he defiantly needs to read the timestamp back from his view. I was confused by the fact that he reads Person again in the POST. I updated my answer. Craig, Thanks very much for your input on this.
Morteza Manavi
Thanks. I think Craig's workaround (using GetObjectStateEntry.AcceptChanges to pretend the original timestamp) works better. BTW, I think the root cause is that the Person entity is read again in the POST, so it may not be the same entity when the page was originally rendered. Is this the right programming model in EF? Any other ways to avoid this?
Ian
A: 

I ended up doing this in the postback function:

var person = ctx.Persons.Where(s => s.Id == id).FirstOrDefault();
string ts = form.Get("person_ts"); // get the persisted value from form
if (person.TimeStamp != ts)
{
   throw new Exception("Person has been updated by other user");
}
TryUpdateModel(person, form.ToValueProvider());     
// EF will check the timestamp again if the timestamp column's 
// ConcurrencyMode is set to fixed.
ctx.SaveChanges();

So the optimistic concurrency is checked twice. Just wondering if there is a better way to do this?

Thanks.

Ian
I don't think your code even compiles:Operator '!=' cannot be applied to operands of type 'byte[]' and 'string'. Anyhow, even if you make it to be compiled, still this is not a robust programming model to check for concurrency, because someone might changes the row after your if statement and just before SaveChanges() and you will out of luck catch this.
Morteza Manavi
Sorry for the brevity '!=', anyway you get the idea (assume there is some helper method to do the comparison). That's why I said that the concurrency checking is done twice in my approach: one in my "if" statement, the other in EF SaveChanges() using ConcurrencyMode. If someone changes the row after the if statement and before SaveChanges, EF will catch it.
Ian
Ian, it's just fine to reread the entity during POST. It isn't required, but there's no problem with doing so.
Craig Stuntz
+1  A: 

This is actually a little harder than it perhaps should be. In addition to changing the concurrency mode to fixed, as Morteza says, you have to inject the concurrency value which you read during the GET before updating the entity during the POST. The way to think about this is that you're trying to get the entity back into the state it was in during the GET, before updating it. I have a code example in this answer:

http://stackoverflow.com/questions/2775450/asp-net-mvc-concurrency-with-rowversion-in-edit-action/2775887#2775887

Craig Stuntz