How do you use optimistic concurrency with WebAPI OData controller

2.7k views Asked by At

I've got a WebAPI OData controller which is using the Delta to do partial updates of my entity.

In my entity framework model I've got a Version field. This is a rowversion in the SQL Server database and is mapped to a byte array in Entity Framework with its concurrency mode set to Fixed (it's using database first).

I'm using fiddler to send back a partial update using a stale value for the Version field. I load the current record from my context and then I patch my changed fields over the top which changes the values in the Version column without throwing an error and then when I save changes on my context everything is saved without error. Obviously this is expected, the entity which is being saved has not been detacched from the context so how can I implement optimistic concurrency with a Delta.

I'm using the very latest versions of everything (or was just before christmas) so Entity Framework 6.0.1 and OData 5.6.0

public IHttpActionResult Put([FromODataUri]int key, [FromBody]Delta<Job> delta)
{
    using (var tran = new TransactionScope())
    {
        Job j = this._context.Jobs.SingleOrDefault(x => x.JobId == key);

        delta.Patch(j);

        this._context.SaveChanges();

        tran.Complete();

        return Ok(j);
    }
}

Thanks

3

There are 3 answers

3
Roy Dictus On

Simple, the way you always do it with Entity Framework: you add a Timestamp field and put that field's Concurrency Mode to Fixed. That makes sure EF knows this timestamp field is not part of any queries but is used to determine versioning.

See also http://blogs.msdn.com/b/alexj/archive/2009/05/20/tip-19-how-to-use-optimistic-concurrency-in-the-entity-framework.aspx

1
bambam On

I've just come across this too using Entity Framework 6 and Web API 2 OData controllers.

The EF DbContext seems to use the original value of the timestamp obtained when the entity was loaded at the start of the PUT/PATCH methods for the concurrency check when the subsequent update takes place.

Updating the current value of the timestamp to a value different to that in the database before saving changes does not result in a concurrency error.

I've found you can "fix" this behaviour by forcing the original value of the timestamp to be that of the current in the context.

For example, you can do this by overriding SaveChanges on the context, e.g.:

public partial class DataContext
{
    public override int SaveChanges()
    {
        foreach (DbEntityEntry<Job> entry in ChangeTracker.Entries<Job>().Where(u => u.State == EntityState.Modified))
            entry.Property("Timestamp").OriginalValue = entry.Property("Timestamp").CurrentValue;

        return base.SaveChanges();
    }
}

(Assuming the concurrency column is named "Timestamp" and the concurrency mode for this column is set to "Fixed" in the EDMX)

A further improvement to this would be to write and apply a custom interface to all your models requiring this fix and just replace "Job" with the interface in the code above.

Feedback from Rowan in the Entity Framework Team (4th August 2015):

This is by design. In some cases it is perfectly valid to update a concurrency token, in which case we need the current value to hold the value it should be set to and the original value to contain the value we should check against. For example, you could configure Person.LastName as a concurrency token. This is one of the downsides of the "query and update" pattern being used in this action.

The logic you added to set the correct original value is the right approach to use in this scenario.

0
coni2k On

When you're posting the data to server, you need to send RowVersion field as well. If you're testing it with fiddler, get the latest RowVersion value from your database and add the value to your Request Body.

Should be something like;

RowVersion: "AAAAAAAAB9E="

If it's a web page, while you're loading the data from the server, again get RowVersion field from server, keep it in a hidden field and send it back to server along with the other changes.

Basically, when you call PATCH method, RowField needs to be in your patch object.

Then update your code like this;

Job j = this._context.Jobs.SingleOrDefault(x => x.JobId == key);

// Concurrency check
if (!j.RowVersion.SequenceEqual(patch.GetEntity().RowVersion))
{
    return Conflict();
}

this._context.Entry(entity).State = EntityState.Modified; // Probably you need this line as well?
this._context.SaveChanges();