I’ve recently had to come back and visit the Entity Framework (EF), as part of this I had the need to provide editing for many entities as a single edit operation. In effect what I'm really saying here is that my collection of entities is really one entity as far as my user is concerned, e.g. An Order entity is comprised of a series of Order Item entities, as far as the user request is concerned they are editing an order.
Now out of the box Entity Framework provides support for Optimistic Locking on a per entity basis. However in my case I don’t want to manage optimistic locking at the level of each EF entity, I wish to do it at the collection/or aggregate level. There is no direct support for this in EF, so it has to be done by the application developer. Martin Fowler presents a serious of patterns for solving this problem which he calls the Coarse Grained Lock. The Coarse Grained lock is in effect using a single lock for a collection of items, rather than have a lock per item.
Two possible approaches are put forward one is to nominate a root entity to act as the lock for all other entities, the other is to create a separate entity that represents a shared lock.
The root entity approach works assuming that from any entity in the aggregate you can easily find and fetch the root entity in order to perform the necessary locking.
The shared lock approach requires each entity to have a direct reference to the shared lock.
Neither of these approaches looked especially simple to implement in EF, after playing around with both approaches I finally opted for the shared Optimistic lock pattern.
The use case I opted for was a simple one were I’m modelling an Order and a series of Order Items.
Using simple EF optimistic concurrency I would place a version column in each entity table. The problem with this is that if two users are editing the same order, by simply modifying different order items or adding items there is no way to ensure that at the end of both edits we have a valid order, since the result in the store will be a merge of both edits whilst each edit session will only see the original and its own changes. If the two edits were modifying the same order item a concurrency conflict would be detected. Now in some cases having the ability for two users to edit the same Order concurrently is great, but what if I want to ensure that the composite as a result of both users modifications is in fact valid, perhaps you wish to run some validation logic across the entire composite before saving it.
You could start a system transaction per user update, perform the users updates, reload the entire composite, validate it and if it checks out commit the transaction. However this may be too expensive, and if most of the time there is no contention then this approach is expensive.
By having a shared version across all entities that make up the order we can be sure that when we save the parts of the composite that we have changed that the parts that we haven’t changed are still the same.
When any entity that references a shared version is modified it must update the version entity’s version number, this will only be successfully persisted if the version number in the store is the same as it was when the entity was loaded.
Now to see if EF can implement this, when building the mappings we end up with an entity for each table, and a reference from the Orders and OrderItems to a Versions Entity.
The norm in EF when doing optimistic locking is to use a database timestamp column for the version number, in this case this wasn’t possible since we needed to make the version entity be persisted when a change was made to any entity that referenced it, so the version is represented as an integer, when a change is made to any entity that references that version the version number needs to be updated, and thus it will be persisted by EF when we decide to save our changes.
All of this could be done manually when ever you load an entity be sure to include the Version its associated with and when you wish to perform a modification increment its associated version, version number. This seems tedious and in a production environment missing this step is very likely, the result of which would result in possible corruption assuming two edits happen at the same time. So I wasn’t happy with that, so what I decided to do was to automate this part of the process.
Normally in EF you call SaveChanges on the context to persist any changes, the changes are stored in the context and are available to see what has changed, so all I would need to do was to provide my own SaveChanges and look at the change set and for every entity in that set that references a shared version I increment that version, thus modifying the version and now placing it into the change set.
Unfortunately you can’t override SaveChanges, ( apparently you will in EF 2 ) so you end up writing a new method on the ObjectContext in my case I called it Submit, which performs the necessary checks to see what has changed, and then calls SaveChanges. Note that the type SharedVersion represents the entities in the Version table.
2: public partial class AcmeWidgets
4: public void Submit()
6: // Establish all the entities that have changed
8: var entities = this.ObjectStateManager
9: .GetObjectStateEntries(EntityState.Modified | EntityState.Added );
11: var toIncrement = new Dictionary<int, SharedVersion>();
13: // Foreach entity that has changed, if it has a shared version
14: // then increment it.
15: foreach (ObjectStateEntry attachedObject in entities.Where(e => e.IsRelationship == false ) )
17: // If No property of type SharedVersion then it will not attempt to increment the version
18: // if many it will throw an exception
19: // if just one will increment the version number, assuming it has not
20: // previously been incremented
21: PropertyInfo versionProperty = (from prop in attachedObject.Entity.GetType().GetProperties()
22: where prop.PropertyType == typeof(SharedVersion)
23: select prop)
27: if (versionProperty != null)
29: SharedVersion versionEntity = (SharedVersion) versionProperty.GetValue(attachedObject.Entity, null);
31: if (versionEntity != null)
34: if (!toIncrement.ContainsKey(versionEntity.id))
36: toIncrement.Add(versionEntity.id, versionEntity);
43: throw new SharedVersionException("You must load the version of the entity if you wish to modify it");
48: // Now call down to the entity framework to do persist
49: // any version entities used will now be marked.
In addition to my own Submit method, I also needed to write strongly typed Delete methods on the context, so that if the final part of the composite was deleted the version entity associated with the composite would also be deleted.
Finally it would be nice to automatically assign the version reference to any new object that becomes part of the composite. To do this I register for change on the OrderItems collection of the order and assign it the same version object of the Order its part of.
1: public partial class Order
3: public Order()
5: this.OrderItems.AssociationChanged += new System.ComponentModel.CollectionChangeEventHandler(OrderItems_AssociationChanged);
8: void OrderItems_AssociationChanged(object sender, System.ComponentModel.CollectionChangeEventArgs e)
10: if (e.Action == System.ComponentModel.CollectionChangeAction.Add)
12: OrderItem item = (OrderItem)e.Element;
13: if (item.Version == null)
15: item.Version = this.Version;
18: else if (e.Action == System.ComponentModel.CollectionChangeAction.Remove)
The last part and probably the bit I like least is the fact that you need to ensure that for every entity that is loaded you must insure that the associated version entity is loaded at the same time. This can’t be lazily loaded as its important to know the entity and associated version are in step with each other.
1: Order order = ctx.OrderSet
So whilst this all works, its a real shame coarse grained locking has not been considered by the framework providers, EF is at best an ok ORM for simple entities but fails to support out of the box moderately complex real world examples. I think its having to go to this level of effort that still puts developers off betting on a ORM. All the code can be downloaded from here. Note the database creation script is for SQL 2008.
Post a Comment