Share via


How to: Create a Managed Synchronization Provider

This topic describes how to use a managed language to create a Sync Framework synchronization provider that synchronizes data from a custom data store.

This topic assumes a basic familiarity with C# and Microsoft .NET Framework concepts.

The examples in this topic focus on the following Sync Framework classes and interfaces:

Understanding Synchronization Providers

A synchronization provider is a software component that represents a replica during synchronization. This enables the replica to synchronize its data with other replicas. For synchronization to occur, an application first creates a SyncOrchestrator object, connects it to two SyncProvider objects, and then starts synchronization. One of the providers represents the source replica. The source replica supplies metadata for changed items through its GetChangeBatch method and item data through an IChangeDataRetriever object. The other provider represents the destination replica. The destination replica receives metadata for changed items through its ProcessChangeBatch method and applies the changes to its item store by using a Sync Framework-supplied NotifyingChangeApplier object together with its own INotifyingChangeApplierTarget object.

For more information about the role of the synchronization provider, see Implementing a Standard Custom Provider.

Build Requirements

Example

The example code in this topic shows how to implement the basic class and interface methods that are required for a replica to participate in a Sync Framework synchronization community, both as a source and as a destination. The replica in this example is a text file that stores contact information as a list of comma-separated values. The items to synchronize are the contacts that are contained in this file. This example also uses a custom metadata store that is implemented by using the metadata storage service API. For information about the metadata storage service, see Sync Framework Metadata Storage Service.

In the following examples, _itemStore represents an object that contains both the item store and the metadata store.

Implementing SyncProvider and KnowledgeSyncProvider

The entry point into the provider is the SyncProvider class. This class is intended to function as a base class for other, more powerful provider classes. This example uses the KnowledgeSyncProvider class.

Declaring KnowledgeSyncProvider

Add KnowledgeSyncProvider to the class inheritance list.

class ContactsProviderXmlMetadataNoChangeUnits : KnowledgeSyncProvider

Add the SyncProvider and KnowledgeSyncProvider methods to the class.

IdFormats Property

Sync Framework calls IdFormats on both the source and destination providers when Synchronize is called on the SyncOrchestrator object. The IdFormats property returns the ID format schema that is used by the provider. This schema must be the same for both providers. This example defines sizes for item IDs that contain a SyncGlobalId, replica IDs that contain the absolute path of the replica, and change unit IDs that are members of an enumeration.

public override SyncIdFormatGroup IdFormats
{
    get 
    {
        SyncIdFormatGroup FormatGroup = new SyncIdFormatGroup();

        // Item IDs are of SyncGlobalId type, so they are fixed length and contain a ulong prefix plus a Guid.
        FormatGroup.ItemIdFormat.IsVariableLength = false;
        FormatGroup.ItemIdFormat.Length = (ushort)(sizeof(ulong) + Marshal.SizeOf(typeof(Guid)));

        // Replica IDs are the absolute path to the item store, so they are variable length with maximum
        // length equal to the maximum length of a path.
        FormatGroup.ReplicaIdFormat.IsVariableLength = true;
        FormatGroup.ReplicaIdFormat.Length = 260 * sizeof(char);

        return FormatGroup;
    }
}

BeginSession Method

Sync Framework calls BeginSession on both the source and destination providers before it calls any other methods. This method informs a provider that it is joining a synchronization session and passes the provider an object that contains session state information. This implementation stores the session state object or throws SyncInvalidOperationException if the provider has already joined a synchronization session.

public override void BeginSession(SyncProviderPosition position, SyncSessionContext syncSessionContext)
{
    // If this object is already in a session, throw an exception.
    if (null != _sessionContext)
    {
        throw new SyncInvalidOperationException();
    }
    
    _sessionContext = syncSessionContext;
}

GetSyncBatchParameters Method

Sync Framework calls GetSyncBatchParameters on the destination provider. This method retrieves the number of changes the source provider should include in a change batch, and obtains the destination provider's current knowledge. This implementation extracts the knowledge from the metadata store, and sets the batch size to 10.

public override void GetSyncBatchParameters(out uint batchSize, out SyncKnowledge knowledge)
{
    // Set a batch size of 10.
    batchSize = 10;

    // Return the current knowledge of the replica.
    knowledge = _itemStore.ContactReplicaMetadata.GetKnowledge();
}

The metadata store object returns the current knowledge of the replica by using GetKnowledge. This implementation creates a new SyncKnowledge object if the replica does not yet contain any knowledge, and sets the tick count of the knowledge to the current tick count of the replica.

public override SyncKnowledge GetKnowledge()
{
    // If the replica does not yet contain any knowledge, create a new knowledge object.
    if (null == _knowledge)
    {
        _knowledge = new SyncKnowledge(IdFormats, ReplicaId, _tickCount);            
    }

    // Ensure the tick count of the knowledge is set to the current tick count of the replica.
    _knowledge.SetLocalTickCount(_tickCount);

    return _knowledge;
}

GetChangeBatch Method

The synchronization session starts in earnest when Sync Framework calls GetChangeBatch on the source provider. This method retrieves a batch of changes to send to the destination provider and also returns the data retriever interface. Sync Framework uses the data retriever interface to obtain an object that the destination provider uses to retrieve item data for changes that are applied to the destination replica. Sync Framework calls GetChangeBatch repeatedly until the last batch is sent. The source provider indicates that a batch is the last batch by calling the SetLastBatch method on the change batch object. This implementation delegates the change enumeration task to the metadata store object. The provider object implements IChangeDataRetriever; therefore, it is returned in the changeDataRetriever parameter.

public override ChangeBatch GetChangeBatch(uint batchSize, SyncKnowledge destinationKnowledge, out object changeDataRetriever)
{
    // Return this object as the IChangeDataRetriever object that is called to retrieve item data.
    changeDataRetriever = this;

    // Call the metadata store to get a batch of changes.
    return _itemStore.ContactReplicaMetadata.GetChangeBatch(batchSize, destinationKnowledge);
}

The metadata store object returns a batch of changes by using GetChangeBatch. This implementation stores items in the metadata store as a list of ItemMetadata objects that are ordered by item ID. The items are enumerated and a change is added to an ordered group in the change batch object if the change is not contained in the destination knowledge. When all items in the metadata store have been enumerated, SetLastBatch is called on the change batch.

public override ChangeBatch GetChangeBatch(uint batchSize, SyncKnowledge destinationKnowledge)
{
    // The destination knowledge must be converted to be compatible with the source replica
    // before it can be used.
    SyncKnowledge mappedDestKnowledge = _knowledge.MapRemoteKnowledgeToLocal(destinationKnowledge);

    // Create a new change batch, initialized by using the current knowledge of the source replica
    // and a new ForgottenKnowledge object.
    ChangeBatch changeBatch = new ChangeBatch(IdFormats, GetKnowledge(), new ForgottenKnowledge());

    // Start a group of changes in the change batch. The group is ordered by item ID.
    // _getChangeBatchCurrent is 0 the first time GetChangeBatch is called, and is used to track the
    // position in the metadata store for subsequent calls to GetChangeBatch.
    changeBatch.BeginOrderedGroup(_items.Values[_getChangeBatchCurrent].GlobalId);
    
    // itemsAdded is incremented each time a change is added to the change batch. When itemsAdded
    // is greater than the requested batch size, enumeration stops and the change batch is returned.
    int itemsAdded = 0;
    
    ItemMetadata itemMeta;

    // Enumerate items and add a change to the change batch if it is not contained in the 
    // destination knowledge.
    // _items is a SortedList that contains ItemMetadata objects that are ordered by item ID.
    for (; itemsAdded <= batchSize && _getChangeBatchCurrent < _items.Count; _getChangeBatchCurrent++)
    {
        itemMeta = _items.Values[_getChangeBatchCurrent];
        ChangeKind kind = (itemMeta.IsDeleted) ? ChangeKind.Deleted : ChangeKind.Update;
        ItemChange change = new ItemChange(IdFormats, ReplicaId, itemMeta.GlobalId, kind, itemMeta.CreationVersion, 
            itemMeta.ChangeVersion);

        // If the change is not contained in the destination knowledge, add it to the change batch.
        if (!mappedDestKnowledge.Contains(change))
        {
            changeBatch.AddChange(change);
            itemsAdded++;
        }
    }

    // End the group of changes in the change batch. Pass the current source knowledge.
    changeBatch.EndOrderedGroup(_items.Values[_getChangeBatchCurrent - 1].GlobalId, _knowledge);

    // When all items in the metadata store have been enumerated, set this batch as the
    // last batch.
    if (_getChangeBatchCurrent == _items.Count)
    {
        changeBatch.SetLastBatch();
    }

    return changeBatch;
}

ProcessChangeBatch Method

After Sync Framework has obtained a batch of changes from the source provider by calling its GetChangeBatch method, Sync Framework calls ProcessChangeBatch on the destination provider. This method applies the changes to the destination replica. This method is called one time for each change batch that is retrieved from GetChangeBatch on the source provider. This implementation uses the metadata store object to obtain local version information for items from the source provider. It then creates a NotifyingChangeApplier object implemented by Sync Framework and calls its ApplyChanges method.

public override void ProcessChangeBatch(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
{
    // Use the metadata store to get the local versions of changes received from the source provider.
    IEnumerable<ItemChange> destVersions = _itemStore.ContactReplicaMetadata.GetLocalVersions(sourceChanges);

    // Use a NotifyingChangeApplier object to process the changes. Note that this object is passed as the INotifyingChangeApplierTarget
    // object that will be called to apply changes to the item store.
    NotifyingChangeApplier changeApplier = new NotifyingChangeApplier(IdFormats);
    changeApplier.ApplyChanges(resolutionPolicy, sourceChanges, (IChangeDataRetriever)changeDataRetriever, destVersions,
        _itemStore.ContactReplicaMetadata.GetKnowledge(), _itemStore.ContactReplicaMetadata.GetForgottenKnowledge(), 
        this, _sessionContext, syncCallbacks);
}

The metadata store object returns local version information for items from the source provider by using GetLocalVersions. This implementation enumerates the changes sent in the change batch from the source provider. If an item is in the destination metadata, its version information is added to a list of changes that contain the version information. If an item does not exist in the destination metadata, it is flagged as an unknown item in the local version list.

public override IEnumerable<ItemChange> GetLocalVersions(ChangeBatch sourceChanges)
{
    List<ItemChange> localVersions = new List<ItemChange>();

    // Enumerate the source changes and retrieve the destination version for each source change. 
    foreach (ItemChange srcItem in sourceChanges)
    {
        ItemChange localVer;

        // When the source item exists in the destination metadata store, retrieve the destination version of the item.
        if (_items.ContainsKey(srcItem.ItemId))
        {
            XmlItemMetadata localMeta = _items[srcItem.ItemId];
            ChangeKind kind = (localMeta.IsDeleted) ? ChangeKind.Deleted : ChangeKind.Update;
            localVer = new ItemChange(IdFormats, ReplicaId, srcItem.ItemId, kind, localMeta.CreationVersion, localMeta.ChangeVersion);
        }
        // When the source item does not exist in the destination metadata store, create a new change with unknown
        // version information.
        else
        {
            localVer = new ItemChange(IdFormats, ReplicaId, srcItem.ItemId, ChangeKind.UnknownItem, SyncVersion.UnknownVersion, SyncVersion.UnknownVersion);
        }

        localVersions.Add(localVer);
    }

    return localVersions;
}

EndSession Method

After the source provider sends its last batch and the destination provider has applied the changes to its data store, Sync Framework calls EndSession on both the source and destination providers. This method informs a provider that it is leaving a synchronization session and should free any resources that are associated with the session. This implementation frees the session state object that it stored in the BeginSession call or throws SyncInvalidOperationException if the provider did not previously join a synchronization session.

public override void EndSession(SyncSessionContext syncSessionContext)
{
    // If this object is not in a session, throw an exception.
    if (null == _sessionContext)
    {
        throw new SyncInvalidOperationException();            
    }

    _sessionContext = null;
}

Methods That Are Not Implemented

The following methods are not required because this sample never removes items marked as deleted in the metadata store. These methods can throw NotImplementedException:

Implementing INotifyingChangeApplierTarget

This interface is provided to Sync Framework when the destination provider calls the ApplyChanges method, typically in the ProcessChangeBatch method. INotifyingChangeApplierTarget contains methods that are called during change application. These methods are only called on the destination provider.

Declaring INotifyingChangeApplierTarget

Add INotifyingChangeApplierTarget to your class inheritance list.

class ContactsProviderXmlMetadataNoChangeUnits : KnowledgeSyncProvider
    , INotifyingChangeApplierTarget

Add the INotifyingChangeApplierTarget methods to your class.

IdFormats Property

Sync Framework calls IdFormats to retrieve the ID format schema of the provider. This example uses the same class to implement both KnowledgeSyncProvider and INotifyingChangeApplierTarget. Therefore, this implementation is the same as that for the IdFormats property of KnowledgeSyncProvider, above.

GetNextTickCount

Sync Framework calls GetNextTickCount to increment and retrieve the tick count for the replica. This implementation calls the GetNextTickCount method of the metadata store object.

public ulong GetNextTickCount()
{
    return _itemStore.ContactReplicaMetadata.GetNextTickCount();
}

The metadata store implementation of GetNextTickCount increments the tick count of the replica and returns it.

public override ulong GetNextTickCount()
{
    return ++_tickCount;
}

SaveItemChange

During change application, Sync Framework calls SaveItemChange for each change that is to be applied to the destination replica. This implementation updates the item store and metadata store for each type of change received. When an item is created or updated, the item data is received in the ChangeData property of the context parameter. The ChangeData property contains the object returned from the LoadChangeData method of the source provider. After the change is applied, the updated destination knowledge is stored.

public void SaveItemChange(SaveChangeAction saveChangeAction, ItemChange change, SaveChangeContext context)
{
    switch (saveChangeAction)
    {
        // Update the item store and metadata store when an item is created or updated.
        case SaveChangeAction.Create:
        case SaveChangeAction.UpdateVersionAndData:
        {
            try
            {
                _itemStore.UpdateContactFromSync(change, (string)context.ChangeData);
            }
            catch (Exception ex)
            {
                RecoverableErrorData errData = new RecoverableErrorData(ex);
                context.RecordRecoverableErrorForItem(errData);
            }
            break;
        }

        // Update only the version of this item in the metadata store.
        case SaveChangeAction.UpdateVersionOnly:
        {
            try
            {
                _itemStore.UpdateContactVersion(change.ItemId, change.ChangeVersion);
            }
            catch (Exception ex)
            {
                RecoverableErrorData errData = new RecoverableErrorData(ex);
                context.RecordRecoverableErrorForItem(errData);
            }
            break;
        }

        // Delete the item from the item store and store a tombstone for it in the metadata store.
        case SaveChangeAction.DeleteAndStoreTombstone:
        {
            try
            {
                _itemStore.DeleteContactFromSync(change.ItemId, change.ChangeVersion);
            }
            catch (Exception ex)
            {
                RecoverableErrorData errData = new RecoverableErrorData(ex);
                context.RecordRecoverableErrorForItem(errData);
            }

            break;
        }

        // Neither merging of data nor removing tombstones is supported.
        case SaveChangeAction.UpdateVersionAndMergeData:
        case SaveChangeAction.DeleteAndRemoveTombstone:
        {
            throw new NotImplementedException();
        }

        default:
        {
            throw new ArgumentOutOfRangeException();
        }
    }

    // Save the knowledge in the metadata store as each change is applied. Saving knowledge as each change is applied is 
    // not required. It is more robust than saving the knowledge only after each change batch, because if synchronization is interrupted 
    // before the end of a change batch, the knowledge will still reflect all of the changes applied. However, it is less efficient because 
    // knowledge must be stored more frequently.
    SyncKnowledge knowledge;
    ForgottenKnowledge forgottenKnowledge;
    context.GetUpdatedDestinationKnowledge(out knowledge, out forgottenKnowledge);
    _itemStore.ContactReplicaMetadata.SetKnowledge(knowledge);
}

In the following examples, _ContactItemMetaList contains ItemMetadata objects.

The UpdateContactFromSync method of the contact store updates the specified contact. If the contact does not exist, a new contact is created and added to the contact store. The metadata store is also updated to reflect changes to the contact store.

public void UpdateContactFromSync(ItemChange itemChange, string changeData)
{
    // If the item does not exist, create a new contact and add it to the contact and metadata store.
    if (!_ContactList.ContainsKey(itemChange.ItemId))
    {
        Contact contact = new Contact();
        ItemMetadata newItemMeta = _ContactReplicaMetadata.CreateItemMetadata(itemChange.ItemId,
            itemChange.CreationVersion);

        _ContactList.Add(newItemMeta.GlobalId, contact);
        _ContactItemMetaList.Add(newItemMeta.GlobalId, newItemMeta);
    }

    // Update the specified contact in the contact store. changeData is the contact data returned by the
    // IChangeDataRetriever.LoadChangeData method of the source provider.
    _ContactList[itemChange.ItemId].FromString(changeData);

    // Get the metadata for the specified item.
    ItemMetadata itemMeta = _ContactItemMetaList[itemChange.ItemId];

    // Update the index fields for the item. This implementation defines an index that uniquely identifies each contact.
    // The index consists of the first name, last name, and phone number of the contact.
    itemMeta.SetCustomField(FirstNameField, _ContactList[itemChange.ItemId].FirstName);
    itemMeta.SetCustomField(LastNameField, _ContactList[itemChange.ItemId].LastName);
    itemMeta.SetCustomField(PhoneNumberField, _ContactList[itemChange.ItemId].PhoneNumber);

    // Update the version for the change.
    itemMeta.ChangeVersion = itemChange.ChangeVersion;
}

The UpdateContactVersion method of the contact store updates the version metadata for the specified item.

public void UpdateContactVersion(SyncId itemId, SyncVersion itemVersion)
{
    // Update the version metadata for the specified item.
    _ContactItemMetaList[itemId].ChangeVersion = itemVersion;
}

The DeleteContactFromSync method of the contact store removes the item from the contact store and marks the item as deleted in the metadata store.

public void DeleteContactFromSync(SyncId itemId, SyncVersion version)
{
    if (_ContactList.ContainsKey(itemId))
    {
        // Remove the item from the contact store.
        _ContactList.Remove(itemId);

        // Mark the item as deleted in the metadata store.
        ItemMetadata itemMeta = _ContactItemMetaList[itemId];
        itemMeta.MarkAsDeleted(version);

        // Change the first index field so the index fields don't collide with future items.
        itemMeta.SetCustomField(FirstNameField, itemId.ToString());

        // Move the metadata for the deleted item to a separate list. 
        // The deleted item metadata must be kept so that it can be committed when
        // SaveChanges is called.
        _ContactDeletedItemMetaList.Add(itemMeta);
        _ContactItemMetaList.Remove(itemId);
    }
    else 
    {
        // An item marked as deleted has been received as part of synchronization, but it does not exist in
        // the item store. Create a tombstone for it in the metadata store.
        ItemMetadata itemMeta = _ContactReplicaMetadata.CreateItemMetadata(itemId, version);
        itemMeta.MarkAsDeleted(version);

        // Clear the index fields so they don't collide with future items.
        itemMeta.SetCustomField(FirstNameField, itemId.ToString());

        _ContactDeletedItemMetaList.Add(itemMeta);
    }
}

StoreKnowledgeForScope

After processing each change batch, Sync Framework calls StoreKnowledgeForScope so that the destination provider can save the knowledge that contains the new changes. This implementation saves the knowledge objects to the metadata store and overwrites previously existing knowledge. Changes made to the contact store and to the metadata store during processing of the change batch are committed to the files on the disk.

public void StoreKnowledgeForScope(SyncKnowledge knowledge, ForgottenKnowledge forgottenKnowledge)
{
    _itemStore.ContactReplicaMetadata.SetKnowledge(knowledge);

    // Commit changes made to the in-memory item store to the file on disk.
    _itemStore.SaveContactChanges();

    // Commit changes made to the in-memory metadata store to the file on disk.
    _itemStore.SaveMetadataChanges();
}

The SaveMetadataChanges method of the contact store commits changes made to the metadata store to the file on disk.

public void SaveMetadataChanges()
{
    // A transaction is required for saving changes to the metadata store.
    _ContactMetadataStore.BeginTransaction(IsolationLevel.ReadCommitted);

    // Enumerate the deleted items list.
    if (null != _ContactDeletedItemMetaList)
    {
        foreach (ItemMetadata contactMeta in _ContactDeletedItemMetaList)
        {
            // Save the deleted item metadata to the metadata store.
            _ContactReplicaMetadata.SaveItemMetadata(contactMeta);
        }
    }

    // Save renamed items first to avoid collisions in the metadata store.
    foreach (SyncId itemId in _ContactRenameList)
    {
        _ContactReplicaMetadata.SaveItemMetadata(_ContactItemMetaList[itemId]);            
    }

    // Enumerate the active contacts.
    for (int iCon = 0; iCon < _ContactItemMetaList.Count; iCon++)
    { 
        // Save the item metadata to the metadata store.
        _ContactReplicaMetadata.SaveItemMetadata(_ContactItemMetaList.Values[iCon]);
    }

    // Save the replica metadata to the metadata store.
    _ContactReplicaMetadata.SaveReplicaMetadata();

    // Commit the metadata store transaction.
    _ContactMetadataStore.CommitTransaction();
}

Methods That Are Not Implemented

The following methods are not required for basic synchronization scenarios and can throw NotImplementedException:

Implementing IChangeDataRetriever

IChangeDataRetriever is returned to Sync Framework by the source provider in response to the GetChangeBatch call. IChangeDataRetriever is sent to the destination provider in the ProcessChangeBatch call, where it is typically passed on to the ApplyChanges method of a change applier. The change applier then calls LoadChangeData to obtain an object that represents the item data. The change applier passes this interface to the SaveItemChange or SaveChangeWithChangeUnits method of the destination provider. The destination provider uses this object to retrieve item data for new or changed items and applies the item data to the destination replica.

Declaring IChangeDataRetriever

Add IChangeDataRetriever to the class inheritance list.

class ContactsProviderXmlMetadataNoChangeUnits : KnowledgeSyncProvider
    , INotifyingChangeApplierTarget
    , IChangeDataRetriever

Add the IChangeDataRetriever methods to the class.

IdFormats Property

Sync Framework calls IdFormats to retrieve the ID format schema of the provider. This example uses the same class to implement both KnowledgeSyncProvider and IChangeDataRetriever. Therefore, this implementation is the same as that for the IdFormats property of KnowledgeSyncProvider, above.

LoadChangeData Method

During change application, Sync Framework calls LoadChangeData to obtain an object that the destination provider can use to retrieve item data. This implementation returns the contact data serialized as a string.

public object LoadChangeData(LoadChangeContext loadChangeContext)
{
    // Return the specified contact serialized as a string.
    return _itemStore.ContactList[loadChangeContext.ItemChange.ItemId].ToString();
}

Next Steps

Next, you might want to create an application that can host the synchronization session and connect it to the provider. For information about how to do this, see How to: Create an Unmanaged Synchronization Application.

You can also enhance the provider to filter the items or change units that are synchronized. For more information about filtering, see Filtering Synchronization Data.

You can also enhance the provider to handle change units. For more information about change units, see Synchronizing Change Units.

You might also want to create a custom metadata store. For more information about how to handle synchronization metadata, see Managing Metadata for Standard Providers.

See Also

Reference

SyncProvider
KnowledgeSyncProvider
INotifyingChangeApplierTarget
IChangeDataRetriever
NotifyingChangeApplier

Concepts

Implementing a Standard Custom Provider
Sync Framework Core Components