I’m building an ASP.NET MVC 2 site that uses LINQ to SQL. In one of the places where my site accesses the DB, I think a race condition is possible.
DB Architecture
Here are some of the columns of the relevant DB table, named Revisions:
- RevisionID – bigint, IDENTITY, PK
- PostID – bigint, FK to PK of
Poststable - EditNumber – int
- RevisionText – nvarchar(max)
On my site, users can submit a Post and edit a Post later on. Users other than the original poster are able to edit a Post – so there is scope for multiple edits on a single Post simultaneously.
When submitting a Post, a record in the Posts table is created, as well as a record in the Revisions table with PostID set to the ID of the Posts record, RevisionText set to the Post text, and EditNumber set to 1.
When editing a Post, only a Revisions record is created, with EditNumber being set to 1 higher than the latest edit number.
Thus, the EditNumber column refers to how many times a Post has been edited.
Incrementing EditNumber
The challenge that I see in implementing those functions is incrementing the EditNumber column. As that column can’t be an IDENTITY, I have to manipulate its value manually.
Here’s my LINQ query for determining what EditNumber a new Revision should have:
using(var db = new DBDataContext())
{
var rev = new Revision();
rev.EditNumber = db.Revisions.Where(r => r.PostID == postID).Max(r => r.EditNumber) + 1;
// ... (fill other properties)
db.Revisions.InsertOnSubmit(rev);
db.SubmitChanges();
}
Calculating a maximum and incrementing it can lead to a race condition.
Is there a better way to implement that function?
Update directly in the database and return the new revision:
Unfortunately, this is not possible in LINQ. In fact, is not possible in the client at all, no matter the technology used, short of doing pessimistic locking which has too many drawback to worth considering.
Updated:
Here is how I would insert a new revision (including first revision):
I omitted the error handling, see Exception handling and nested transactions for a template procedure than handles errors and nested transactions properly.
You’ll notice the Posts table has a LastRevision field that is used as the increment for the post revisions. This is much better than computing the MAX each time you add a revision, as it avoid a (range) scan of Revisions. It also acts as a concurrency protection: only one transaction at a time will be able to update it, and only that transaction will proceed with inserting a new revision. Concurrent transactions will block and wait until the first one commits, then the next transaction unblocked will correctly update the revision number to +1.