Optimistic Concurrency with MongoDB C# driver and ASP.NET MVC – Prevent multiple users updating the same record

Over on my MongoDB C# Tutorial Richard asked how you would prevent multiple users from updating the same document. In this post I’m going to update the project in the tutorial to support this.

In my blog example this issue could occur when user 1 clicks to edit a post. While editing, user 2 also edits the same post. User 2 finishes their edit before user 1 and saves the post. User 1 continues editing and saves their changes. User 2’s changes are completely overwritten and neither user has any idea that it happened.

In this implementation, based on the above scenario I’m going to display an error to user 2 to tell them somebody else has updated to the post. They won’t be able to save their changes and it would be up to them to reload the post and make their changes again. Of course you could do something much more exciting, like show the two versions side by side and allow the user to merge the changes.

Source code

To make this work I’m going to add a version number to my post object which gets incremented each time I save a post. First off I need to add this to my Post object:

[HiddenInput(DisplayValue = false)]
public int Version { get; set; }

I’ve added the HiddenInput attribute so that this property is rendered as a hidden field on my form. I’ve also set DisplayValue to false which stops the value and label from being rendered.

That’s all the changes I need to make in order to save a new post with a version number. When a create a new post the hidden input will have the value 0 which will flow through to my service and into MongoDB:

Mongo Vue with version

The next change I need to make is to the Edit method of my PostService. Previously the method looked like this:

public void Edit(Post post)
{
    _posts.Collection.Update(
        Query.EQ("_id", post.PostId),
        Update.Set("Title", post.Title)
            .Set("Url", post.Url)
            .Set("Summary", post.Summary)
            .Set("Details", post.Details));
}

I’ve now updated it to take version into account:

public bool Edit(Post post)
{
    var result = _posts.Collection.FindAndModify(
        Query.And(Query.EQ("_id", post.PostId), Query.EQ("Version", post.Version)),
        null,
        Update.Set("Title", post.Title)
            .Set("Url", post.Url)
            .Set("Summary", post.Summary)
            .Set("Details", post.Details)
            .Inc("Version", 1));

    return result.ModifiedDocument != null;
}

The method now returns a Boolean. This will be true if the update is successful, and false if another update has been made by another user. I’ve also changed Update to FindAndModify. FindAndModify updates a document and then returns that document in the same process. The query now includes the Version field, and I will pass in the version number of the post when it was retrieved for editing. The update query now also increments the version number.

So how does it all work? If the post hasn’t been updated by another user then the version number will still be the same as when I started editing, so the document will be found and the version number incremented. result.ModifiedDocument will be a BsonDocument and the method will return true.  If the post has been updated by another user then the version number in the database will already been incremented, and won’t match the version I retrieved when I started editing. In this instance FindAndModify won’t find any matching document to update, result.ModifiedDocument will be null and the method will return false.

Now all I need to do is handle this in my controller to display an error if somebody else has updated the post. In this example I’m just going to add an extra validation message to the top of my edit form:

@Html.ValidationMessage("ConcurrencyError")

The Update method on my PostController now looks like this:

[HttpPost]
public ActionResult Update(Post post)
{
    if (ModelState.IsValid)
    {
        post.Url = post.Title.GenerateSlug();

        if (_postService.Edit(post))
        {
            return RedirectToAction("Index");
        }

        ModelState.AddModelError("ConcurrencyError", "This post has been updated since you started editing it. Please reload the post to get the latest changes.");
        return View(post);
    }

    return View(post);
}

The post object here will contain the version number from the hidden field which was populated when starting the edit the post. If the service returns true I redirect back to the listing page, otherwise I add a validation model error which gets displayed on the page:

Concurrency Error Message

To try it out open the project and create a new post, then start to edit that post. Open another browser and edit and save the same post. Go back to your first browser and try to save the post. You should see the same error as the screenshot above.

Source code

Posted on by Joe in ASP.NET, C#, MongoDB, MVC

Add a Comment