Share via


Example Code for Reporting and Resolving Constraint Conflicts

The following example program implements a provider that handles constraint conflicts. The provider detects collision constraint conflicts when an item is created in its SaveChangeWithChangeUnits method. When a collision constraint is detected, the provider reports the conflict to the change applier by using RecordConstraintConflictForItem. The provider handles the resolution of collision constraint conflicts in its SaveChangeWithChangeUnits method as well.

This example program is described in detail in How to: Report and Resolve Constraint Conflicts.

Example

class ContactsProviderWithConstraintConflicts : KnowledgeSyncProvider
    , INotifyingChangeApplierTarget
    , INotifyingChangeApplierTarget2
    , IChangeDataRetriever
{
    public ContactsProviderWithConstraintConflicts(ContactStore store)
    {
        _ContactStore = store;
    }

    // Stores the session context and creates an in-memory conflict log.
    public override void BeginSession(SyncProviderPosition position, SyncSessionContext syncSessionContext)
    {
        _sessionContext = syncSessionContext;

        // Create an in-memory conflict log to store temporary conflicts during the session.
        _memConflictLog = new MemoryConflictLog(IdFormats);
    }

    // Releases the session context and conflict log.
    public override void EndSession(SyncSessionContext syncSessionContext)
    {
        _sessionContext = null;
        _memConflictLog = null;
    }

    // Uses the metadata storage service implementation of GetChangeBatch to retrieve a change batch.
    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;

        // Use metadata storage service to get a batch of changes.
        return _ContactStore.ContactReplicaMetadata.GetChangeBatch(batchSize, destinationKnowledge);
    }

    public override FullEnumerationChangeBatch GetFullEnumerationChangeBatch(uint batchSize, SyncId lowerEnumerationBound, SyncKnowledge knowledgeForDataRetrieval, out object changeDataRetriever)
    {
        throw new Exception("The method or operation is not implemented.");
    }

    // Gets the parameters for synchronization.
    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 that is stored in the metadata store.
        knowledge = _ContactStore.ContactReplicaMetadata.GetKnowledge();
    }

    // Gets the ID format schema that is defined for this replica.
    public override SyncIdFormatGroup IdFormats
    {
        get 
        {
            SyncIdFormatGroup FormatGroup = new SyncIdFormatGroup();

            // Change unit IDs are an enumeration, so they are fixed length and contain one byte.
            FormatGroup.ChangeUnitIdFormat.IsVariableLength = false;
            FormatGroup.ChangeUnitIdFormat.Length = sizeof(byte);

            // 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;
        }
    }

    // Uses the metadata storage service to get the local versions of changes received from the source provider.
    // Uses a NotifyingChangeApplier object to process the changes.
    public override void ProcessChangeBatch(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
    {
        // Use the metadata storage service to get the local versions of changes received from the source provider.
        IEnumerable<ItemChange> localVersions = _ContactStore.ContactReplicaMetadata.GetLocalVersions(sourceChanges);

        // Use a NotifyingChangeApplier object to process the changes. Specify a collision conflict resolution policy of 
        // ApplicationDefined and the conflict log that was created when the session started.
        NotifyingChangeApplier changeApplier = new NotifyingChangeApplier(ContactStore.ContactIdFormatGroup);
        changeApplier.ApplyChanges(resolutionPolicy, CollisionConflictResolutionPolicy.ApplicationDefined, sourceChanges, 
            (IChangeDataRetriever)changeDataRetriever, localVersions, _ContactStore.ContactReplicaMetadata.GetKnowledge(), 
            _ContactStore.ContactReplicaMetadata.GetForgottenKnowledge(), this, _memConflictLog, _sessionContext, syncCallbacks);
    }

    public override void ProcessFullEnumerationChangeBatch(ConflictResolutionPolicy resolutionPolicy, FullEnumerationChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
    {
        throw new Exception("The method or operation is not implemented.");
    }

    #region IChangeDataRetriever Members

    // Returns data for the specified change. Changes are represented by a sparsely-populated array of strings. The
    // array is indexed by change unit ID.
    public object LoadChangeData(LoadChangeContext loadChangeContext)
    {
        // Sanity check to ensure the data array is not overrun.
        if (Contact.ChangeUnitFieldCount < loadChangeContext.ItemChange.ChangeUnitChanges.Count)
        {
            throw new ArgumentOutOfRangeException("LoadChangeData received too many change unit changes.");
        }

        // Get the ID of the item to return.
        SyncId itemId = loadChangeContext.ItemChange.ItemId;

        string[] contactData;
        if (loadChangeContext.ItemChange.ChangeUnitChanges.Count == 0)
        {
            // Change unit count is 0, so return the entire item.
            // Create a string array that contains all of the change units.
            contactData = _ContactStore.ContactList[itemId].ToParts();
        }
        else
        {
            // Create a string array to hold the data for each change unit. Some of the elements of this array
            // may be empty.
            contactData = new string[Contact.ChangeUnitFieldCount];

            // Enumerate the change units to retrieve.
            for (int iChange = 0; iChange < loadChangeContext.ItemChange.ChangeUnitChanges.Count; iChange++)
            {
                // Retrieve data for the specified change unit and put the data into the appropriate
                // place in the string array.
                int icu = loadChangeContext.ItemChange.ChangeUnitChanges[iChange].ChangeUnitId.GetByteId();
                contactData[icu] = _ContactStore.GetContactData(itemId,
                    loadChangeContext.ItemChange.ChangeUnitChanges[iChange].ChangeUnitId);
            }
        }

        return contactData;
    }

    #endregion

    #region INotifyingChangeApplierTarget Members

    // Return this object as the IChangeDataRetriever implementation.
    public IChangeDataRetriever GetDataRetriever()
    {
        return this;
    }

    // Use the metadata storage service to get the next tick count.
    public ulong GetNextTickCount()
    {
        return _ContactStore.ContactReplicaMetadata.GetNextTickCount();
    }

    // Saves the specfied change to the item store and metadata store.
    public void SaveChangeWithChangeUnits(ItemChange change, SaveChangeWithChangeUnitsContext context)
    {
        // Actions are stored per change unit, but some actions affect the entire item, whereas some
        // actions affect only individual change units. These two types of actions are handled differently in
        // the following code.

        SaveChangeAction action = context.GetActionForChangeUnit(change.ChangeUnitChanges[0]);

        // Handle changes that affect the entire item.
        if (SaveChangeAction.Create == action ||
            SaveChangeAction.DeleteConflictingAndSaveSourceItem == action ||
            SaveChangeAction.RenameDestinationAndUpdateVersionData == action ||
            SaveChangeAction.RenameSourceAndUpdateVersionAndData == action)
        {
            // Verify that all change unit changes are the same action.
            foreach (ChangeUnitChange cuChange in change.ChangeUnitChanges)
            {
                if (context.GetActionForChangeUnit(cuChange) != action)
                {
                    throw new SyncInvalidOperationException("More than one kind of item-wide action received in SaveChangeWithChangeUnits.");
                }
            }

            switch (action)
            {
                case SaveChangeAction.Create:
                {
                    // Create a new item. Report a constraint conflict if one occurs.
                    try
                    {
                        ConstraintConflictReason constraintReason;
                        SyncId conflictingItemId;
                        // Check if the item can be created or if it causes a constraint conflict.
                        if (_ContactStore.CanCreateContact(change, (string[])context.ChangeData, out constraintReason, out conflictingItemId))
                        {
                            // No conflict, so create the item.
                            _ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);
                        }
                        else
                        {
                            // A constraint conflict occurred, so report this to the change applier.
                            context.RecordConstraintConflictForItem(conflictingItemId, constraintReason);
                        }
                    }
                    catch (Exception ex)
                    {
                        // Some other error occurred, so exclude this item for the rest of the session.
                        RecoverableErrorData errData = new RecoverableErrorData(ex);
                        context.RecordRecoverableErrorForItem(errData);
                    }
                    break;
                }
                case SaveChangeAction.DeleteConflictingAndSaveSourceItem:
                {
                    // Delete the destination item that is in conflict and save the source item.

                    // Make a new local version for the delete so it will propagate correctly throughout the synchronization community.
                    SyncVersion version = new SyncVersion(0, _ContactStore.ContactReplicaMetadata.GetNextTickCount());
                    _ContactStore.DeleteContactFromSync(context.ConflictingItemId, version);

                    // Save the source change as a new item.
                    _ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);

                    break;
                }
                case SaveChangeAction.RenameDestinationAndUpdateVersionData:
                {
                    // Rename the destination item so that it no longer conflicts with the source item and save the source item.

                    // Rename the destination item. This is done by appending a value to the name and updating the metadata to treat
                    // this as a local change, which will be propagated throughout the synchronization community.
                    _ContactStore.RenameExistingContact(context.ConflictingItemId);

                    // Save the source change as a new item.
                    _ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);

                    break;
                }
                case SaveChangeAction.RenameSourceAndUpdateVersionAndData:
                {
                    // Rename the source item so that it no longer conflicts with the destination item and save the source item.
                    // The destination item in conflict remains untouched.

                    // Rename the source item. This is done by appending a value to the name.
                    _ContactStore.FindNewNameForContact((string[])context.ChangeData);

                    // Save the renamed source change as a new item.
                    _ContactStore.CreateContactFromSync(change, (string[])context.ChangeData);

                    break;
                }
            }
        }
        else
        {
            // Enumerate the change units for changes that affect individual change units and apply 
            // them by using the specified action.
            foreach (ChangeUnitChange cuChange in change.ChangeUnitChanges)
            {
                switch (context.GetActionForChangeUnit(cuChange))
                {
                    case SaveChangeAction.Create:
                    case SaveChangeAction.DeleteConflictingAndSaveSourceItem:
                    case SaveChangeAction.RenameDestinationAndUpdateVersionData:
                    case SaveChangeAction.RenameSourceAndUpdateVersionAndData:
                    {
                        throw new SyncInvalidOperationException("Mix of item-wide changes and change unit changes received in SaveChangeWithChangeUnits.");
                    }
                    case SaveChangeAction.UpdateVersionAndData:
                    {
                        // Update the item store and metadata store for the specified change unit.
                        try
                        {
                            string cuData = ((string[])context.ChangeData)[cuChange.ChangeUnitId.GetByteId()];

                            if (_ContactStore.CanUpdateContact(change.ItemId, cuChange.ChangeUnitId, cuData))
                            {
                                _ContactStore.UpdateContactFromSync(change, cuChange, cuData);
                            }
                            else
                            {
                                context.RecordConstraintConflictForChangeUnit(cuChange);
                            }

                        }
                        catch (Exception ex)
                        {
                            RecoverableErrorData errData = new RecoverableErrorData(ex);
                            context.RecordRecoverableErrorForChangeUnit(cuChange, errData);
                        }
                        break;
                    }
                    case SaveChangeAction.UpdateVersionAndMergeData:
                    {
                        // Merge actions are not supported by this implementation.
                        throw new NotImplementedException("UpdateVersionAndMergeData is not supported.");
                    }
                    case SaveChangeAction.UpdateVersionOnly:
                    {
                        // Update only the version of this change unit in the metadata store.
                        try
                        {
                            _ContactStore.UpdateContactVersion(change.ItemId, cuChange.ChangeUnitId, cuChange.ChangeUnitVersion);
                        }
                        catch (Exception ex)
                        {
                            RecoverableErrorData errData = new RecoverableErrorData(ex);
                            context.RecordRecoverableErrorForChangeUnit(cuChange, errData);
                        }
                        break;
                    }
                    case SaveChangeAction.DeleteAndRemoveTombstone:
                    case SaveChangeAction.DeleteAndStoreTombstone:
                    {
                        // Delete actions are handled in SaveItemChange, so throw an exception.
                        throw new InvalidOperationException("SaveChangeWithChangeUnits received a delete action.");
                    }
                    default:
                    {
                        throw new ArgumentOutOfRangeException("SaveChangeWithChangeUnits received an out-of-range action.");
                    }
                }
            }
        }

        // Use the metadata storage service to save the knowledge 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 updatedKnowledge;
        ForgottenKnowledge updatedForgottenKnowledge;
        context.GetUpdatedDestinationKnowledge(out updatedKnowledge, out updatedForgottenKnowledge);
        _ContactStore.ContactReplicaMetadata.SetKnowledge(updatedKnowledge);
    }

    // This implementation does not support saving concurrency conflicts.
    public void SaveConflict(ItemChange conflictingChange, object conflictingChangeData, SyncKnowledge conflictingChangeKnowledge)
    {
        throw new Exception("The method or operation is not implemented.");
    }

    // Saves the specfied change to the item store and metadata store.
    public void SaveItemChange(SaveChangeAction saveChangeAction, ItemChange change, SaveChangeContext context)
    {
        // This provider uses change units, so SaveChangeWithChangeUnits is called for all actions except delete actions.
        if (SaveChangeAction.DeleteAndStoreTombstone == saveChangeAction)
        {
            // Delete the item from the item store and store a tombstone for it in the metadata store.
            try
            {
                _ContactStore.DeleteContactFromSync(change.ItemId, change.ChangeVersion);
            }
            catch (Exception ex)
            {
                RecoverableErrorData errData = new RecoverableErrorData(ex);
                context.RecordRecoverableErrorForItem(errData);
            }
        }
        else 
        {
            // SaveChangeWithChangeUnits should be called for all actions other than delete actions.
            throw new NotImplementedException("SaveItemChange only handles deletes!");
        }

        // Use the metadata storage service to save the knowledge 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 updatedKnowledge;
        ForgottenKnowledge updatedForgottenKnowledge;
        context.GetUpdatedDestinationKnowledge(out updatedKnowledge, out updatedForgottenKnowledge);
        _ContactStore.ContactReplicaMetadata.SetKnowledge(updatedKnowledge);
    }

    // Use the metadata storage service to save the knowledge after the change batch is applied.
    // Also, commit changes for the batch to the files on disk.
    public void StoreKnowledgeForScope(SyncKnowledge knowledge, ForgottenKnowledge forgottenKnowledge)
    {
        // Use the metadata storage service to save the knowledge and forgotten knowledge.
        _ContactStore.ContactReplicaMetadata.SetKnowledge(knowledge);
        _ContactStore.ContactReplicaMetadata.SetForgottenKnowledge(forgottenKnowledge);

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

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

    // When constraint conflicts are reported, this method must be implemented or synchronization
    // will fail.
    public bool TryGetDestinationVersion(ItemChange sourceChange, out ItemChange destinationVersion)
    {
        bool found = false;
        // Get the item metadata from the metadata store.
        ItemMetadata itemMeta = _ContactStore.ContactReplicaMetadata.FindItemMetadataById(sourceChange.ItemId);
        if (null != itemMeta)
        {
            // The item metadata exists, so translate the change unit metadata to the proper format and
            // return the item change object.
            ChangeUnitChange cuChange;
            List<ChangeUnitChange> cuChanges = new List<ChangeUnitChange>();
            foreach (ChangeUnitMetadata cuMeta in itemMeta.GetChangeUnitEnumerator())
            {
                cuChange = new ChangeUnitChange(IdFormats, cuMeta.ChangeUnitId, cuMeta.ChangeUnitVersion);
                cuChanges.Add(cuChange);
            }
            destinationVersion = new ItemChange(IdFormats, _ContactStore.ContactReplicaMetadata.ReplicaId, sourceChange.ItemId,
                ChangeKind.Update, itemMeta.CreationVersion, cuChanges);

            found = true;
        }
        else
        {
            destinationVersion = null;
        }
        return found;
    }

    #endregion

    #region INotifyingChangeApplierTarget2 Members

    // Save temporary constraint conflicts in the in-memory conflict log.
    public void SaveConstraintConflict(ItemChange conflictingChange, SyncId conflictingItemId, 
        ConstraintConflictReason reason, object conflictingChangeData, SyncKnowledge conflictingChangeKnowledge, 
        bool temporary)
    {
        if (!temporary)
        {
            // The in-memory conflict log is used, so if a non-temporary conflict is saved, it's
            // an error.
            throw new NotImplementedException("SaveConstraintConflict can only save temporary conflicts.");
        }
        else
        {
            // For temporary conflicts, just pass on the data and let the conflict log handle it.
            _memConflictLog.SaveConstraintConflict(conflictingChange, conflictingItemId, reason, 
                conflictingChangeData, conflictingChangeKnowledge, temporary);
        }
    }

    #endregion

    protected ContactStore _ContactStore;
    private SyncSessionContext _sessionContext;
    private MemoryConflictLog _memConflictLog;
}

See Also

Concepts

How to: Report and Resolve Constraint Conflicts