Export (0) Print
Expand All
This topic has not yet been rated - Rate this topic

How to: Handle Data Conflicts and Errors

This topic shows you how to handle data conflicts and errors in Microsoft Synchronization Services for ADO.NET. The examples in this topic focus on the following Synchronization Services types and events:

For information about how to run sample code, see "Example Applications in the How to Topics" in Programming Common Synchronization Tasks.

In Synchronization Services, conflicts and errors are detected at the level of the row. Errors during synchronization are essentially a type of conflict and typically involve a constraint violation, such as a duplicate primary key. A row is in conflict if it has been changed at more than one node between synchronizations. Applications should be designed to avoid conflicts if they can, because conflict detection and resolution introduce additional complexity, processing, and network traffic. The most common ways to avoid conflicts are as follows: to update a table at only one node (typically the server); or to filter data so that only one node updates a particular row. For more information about filtering, see How to: Filter Rows and Columns. In some applications, conflicts cannot be avoided; therefore, Synchronization Services provides a set of features that can be used to detect and resolve conflicts.

Data conflicts can occur in any synchronization scenario in which changes are made at more than one node. Obviously, conflicts can occur in bidirectional synchronization, but they can also occur in download-only and upload-only synchronization. For example, if a row is deleted at the server and the same row is updated at the client, there is a conflict when Synchronization Services tries to apply the update that is uploaded to the server. Conflicts are always between the server and the client that is currently synchronizing. Consider the following example:

  1. Client A and client B synchronize with the server.
  2. A row is updated at client A, and then client A synchronizes. There is no conflict, and the row is applied at the server.
  3. The same row is updated at client B, and then client B synchronizes. The row from client B is now in conflict with the row from the server because of the update that originated at client A.
  4. If you resolve this conflict in favor of the server, Synchronization Services can apply the row from the server to client B. If you resolve in favor of client B, Synchronization Services can apply the row from client B to the server. During a later synchronization between client A and the server, the update that originated at client B is applied to client A.

Types of Conflicts and Errors

Synchronization Services detects the following types of conflicts. These are defined in the ConflictType enumeration:

  • A ClientInsertServerInsert conflict occurs when the client and server both insert a row with the same primary key. This type of conflict is also known as a primary key collision.
  • A ClientUpdateServerUpdate conflict occurs when the client and server change the same row. This is the most common type of conflict.
  • A ClientUpdateServerDelete conflict occurs when the client updates a row and the server deletes the same row.
  • A ClientDeleteServerUpdate conflict occurs when the client deletes a row and the server updates the same row.
  • An ErrorsOccurred conflict occurs when an error prevents a row from being applied.

Conflict and Error Detection

If a row cannot be applied during synchronization, it is typically because either an error or a data conflict occurred. In both cases, the DbServerSyncProvider ApplyChangeFailed event or SqlCeClientSyncProvider ApplyChangeFailed event is raised, depending on whether the error or conflict occurred during the upload or download phase of synchronization. If the client ApplyChangeFailed event is raised, Synchronization Services selects any conflicting rows for you. Then you decide how to resolve those conflicts. If the server ApplyChangeFailed event is raised, conflicting rows are selected by using two commands that you define on the SyncAdapter for each table:

  • The query or stored procedure that you specify for the SelectConflictUpdatedRowsCommand property selects conflicting rows from the base table in the server database. Synchronization Services executes this command if an insert, update or delete operation returns a @sync_row_count value of 0. This value indicates that the operation failed. This command selects the rows for ClientInsertServerInsert, ClientUpdateServerUpdate, and ClientDeleteServerUpdate conflicts.
  • The query or stored procedure that you specify for the SelectConflictDeletedRowsCommand selects conflicting rows from the tombstone table in the server database. Synchronization Services executes this command if the conflicting row was not found in the base table. This command selects the rows for the ClientUpdateServerDelete conflict.

You should understand that conflict detection and resolution are performed independently at each node, and conflict metadata does not travel between nodes. Consider the following example:

  1. A row is updated at the client and the server.
  2. During the upload phase of synchronization, the DbServerSyncProvider tries to apply the client row at the server, but a conflict is detected. You resolve the conflict in favor of the server.
  3. During the download phase of synchronization, the SqlCeClientSyncProvider tries to apply the server row at the client, but a conflict is detected. The client does not recognize the row as the result of a previous conflict resolution at the server. It only detects it as a row that is in conflict. Typically, you would resolve the conflict in favor of the server so that the row would be the same at both nodes, but there is no requirement to do this.

The data from each conflicting row is stored in a SyncConflict collection. This collection could grow large enough to cause an out-of-memory error in the following situations:

  • There are a large number of conflicting rows. Consider synchronizing a smaller number of rows in each session, or limit the number of conflicts by updating a particular row at only one node.
  • The conflicting rows contain columns that use large data types. Consider not including the columns that use large data types in the set of columns that is synchronized. For more information, see How to: Filter Rows and Columns.

Conflict and Error Resolution

Conflict and error resolution should be handled in response to the DbServerSyncProvider ApplyChangeFailed event and the SqlCeClientSyncProvider ApplyChangeFailed event. If errors and conflicts are not handled during the synchronization session in which they occur, the affected rows are not synchronized again unless those rows are updated. If errors and conflicts cannot be handled during the current session, you might need to perform dummy updates to ensure that those rows are synchronized during a subsequent session.

The ApplyChangeFailedEventArgs object provides access to several properties that can be used during conflict resolution:

  • Specify how to resolve the conflict by setting the Action property to one of the values of the ApplyAction enumeration:
    • Continue: ignore the conflict and continue synchronization.
    • RetryApplyingRow: retry applying the row. The retry will fail, and the event will be raised again if you do not address the cause of the conflict by changing one or both of the conflicting rows.
    • RetryWithForceWrite: retry with logic to force applying the change. The SqlCeClientSyncProvider has built-in support for this option. To use this option on the server, use the @sync_force_write parameter and add support in the commands that apply changes to the server database. For example, for a ClientUpdateServerDelete conflict, you could change the update into an insert when @sync_force_write is set to 1. For example code, see the "Examples" section later in this topic.
  • Obtain the conflict type and view the conflicting rows from the client and server by using the Conflict property.
  • Obtain the dataset of changes that is being synchronized by using the Context property. The rows exposed by the Conflict property are copies; therefore, overwriting them does not change the rows that are applied. Use the dataset exposed by the Context property to develop custom resolution schemes if the application requires them. For example code, see the "Examples" section later in this topic.

The SqlCeClientSyncProvider also includes a ConflictResolver property that you can use to resolve conflicts on the client. For each type of conflict, you can set a value from the ResolveAction enumeration:

  • ClientWins: equivalent to setting an ApplyAction of Continue.
  • ServerWins: equivalent to setting an ApplyAction of RetryWithForceWrite.
  • FireEvent: fire the ApplyChangeFailed event, the default, and then handle the event.

There is no requirement to set the ConflictResolver for each type of conflict. You can resolve conflicts as you do on the server, by handling the ApplyChangeFailed event. However, the ConflictResolver property does provide an easy way to specify conflict resolution options on the client.

The following code examples show how to configure conflict detection and resolution for the Customer table in the Synchronization Services sample database. In this example, the synchronization commands are manually created, instead of by using SqlSyncAdapterBuilder. You can use conflict detection and resolution with the commands that are generated by SqlSyncAdapterBuilder, but manual commands provide much more flexibility, especially in how you can force the application of conflicting changes.

Key Parts of the API

This section provides code examples that point out the key parts of the API that are used in conflict detection and resolution. The following query selects conflicting rows from the base table in the server database.

SqlCommand customerUpdateConflicts = new SqlCommand();
customerUpdateConflicts.CommandText =
    "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
    "FROM Sales.Customer " +
    "WHERE CustomerId = @CustomerId";
customerUpdateConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
customerUpdateConflicts.Connection = serverConn;
customerSyncAdapter.SelectConflictUpdatedRowsCommand = customerUpdateConflicts;

The following query selects conflicting rows from the tombstone table in the server database.

SqlCommand customerDeleteConflicts = new SqlCommand();
customerDeleteConflicts.CommandText =
    "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
    "FROM Sales.Customer_Tombstone " +
    "WHERE CustomerId = @CustomerId";
customerDeleteConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
customerDeleteConflicts.Connection = serverConn;
customerSyncAdapter.SelectConflictDeletedRowsCommand = customerDeleteConflicts;

The following code example creates a stored procedure that applies updates to the server database. This procedure is specified for the UpdateCommand property. Stored procedures can also be used to apply inserts and deletes to the server database. For examples of these procedures, see Setup Scripts for Synchronization Services How-to Topics.

The usp_CustomerApplyUpdate update procedure tries either an update or an insert operation, based on the value of the @sync_force_write parameter and whether the row to update exists in the server database. If the row does not exist, the procedure turns the update into an insert operation. In this example, the missing row is caused by an update/delete conflict.

CREATE PROCEDURE usp_CustomerApplyUpdate (	
	@sync_last_received_anchor binary(8), 
	@sync_client_id uniqueidentifier,
	@sync_force_write int,
	@sync_row_count int out,
    @CustomerId uniqueidentifier,
    @CustomerName nvarchar(100),
	@SalesPerson nvarchar(100),
	@CustomerType nvarchar(100))        
AS		
	-- Try to apply an update if the RetryWithForceWrite option
	-- was not specified for the sync adapter's update command.
	IF @sync_force_write = 0
	BEGIN	
		UPDATE Sales.Customer 
		SET CustomerName = @CustomerName, SalesPerson = @SalesPerson,
		CustomerType = @CustomerType, UpdateId = @sync_client_id
		WHERE CustomerId = @CustomerId
		AND (UpdateTimestamp <= @sync_last_received_anchor
		OR UpdateId = @sync_client_id)
	END
	ELSE
	-- Try to apply an update if the RetryWithForceWrite option
	-- was specified for the sync adapter's update command.
	BEGIN
		--If the row exists, update it.
		-- You might want to include code here to handle 
		-- possible error conditions.
		IF EXISTS (SELECT CustomerId FROM Sales.Customer
				   WHERE CustomerId = @CustomerId)
		BEGIN
			UPDATE Sales.Customer 
			SET CustomerName = @CustomerName, SalesPerson = @SalesPerson,
			CustomerType = @CustomerType, UpdateId = @sync_client_id
			WHERE CustomerId = @CustomerId			
		END
		
		-- The row does not exist, possibly due to a client-update/
		-- server-delete conflict. Change the update into an insert.
		ELSE
		BEGIN
			INSERT INTO Sales.Customer 
				   (CustomerId, CustomerName, SalesPerson,
					CustomerType, UpdateId)
			VALUES (@CustomerId, @CustomerName, @SalesPerson,
					@CustomerType, @sync_client_id)
		END
	END

	SET @sync_row_count = @@rowcount

The following code example sets conflict resolution options for SqlCeClientSyncProvider. As it is noted earlier, these options are not required, but they provide an easy way to resolve conflicts. In this example, updates should always win in update/delete conflicts, and all other conflicts should raise the client ApplyChangeFailed event.

this.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ServerWins;            
this.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ClientWins;
//If any of the following conflicts or errors occur, the ApplyChangeFailed
//event is raised.
this.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.FireEvent;
this.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.FireEvent;
this.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent;

//Log information for the ApplyChangeFailed event and handle any
//ResolveAction.FireEvent cases.
this.ApplyChangeFailed +=new EventHandler<ApplyChangeFailedEventArgs>(SampleClientSyncProvider_ApplyChangeFailed);

For client update/server delete conflicts, the update is force-written at the server, as shown in the following code example. The client update/server delete conflict is handled on the server by using the RetryWithForceWrite option in the server ApplyChangeFailed event handler. When you use this option, it means that the @sync_force_write parameter is set to 1 when the update stored procedure is called at the server.

if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerDelete)
{

    //For client-update/server-delete conflicts, we force the client 
    //change to be applied at the server. The stored procedure specified for 
    //customerSyncAdapter.UpdateCommand accepts the @sync_force_write parameter
    //and includes logic to handle this case.
    Console.WriteLine(String.Empty);
    Console.WriteLine("***********************************");
    Console.WriteLine("A client update / server delete conflict was detected.");

    e.Action = ApplyAction.RetryWithForceWrite;
    
    Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.");
    Console.WriteLine("***********************************"); 
    Console.WriteLine(String.Empty);
 
}

The following code example logs conflict information and force-writes any conflicting inserts in the client ApplyChangeFailed event handler.

private void SampleClientSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
{

    //Log event data from the client side.
    EventLogger.LogEvents(sender, e);

    //Force write any inserted server rows that are in conflict 
    //when they are downloaded.
    if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert)
    {
        e.Action = ApplyAction.RetryWithForceWrite;
    }

    if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate)
    {
        //Logic goes here.
    }

    if (e.Conflict.ConflictType == ConflictType.ErrorsOccurred)
    {
        //Logic goes here.
    }

}

Complete Code Example

The following complete code example includes the code examples that are described earlier and additional code to perform synchronization. Also, be aware that the example provides users of the application a choice of how to resolve update/update conflicts. One of the options is a custom resolution scheme that combines column values from the conflicting rows. Code for the custom resolution scheme is contained in the SampleServerSyncProvider_ApplyChangeFailed and SampleServerSyncProvider_ChangesApplied event handlers. The example requires the Utility class that is available in Utility Class for Synchronization Services How-to Topics.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.Server;
using Microsoft.Synchronization.Data.SqlServerCe;

namespace Microsoft.Samples.Synchronization
{
    class Program
    {
        static void Main(string[] args)
        {
            //The Utility class handles all functionality that is not
            //directly related to synchronization, such as holding connection 
            //string information and making changes to the server and client databases.
            Utility util = new Utility();

            //The SampleStats class handles information from the SyncStatistics
            //object that the Synchronize method returns.
            SampleStats sampleStats = new SampleStats();

            //Request a password for the client database, and delete
            //and re-create the database. The client synchronization
            //provider also enables you to create the client database 
            //if it does not exist.
            util.SetClientPassword();
            //util.RecreateClientDatabase();

            //Initial synchronization. Instantiate the SyncAgent
            //and call Synchronize.
            SampleSyncAgent sampleSyncAgent = new SampleSyncAgent();
            SyncStatistics syncStatistics = sampleSyncAgent.Synchronize();
            sampleStats.DisplayStats(syncStatistics, "initial");

            //Make a change at the client that fails when it is
            //applied at the server.
            util.MakeFailingChangesOnClient();

            //Make changes at the client and server that conflict
            //when they are synchronized.
            util.MakeConflictingChangesOnClientAndServer();

            //Subsequent synchronization.
            syncStatistics = sampleSyncAgent.Synchronize();
            sampleStats.DisplayStats(syncStatistics, "subsequent");

            //Return server data back to its original state.
            //Comment out this line if you want to view the
            //state of the data after all conflicts are resolved.
            util.CleanUpServer();

            //Exit.
            Console.Write("\nPress Enter to close the window.");
            Console.ReadLine();
        }
    }

    //Create a class that is derived from 
    //Microsoft.Synchronization.SyncAgent.
    public class SampleSyncAgent : SyncAgent
    {
        public SampleSyncAgent()
        {
            //Instantiate a client synchronization provider and specify it
            //as the local provider for this synchronization agent.
            this.LocalProvider = new SampleClientSyncProvider();

            //Instantiate a server synchronization provider and specify it
            //as the remote provider for this synchronization agent.
            this.RemoteProvider = new SampleServerSyncProvider();

            //Add the Customer table: specify a synchronization direction 
            //of Bidirectional.
            SyncTable customerSyncTable = new SyncTable("Customer");
            customerSyncTable.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;
            customerSyncTable.SyncDirection = SyncDirection.Bidirectional;
            this.Configuration.SyncTables.Add(customerSyncTable);
        }
    }


    //Create a class that is derived from 
    //Microsoft.Synchronization.Server.DbServerSyncProvider.
    public class SampleServerSyncProvider : DbServerSyncProvider
    {
        public SampleServerSyncProvider()
        {
            //Create a connection to the sample server database.
            Utility util = new Utility();
            SqlConnection serverConn = new SqlConnection(util.ServerConnString);
            this.Connection = serverConn;

            //Create a command to retrieve a new anchor value from
            //the server. In this case, we use a timestamp value
            //that is retrieved and stored in the client database.
            //During each synchronization, the new anchor value and
            //the last anchor value from the previous synchronization
            //are used: the set of changes between these upper and
            //lower bounds is synchronized.
            //
            //SyncSession.SyncNewReceivedAnchor is a string constant; 
            //you could also use @sync_new_received_anchor directly in 
            //your queries.
            SqlCommand selectNewAnchorCommand = new SqlCommand();
            string newAnchorVariable = "@" + SyncSession.SyncNewReceivedAnchor;
            selectNewAnchorCommand.CommandText = "SELECT " + newAnchorVariable + " = min_active_rowversion() - 1";
            selectNewAnchorCommand.Parameters.Add(newAnchorVariable, SqlDbType.Timestamp);
            selectNewAnchorCommand.Parameters[newAnchorVariable].Direction = ParameterDirection.Output;
            selectNewAnchorCommand.Connection = serverConn;
            this.SelectNewAnchorCommand = selectNewAnchorCommand;


            //Create a SyncAdapter for the Customer table, and then define
            //the commands to synchronize changes:
            //* SelectConflictUpdatedRowsCommand SelectConflictDeletedRowsCommand
            //  are used to detect if there are conflicts on the server during
            //  synchronization.
            //* SelectIncrementalInsertsCommand, SelectIncrementalUpdatesCommand,
            //  and SelectIncrementalDeletesCommand are used to select changes
            //  from the server that the client provider then applies to the client.
            //* InsertCommand, UpdateCommand, and DeleteCommand are used to apply
            //  to the server the changes that the client provider has selected
            //  from the client.

            //Create the SyncAdapter.
            SyncAdapter customerSyncAdapter = new SyncAdapter("Customer");

            //This command is used if @sync_row_count returns
            //0 when changes are applied to the server.
            SqlCommand customerUpdateConflicts = new SqlCommand();
            customerUpdateConflicts.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer " +
                "WHERE CustomerId = @CustomerId";
            customerUpdateConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerUpdateConflicts.Connection = serverConn;
            customerSyncAdapter.SelectConflictUpdatedRowsCommand = customerUpdateConflicts;

            //This command is used if the server provider cannot find
            //a row in the base table.
            SqlCommand customerDeleteConflicts = new SqlCommand();
            customerDeleteConflicts.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer_Tombstone " +
                "WHERE CustomerId = @CustomerId";
            customerDeleteConflicts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerDeleteConflicts.Connection = serverConn;
            customerSyncAdapter.SelectConflictDeletedRowsCommand = customerDeleteConflicts;

            //Select inserts from the server.
            SqlCommand customerIncrInserts = new SqlCommand();
            customerIncrInserts.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer " +
                "WHERE (InsertTimestamp > @sync_last_received_anchor " +
                "AND InsertTimestamp <= @sync_new_received_anchor " +
                "AND InsertId <> @sync_client_id)";
            customerIncrInserts.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerIncrInserts.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp);
            customerIncrInserts.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerIncrInserts.Connection = serverConn;
            customerSyncAdapter.SelectIncrementalInsertsCommand = customerIncrInserts;

            //Apply inserts to the server.
            SqlCommand customerInserts = new SqlCommand();
            customerInserts.CommandType = CommandType.StoredProcedure;
            customerInserts.CommandText = "usp_CustomerApplyInsert";
            customerInserts.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerInserts.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit); 
            customerInserts.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            customerInserts.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerInserts.Parameters.Add("@CustomerName", SqlDbType.NVarChar);
            customerInserts.Parameters.Add("@SalesPerson", SqlDbType.NVarChar);
            customerInserts.Parameters.Add("@CustomerType", SqlDbType.NVarChar);
            customerInserts.Connection = serverConn;
            customerSyncAdapter.InsertCommand = customerInserts;


            //Select updates from the server.
            SqlCommand customerIncrUpdates = new SqlCommand();
            customerIncrUpdates.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer " +
                "WHERE (UpdateTimestamp > @sync_last_received_anchor " +
                "AND UpdateTimestamp <= @sync_new_received_anchor " +
                "AND UpdateId <> @sync_client_id " +
                "AND NOT (InsertTimestamp > @sync_last_received_anchor " +
                "AND InsertId <> @sync_client_id))";
            customerIncrUpdates.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerIncrUpdates.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp);
            customerIncrUpdates.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerIncrUpdates.Connection = serverConn;
            customerSyncAdapter.SelectIncrementalUpdatesCommand = customerIncrUpdates;

            //Apply updates to the server.
            SqlCommand customerUpdates = new SqlCommand();
            customerUpdates.CommandType = CommandType.StoredProcedure;
            customerUpdates.CommandText = "usp_CustomerApplyUpdate";
            customerUpdates.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerUpdates.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerUpdates.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit);            
            customerUpdates.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            customerUpdates.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerUpdates.Parameters.Add("@CustomerName", SqlDbType.NVarChar);
            customerUpdates.Parameters.Add("@SalesPerson", SqlDbType.NVarChar);
            customerUpdates.Parameters.Add("@CustomerType", SqlDbType.NVarChar);
            customerUpdates.Connection = serverConn;
            customerSyncAdapter.UpdateCommand = customerUpdates;


            //Select deletes from the server.
            SqlCommand customerIncrDeletes = new SqlCommand();
            customerIncrDeletes.CommandText =
                "SELECT CustomerId, CustomerName, SalesPerson, CustomerType " +
                "FROM Sales.Customer_Tombstone " +
                "WHERE (@sync_initialized = 1 " +
                "AND DeleteTimestamp > @sync_last_received_anchor " +
                "AND DeleteTimestamp <= @sync_new_received_anchor " +
                "AND DeleteId <> @sync_client_id)";
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncInitialized, SqlDbType.Bit);
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.Timestamp);
            customerIncrDeletes.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerIncrDeletes.Connection = serverConn;
            customerSyncAdapter.SelectIncrementalDeletesCommand = customerIncrDeletes;

            //Apply deletes to the server.
            SqlCommand customerDeletes = new SqlCommand();
            customerDeletes.CommandType = CommandType.StoredProcedure;
            customerDeletes.CommandText = "usp_CustomerApplyDelete";
            customerDeletes.Parameters.Add("@" + SyncSession.SyncLastReceivedAnchor, SqlDbType.Timestamp);
            customerDeletes.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier);
            customerDeletes.Parameters.Add("@" + SyncSession.SyncForceWrite, SqlDbType.Bit);           
            customerDeletes.Parameters.Add("@" + SyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            customerDeletes.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            customerDeletes.Connection = serverConn;
            customerSyncAdapter.DeleteCommand = customerDeletes;


            //Add the SyncAdapter to the server synchronization provider.
            this.SyncAdapters.Add(customerSyncAdapter);


            //Handle the ApplyChangeFailed and ChangesApplied events. 
            //This allows us to respond to any conflicts that occur, and to 
            //make changes that are downloaded to the client during the same
            //session.
            this.ApplyChangeFailed +=new EventHandler<ApplyChangeFailedEventArgs>(SampleServerSyncProvider_ApplyChangeFailed);
            this.ChangesApplied +=new EventHandler<ChangesAppliedEventArgs>(SampleServerSyncProvider_ChangesApplied);
        }

        //Create a list to hold primary keys from the Customer
        //table. This list is used when we handle the ApplyChangeFailed 
        //and ChangesApplied events.
        private List<Guid> _updateConflictGuids = new List<Guid>();
        
        private void SampleServerSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
        {

            //Log information for the ApplyChangeFailed event.
            EventLogger.LogEvents(sender, e);

            //Respond to four different types of conflicts:
            // * ClientDeleteServerUpdate
            // * ClientUpdateServerDelete
            // * ClientInsertServerInsert
            // * ClientUpdateServerUpdate
            //
            if (e.Conflict.ConflictType == ConflictType.ClientDeleteServerUpdate)
            {
                //With the commands we are using, the default is for the server 
                //change to win and be applied to the client. Here, we accept the 
                //default on the server side. We also set ConflictResolver.ServerWins 
                //for this conflict in the client provider. This ensures that the server
                //change is applied to the client during the download phase.
                Console.WriteLine(String.Empty);
                Console.WriteLine("***********************************");
                Console.WriteLine("A client delete / server update conflict was detected.");

                e.Action = ApplyAction.Continue;

                Console.WriteLine("The server change will be applied at the client.");
                Console.WriteLine("***********************************");
                Console.WriteLine(String.Empty);
            }

            if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerDelete)
            {

                //For client-update/server-delete conflicts, we force the client 
                //change to be applied at the server. The stored procedure specified for 
                //customerSyncAdapter.UpdateCommand accepts the @sync_force_write parameter
                //and includes logic to handle this case.
                Console.WriteLine(String.Empty);
                Console.WriteLine("***********************************");
                Console.WriteLine("A client update / server delete conflict was detected.");

                e.Action = ApplyAction.RetryWithForceWrite;
                
                Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.");
                Console.WriteLine("***********************************"); 
                Console.WriteLine(String.Empty);
             
            }

            if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert)
            {
                //Similar to how we handled the client-delete/server-update conflict.
                //In this case, we set ConflictResolver.FireEvent and use RetryWithForceWrite
                //for this conflict in the client provider. This is equivalent to 
                //ConflictResolver.ServerWins, and ensures that the server
                //change is applied to the client during the download phase.
                Console.WriteLine(String.Empty);
                Console.WriteLine("***********************************");
                Console.WriteLine("A client insert / server insert conflict was detected.");

                e.Action = ApplyAction.Continue;

                Console.WriteLine("The server change will be applied at the client.");
                Console.WriteLine("***********************************");
                Console.WriteLine(String.Empty);
            }

            if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate)
            {

                //For client-update/server-update conflicts, we want to
                //allow the user to specify the conflict resolution option.
                //
                //It is possible for the Conflict object from the
                //server to have more than one row. Because our custom
                //resolution code only works with one row at a time,
                //we only allow the user to select a resolution
                //option if the object contains a single row.
                if (e.Conflict.ServerChange.Rows.Count > 1)
                {
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("***********************************");
                    Console.WriteLine("A client update / server update conflict was detected.");

                    e.Action = ApplyAction.Continue;

                    Console.WriteLine("The server change will be applied at the client.");
                    Console.WriteLine("***********************************");
                    Console.WriteLine(String.Empty);
                }
                else
                {
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("***********************************");
                    Console.WriteLine("A client update / server update conflict was detected.");
                    Console.WriteLine("Conflicting rows are displayed below.");
                    Console.WriteLine("***********************************");

                    //Get the conflicting changes from the Conflict object
                    //and display them. The Conflict object holds a copy
                    //of the changes; updates to this object will not be 
                    //applied. To make changes, use the Context object,
                    //which is demonstrated in the next section of code
                    //under ' case "CU" '.
                    DataTable conflictingServerChange = e.Conflict.ServerChange;
                    DataTable conflictingClientChange = e.Conflict.ClientChange;
                    int serverColumnCount = conflictingServerChange.Columns.Count;
                    int clientColumnCount = conflictingClientChange.Columns.Count;
                    
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("Server row: ");
                    Console.Write(" | ");

                    //Display the server row.
                    for (int i = 0; i < serverColumnCount; i++)
                    {
                        Console.Write(conflictingServerChange.Rows[0][i] + " | ");
                    }

                    Console.WriteLine(String.Empty);
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("Client row: ");
                    Console.Write(" | ");

                    //Display the client row.
                    for (int i = 0; i < clientColumnCount; i++)
                    {
                        Console.Write(conflictingClientChange.Rows[0][i] + " | ");
                    }

                    Console.WriteLine(String.Empty);
                    Console.WriteLine(String.Empty);

                    //Ask for a conflict resolution option.
                    Console.WriteLine("Enter a resolution option for this conflict:");
                    Console.WriteLine("SE = server change wins");
                    Console.WriteLine("CL = client change wins");
                    Console.WriteLine("CU = custom resolution (combine rows)");

                    string conflictResolution = Console.ReadLine();
                    conflictResolution.ToUpper();

                    switch (conflictResolution)
                    {
                        case "SE":

                            //Again, this this is the default for the commands we are using:
                            //the server change is persisted and then downloaded to the client.
                            e.Action = ApplyAction.Continue;
                            Console.WriteLine(String.Empty);
                            Console.WriteLine("The server change will be applied at the client.");

                            break;

                        case "CL":

                            //Override the default by specifying that the client row
                            //should be applied at the server. The stored procedure specified  
                            //for customerSyncAdapter.UpdateCommand accepts the @sync_force_write 
                            //parameter and includes logic to handle this case.
                            e.Action = ApplyAction.RetryWithForceWrite;
                            Console.WriteLine(String.Empty);
                            Console.WriteLine("The client change was retried at the server with RetryWithForceWrite.");

                            break;

                        case "CU":

                            //Provide a custom resolution scheme that takes each conflicting
                            //column and applies the combined contents of the column to the 
                            //client and server. This is not necessarily a resolution scheme 
                            //that you would use in production. Instead, it is used to 
                            //demonstrate the various ways you can interact with conflicting 
                            //data during synchronization.
                            //
                            //Get the ID for the conflicting row from the client data table,
                            //and add it to a list of GUIDs. We update rows at the server
                            //based on this list.
                            Guid customerId = (Guid)conflictingClientChange.Rows[0]["CustomerId"];
                            _updateConflictGuids.Add(customerId);
                            
                            //Create a dictionary to hold the column ordinal and value for
                            //any columns that are in confict.
                            Dictionary<int, string> conflictingColumns = new Dictionary<int, string>();
                            string combinedColumnValue;

                            //Determine which columns are different at the client and server.
                            //We already looped through these columns once, but we wanted to
                            //keep this code separate from the display code above.
                            for (int i = 0; i < clientColumnCount; i++)
                            {
                                if (conflictingClientChange.Rows[0][i].ToString() != conflictingServerChange.Rows[0][i].ToString())
                                {
                                    //If we find a column that is different, combine the values from
                                    //the client and server, and write "| conflict |" between them.
                                    combinedColumnValue = conflictingClientChange.Rows[0][i] + "  | conflict |  " + 
                                        conflictingServerChange.Rows[0][i];
                                    conflictingColumns.Add(i, combinedColumnValue);
                                }
                            }

                            //Loop through the rows in the Context object, which exposes
                            //the set of changes that are uploaded from the client.
                            //Note: In the ApplyChangeFailed event for the client provider,  
                            //you have access to the set of changes that was downloaded from
                            //the server.
                            DataTable allClientChanges = e.Context.DataSet.Tables["Customer"];
                            int allClientRowCount = allClientChanges.Rows.Count;
                            int allClientColumnCount = allClientChanges.Columns.Count;

                            for (int i = 0; i < allClientRowCount; i++)
                            {
                                //Find the changed row with the GUID from the Conflict object.
                                if (allClientChanges.Rows[i].RowState == DataRowState.Modified &&
                                    (Guid)allClientChanges.Rows[i]["CustomerId"] == customerId)
                                {
                                    //Loop through the columns and check whether the column
                                    //is in the conflictingColumns dictionary. If it is,
                                    //update the value in the allClientChanges Context object.
                                    for (int j = 0; j < allClientColumnCount; j++)
                                    {
                                        if (conflictingColumns.ContainsKey(j))
                                        {
                                            allClientChanges.Rows[i][j] = conflictingColumns[j];
                                        }
                                    }
                                }
                            }

                            //Apply the changed row with its combined values to the server.
                            //This change will persist at the server, but it will not be 
                            //downloaded with the SelectIncrementalUpdate command that we use.
                            //It will not download the change because it checks for the UpdateId,
                            //which is still set to the client that made the upload.
                            //We use the ChangesApplied event to set the UpdateId for the
                            //change to a value that represents the server. This ensures
                            //that the change is applied at the client during the download
                            //phase of synchronization (see SampleServerSyncProvider_ChangesApplied).
                            e.Action = ApplyAction.RetryWithForceWrite;

                            Console.WriteLine(String.Empty);
                            Console.WriteLine("The custom change was retried at the server with RetryWithForceWrite.");

                            break;

                        default:
                            Console.WriteLine(String.Empty);
                            Console.WriteLine("Not a valid resolution option.");
                            
                            break;
                    }
                
                }

                Console.WriteLine(String.Empty);
            }
        }

        private void SampleServerSyncProvider_ChangesApplied(object sender, ChangesAppliedEventArgs e)
        {
            //If _updateConflictGuids contains at least one GUID, update the UpdateId
            //column so that each change is downloaded to the client. For more
            //information, see SampleServerSyncProvider_ApplyChangeFailed.
            if (_updateConflictGuids.Count > 0)
            {
                SqlCommand updateTable = new SqlCommand();
                updateTable.Connection = (SqlConnection)e.Connection;
                updateTable.Transaction = (SqlTransaction)e.Transaction;
                updateTable.CommandText = String.Empty;

                for (int i = 0; i < _updateConflictGuids.Count; i++)
                {
                    updateTable.CommandText +=
                        " UPDATE Sales.Customer SET UpdateId = '00000000-0000-0000-0000-000000000000' " +
                        " WHERE CustomerId='" + _updateConflictGuids[i].ToString() + "'";
                }

                updateTable.ExecuteNonQuery();
            }
        }
    }

    //Create a class that is derived from 
    //Microsoft.Synchronization.Data.SqlServerCe.SqlCeClientSyncProvider.
    //You can just instantiate the provider directly and associate it
    //with the SyncAgent, but here we use this class to handle client 
    //provider events.
    public class SampleClientSyncProvider : SqlCeClientSyncProvider
    {

        public SampleClientSyncProvider()
        {
            //Specify a connection string for the sample client database.
            //By default, the client database is created if it does not
            //exist.
            Utility util = new Utility();
            this.ConnectionString = util.ClientConnString;

            //Specify conflict resolution options for each type of
            //conflict or error that can occur. The client and server are
            //independent; therefore, these settings have no effect when changes 
            //are applied at the server. However, settings should agree with each
            //other. For example:
            // * We specify a value of ServerWins for client delete /
            //   server update. On the server side, by default our commands will 
            //   ignore the conflicting delete and download the update to the 
            //   client. ServerWins is equivalent to setting RetryWithForceWrite
            //   on the client.
            // * Conversely, we specify a value of ClientWins for client update /
            //   server delete. On the server side, we specify that our commands 
            //   should force write the update by turning it into an insert.
            this.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ServerWins;            
            this.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ClientWins;
            //If any of the following conflicts or errors occur, the ApplyChangeFailed
            //event is raised.
            this.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.FireEvent;
            this.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.FireEvent;
            this.ConflictResolver.StoreErrorAction = ResolveAction.FireEvent;

            //Log information for the ApplyChangeFailed event and handle any
            //ResolveAction.FireEvent cases.
            this.ApplyChangeFailed +=new EventHandler<ApplyChangeFailedEventArgs>(SampleClientSyncProvider_ApplyChangeFailed);

            //Use the following events to fix up schema on the client.
            //We use the CreatingSchema event to change the schema
            //by using the API. We use the SchemaCreated event 
            //to change the schema by using SQL.
            this.CreatingSchema += new EventHandler<CreatingSchemaEventArgs>(SampleClientSyncProvider_CreatingSchema);
            this.SchemaCreated += new EventHandler<SchemaCreatedEventArgs>(SampleClientSyncProvider_SchemaCreated);

        }

        private void SampleClientSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e)
        {

            //Log event data from the client side.
            EventLogger.LogEvents(sender, e);

            //Force write any inserted server rows that are in conflict 
            //when they are downloaded.
            if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert)
            {
                e.Action = ApplyAction.RetryWithForceWrite;
            }

            if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate)
            {
                //Logic goes here.
            }

            if (e.Conflict.ConflictType == ConflictType.ErrorsOccurred)
            {
                //Logic goes here.
            }

        }

        private void SampleClientSyncProvider_CreatingSchema(object sender, CreatingSchemaEventArgs e)
        {
            
            //Set the RowGuid property because it is not copied
            //to the client by default. This is also a good time
            //to specify literal defaults with .Columns[ColName].DefaultValue,
            //but we will specify defaults like NEWID() by calling
            //ALTER TABLE after the table is created.
            e.Schema.Tables["Customer"].Columns["CustomerId"].RowGuid = true;
          
        }

        private void SampleClientSyncProvider_SchemaCreated(object sender, SchemaCreatedEventArgs e)
        {
            string tableName = e.Table.TableName;
            Utility util = new Utility();

            //Call ALTER TABLE on the client. This must be done
            //over the same connection and within the same
            //transaction that Synchronization Services uses
            //to create the schema on the client.
            util.MakeSchemaChangesOnClient(e.Connection, e.Transaction, "Customer");
 
        }
    }

    //Handle the statistics that are returned by the SyncAgent.
    public class SampleStats
    {
        public void DisplayStats(SyncStatistics syncStatistics, string syncType)
        {
            Console.WriteLine(String.Empty);
            if (syncType == "initial")
            {
                Console.WriteLine("****** Initial Synchronization ******");
            }
            else if (syncType == "subsequent")
            {
                Console.WriteLine("***** Subsequent Synchronization ****");
            }

            Console.WriteLine("Start Time: " + syncStatistics.SyncStartTime);
            Console.WriteLine("Upload Changes Applied: " + syncStatistics.UploadChangesApplied);
            Console.WriteLine("Upload Changes Failed: " + syncStatistics.UploadChangesFailed);
            Console.WriteLine("Total Changes Uploaded: " + syncStatistics.TotalChangesUploaded);
            Console.WriteLine("Download Changes Applied: " + syncStatistics.DownloadChangesApplied);
            Console.WriteLine("Download Changes Failed: " + syncStatistics.DownloadChangesFailed);
            Console.WriteLine("Total Changes Downloaded: " + syncStatistics.TotalChangesDownloaded);
            Console.WriteLine("Complete Time: " + syncStatistics.SyncCompleteTime);
            Console.WriteLine(String.Empty);
        }
    }

    public class EventLogger
    {
        //Create client and server log files, and write to them
        //based on data from the ApplyChangeFailedEventArgs.
        public static void LogEvents(object sender, ApplyChangeFailedEventArgs e)
        {
            string logFile = String.Empty;
            string site = String.Empty;

            if (sender is SampleServerSyncProvider)
            {
                logFile = "ServerLogFile.txt";
                site = "server";
            }
            else if (sender is SampleClientSyncProvider)
            {
                logFile = "ClientLogFile.txt";
                site = "client";
            }

            StreamWriter streamWriter = File.AppendText(logFile);
            StringBuilder outputText = new StringBuilder();

            outputText.AppendLine("** CONFLICTING CHANGE OR ERROR AT " + site.ToUpper() + " **");
            outputText.AppendLine("Table for which error or conflict occurred: " + e.TableMetadata.TableName);
            outputText.AppendLine("Sync stage: " + e.Conflict.SyncStage);
            outputText.AppendLine("Conflict type: " + e.Conflict.ConflictType);

            //If it is a data conflict instead of an error, print out
            //the values of the rows at the client and server.
            if (e.Conflict.ConflictType != ConflictType.ErrorsOccurred && 
                e.Conflict.ConflictType != ConflictType.Unknown)
            {

                DataTable serverChange = e.Conflict.ServerChange;
                DataTable clientChange = e.Conflict.ClientChange;
                int serverRows = serverChange.Rows.Count;
                int clientRows = clientChange.Rows.Count;
                int serverColumns = serverChange.Columns.Count;
                int clientColumns = clientChange.Columns.Count;

                for (int i = 0; i < serverRows; i++)
                {
                    outputText.Append("Server row: ");
                    
                    for (int j = 0; j < serverColumns; j++)
                    {
                        outputText.Append(serverChange.Rows[i][j] + " | ");

                    }

                    outputText.AppendLine(String.Empty);
                }

                for (int i = 0; i < clientRows; i++)
                {
                    outputText.Append("Client row: ");
                    
                    for (int j = 0; j < clientColumns; j++)
                    {
                        outputText.Append(clientChange.Rows[i][j] + " | ");
                    }

                    outputText.AppendLine(String.Empty);
                }
            }

            if (e.Conflict.ConflictType == ConflictType.ErrorsOccurred)
            {
                outputText.AppendLine("Error message: " + e.Error.Message);
            }

            streamWriter.WriteLine(DateTime.Now.ToShortTimeString() + " | " + outputText.ToString());
            streamWriter.Flush();
            streamWriter.Dispose();
            
        }
    }
}

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.