3. Handling Concurrency Errors when Updating Data
Let’s add another button to our sample application so that we can simulate concurrent updates. Label the button Concurrent Update and add the following code in the on-click event handler:
private void button13_Click(object sender, EventArgs e)
{
using(HireSampleDataContext dxWrite = new HireSampleDataContext(SiteUrl.Text))
{
//Disable deferred loading, so that all data is
//loaded when the query is parsed
dxWrite.DeferredLoadingEnabled = false;
StringBuilder sb = new StringBuilder();
sb.Append("<Queries>");
using (StringWriter logWriter = new StringWriter(sb))
{
dxWrite.Log = logWriter;
dxWrite.ObjectTrackingEnabled = true;
_basicQuery = from n in dxWrite.AssetNotes
select n;
//enumerate the query to populate the entity objects
//with the current data
foreach (var n in _basicQuery)
{
//Edit each location code
n.LocationCode += "-Edit";
}
//Perform a concurrent update
using(HireSampleDataContext dxWrite2 =
new HireSampleDataContext(SiteUrl.Text))
{
var concurrentQuery = from n in dxWrite2.AssetNotes
select n;
foreach (var n in concurrentQuery)
{
n.LocationCode = n.LocationCode += "-Concurrent";
}
dxWrite2.SubmitChanges();
}
try
{
dxWrite.SubmitChanges();
}
catch (ChangeConflictException)
{
//bind any conflicts to a data grid so that we can see them
dataGridView2.DataSource = dxWrite.ChangeConflicts.ToList();
}
dxWrite.Log = null;
dataGridView1.DataSource = _basicQuery.ToList();
}
sb.Append("</Queries>");
//create a temporary file for the generated CAML
string fileName = Path.Combine(Path.GetTempPath(), "tmpCaml.xml");
XmlDocument doc = new XmlDocument();
doc.LoadXml(sb.ToString());
doc.Save(fileName);
webBrowser1.Navigate(fileName);
}
}
Notice a few significant aspects of this sample code: First, notice the introduction of the DeferredLoadingEnabled property. By default, deferred loading is enabled on a DataContext
object. This means that child entities are loaded dynamically if and
when they are required. So, for example, our LINQ query returns a
collection of AssetNote objects. Each AssetNote object has an AssetReference property that refers to an OnHireAsset object. Since the OnHireAsset
object is not used by the query, it isn’t loaded until it’s required.
Our code doesn’t need this property, so to prevent it from being loaded
we’ve set DeferredLoadingEnabled to false.
Next, a separate DataContext object has been used to simulate a concurrent update, because if we were to use the same DataContext object to attempt a concurrent update, the DataContext object itself would merge the updates internally, resolving the concurrency issue. Internally, the DataContext object uses a dictionary type object known as the EntityTracker to manage all changes that have occurred to entities attached to the DataContext. Attempting to apply concurrent updates to the same DataContext would effectively be handled internally by the EntityTracker.
By using a second DataContext object, when SubmitChanges
is called, the changes are committed to the content database. This is
significant because each entity implements an interface named ITrackOriginalValues, and this interface defines an OriginalValues Dictionary
object that is used to store the values of each property when the
entity is created. A change conflict occurs when the original value for
a property does not match the current content database value before an
item is updated.
By clicking the Concurrent Update button, you’ll see
that an error row appears in the lower DataGridView, similar to the
illustration. Checking the contents of the Asset Notes list using the
SharePoint user interface will confirm that, other than the simulated
concurrent update of adding “–Concurrent” to each location code, no other updates have been performed. In effect, the change conflict has aborted any additional changes.
You may be wondering why there is only one row in the ChangeConflicts data grid. Earlier, when we covered the properties and methods of the DataContext object, we found that SubmitChanges has three overloads. The overload that our sample code uses does not specify a value for ConflictMode and so the default value of ConflictMode.FailOnFirstConflict
has been used. This is the reason for us only seeing one conflict. As
soon as the first conflict is found, the update is aborted and all
changes are rolled back.
So that we can see all of the change conflict messages, we can change the SubmitChanges method call to this:
dxWrite.SubmitChanges(ConflictMode.ContinueOnConflict);
Rerunning the application will now show all the conflicts in the lower DataGridView.
As before, checking the list using the SharePoint user interface will
confirm that no changes have actually been made. By setting ConflictMode to ContinueOnConflict,
we’ve instructed the LINQ to SharePoint provider to attempt all updates
before rolling back the changes if any conflicts occurred.