How to: Handle Data Conflicts and Errors for Peer-to-Peer Synchronization

This topic shows you how to handle data conflicts and errors for peer-to-peer synchronization in Sync Services for ADO.NET 2.0. The examples in this topic focus on the following Sync Services types and events:

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

Understanding Data Conflicts and Errors

In Sync Services, conflicts and errors are detected at the level of the row. A row is in conflict when it has been changed at more than one node between synchronizations. Errors that occur during synchronization typically involve a constraint violation, such as a duplicate primary key. 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: update a table at only one node, or filter data so that only one node updates a particular row. In some applications, conflicts cannot be avoided. For example, in a sales force application, two salespeople might share a territory. Both salespeople could update the data for the same customer and orders. Therefore, Sync Services provides a set of features that application can use to detect and resolve conflicts.

Data conflicts can occur in any synchronization scenario in which changes are made at more than one node. 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 one node and the same row is updated at another node, there is a conflict when Sync Services tries to upload and apply the update at the first node.

Conflicts are always between the two nodes that are currently synchronizing. Consider the following scenario:

  1. Node A and node B both perform bidirectional synchronization with node C.

  2. A row is updated at node A, and then node A synchronizes. There is no conflict, and the row is applied at node C.

  3. The same row is updated at node B, and then node B synchronizes. The row from node B is now in conflict with the row from node C because of the update that originated at node A.

  4. If you resolve this conflict in favor of node C, Sync Services can apply the row from node C to node B. If you resolve in favor of node B, Sync Services can apply the row from node B to node. During a later synchronization between node A and node C, the update that originated at node B is applied to node A.

Kinds of Conflicts and Errors

Sync Services detects the following kinds of conflicts. These are defined in the DbConflictType enumeration:

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 ApplyChangeFailed event is raised. The provider raises the error for the node at which the conflict is detected. For example, if you specify a value of UploadAndDownload for the Direction property, changes are first uploaded from the local provider to the remote provider. In this case, the event is raised by the provider that you specified for the RemoteProvider property. If changes were first downloaded and then uploaded, the event would be raised by the provider that you specified for the LocalProvider property. Regardless of which provider raises the event and where the synchronization components are located, the data change at the node on which the event is raised is considered the local change (LocalChange), and the other row is considered the remote change (RemoteChange). This differs from client and server synchronization, in which ClientChange and ServerChange are always associated with, respectively, the client database and the server database.

After the ApplyChangeFailed event is raised, conflicting rows are selected by using a command that you define on the DbSyncAdapter object for each table. The query or stored procedure that you specify for the SelectRowCommand property selects conflicting rows from the base table in the peer database. Sync Services executes this command when an insert, update or delete operation returns a @sync_row_count value of 0. This value indicates that the operation failed.

Conflict and Error Resolution

Conflict and error resolution should be handled in response to the ApplyChangeFailed event. The DbApplyChangeFailedEventArgs 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 and RetryNextSync: 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. To use this option, use the @sync_force_write parameter and add support in the commands that apply changes to the remote database. For example, for a LocalUpdateRemoteUpdate conflict, you can force the change from the local provider to win when @sync_force_write is set to 1. For example code, see the "Example" section later in this topic.

  • Obtain the conflict type and view the conflicting rows from the each node by using the Conflict property.

  • Obtain the dataset of changes that is being synchronized by using the Context property. The rows that are 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.

Note

The Sync Services API contains one type and one property that are related to conflict resolution but that are not used in this version of the API: DbResolveAction and ConflictResolutionPolicy.

Examples

The following code examples show how to configure conflict detection and resolution for the Customer table in the Sync Services peer sample databases.

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 code example creates a stored procedure that selects conflicting rows from the base table in the peer database.

CREATE PROCEDURE Sales.sp_Customer_SelectRow
        @CustomerId uniqueidentifier
AS
   SELECT  t.CustomerId, 
           c.CustomerName,
           c.SalesPerson,
           c.CustomerType,                 
           t.sync_row_timestamp, 
           t.sync_row_is_tombstone,
           t.sync_update_peer_key, 
           t.sync_update_peer_timestamp, 
           t.sync_create_peer_key, 
           t.sync_create_peer_timestamp 
    FROM Customer c RIGHT JOIN Customer_Tracking t ON c.CustomerId = t.CustomerId 
    WHERE t.CustomerId = @CustomerId 

The following code example specifies the sp_Customer_SelectRow stored procedure for the SelectRowCommand property.

SqlCommand selRowCustomerCmd = new SqlCommand();
selRowCustomerCmd.CommandType = CommandType.StoredProcedure;
selRowCustomerCmd.CommandText = "Sales.sp_Customer_SelectRow";
selRowCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);

adapterCustomer.SelectRowCommand = selRowCustomerCmd;
Dim selRowCustomerCmd As New SqlCommand()

With selRowCustomerCmd
    .CommandType = CommandType.StoredProcedure
    .CommandText = "Sales.sp_Customer_SelectRow"
    .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
End With

adapterCustomer.SelectRowCommand = selRowCustomerCmd

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

CREATE PROCEDURE Sales.sp_Customer_ApplyUpdate (                                   
        @CustomerId uniqueidentifier,
        @CustomerName nvarchar(100),
        @SalesPerson nvarchar(100),
        @CustomerType nvarchar(100),
        @sync_min_timestamp bigint ,                                
        @sync_row_count int OUT,
        @sync_force_write int)        
AS      
    UPDATE c
    SET c.CustomerName = @CustomerName, c.SalesPerson = @SalesPerson, c.CustomerType = @CustomerType      
    FROM Customer c JOIN Customer_Tracking t ON c.CustomerId = t.CustomerId
    WHERE ((t.sync_row_timestamp <= @sync_min_timestamp) OR @sync_force_write = 1)
        AND t.CustomerId = @CustomerId  
    SET @sync_row_count = @@rowcount

The sp_Customer_ApplyUpdate update procedure performs an update based on the value of the @sync_force_write parameter. If the row has been updated in the local database and the parameter is set to 0, the remote update is not applied. However, if the parameter is set to 1, the remote update overwrites the local update.

The following code example shows how update-update conflicts can be processed in an ApplyChangeFailed event handler. In the example, the conflicting rows are displayed to the console with an option to specify which row should win the conflict. If you run the complete code example at the end of this topic, you will see two sets of conflicting rows: when peer 1 synchronizes with peer 2 and when peer 2 synchronizes with peer3.

if (e.Conflict.Type == DbConflictType.LocalUpdateRemoteUpdate)
{
        
    //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.
    DataTable conflictingRemoteChange = e.Conflict.RemoteChange;
    DataTable conflictingLocalChange = e.Conflict.LocalChange;
    int remoteColumnCount = conflictingRemoteChange.Columns.Count;
    int localColumnCount = conflictingLocalChange.Columns.Count;

    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine("Row from database " + DbConflictDetected);
    Console.Write(" | ");

    //Display the local row. As mentioned above, this is the row
    //from the database at which the conflict was detected.
    for (int i = 0; i < localColumnCount; i++)
    {
        Console.Write(conflictingLocalChange.Rows[0][i] + " | ");
    }

    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine("Row from database " + DbOther);
    Console.Write(" | ");

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

    //Ask for a conflict resolution option.
    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine("Enter a resolution option for this conflict:");
    Console.WriteLine("A = change from " + DbConflictDetected + " wins.");
    Console.WriteLine("B = change from " + DbOther + " wins.");

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

    if (conflictResolution == "A")
    {
        e.Action = ApplyAction.Continue;
    }

    else if (conflictResolution == "B")
    {
        e.Action = ApplyAction.RetryWithForceWrite;
    }

    else
    {
        Console.WriteLine(String.Empty);
        Console.WriteLine("Not a valid resolution option.");
    }
}
If e.Conflict.Type = DbConflictType.LocalUpdateRemoteUpdate Then

    '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.
    Dim conflictingRemoteChange As DataTable = e.Conflict.RemoteChange
    Dim conflictingLocalChange As DataTable = e.Conflict.LocalChange
    Dim remoteColumnCount As Integer = conflictingRemoteChange.Columns.Count
    Dim localColumnCount As Integer = conflictingLocalChange.Columns.Count

    Console.WriteLine(String.Empty)
    Console.WriteLine(String.Empty)
    Console.WriteLine("Row from database " & DbConflictDetected)
    Console.Write(" | ")

    'Display the local row. As mentioned above, this is the row
    'from the database at which the conflict was detected.
    Dim i As Integer
    For i = 0 To localColumnCount - 1
        Console.Write(conflictingLocalChange.Rows(0)(i).ToString & " | ")
    Next i

    Console.WriteLine(String.Empty)
    Console.WriteLine(String.Empty)
    Console.WriteLine(String.Empty)
    Console.WriteLine("Row from database " & DbOther)
    Console.Write(" | ")

    'Display the remote row.
    For i = 0 To remoteColumnCount - 1
        Console.Write(conflictingRemoteChange.Rows(0)(i).ToString & " | ")
    Next i

    'Ask for a conflict resolution option.
    Console.WriteLine(String.Empty)
    Console.WriteLine(String.Empty)
    Console.WriteLine("Enter a resolution option for this conflict:")
    Console.WriteLine("A = change from " & DbConflictDetected & " wins.")
    Console.WriteLine("B = change from " & DbOther & " wins.")

    Dim conflictResolution As String = Console.ReadLine()
    conflictResolution.ToUpper()

    If conflictResolution = "A" Then
        e.Action = ApplyAction.Continue

    ElseIf conflictResolution = "B" Then
        e.Action = ApplyAction.RetryWithForceWrite

    Else
        Console.WriteLine(String.Empty)
        Console.WriteLine("Not a valid resolution option.")
    End If

The following code example logs error information to a file.

else if (e.Conflict.Type == DbConflictType.ErrorsOccurred)
{

    string logFile = @"C:\SyncErrorLog.txt";
    
    Console.WriteLine(String.Empty);
    Console.WriteLine("An error occurred during synchronization.");
    Console.WriteLine("This error has been logged to " + logFile + ".");

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

    outputText.AppendLine("** APPLY CHANGE FAILURE AT " + DbConflictDetected.ToUpper() + " **");
    outputText.AppendLine("Error source: " + e.Error.Source);
    outputText.AppendLine("Error message: " + e.Error.Message); 

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

}
ElseIf e.Conflict.Type = DbConflictType.ErrorsOccurred Then

    Dim logFile As String = "C:\SyncErrorLog.txt"

    Console.WriteLine(String.Empty)
    Console.WriteLine("An error occurred during synchronization.")
    Console.WriteLine("This error has been logged to " & logFile & ".")

    Dim streamWriter As StreamWriter = File.AppendText(logFile)
    Dim outputText As New StringBuilder()

    outputText.AppendLine("** APPLY CHANGE FAILURE AT " & DbConflictDetected.ToUpper() & " **")
    outputText.AppendLine("Error source: " & e.Error.Source)
    outputText.AppendLine("Error message: " & e.Error.Message)

    streamWriter.WriteLine(DateTime.Now.ToShortTimeString() & " | " & outputText.ToString())
    streamWriter.Flush()
    streamWriter.Dispose()
End If

Complete Code Example

The following complete code example includes the code examples that are described earlier and additional code to perform synchronization. The example requires the Utility class that is available in Utility Class for Sync Services How-to Topics.

using System;
using System.IO;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;

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 peerConnection 
            //string information and making changes to the server database.
            Utility util = new Utility();

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

            try
            {
                //Initial synchronization. Instantiate the SyncOrchestrator
                //and call Synchronize. Note that data is not synchronized during the
                //session between peer 1 and peer 3, because all rows have already
                //been delivered to peer 3 during its synchronization session with peer 2.     
                SyncOrchestrator sampleSyncAgent;
                SyncOperationStatistics syncStatistics;

                sampleSyncAgent = new SampleSyncAgent(util.Peer1ConnString, util.Peer2ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "initial");

                sampleSyncAgent = new SampleSyncAgent(util.Peer2ConnString, util.Peer3ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "initial");

                sampleSyncAgent = new SampleSyncAgent(util.Peer1ConnString, util.Peer3ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "initial");
            }


            catch (DbOutdatedSyncException ex)
            {
                Console.WriteLine("Outdated Knowledge: " + ex.OutdatedPeerSyncKnowledge.ToString() +
                                  " Clean up knowledge: " + ex.MissingCleanupKnowledge.ToString());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            //Make conflicting changes in each peer database.
            util.MakeConflictingChangesOnPeer(util.Peer1ConnString, "Customer");
            util.MakeConflictingChangesOnPeer(util.Peer2ConnString, "Customer");
            util.MakeConflictingChangesOnPeer(util.Peer3ConnString, "Customer");

            try
            {
                //Subsequent synchronization. Changes are now synchronized between all
                //peers.
                SyncOrchestrator sampleSyncAgent;
                SyncOperationStatistics syncStatistics;

                sampleSyncAgent = new SampleSyncAgent(util.Peer1ConnString, util.Peer2ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "subsequent");

                sampleSyncAgent = new SampleSyncAgent(util.Peer2ConnString, util.Peer3ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "subsequent");

                sampleSyncAgent = new SampleSyncAgent(util.Peer1ConnString, util.Peer3ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "subsequent");
            }


            catch (DbOutdatedSyncException ex)
            {
                Console.WriteLine("Outdated Knowledge: " + ex.OutdatedPeerSyncKnowledge.ToString() +
                                  " Clean up knowledge: " + ex.MissingCleanupKnowledge.ToString());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            //Make a change in SyncSamplesDb_Peer2 that will fail when it
            //is synchronized with SyncSamplesDb_Peer1.
            util.MakeFailingChangeOnPeer(util.Peer2ConnString);

            try
            {
                SyncOrchestrator sampleSyncAgent;
                SyncOperationStatistics syncStatistics;

                sampleSyncAgent = new SampleSyncAgent(util.Peer1ConnString, util.Peer2ConnString);
                syncStatistics = sampleSyncAgent.Synchronize();
                sampleStats.DisplayStats(syncStatistics, "subsequent");
            }


            catch (DbOutdatedSyncException ex)
            {
                Console.WriteLine("Outdated Knowledge: " + ex.OutdatedPeerSyncKnowledge.ToString() +
                                  " Clean up knowledge: " + ex.MissingCleanupKnowledge.ToString());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            //Return peer data back to its original state.
            util.CleanUpPeer(util.Peer1ConnString);
            util.CleanUpPeer(util.Peer2ConnString);
            util.CleanUpPeer(util.Peer3ConnString);

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

    
    //Create a class that is derived from 
    //Microsoft.Synchronization.SyncOrchestrator.
    public class SampleSyncAgent : SyncOrchestrator
    {

        //Create class-level variables so that the ApplyChangeFailedEvent 
        //handler can use them.
        private string _localProviderDatabase;
        private string _remoteProviderDatabase;

        public SampleSyncAgent(string localProviderConnString, string remoteProviderConnString)
        {

            //Instantiate the sample provider that allows us to create a provider
            //for both of the peers that are being synchronized.
            SampleSyncProvider sampleSyncProvider = new SampleSyncProvider();

            //Instantiate a DbSyncProvider for the local peer and the remote peer.
            //For example, if this code is running at peer1 and is
            //synchronizing with peer2, peer1 would be the local provider
            //and peer2 the remote provider.                 
            DbSyncProvider localProvider = new DbSyncProvider();
            DbSyncProvider remoteProvider = new DbSyncProvider();

            //Create a provider by using the SetupSyncProvider on the sample class.             
            sampleSyncProvider.SetupSyncProvider(localProviderConnString, localProvider);
            localProvider.SyncProviderPosition = SyncProviderPosition.Local;

            sampleSyncProvider.SetupSyncProvider(remoteProviderConnString, remoteProvider);
            remoteProvider.SyncProviderPosition = SyncProviderPosition.Remote;

            //Specify event handlers for the ApplyChangeFailed event for each provider.
            //The handlers are used to resolve conflicting rows and log error information.
            localProvider.ApplyChangeFailed += new EventHandler<DbApplyChangeFailedEventArgs>(dbProvider_ApplyChangeFailed);
            remoteProvider.ApplyChangeFailed += new EventHandler<DbApplyChangeFailedEventArgs>(dbProvider_ApplyChangeFailed);

            //Specify the local and remote providers that should be synchronized,
            //and the direction and order of changes. In this case, changes are first
            //uploaded from remote to local and then downloaded in the other direction.
            this.LocalProvider = localProvider;
            this.RemoteProvider = remoteProvider;
            this.Direction = SyncDirectionOrder.UploadAndDownload;

            _localProviderDatabase = localProvider.Connection.Database.ToString();
            _remoteProviderDatabase = remoteProvider.Connection.Database.ToString();

        }

        private void dbProvider_ApplyChangeFailed(object sender, DbApplyChangeFailedEventArgs e)
        {

            //For conflict detection, the "local" database is the one at which the
            //ApplyChangeFailed event occurs. We determine at which database the event
            //fired and then compare the name of that database to the names of
            //the databases specified as the LocalProvider and RemoteProvider.
            string DbConflictDetected = e.Connection.Database.ToString();
            string DbOther;
            
            DbOther = DbConflictDetected == _localProviderDatabase ? _remoteProviderDatabase : _localProviderDatabase;
        
            Console.WriteLine(String.Empty);
            Console.WriteLine("Conflict of type " + e.Conflict.Type + " was detected at " + DbConflictDetected + ".");

            if (e.Conflict.Type == DbConflictType.LocalUpdateRemoteUpdate)
            {
        
                //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.
                DataTable conflictingRemoteChange = e.Conflict.RemoteChange;
                DataTable conflictingLocalChange = e.Conflict.LocalChange;
                int remoteColumnCount = conflictingRemoteChange.Columns.Count;
                int localColumnCount = conflictingLocalChange.Columns.Count;

                Console.WriteLine(String.Empty);
                Console.WriteLine(String.Empty);
                Console.WriteLine("Row from database " + DbConflictDetected);
                Console.Write(" | ");

                //Display the local row. As mentioned above, this is the row
                //from the database at which the conflict was detected.
                for (int i = 0; i < localColumnCount; i++)
                {
                    Console.Write(conflictingLocalChange.Rows[0][i] + " | ");
                }

                Console.WriteLine(String.Empty);
                Console.WriteLine(String.Empty);
                Console.WriteLine(String.Empty);
                Console.WriteLine("Row from database " + DbOther);
                Console.Write(" | ");

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

                //Ask for a conflict resolution option.
                Console.WriteLine(String.Empty);
                Console.WriteLine(String.Empty);
                Console.WriteLine("Enter a resolution option for this conflict:");
                Console.WriteLine("A = change from " + DbConflictDetected + " wins.");
                Console.WriteLine("B = change from " + DbOther + " wins.");

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

                if (conflictResolution == "A")
                {
                    e.Action = ApplyAction.Continue;
                }

                else if (conflictResolution == "B")
                {
                    e.Action = ApplyAction.RetryWithForceWrite;
                }

                else
                {
                    Console.WriteLine(String.Empty);
                    Console.WriteLine("Not a valid resolution option.");
                }
            }
                
            //Write any errors to a log file.
            else if (e.Conflict.Type == DbConflictType.ErrorsOccurred)
            {

                string logFile = @"C:\SyncErrorLog.txt";
                
                Console.WriteLine(String.Empty);
                Console.WriteLine("An error occurred during synchronization.");
                Console.WriteLine("This error has been logged to " + logFile + ".");

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

                outputText.AppendLine("** APPLY CHANGE FAILURE AT " + DbConflictDetected.ToUpper() + " **");
                outputText.AppendLine("Error source: " + e.Error.Source);
                outputText.AppendLine("Error message: " + e.Error.Message); 

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

            }
        }            
    }
    
    public class SampleSyncProvider
    {
        public DbSyncProvider SetupSyncProvider(string peerConnString, DbSyncProvider peerProvider)
        {

            //Set the amount of time to retain metadata.
            const int MetadataAgingInHours = 100;

            SqlConnection peerConnection = new SqlConnection(peerConnString);
            peerProvider.Connection = peerConnection;
            peerProvider.ScopeName = "Sales";

            //Create a DbSyncAdapter object for the Customer table and associate it 
            //with the DbSyncProvider. Following the DataAdapter style in ADO.NET, 
            //DbSyncAdapter is the equivalent for synchronization. The commands that 
            //are specified for the DbSyncAdapter object call stored procedures
            // that are created in each peer database.
            DbSyncAdapter adapterCustomer = new DbSyncAdapter("Customer");


            //Specify the primary key, which Sync Services uses
            //to identify each row during synchronization.
            adapterCustomer.RowIdColumns.Add("CustomerId");


            //Specify the command to select incremental changes.
            //In this command and other commands, session variables are
            //used to pass information at runtime. DbSyncSession.SyncMetadataOnly 
            //and SyncMinTimestamp are two of the string constants that
            //the DbSyncSession class exposes. You could also include 
            //@sync_metadata_only and @sync_min_timestamp directly in your 
            //queries:
            //*  sync_metadata_only is used by Sync Services as an optimization
            //   in some queries.
            //* The value of the sync_min_timestamp session variable is compared to
            //   values in the sync_row_timestamp column in the tracking table to 
            //   determine which rows to select.
            SqlCommand chgsCustomerCmd = new SqlCommand();
            chgsCustomerCmd.CommandType = CommandType.StoredProcedure;
            chgsCustomerCmd.CommandText = "Sales.sp_Customer_SelectChanges";
            chgsCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncMetadataOnly, SqlDbType.Int);
            chgsCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncMinTimestamp, SqlDbType.BigInt);
            chgsCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncInitialize, SqlDbType.Int);

            adapterCustomer.SelectIncrementalChangesCommand = chgsCustomerCmd;

            //Specify the command to insert rows.
            //The sync_row_count session variable is used in this command 
            //and other commands to return a count of the rows affected by an operation. 
            //A count of 0 indicates that an operation failed.
            SqlCommand insCustomerCmd = new SqlCommand();
            insCustomerCmd.CommandType = CommandType.StoredProcedure;
            insCustomerCmd.CommandText = "Sales.sp_Customer_ApplyInsert";
            insCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            insCustomerCmd.Parameters.Add("@CustomerName", SqlDbType.NVarChar);
            insCustomerCmd.Parameters.Add("@SalesPerson", SqlDbType.NVarChar);
            insCustomerCmd.Parameters.Add("@CustomerType", SqlDbType.NVarChar);
            insCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;

            adapterCustomer.InsertCommand = insCustomerCmd;


            //Specify the command to update rows.
            //The value of the sync_min_timestamp session variable is compared to
            //values in the sync_row_timestamp column in the tracking table to 
            //determine which rows to update.
            SqlCommand updCustomerCmd = new SqlCommand();
            updCustomerCmd.CommandType = CommandType.StoredProcedure;
            updCustomerCmd.CommandText = "Sales.sp_Customer_ApplyUpdate";
            updCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            updCustomerCmd.Parameters.Add("@CustomerName", SqlDbType.NVarChar);
            updCustomerCmd.Parameters.Add("@SalesPerson", SqlDbType.NVarChar);
            updCustomerCmd.Parameters.Add("@CustomerType", SqlDbType.NVarChar);
            updCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncMinTimestamp, SqlDbType.BigInt);
            updCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            updCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncForceWrite, SqlDbType.Int);

            adapterCustomer.UpdateCommand = updCustomerCmd;


            //Specify the command to delete rows.
            //The value of the sync_min_timestamp session variable is compared to
            //values in the sync_row_timestamp column in the tracking table to 
            //determine which rows to delete.
            SqlCommand delCustomerCmd = new SqlCommand();
            delCustomerCmd.CommandType = CommandType.StoredProcedure;
            delCustomerCmd.CommandText = "Sales.sp_Customer_ApplyDelete";
            delCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            delCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncMinTimestamp, SqlDbType.BigInt);
            delCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;

            adapterCustomer.DeleteCommand = delCustomerCmd;

            //Specify the command to select any conflicting rows.
            SqlCommand selRowCustomerCmd = new SqlCommand();
            selRowCustomerCmd.CommandType = CommandType.StoredProcedure;
            selRowCustomerCmd.CommandText = "Sales.sp_Customer_SelectRow";
            selRowCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);

            adapterCustomer.SelectRowCommand = selRowCustomerCmd;


            //Specify the command to insert metadata rows.
            //The session variables in this command relate to columns in
            //the tracking table.
            SqlCommand insMetadataCustomerCmd = new SqlCommand();
            insMetadataCustomerCmd.CommandType = CommandType.StoredProcedure;
            insMetadataCustomerCmd.CommandText = "Sales.sp_Customer_InsertMetadata";
            insMetadataCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            insMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncCreatePeerKey, SqlDbType.Int);
            insMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncCreatePeerTimestamp, SqlDbType.BigInt);
            insMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncUpdatePeerKey, SqlDbType.Int);
            insMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncUpdatePeerTimestamp, SqlDbType.BigInt);
            insMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowIsTombstone, SqlDbType.Int);
            insMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;

            adapterCustomer.InsertMetadataCommand = insMetadataCustomerCmd;


            //Specify the command to update metadata rows.
            SqlCommand updMetadataCustomerCmd = new SqlCommand();
            updMetadataCustomerCmd.CommandType = CommandType.StoredProcedure;
            updMetadataCustomerCmd.CommandText = "Sales.sp_Customer_UpdateMetadata";
            updMetadataCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncCreatePeerKey, SqlDbType.Int);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncCreatePeerTimestamp, SqlDbType.BigInt);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncUpdatePeerKey, SqlDbType.Int);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncUpdatePeerTimestamp, SqlDbType.BigInt);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncCheckConcurrency, SqlDbType.Int);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowTimestamp, SqlDbType.BigInt);
            updMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;

            adapterCustomer.UpdateMetadataCommand = updMetadataCustomerCmd;

            //Specify the command to delete metadata rows.
            SqlCommand delMetadataCustomerCmd = new SqlCommand();
            delMetadataCustomerCmd.CommandType = CommandType.StoredProcedure;
            delMetadataCustomerCmd.CommandText = "Sales.sp_Customer_DeleteMetadata";
            delMetadataCustomerCmd.Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier);
            delMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncCheckConcurrency, SqlDbType.Int);
            delMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowTimestamp, SqlDbType.BigInt);
            delMetadataCustomerCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;

            adapterCustomer.DeleteMetadataCommand = delMetadataCustomerCmd;


            //Specify the command to select metadata rows for cleanup.
            SqlCommand selMetadataCustomerCmd = new SqlCommand();
            selMetadataCustomerCmd.CommandType = CommandType.StoredProcedure;
            selMetadataCustomerCmd.CommandText = "Sales.sp_Customer_SelectMetadata";
            selMetadataCustomerCmd.Parameters.Add("@metadata_aging_in_hours", SqlDbType.Int).Value = MetadataAgingInHours;

            adapterCustomer.SelectMetadataForCleanupCommand = selMetadataCustomerCmd;

            peerProvider.SyncAdapters.Add(adapterCustomer);

            // Configure commands that relate to the provider itself rather 
            // than the DbSyncAdapter object for each table:
            // * SelectNewTimestampCommand: Returns the new high watermark for 
            //   the current synchronization session.
            // * SelectScopeInfoCommand: Returns sync knowledge, cleanup knowledge, 
            //   and a scope version (timestamp).
            // * UpdateScopeInfoCommand: Sets new values for sync knowledge and cleanup knowledge.            

            //Select a new timestamp.
            //During each synchronization, the new value and
            //the last value from the previous synchronization
            //are used: the set of changes between these upper and
            //lower bounds is synchronized.
            SqlCommand selectNewTimestampCommand = new SqlCommand();
            string newTimestampVariable = "@" + DbSyncSession.SyncNewTimestamp;
            selectNewTimestampCommand.CommandText = "SELECT " + newTimestampVariable + " = min_active_rowversion() - 1";
            selectNewTimestampCommand.Parameters.Add(newTimestampVariable, SqlDbType.Timestamp);
            selectNewTimestampCommand.Parameters[newTimestampVariable].Direction = ParameterDirection.Output;
            peerProvider.SelectNewTimestampCommand = selectNewTimestampCommand;

            //Specify the command to select local replica metadata. 
            //Set session variables with values from the Sales.ScopeInfo
            //metadata table.
            SqlCommand selReplicaInfoCmd = new SqlCommand();
            selReplicaInfoCmd.CommandType = CommandType.Text;
            selReplicaInfoCmd.CommandText = "SELECT " +
                                            "@" + DbSyncSession.SyncScopeId + " = scope_id, " +
                                            "@" + DbSyncSession.SyncScopeKnowledge + " = scope_sync_knowledge, " +
                                            "@" + DbSyncSession.SyncScopeCleanupKnowledge + " = scope_tombstone_cleanup_knowledge, " +
                                            "@" + DbSyncSession.SyncScopeTimestamp + " = scope_timestamp " +
                                            "FROM Sales.ScopeInfo " +
                                            "WHERE scope_name = @" + DbSyncSession.SyncScopeName;
            selReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeName, SqlDbType.NVarChar, 100);
            selReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeId, SqlDbType.UniqueIdentifier).Direction = ParameterDirection.Output;
            selReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeKnowledge, SqlDbType.VarBinary, 10000).Direction = ParameterDirection.Output;
            selReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeCleanupKnowledge, SqlDbType.VarBinary, 10000).Direction = ParameterDirection.Output;
            selReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeTimestamp, SqlDbType.BigInt).Direction = ParameterDirection.Output;
            peerProvider.SelectScopeInfoCommand = selReplicaInfoCmd;


            //Specify the command to select local replica metadata. 
            //Update the Sales.ScopeInfo metadata table with values
            //from session variables.
            SqlCommand updReplicaInfoCmd = new SqlCommand();
            updReplicaInfoCmd.CommandType = CommandType.Text;
            updReplicaInfoCmd.CommandText = "UPDATE  Sales.ScopeInfo SET " +
                                            "scope_sync_knowledge = @" + DbSyncSession.SyncScopeKnowledge + ", " +
                                            "scope_tombstone_cleanup_knowledge = @" + DbSyncSession.SyncScopeCleanupKnowledge + " " +
                                            "WHERE scope_name = @" + DbSyncSession.SyncScopeName + " AND " +
                                            " ( @" + DbSyncSession.SyncCheckConcurrency + " = 0 or scope_timestamp = @" + DbSyncSession.SyncScopeTimestamp + "); " +
                                            "SET @" + DbSyncSession.SyncRowCount + " = @@rowcount";
            updReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeKnowledge, SqlDbType.VarBinary, 10000);
            updReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeCleanupKnowledge, SqlDbType.VarBinary, 10000);
            updReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeName, SqlDbType.NVarChar, 100);
            updReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncCheckConcurrency, SqlDbType.Int);
            updReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncScopeTimestamp, SqlDbType.BigInt);
            updReplicaInfoCmd.Parameters.Add("@" + DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output;
            peerProvider.UpdateScopeInfoCommand = updReplicaInfoCmd;

            return peerProvider;
        }
    }

    //Handle the statistics that are returned by the SyncAgent.
    public class SampleStats
    {
        public void DisplayStats(SyncOperationStatistics 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("Total Changes Uploaded: " + syncStatistics.UploadChangesTotal);
            Console.WriteLine("Total Changes Downloaded: " + syncStatistics.DownloadChangesTotal);
            Console.WriteLine("Complete Time: " + syncStatistics.SyncEndTime);
            Console.WriteLine(String.Empty);
        }
    }
}
Imports System
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.Synchronization
Imports Microsoft.Synchronization.Data


Class Program

    Shared Sub Main(ByVal args() As String)

        'The Utility class handles all functionality that is not
        'directly related to synchronization, such as holding peerConnection 
        'string information and making changes to the server database.
        Dim util As New Utility()

        'The SampleStats class handles information from the SyncStatistics
        'object that the Synchronize method returns.
        Dim sampleStats As New SampleStats()

        Try
            'Initial synchronization. Instantiate the SyncOrchestrator
            'and call Synchronize. Note that data is not synchronized during the
            'session between peer 1 and peer 3, because all rows have already
            'been delivered to peer 3 during its synchronization session with peer 2.     
            Dim sampleSyncAgent As SyncOrchestrator
            Dim syncStatistics As SyncOperationStatistics

            sampleSyncAgent = New SampleSyncAgent(util.Peer1ConnString, util.Peer2ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "initial")

            sampleSyncAgent = New SampleSyncAgent(util.Peer2ConnString, util.Peer3ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "initial")

            sampleSyncAgent = New SampleSyncAgent(util.Peer1ConnString, util.Peer3ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "initial")


        Catch ex As DbOutdatedSyncException
            Console.WriteLine("Outdated Knowledge: " & ex.OutdatedPeerSyncKnowledge.ToString() & " Clean up knowledge: " & ex.MissingCleanupKnowledge.ToString())
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try

        'Make conflicting changes in each peer database.
        util.MakeConflictingChangesOnPeer(util.Peer1ConnString, "Customer")
        util.MakeConflictingChangesOnPeer(util.Peer2ConnString, "Customer")
        util.MakeConflictingChangesOnPeer(util.Peer3ConnString, "Customer")

        Try
            'Subsequent synchronization. Changes are now synchronized between all
            'peers.
            Dim sampleSyncAgent As SyncOrchestrator
            Dim syncStatistics As SyncOperationStatistics

            sampleSyncAgent = New SampleSyncAgent(util.Peer1ConnString, util.Peer2ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "subsequent")

            sampleSyncAgent = New SampleSyncAgent(util.Peer2ConnString, util.Peer3ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "subsequent")

            sampleSyncAgent = New SampleSyncAgent(util.Peer1ConnString, util.Peer3ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "subsequent")


        Catch ex As DbOutdatedSyncException
            Console.WriteLine("Outdated Knowledge: " & ex.OutdatedPeerSyncKnowledge.ToString() & " Clean up knowledge: " & ex.MissingCleanupKnowledge.ToString())
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try

        'Make a change in SyncSamplesDb_Peer2 that will fail when it
        'is synchronized with SyncSamplesDb_Peer1.
        util.MakeFailingChangeOnPeer(util.Peer2ConnString)

        Try
            Dim sampleSyncAgent As SyncOrchestrator
            Dim syncStatistics As SyncOperationStatistics

            sampleSyncAgent = New SampleSyncAgent(util.Peer1ConnString, util.Peer2ConnString)
            syncStatistics = sampleSyncAgent.Synchronize()
            sampleStats.DisplayStats(syncStatistics, "subsequent")


        Catch ex As DbOutdatedSyncException
            Console.WriteLine("Outdated Knowledge: " & ex.OutdatedPeerSyncKnowledge.ToString() & " Clean up knowledge: " & ex.MissingCleanupKnowledge.ToString())
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try

        'Return peer data back to its original state.
        util.CleanUpPeer(util.Peer1ConnString)
        util.CleanUpPeer(util.Peer2ConnString)
        util.CleanUpPeer(util.Peer3ConnString)

        'Exit.
        Console.Write(vbLf & "Press Enter to close the window.")
        Console.ReadLine()

    End Sub 'Main
End Class 'Program


'Create a class that is derived from 
'Microsoft.Synchronization.SyncOrchestrator.
Public Class SampleSyncAgent
    Inherits SyncOrchestrator

    'Create class-level variables so that the ApplyChangeFailedEvent 
    'handler can use them.
    Private _localProviderDatabase As String
    Private _remoteProviderDatabase As String


    Public Sub New(ByVal localProviderConnString As String, ByVal remoteProviderConnString As String)

        'Instantiate the sample provider that allows us to create a provider
        'for both of the peers that are being synchronized.
        Dim sampleSyncProvider As New SampleSyncProvider()

        'Instantiate a DbSyncProvider for the local peer and the remote peer.
        'For example, if this code is running at peer1 and is
        'synchronizing with peer2, peer1 would be the local provider
        'and peer2 the remote provider.                 
        Dim localProvider As New DbSyncProvider()
        Dim remoteProvider As New DbSyncProvider()

        'Create a provider by using the SetupSyncProvider on the sample class.             
        sampleSyncProvider.SetupSyncProvider(localProviderConnString, localProvider)
        localProvider.SyncProviderPosition = SyncProviderPosition.Local

        sampleSyncProvider.SetupSyncProvider(remoteProviderConnString, remoteProvider)
        remoteProvider.SyncProviderPosition = SyncProviderPosition.Remote

        'Specify event handlers for the ApplyChangeFailed event for each provider.
        'The handlers are used to resolve conflicting rows and log error information.
        AddHandler localProvider.ApplyChangeFailed, AddressOf dbProvider_ApplyChangeFailed
        AddHandler remoteProvider.ApplyChangeFailed, AddressOf dbProvider_ApplyChangeFailed

        Me.LocalProvider = localProvider
        Me.RemoteProvider = remoteProvider
        Me.Direction = SyncDirectionOrder.UploadAndDownload

        _localProviderDatabase = localProvider.Connection.Database.ToString()
        _remoteProviderDatabase = remoteProvider.Connection.Database.ToString()

    End Sub 'New


    Private Sub dbProvider_ApplyChangeFailed(ByVal sender As Object, ByVal e As DbApplyChangeFailedEventArgs)

        'For conflict detection, the "local" database is the one at which the
        'ApplyChangeFailed event occurs. We determine at which database the event
        'fired and then compare the name of that database to the names of
        'the databases specified as the LocalProvider and RemoteProvider.
        Dim DbConflictDetected As String = e.Connection.Database.ToString()
        Dim DbOther As String

        DbOther = IIf(DbConflictDetected = _localProviderDatabase, _remoteProviderDatabase, _localProviderDatabase)

        Console.WriteLine(String.Empty)
        Console.WriteLine("Conflict of type " & e.Conflict.Type.ToString & " was detected at " & DbConflictDetected & ".")

        If e.Conflict.Type = DbConflictType.LocalUpdateRemoteUpdate Then

            '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.
            Dim conflictingRemoteChange As DataTable = e.Conflict.RemoteChange
            Dim conflictingLocalChange As DataTable = e.Conflict.LocalChange
            Dim remoteColumnCount As Integer = conflictingRemoteChange.Columns.Count
            Dim localColumnCount As Integer = conflictingLocalChange.Columns.Count

            Console.WriteLine(String.Empty)
            Console.WriteLine(String.Empty)
            Console.WriteLine("Row from database " & DbConflictDetected)
            Console.Write(" | ")

            'Display the local row. As mentioned above, this is the row
            'from the database at which the conflict was detected.
            Dim i As Integer
            For i = 0 To localColumnCount - 1
                Console.Write(conflictingLocalChange.Rows(0)(i).ToString & " | ")
            Next i

            Console.WriteLine(String.Empty)
            Console.WriteLine(String.Empty)
            Console.WriteLine(String.Empty)
            Console.WriteLine("Row from database " & DbOther)
            Console.Write(" | ")

            'Display the remote row.
            For i = 0 To remoteColumnCount - 1
                Console.Write(conflictingRemoteChange.Rows(0)(i).ToString & " | ")
            Next i

            'Ask for a conflict resolution option.
            Console.WriteLine(String.Empty)
            Console.WriteLine(String.Empty)
            Console.WriteLine("Enter a resolution option for this conflict:")
            Console.WriteLine("A = change from " & DbConflictDetected & " wins.")
            Console.WriteLine("B = change from " & DbOther & " wins.")

            Dim conflictResolution As String = Console.ReadLine()
            conflictResolution.ToUpper()

            If conflictResolution = "A" Then
                e.Action = ApplyAction.Continue

            ElseIf conflictResolution = "B" Then
                e.Action = ApplyAction.RetryWithForceWrite

            Else
                Console.WriteLine(String.Empty)
                Console.WriteLine("Not a valid resolution option.")
            End If

            'Write any errors to a log file.
        ElseIf e.Conflict.Type = DbConflictType.ErrorsOccurred Then

            Dim logFile As String = "C:\SyncErrorLog.txt"

            Console.WriteLine(String.Empty)
            Console.WriteLine("An error occurred during synchronization.")
            Console.WriteLine("This error has been logged to " & logFile & ".")

            Dim streamWriter As StreamWriter = File.AppendText(logFile)
            Dim outputText As New StringBuilder()

            outputText.AppendLine("** APPLY CHANGE FAILURE AT " & DbConflictDetected.ToUpper() & " **")
            outputText.AppendLine("Error source: " & e.Error.Source)
            outputText.AppendLine("Error message: " & e.Error.Message)

            streamWriter.WriteLine(DateTime.Now.ToShortTimeString() & " | " & outputText.ToString())
            streamWriter.Flush()
            streamWriter.Dispose()
        End If

    End Sub 'dbProvider_ApplyChangeFailed
End Class 'SampleSyncAgent 

Public Class SampleSyncProvider

    Public Function SetupSyncProvider(ByVal peerConnString As String, ByVal peerProvider As DbSyncProvider) As DbSyncProvider

        'Set the amount of time to retain metadata.
        Const MetadataAgingInHours As Integer = 100

        Dim peerConnection As New SqlConnection(peerConnString)
        peerProvider.Connection = peerConnection
        peerProvider.ScopeName = "Sales"

        'Create a DbSyncAdapter object for the Customer table and associate it 
        'with the DbSyncProvider. Following the DataAdapter style in ADO.NET, 
        'DbSyncAdapter is the equivalent for synchronization. The commands that 
        'are specified for the DbSyncAdapter object call stored procedures
        ' that are created in each peer database.
        Dim adapterCustomer As New DbSyncAdapter("Customer")

        'Specify the primary key, which Sync Services uses
        'to identify each row during synchronization.
        adapterCustomer.RowIdColumns.Add("CustomerId")

        'Specify the command to select incremental changes.
        'In this command and other commands, session variables are
        'used to pass information at runtime. DbSyncSession.SyncMetadataOnly 
        'and SyncMinTimestamp are two of the string constants that
        'the DbSyncSession class exposes. You could also include 
        '@sync_metadata_only and @sync_min_timestamp directly in your 
        'queries:
        '*  sync_metadata_only is used by Sync Services as an optimization
        '   in some queries.
        '* The value of the sync_min_timestamp session variable is compared to
        '   values in the sync_row_timestamp column in the tracking table to 
        '   determine which rows to select.
        Dim chgsCustomerCmd As New SqlCommand()

        With chgsCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_SelectChanges"
            .Parameters.Add("@" & DbSyncSession.SyncMetadataOnly, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncMinTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncInitialize, SqlDbType.Int)
        End With

        adapterCustomer.SelectIncrementalChangesCommand = chgsCustomerCmd

        'Specify the command to insert rows.
        'The sync_row_count session variable is used in this command 
        'and other commands to return a count of the rows affected by an operation. 
        'A count of 0 indicates that an operation failed.
        Dim insCustomerCmd As New SqlCommand()

        With insCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_ApplyInsert"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Parameters.Add("@CustomerName", SqlDbType.NVarChar)
            .Parameters.Add("@SalesPerson", SqlDbType.NVarChar)
            .Parameters.Add("@CustomerType", SqlDbType.NVarChar)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        End With

        adapterCustomer.InsertCommand = insCustomerCmd


        'Specify the command to update rows.
        'The value of the sync_min_timestamp session variable is compared to
        'values in the sync_row_timestamp column in the tracking table to 
        'determine which rows to update.
        Dim updCustomerCmd As New SqlCommand()

        With updCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_ApplyUpdate"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Parameters.Add("@CustomerName", SqlDbType.NVarChar)
            .Parameters.Add("@SalesPerson", SqlDbType.NVarChar)
            .Parameters.Add("@CustomerType", SqlDbType.NVarChar)
            .Parameters.Add("@" & DbSyncSession.SyncMinTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
            .Parameters.Add("@" & DbSyncSession.SyncForceWrite, SqlDbType.Int)
        End With

        adapterCustomer.UpdateCommand = updCustomerCmd


        'Specify the command to delete rows.
        'The value of the sync_min_timestamp session variable is compared to
        'values in the sync_row_timestamp column in the tracking table to 
        'determine which rows to delete.
        Dim delCustomerCmd As New SqlCommand()

        With delCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_ApplyDelete"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Parameters.Add("@" & DbSyncSession.SyncMinTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        End With

        adapterCustomer.DeleteCommand = delCustomerCmd

        'Specify the command to select any conflicting rows.
        Dim selRowCustomerCmd As New SqlCommand()

        With selRowCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_SelectRow"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
        End With

        adapterCustomer.SelectRowCommand = selRowCustomerCmd


        'Specify the command to insert metadata rows.
        'The session variables in this command relate to columns in
        'the tracking table. These are the same columns
        'that were specified as DbSyncAdapter properties at the beginning 
        'of this code example.
        Dim insMetadataCustomerCmd As New SqlCommand()

        With insMetadataCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_InsertMetadata"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Parameters.Add("@" & DbSyncSession.SyncCreatePeerKey, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncCreatePeerTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncUpdatePeerKey, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncUpdatePeerTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncRowIsTombstone, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        End With

        adapterCustomer.InsertMetadataCommand = insMetadataCustomerCmd


        'Specify the command to update metadata rows.
        Dim updMetadataCustomerCmd As New SqlCommand()

        With updMetadataCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_UpdateMetadata"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Parameters.Add("@" & DbSyncSession.SyncCreatePeerKey, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncCreatePeerTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncUpdatePeerKey, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncUpdatePeerTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncCheckConcurrency, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncRowTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        End With

        adapterCustomer.UpdateMetadataCommand = updMetadataCustomerCmd

        'Specify the command to delete metadata rows.
        Dim delMetadataCustomerCmd As New SqlCommand()

        With delMetadataCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_DeleteMetadata"
            .Parameters.Add("@CustomerId", SqlDbType.UniqueIdentifier)
            .Parameters.Add("@" & DbSyncSession.SyncCheckConcurrency, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncRowTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        End With

        adapterCustomer.DeleteMetadataCommand = delMetadataCustomerCmd


        'Specify the command to select metadata rows for cleanup.
        Dim selMetadataCustomerCmd As New SqlCommand()

        With selMetadataCustomerCmd
            .CommandType = CommandType.StoredProcedure
            .CommandText = "Sales.sp_Customer_SelectMetadata"
            .Parameters.Add("@metadata_aging_in_hours", SqlDbType.Int).Value = MetadataAgingInHours
        End With

        adapterCustomer.SelectMetadataForCleanupCommand = selMetadataCustomerCmd

        peerProvider.SyncAdapters.Add(adapterCustomer)

        ' Configure commands that relate to the provider itself rather 
        ' than the DbSyncAdapter object for each table:
        ' * SelectNewTimestampCommand: Returns the new high watermark for 
        '   the current synchronization session.
        ' * SelectScopeInfoCommand: Returns sync knowledge, cleanup knowledge, 
        '   and a scope version (timestamp).
        ' * UpdateScopeInfoCommand: Sets new values for sync knowledge and cleanup knowledge.            
        'Select a new timestamp.
        'During each synchronization, the new value and
        'the last value from the previous synchronization
        'are used: the set of changes between these upper and
        'lower bounds is synchronized.
        Dim newTimestampVariable As String = "@" & DbSyncSession.SyncNewTimestamp

        Dim selectNewTimestampCommand As New SqlCommand()

        With selectNewTimestampCommand
            .CommandText = "SELECT " & newTimestampVariable & " = min_active_rowversion() - 1"
            .Parameters.Add(newTimestampVariable, SqlDbType.Timestamp)
            .Parameters(newTimestampVariable).Direction = ParameterDirection.Output
        End With

        peerProvider.SelectNewTimestampCommand = selectNewTimestampCommand

        'Specify the command to select local replica metadata. 
        'Set session variables with values from the Sales.ScopeInfo
        'metadata table.
        Dim selReplicaInfoCmd As New SqlCommand()

        With selReplicaInfoCmd
            .CommandType = CommandType.Text
            .CommandText = "SELECT " _
                         & "@" & DbSyncSession.SyncScopeId & " = scope_id, " _
                         & "@" & DbSyncSession.SyncScopeKnowledge & " = scope_sync_knowledge, " _
                         & "@" & DbSyncSession.SyncScopeCleanupKnowledge & " = scope_tombstone_cleanup_knowledge, " _
                         & "@" & DbSyncSession.SyncScopeTimestamp & " = scope_timestamp " _
                         & "FROM Sales.ScopeInfo " _
                         & "WHERE scope_name = @" & DbSyncSession.SyncScopeName
            .Parameters.Add("@" & DbSyncSession.SyncScopeName, SqlDbType.NVarChar, 100)
            .Parameters.Add("@" & DbSyncSession.SyncScopeId, SqlDbType.UniqueIdentifier).Direction = ParameterDirection.Output
            .Parameters.Add("@" & DbSyncSession.SyncScopeKnowledge, SqlDbType.VarBinary, 10000).Direction = ParameterDirection.Output
            .Parameters.Add("@" & DbSyncSession.SyncScopeCleanupKnowledge, SqlDbType.VarBinary, 10000).Direction = ParameterDirection.Output
            .Parameters.Add("@" & DbSyncSession.SyncScopeTimestamp, SqlDbType.BigInt).Direction = ParameterDirection.Output
        End With

        peerProvider.SelectScopeInfoCommand = selReplicaInfoCmd


        'Specify the command to select local replica metadata. 
        'Update the Sales.ScopeInfo metadata table with values
        'from session variables.
        Dim updReplicaInfoCmd As New SqlCommand()

        With updReplicaInfoCmd
            .CommandType = CommandType.Text
            .CommandText = "UPDATE  Sales.ScopeInfo SET " _
                         & "scope_sync_knowledge = @" & DbSyncSession.SyncScopeKnowledge & ", " _
                         & "scope_tombstone_cleanup_knowledge = @" & DbSyncSession.SyncScopeCleanupKnowledge & " " _
                         & "WHERE scope_name = @" & DbSyncSession.SyncScopeName & " AND " _
                         & " ( @" & DbSyncSession.SyncCheckConcurrency & " = 0 or scope_timestamp = @" & DbSyncSession.SyncScopeTimestamp & "); " _
                         & "SET @" & DbSyncSession.SyncRowCount & " = @@rowcount"
            .Parameters.Add("@" & DbSyncSession.SyncScopeKnowledge, SqlDbType.VarBinary, 10000)
            .Parameters.Add("@" & DbSyncSession.SyncScopeCleanupKnowledge, SqlDbType.VarBinary, 10000)
            .Parameters.Add("@" & DbSyncSession.SyncScopeName, SqlDbType.NVarChar, 100)
            .Parameters.Add("@" & DbSyncSession.SyncCheckConcurrency, SqlDbType.Int)
            .Parameters.Add("@" & DbSyncSession.SyncScopeTimestamp, SqlDbType.BigInt)
            .Parameters.Add("@" & DbSyncSession.SyncRowCount, SqlDbType.Int).Direction = ParameterDirection.Output
        End With

        peerProvider.UpdateScopeInfoCommand = updReplicaInfoCmd

        Return peerProvider

    End Function 'SetupSyncProvider
End Class 'SampleSyncProvider


'Handle the statistics that are returned by the SyncAgent.
Public Class SampleStats

    Public Sub DisplayStats(ByVal syncStatistics As SyncOperationStatistics, ByVal syncType As String)
        Console.WriteLine(String.Empty)
        If syncType = "initial" Then
            Console.WriteLine("****** Initial Synchronization ******")
        ElseIf syncType = "subsequent" Then
            Console.WriteLine("***** Subsequent Synchronization ****")
        End If

        Console.WriteLine("Start Time: " & syncStatistics.SyncStartTime)
        Console.WriteLine("Total Changes Uploaded: " & syncStatistics.UploadChangesTotal)
        Console.WriteLine("Total Changes Downloaded: " & syncStatistics.DownloadChangesTotal)
        Console.WriteLine("Complete Time: " & syncStatistics.SyncEndTime)
        Console.WriteLine(String.Empty)

    End Sub 'DisplayStats
End Class 'SampleStats

See Also

Concepts

Programming Common Peer-to-Peer Synchronization Tasks