Practical .NET

Handling Record Contention in Code-First Entity Framework

Peter returns to the topic of managing multiple users accessing the same row in a table using Entity Framework, but this time using code-first development. There are some unexpected issues to deal with.

This column is really a follow-up to one that I did in 2009. In that column, I discussed strategies for handling two users updating the same row in a table when using Entity Framework (one strategy is "Don't worry about it"). I recommended setting the ConcurrencyMode property on the entity property that corresponds to a table's timestamp column to Fixed. I still think that's a great answer, but it only applies in database-first or model-first development.

In code-first development, you achieve the same effect by adding the ConcurrencyCheck attribute to one or more properties in your business entity class. In the following code, for instance, I've got a Customer class with a column called TimeStamp that, presumably, maps to a timestamp or rowversion column on the Customer table in the database. In order to keep Entity Framework (EF) from trying to update the TimeStamp column, I also decorate it with the TimeStamp attribute:

Public Class Customer
  Id As String
  FirstName As String
  LastName As String
  <ConcurrencyCheck>
  <TimeStamp>
  TimeStamp As Byte()
End Class

Now, when I save changes to the class, if the TimeStamp column in the table has been changed since I retrieved the row, I'll get a DbUpdateConcurrencyException. When that exception is raised, I update the Customer object with the current data from the database so that I can show it to the user and they can retry their changes:

Dim saved As Boolean
Dim cust As Customer
Dim dbc As New CustomerContext
custExisting = dbc.Customers.Single(Function(c) c.Id = "A123")

...update the entity object...

Try
  dbc.SaveChanges();
catch dbuEx as DbUpdateConcurrencyException
  dbc.Entry(custExisting).Reload()
End Try
...redisplay entity...

Managing the Concurrency Token
Technically, this timestamp is acting as what EF calls a "concurrency token." Under the hood, when it comes time to do the updates, EF uses the original value retrieved with the row to see if the row has been changed since the row was retrieved. For this to work, however, the entity that I update before calling SaveChanges must hold the timestamp value that I got when I first retrieved the Customer object. My previous code -- really, only appropriate for a desktop application -- ignores some issues by keeping the retrieved entity object in memory from retrieval to update. In an ASP.NET application (Web Forms, MVC or Web API) or a Windows Communication Foundation (WCF) service, the problem is more complicated.

If you're retrieving a Customer object and moving its data to a Data Transfer Object before sending that DTO to the presentation layer, you must move the TimeStamp column into the DTO also. In an ASP.NET MVC project, you'll need to store the value in a hidden field. In an ASP.NET Web Forms project, you can put the value in a hidden control or the ViewState. In a service, you'll need to send the TimeStamp column along with the rest of the data and tell the client not to modify the value.

When the DTO comes back from the presentation layer, you'll need to re-fetch the entity object from the database and update it with the values in the DTO before calling SaveChanges. As part of that, you must move the timestamp value into the entity. This ensures that the ConcurrencyCheck column has the value from when the entity object was first retrieved.

There's a wrinkle here, though: Updating the TimeStamp column, even if you update it with the same value that it currently has in the database, causes EF to flag the entity as having been changed. That can cause SaveChanges to generate unnecessary trips to the database. So, when you update the entity object, you'll want to skip updating the TimeStamp column until you've determined that there's been some change to the entity.

You can build in your own mechanism for checking for changes or, if you're using EF 5.0, you can use EF's ChangeTracker object. When writing your own assignment statements, that just means that you check the HasChanges property on the ChangeTracker object on the DbContext object before updating the TimeStamp column on the entity, using code like this:

custExisting.FirstName = custDTO.FirstName
custExisting.LastName = custDTO.LastName
If dbc.ChangeTracker.HasChanges Then
   custExisting.TimeStamp = custDTO.TimeStamp

If, as I do, you're using AutoMapper to move your data around, then you'll use code like this to have AutoMapper skip moving the TimeStamp column and do it yourself after checking to see if there were any changes:

Mapper.CreateMap(Of custDTO, Customer).
          ForMember("TimeStamp", Sub(b) b.Ignore())
custExisting = Mapper.Map(Of custDTO, Customer)(custIn, custExisting)
If dbc.ChangeTracker.HasChanges Then
  custExisting.TimeStamp = custIn.Timestamp

I should point out that using ChangeTracker's HasChanges property isn't free -- the property does trigger some trips to the database. If you're confident that you can live with the odd "unnecessary" update, you may want to consider skipping checking the HasChanges property on the basis that the odd extra update will be less expensive than constantly reading the HasChanges property.

That's a lot of words when all you need are two attributes on one of your entity properties (TimeStamp and ConcurrencyCheck) and a little code in your data access layer to manage that property. And if even that sounds like too much work, there's always the "Don't worry about it" option.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.