http://www.developer.com/

Back to article

Handling Data Conflicts in the Microsoft Sync Framework


April 23, 2009

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.



Click here for larger image

Listing 1.2 Conflict resolution demonstration form

We hook up to the ApplyChangeFailed event by modifying some code in the data cache (.sync) file's code-behind, using partial classes. In our example, the code is as follows:

      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:



Click here for larger image

Listing 1.3 ClientUpdateServerUpdate conflict on the server

If we choose Continue, the server changes "win" and the client's change is lost:



Click here for larger image

Listing 1.4 ClientUpdateServerUpdate conflict on the server with ApplyAction.Continue

If we choose RetryApplyingRow instead, the error is re- thrown because we have not made any changes to the data, so we are just presented with our conflict resolution form again.

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



Click here for larger image

Listing 1.5 ClientUpdateServerUpdate conflict on the server with ApplyAction.RetryWithForceWrite

Example 2: ClientInsertServerInsert

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.



Click here for larger image

Listing 1.6 Inserting new rows on both the client and the server

Now, when we try to synchronize, we get a ClientInsertServerInsert conflict on the server SyncProvider:



Click here for larger image

Listing 1.7 ClientInsertServerInsert conflict on the server

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



Click here for larger image

Listing 1.8 ClientInsertServerInsert conflict on the client

If we choose Continue again, you can see that the changes were not applied to either database. Each database keeps its changes and essentially ignores the conflict.



Click here for larger image

Listing 1.9 ClientInsertServerInsert conflict on the server and client with ApplyAction.Continue

As in the previous example, if we choose RetryApplyingRow, we will continue to get the conflict dialog.

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:



Click here for larger image

Listing 1.10 ClientInsertServerInsert conflict on the server with ApplyAction.RetryWithForceWrite

Resolving Client Conflicts with the ConflictResolver

If handling conflicts with specific resolution schemes isn't required and you want to minimize the amount of code you write, MSF offers one additional property on the SqlCeClientSyncProvider to make your life a bit easier. The ConflictResolver property can be set to individually handle all five conflict types with three options.

  • ClientWins
  • ServerWins
  • FireEvent

The final option, FireEvent, is the default for each conflict type and will fire the ApplyChangeFailed event discussed in the previous section.

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 matt.goebel@crowehorwath.com.

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 rachel.baker@crowehorwath.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date