In this post I am going to present my solution for handling optimistic concurrency scenarios in Entity Framework. This solution is somehow an extension of recommended approaches presented on MSDN Entity Framework learning center here.
Apart from pessimistic concurrency pattern, which assumes creating read or update locks on data when it is edited, this approach concerns on optimistic concurrency pattern.
Optimistic pattern is based on handling concurrency conflicts at the moment that user tries to update data. Basically an concurrency conflict occurs when two users simultaneously work on a portion of data not knowing about it. So for instance user A loads product and starts editing it in his client application. A while later user B loads the same product, changes some of its properties and saves it to the database. Now user A, which is still working on its copy of product, does not know anything about the changes made by user B. Now concurrency conflict occurs when he tries to save modified product to the database. The problem also occurs when user B deletes the product while user A is still editing it.
The solution I am going to present is about to handle such conflicts at the moment the user A tries to store his copy of product.
Before we start handling the conflicts, we have to let the EF detect when the conflict occurs. There are two options.
First is pointing out the properties that have to be compared in order to detect if entity was modified by another user. We need to mark those properties with ConcurrencyCheck flag. This will make the EF engine to compare the original values of those properties with the values in database. If values are different then it means the entity was modified or deleted by concurrent user and the concurrency exception will be thrown.
However the preferred way of detecting concurrency changes is the TimeStamp field, which is updated to an new unique value every time the entity is modified. EF then compares it on every update and detects the conflict this way.
In presented solution all entities are derived from a ConcurrencyEntity
We also need some constants.
Above enumeration informs if the conflicted entity was deleted or modified.
This one informs how the conflicts should be resolved. We can either overwrite all modified values with those the current user provided. This is called ClientWins strategy. We can also reload and take fresh copy of conflicted entity which is called StoreWins strategy. Finally we can also try to merge the conflicted entity in the way that it overwrites all properties that were changed by current user and were not modified by concurrency user. This is called Merge strategy obviously.
Next we need some helper objects which represent the result of save operation. Either there was no conflict or all conflicts were resolved or there is a conflict and save transaction was rollbacked. All of those information must be returned to the client code.
Then we have IDbConflictSolver interface.
In the solution we can either assume one concurrency resolution strategy for all kind of entities and for all save operations or we can ask user to react and decide about the strategy every time the changes are persisted. For the later case we need to implement above interface which should simply present the conflict result to the user interface in a comprehensible way and take the strategy user selects.
Next thing is to modify the SaveChanges method and override it in our entity context class.
We try to save changes and call ProcessConflicts method when concurrency exception is thrown. Basically in every call to this method we handle only one entity conflict. This is because the EF will throw concurrency exception as soon as it detects single entity conflict, no aggregation of conflicts or something is made here.
Now lets look at the crucial part of the solution, the conflict processing method.
Here we can either set the default resolving strategy or provide a custom conflict solver. The database values are taken from the store and compared to current values of the conflicted entity. In lines 20-25 the case when the entity was deleted is handled. In line 28 the solver is used if provided and default resolving strategy is overwritten with one selected by user. Next we handles all three resolving strategies merge, client wins and store wins. I hope the rest is self explanatory.