If you use LINQ queries with the OrganizationServiceContext then understanding MergeOptions is vital. At the end of this post I describe the most common 'gotcha' that comes from not fully understanding this setting.
The OrganizationServiceContext implements a version of the 'Unit of Work' pattern (http://martinfowler.com/eaaCatalog/unitOfWork.html ) that allows us to make multiple changes on the client and then submit with a single call to 'SaveChanges'. The MergeOption property alters the way that the OrganizationServiceContext handles the automatic tracking of objects when returned from queries. It is important to understand what's going on since by default LINQ queries may not return you the most recent version of the records from the server, but rather a 'stale' versions that is currently being tracked.
What is this 'Merge' they speak of?!
The SDK entry on MergeOptions talks about 'Client side changes being lost' during merges.
The term 'merge' is nothing to do with merging of contacts/leads/accounts – but describes what happens when the server is re-queried within an existing context and results from a previous query are returned rather than new copies of each record. It is a record ID based combination, not an attribute merge – so a record is either re-used from the current context, or a new instance is returned that represents the version on the server.
In order to describe the different options, consider the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | var contacts = ( from c in context.ContactSet
select new Contact
{
ContactId = c.ContactId,
FirstName = c.FirstName,
LastName = c.LastName,
Address1_City = c.Address1_City
}).Take(1).ToArray();
Contact contact1 = contacts[0];
contact1.Address1_City = DateTime.Now.ToLongTimeString();
context.UpdateObject(contact1);
var contacts2 = ( from c in context.ContactSet
select c
).Take(2).ToArray();
var contact2 = contacts2[0];
contact2.Address1_City = DateTime.Now.ToLongTimeString();
context.UpdateObject(contact2);
context.SaveChanges();
|
MergeOption.NoTracking
Perhaps the best place to start is the behaviour with no tracking at all.
- Query 1 – Will return all matching contacts but not add them to the tracking list
- Update 2 – Will throw and exception because the contact is not being tracked. You would need to use context.Attach(contact) to allow this update to happen
- Query 2 – This query will pull down new copies of all contacts from the server include a new version of contact 1
- Update 2 – We now have two version of the same contact with different city attribute value. The UpdateObject will fail without Attach first being called. If you attempt to attach contact2 after attaching contact1 you will receive the error 'The context is already tracking a different 'contact' entity with the same identity' because contact1 is already tracked and has the same ID.
MergeOption.AppendOnly (Default Setting)
When using the OrganizationServiceContext, by default it will track all objects that are returned from LINQ queries. This means that the second query will return the instance of the contacts that have already been returned from query 1. Critically this means that any changes made on the server between query 1 and query 2 (or any additional attributes queried using projection) will not be returned.
- Query 1 – Will return all matching contacts and add them to the tracking list
- Update 2 – Will succeed because the contact is being tracked
- Query 2 – Will return the same instances that are already being tracked. The only records that will be returned from the server will be those that are not already being tracked. This is the meaning of 'AppendOnly'. The query still returns the data from the server, but the OrganizationServiceContext redirects the results to the instances already in the tracking list meaning that any changes made on the server since Query 1 will not be reflected in the results.
- Update 2 – Will succeed since contact1 and contact2 are the same object. Calling UpdateObject on the same instance more than once is acceptable.
MergeOption.PreserveChanges
PreserveChanges is essentially the same as AppendOnly except:
- Query 2 – Will return the same instances that are already being tracked provided they have an EntityState not equal to Unchanged. This means that contact2 will be the same instance as contac1 because it has been updated, but other instances in the contacts1 and contacts2 results will be new instances.
The result of this is that queries will not pick up the most recent changes on the server if a tracked version of that record has been edited in the current context.
MergeOption.OverwriteChanges
With a MergeOption of OverwriteChanges, the query behaviour will effectively be as per NoTracking however the tracking behaviour is like AppendOnly and PreserverChanges:
- Query 1 – Will return all matching contacts and add each on to the tracking list (as per AppendOnly and PreserveChanges)
- Update 2 – Will succeed because the contact is being tracked (as per AppendOnly and PreserveChanges)
- Query 2 – This query will pull down new copies of all contacts from the server include a new version of contact 1 (as per NoTracking). Previously tracked contact1 will no longer be tracked, but the new version (contact2) will be.
- Update 2 – Will succeed and the values on contact1 will be lost.
The MergeOption has a subtle but important effect on the OrganizationServiceContext, and without truly understanding each setting you might see unexpected results if you stick with the default 'AppendOnly'. For instance, you might update the value on the server between queries, but because a record is already tracked, re-querying will not bring down the latest values. Remember that all of this behaviour only is true for the same context – so if you are creating a new context then any previously tracked/modified records will no longer be tracked.
LINQ Projection 'Gotcha'
The most common issue I see from not fully understanding MergeOptions (and yes I made this mistake too! ) is the use of the default AppendOnly setting in conjunction with LINQ projection. In our code example Query 1 returns a projected version of the contact that only contains 4 attributes. When we re-query in Query 2 we might expect to see all attribute values but because we are already tracking the contacts our query will only return the previously queried 4 attributes! This can hide data from your code and cause some very unexpected results!
In these circumstances, unless you really need tracking and fully understand MergeOptions, I recommend changing the MergeOptions to 'NoTracking'.
@ScottDurow