Microsoft & .NET .NET Handling Data Conflicts in the Microsoft Sync Framework

Handling Data Conflicts in the Microsoft Sync Framework

Introduction

Data conflicts are inevitable in any application or framework that synchronizes data from multiple data sources that are influenced by numerous people. The Microsoft Sync Framework (MSF) is no exception. MSF does, however, have built-in features to help developers deal with the most common conflict scenarios. Let’s look at how MSF Services for ADO.NET v1.0 helps to lighten the burden of conflict resolution on the developer.

If you are new to the Microsoft Synchronization Framework and need to get up to speed before jumping into conflict resolution, you can check out this article and this blog post to get started. We assume a general knowledge of the MSF framework in the following examples.


First, we must understand what a data conflict is and how it is generated. Listing 1.1 shows the most common data conflict that occurs when two sources make updates to a single row of data. For example, imagine a situation where you have two clients (Client A and Client B) making updates to a local data cache that is then synchronized to a server. Client A and Client B both synchronize with the server, receiving the same copy of the information in Table X. Client A updates a phone number in Row #17 of Table X and syncs with the server. Meanwhile, Client B updates an email address in Row #17. When Client B then syncs with the server, a data conflict occurs as the server tries to decide which copy of the data (Client A’s row or Client B’s row) should “win” and be written to the master copy in the server. Decisions like this can be made in the application’s business logic. MSF provides a couple of different ways to define this logic.



Listing 1.1 Common synchronization conflict example

Conflict Types and Apply Actions

The MSF defines five distinct conflict types, which are defined as the ConflictType enumerations.

  • ClientInsertServerInsert – A new row is created with the same primary key.
  • ClientUpdateServerUpdate – The same row is updated. This is the most common conflict, as shown in Listing 1.1.
  • ClientUpdateServerDelete – A row is updated on the client but has been deleted on the server.
  • ClientDeleteServerUpdate – A row is deleted on the client but has been updated on the server.
  • ErrorsOccurred – The “catch all” that is used when any error (not covered by the above cases) prevents a row from being inserted, updated or deleted.

Our example in Listing 1.1 might have you thinking that data conflicts will only occur when the synchronization scheme is bi-directional. However, this is not the case. Imagine an upload-only situation where the client doesn’t care about updates made on the server and just wants to feed its new information to the server. The client will create a row of data and sync with the client. At some point, some pesky user mucking around with the server data decides this row is no longer needed and deletes it. In the meantime, the client makes a change to that row and tries to sync with the server to update the server copy. This will still cause a conflict because the row that the client updated has been deleted. In this situation, one conflict resolution scheme might change the update to an insert to resolve the conflict.


In addition to the conflict types described above, MSF defines three built-in actions for resolving a conflict, which are defined in the ApplyAction enumeration.



  • Continue – This is the default action, which allows you to continue to the next conflict in the list.
  • RetryApplyingRow – This will retry to apply the row’s changes. This will fail again unless you change the data in some way (usually with a custom conflict resolution scheme in code that matches the business logic for handling conflicts).
  • RetryWithForceWrite – This will force the application of the row changes (overwriting any conflicting data).

We will take a look at the effect of these actions on some of the conflict types in the following sections.

Resolving Conflicts with the ApplyChangeFailed Event

Knowing the conflict type is important, but you also need to know which rows are in conflict. MSF exposes the ApplyChangeFailed event on both the DbServerSyncProvider and the SqlCeClientSyncProvider that allows you to review the conflict information and decide how to handle it. Each event will be raised on either the server or the client SyncProvider depending on the phase of synchronization involved. The ApplyChangeFailedEventArgs object exposes the Action and Conflict properties. The Action property can be used to resolve the conflict by setting it to one of the ApplyAction types described in the previous section. The Conflict property describes in more detail the conflict, like its type and the conflicting rows. ApplyChangeFailedEventArgs also exposes the Context property, which allows you to programmatically change the data being synchronized, allowing you to create virtually any custom conflict resolution scheme. This gives you the most flexibility in applying custom application business logic to complex data conflicts.

Let’s take a look at some examples. We created a demonstration project that shows a simple table that tracks customers’ favorite numbers. The top half of the form shows the server copy of the data. The bottom half of the form shows the client cache’s copy of the data. We can change the data in either of the tables, save the data to the server or the client, and then attempt to synchronize. If a data conflict is encountered, a custom form is displayed that allows us to choose one of the ApplyAction types to resolve the conflict.

      public partial class DataConflictsDataCacheServerSyncProvider
{
partial void OnInitialized()
{
this.ApplyChangeFailed += new
System.EventHandler<MICROSOFT.SYNCHRONIZATION.DATA.APPLYCHANGEFAILEDEVENTARGS>(
DataConflictsDataCacheServerSyncProvider_ApplyChangeFailed);
}

void DataConflictsDataCacheServerSyncProvider_ApplyChangeFailed(object sender,
Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e)
{
ConflictResolverForm cr = new ConflictResolverForm();
cr.Text = “Server Data Conflict Detected”;
cr.ApplyChangeEventArgs = e;
cr.ShowDialog();
}
}

public partial class DataConflictsDataCacheClientSyncProvider
{
void DataConflictsDataCacheClientSyncProvider_ApplyChangeFailed(
object sender, Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e)
{
ConflictResolverForm cr = new ConflictResolverForm();
cr.Text = “Client Data Conflict Detected”;
cr.ApplyChangeEventArgs = e;
cr.ShowDialog();
}
}


We also must add the following line of code in the client SyncProvider’s constructor (you can find it in the code of the .designer.cs code file):

              this.ApplyChangeFailed +=new
System.EventHandler
(DataConflictsDataCacheClientSyncProvider_ApplyChangeFailed);

In the ConflictResolverForm, we tell the Sync framework how to solve the conflict by setting the appropriate ApplyAction as follows:

applyChangeEventArgs.Action = Microsoft.Synchronization.Data.ApplyAction.Continue;

applyChangeEventArgs.Action = Microsoft.Synchronization.Data.ApplyAction.RetryApplyingRow;

applyChangeEventArgs.Action = Microsoft.Synchronization.Data.ApplyAction.RetryWithForceWrite;

Each of these actions is described in the examples on the following page.


Example 1: ClientUpdateServerUpdate


In this example, we update the row with CustomerId of 2 in both the server and the client and then attempt to re- sync. On the server, we change the FirstName field to Chad. On the client, we change the FavoriteNumber field to 5. We get a ClientUpdateServerUpdate on the server SyncProvider:

If we choose RetryWithForceWrite, the client “wins” and the server changes are lost:


In this example, we insert a new row into the server that gets a CustomerId (primary key) of 3. We also insert a new row into the client that gets a CustomerId (primary key) of 3.

If we choose Continue, we get another ClientInsertServerInsert conflict, this time on the client SyncProvider:

If we choose RetryWithForceWrite, we are not presented with the client SyncProvider conflict (the conflict has already been resolved) and the client changes will be uploaded, overwriting the server changes:

The code for setting up ConflictResolver can be added to the client SyncProvider’s constructor (you can find it in the code of the .designer.cs code file) along with the ApplyChangeFailed handler if needed:

this.ConflictResolver.ClientDeleteServerUpdateAction =
Microsoft.Synchronization.Data.ResolveAction.ServerWins;
this.ConflictResolver.ClientUpdateServerDeleteAction =
Microsoft.Synchronization.Data.ResolveAction.ClientWins;
this.ConflictResolver.ClientInsertServerInsertAction =
Microsoft.Synchronization.Data.ResolveAction.FireEvent;
this.ConflictResolver.ClientUpdateServerUpdateAction =
Microsoft.Synchronization.Data.ResolveAction.FireEvent;

this.ApplyChangeFailed +=new
System.EventHandler<MICROSOFT.SYNCHRONIZATION.DATA.APPLYCHANGEFAILEDEVENTARGS>
(DataConflictsDataCacheClientSyncProvider_ApplyChangeFailed);


In the above example, we always allow updates to win over deletes, and we let update conflicts be resolved through custom business logic to be defined in the handler for the ApplyChangeFailed event.


Conclusion

All data synchronization solutions will require some form of data conflict resolution. With the Microsoft Sync Framework, there is a built-in conflict resolution mechanism that allows you to define a conflict resolution scheme that is as simple (ConflictResolver) or as involved (custom business logic resolution through the ApplyChangeFailed event) as you need it to be to fit your application.

About the Authors


Matt Goebel is a manager with Crowe Horwath LLP in the Indianapolis, Indiana, office. He can be reached at 317.208.2555 or [email protected]


Rachel Baker is a senior developer with Crowe Horwath LLP in the Oak Brook, Illinois, office. She can be reached at 630.990.4434 or [email protected]

Latest Posts

Related Stories