Practical ASP.NET

Handling Data Contention with Optimistic Concurrency

Optimistic concurrency doesn't work for most tables, but Peter shows you how to change that.

One way to handle data contention is to turn on optimistic concurrency in your DataSources. However, the problem is that for most tables, it won't work. Here's what optimistic concurrency actually does and what you can do to make it work.

In a previous column, I outlined the problem that occurs when two users access the same record in a Web application, and suggested that you solve the problem by updating only the changed fields in the record. The ASP.NET DataSources offer a built-in solution called "Optimistic Concurrency." Unfortunately, in ASP.NET 2.0/3.5, it doesn't work most of the time.


Optimistic Concurrency
When you check off the Optimistic Concurrency option in a DataSource, you don't enable or disable any form of record locking. Instead, something almost very clever happens: Optimistic Concurrency simply extends the Where clause of your SQL statement to check that the record still has the data from when the record was originally retrieved. So, if the user changes the customer's name from "Irvine" to "Vogel," the Update statement looks like this:

Update CustomerTable
   Set LastName = "Vogel"
   Where CustomerId = "A123"
       And LastName = "Irvine"
       And FirstName = "Jan"
       And …tests on remaining fields….

If some other user has retrieved the record after you, made a change to some field and put the record back, this Where clause ensures that your update won't find the revised record. The database engine will still use the primary key field to retrieve the record so retrieval will be fast, making the process reasonably efficient.

You do need to write some code to check, after any update, to see if any records have been changed. Here's some typical code for a GridView's ItemUpdated event that checks to see if there's been any updates and, if not, calls the GridView's DataBind to display the new data:

Protected Sub CustomerView_ItemUpdated( _
  ByVal sender As Object, _
  ByVal e As WebControls.DetailsViewUpdatedEventArgs)
  
If e.AffectedRows = 0 Then
     Me.UpdateErrorLabel.Text = "Record not updated…"
     Me.CustomerView.DataBind()
End If

End Sub

After the databind the user will see the changes made by the other user.

The "almost" part comes from a failure in the way that the DataSource clause is extended. ASP.NET 1.1 got it right, generating this code for the Where clause:

Where CustomerId = "A123" 
   And (LastName = @Original_LastName 
              Or (@Original_LastName Is NULL 
                    AND LastName Is NULL))
    And …more fields…

This is almost unreadable code but what it does is check whether the data originally retrieved (that's what ends up in the @Original_LastName parameter) is NULL and if the data in the table is still NULL. The code generated in ASP.NET 2.0/3.5 doesn't include this test for NULL; if any field in the record has a NULL in it, then the Where clause always returns False and you never get an update.

Optimistic Concurrency Working
There is a solution: Use the TimeStamp (also known as the row version) field. Every database engine -- at least, all the ones that I know of -- have a TimeStamp-type field. This field is (a) never NULL and (b) has a value that changes with every update. Those two characteristics make it an ideal choice to use for checking to see if the record was changed by someone else after you retrieved it -- and it does it without an unreadable Where clause).

To implement this, make life easy on yourself and use the DataSource wizard to generate the initial versions of your Select, Update, Insert and Delete statements using the query-by-form page to select your fields. Then, in the "Custom SQL" page modify, your Select statement to retrieve the TimeStamp column (in SQL Server, the field is called @@DBTS, without any square brackets around it). You might also want to give the column a useful name, as this example does:

Select CustomerID, LastName, 
                 @@DBTS As RowVersion 
From CustomerTable

Finally, modify the Update statement's Where clause to use just the table's primary key and the TimeStamp column:

Update CustomerTable 
   Set LastName = @LastName
Where (CustomerId = @original_CustomerId) 
    And (@@DBTS = @original_RowVersion)

While you're at it, you can reduce the Where clause in the Delete statement to just the test on the primary key (optimistic concurrency isn't worth much on deletes).

With this simple change and the code in your Updated event, you can reliably (and efficiently) implement optimistic concurrency for Updates.

About the Author

Peter Vogel is a principal in PH&V Information Services, specializing in ASP.NET development with expertise in SOA, XML, database, and user interface design. His most recent book ("rtfm*") is on writing effective user manuals, and his blog on technical writing can be found at rtfmphvis.blogspot.com.

Reader Comments:

Mon, Jun 15, 2009 Zack Jones

After reading and doing some testing I'm bummed that @@DBTS doesn't work as described in the article. I'll just roll my own method using a Date/Time field to track when the record was last updated.

Sat, Nov 8, 2008 Anonymous Anonymous

Jon from New York is correct: I was going for a

Sat, Nov 8, 2008 Peter Canada

Jon from New York is correct: I was going for a "quick and dirty" solution that would fit in the column and its too course grained. As Jon points out, the full (and correct) implementation is to put a timestamp column on the table--something like this:



CREATE TABLE [dbo].[MyTable](

[CustomerId] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,

[LastName] [nvarchar](50) NOT NULL,

[MyRowVersion] [rowversion] NOT NULL

)



You can then use that column in your code where I have @@DBTS:



Select CustomerID, LastName,

MyRowVersion

From CustomerTable



and



Update CustomerTable

Set LastName = @LastName

Where (CustomerId = @original_CustomerId)

And (MyRowVersion = @original_MyRowVersion)

Fri, Nov 7, 2008 Anonymous Anonymous

Jon from New York is correct: I was going for a

Fri, Nov 7, 2008 Anonymous Anonymous

Jon from New York is correct: I was going for a

Sun, Oct 26, 2008 Jon from New York

This solution is too broad. @@DBTS is a database wide value. Implement a TimeStamp column in each table that is involved in updates. WHen the row is updated, set the TimeStamp column. When you retrieve a row, save the TimeStamp column and if you update, update where Timstamp column equals the saved value. If no rows are updated, another user updated the row after you retrieved it.

Thu, Jul 31, 2008 Peter Canada

In regard to the comment about using rowversion that is what I end up recommending by the end of the column. I'm not sure that a tuple should be treated as a unit: if you have two fields that are dependent on each other, you've violated third normal form. I'm also very uncomfortable with trying to implement your own version of rowversionthis is something that must be handled by the database engine, in my opinion.

In the example of two people updating the same record: I'm not clear why one manager might not mark the employee as due for promotion on Tuesday and, on Wednesday, the second manager mark the employee as deceased. The result would be the same as if the two did the job at the same day.

Thu, Jul 31, 2008 Peter O Edmonton

The problem with the suggestion of 'Peter from Canada' (not to be mistaken with me: Peter O from Edmonton) is that Peter sees a tuple as something you can slice and dice. That is a dangerous proposition. A record (tuple) validates OK or not on its own merit and if you change a field of that record, things can go crazy very fast. Let me give you a simple example: An employee fails to show up to work for over a week. First Manager sets his/her employee status to 'Deceased' and the second Manager set the same employee's 'DueDateForNextPromotion'

Thu, Jul 31, 2008 Peter O Edmonton

There is a more appropriate way of doing this. Either use the rowversion facility of sql server or implement your own.


so, all this long and windy update statements will reduce to

update Customer
set lastName = 'Vogel'

where customerID = @customerID
and
version = @version.

Note: version is a unique rowversion that increments each time the record changes. The user would have obtained @version when they fetched the record to update. If someone had changed it while it was in use, @version will not be the same for that customerID.

Fri, Jul 25, 2008 Chris

Rather than "remember" data for an extended where clause, would it not be just a good to compare the remembered data todata being updated and only update the fields that have changed from what they were when the data was retrieved for the user.
That is NOT to compare to what is the record is now but what it was when the user was given the data.
There can be a problem when the data is incremented (as in say how much a customer owes) but neither solutions work for this
Chris

Fri, Jul 25, 2008 Peter Canada

I could see how the SQL Cache Dependency could be used to notify the application that data retrieved from the database has been changedthe application could then (in the callback function called when something is removed from the cache) decide what to do about the change. However, wouldn't this require putting the retrieved data in the cache? The problem for me is that data can be removed from the cache for reasons other than concurrencyI'm not sure what would be the correct thing to do in those instances. Refetching the data seems to defeat the purpose of the cache.

Fri, Jul 25, 2008 James Chicago, IL

There are two more methods for dealing with this in ASP.NET 2.0 and higher that should be mentioned.

From the msdn content about aspnet_regsql.exe: "An advanced feature of ASP.NET output caching is SQL cache dependency. SQL cache dependency supports two different modes of operation: one that uses an ASP.NET implementation of table polling and a second mode that uses the query notification features of SQL Server 2005."

Wed, Jul 23, 2008 Peter Canada

Excellent!

Tue, Jul 22, 2008 Scott Hunter Redmond

This bug will be fixed in the .NET 3.5 SP1 which will be released shortly.

Sat, Jul 19, 2008 peter Canada

You're mistaken if you're saying that optimistic concurrency checks only the changed fields. In the DataSource, for the Update method, !!every!! field that was retrieved is checked. The net result is the equivalent of checking the timestamp for the cases where you've retrieved every field in the record. More critically, I think that checking just the changed fields is the wrong thing to do: I'm going to overwrite those fields so I don't care if they've changed. What I do care about is if the other fields have been changed: that other field's new value might not be compatible with itso I really want to check all the fields that I've haven't changed. This means that, in those situations where I retrieve only some fields in a row, the timestamp is preferable to checking just the retrieved fields because the timestamp also (indirectly) checks all the 'unretrieved' fieldswhich is probably what I want. It's not a free lunch: On this basis, using the timestamp will, occasionally, reject some changes when only the fields I'm updating will have changed and I just said that I probably don't care about that (though, I suspect, some people will want that also). However, that's preferable to having ALL of my changes rejected because one of the fields has a null in it.

Sat, Jul 19, 2008 Anonymous Anonymous

You're mistaken if you're saying that optimistic concurrency checks only the changed fields. In the DataSource, for the Update method, !!every!! field that was retrieved is checked. The net result is the equivalent of checking the timestamp for the cases where you've retrieved every field in the record. More critically, I think that checking just the changed fields is the wrong thing to do: I'm going to overwrite those fields so I don't care if they've changed. What I do care about is if the other fields have been changed: that other field's new value might not be compatible with itso I really want to check all the fields that I've haven't changed. This means that, in those situations where I retrieve only some fields in a row, the timestamp is preferable to checking just the retrieved fields because the timestamp also (indirectly) checks all the 'unretrieved' fieldswhich is probably what I want. It's not a free lunch: On this basis, using the timestamp will, occasionally, reject some changes when only the fields I'm updating will have changed and I just said that I probably don't care about that (though, I suspect, some people will want that also). However, that's preferable to having ALL of my changes rejected because one of the fields has a null in it.

Sat, Jul 19, 2008 Anonymous Anonymous

You're mistaken if you're saying that optimistic concurrency checks only the changed fields. In the DataSource, for the Update method, !!every!! field that was retrieved is checked. The net result is the equivalent of checking the timestamp for the cases where you've retrieved every field in the record. More critically, I think that checking just the changed fields is the wrong thing to do: I'm going to overwrite those fields so I don't care if they've changed. What I do care about is if the other fields have been changed: that other field's new value might not be compatible with itso I really want to check all the fields that I've haven't changed. This means that, in those situations where I retrieve only some fields in a row, the timestamp is preferable to checking just the retrieved fields because the timestamp also (indirectly) checks all the 'unretrieved' fieldswhich is probably what I want. It's not a free lunch: On this basis, using the timestamp will, occasionally, reject some changes when only the fields I'm updating will have changed and I just said that I probably don't care about that (though, I suspect, some people will want that also). However, that's preferable to having ALL of my changes rejected because one of the fields has a null in it.

Thu, Jul 17, 2008 Brian B

Using the timestamp has been a solution for may years. But this isn't the same as what .NET proposes to correct. The ADO.NET checks if the specific field being updated has changed. This way two users can update different fields on the same record without a conflict. The timestamp just looks that last update without respect to what was changed.

Add Your Comments Now:

Your Name:(optional)
Your Email:(optional)
Your Location:(optional)
Comment:
Please type the letters/numbers you see above