Share via


How to: Report and Resolve Constraint Conflicts

This topic describes how to use a managed language to enable a standard custom provider to report and resolve constraint conflicts. Constraint conflicts are conflicts that violate constraints that are put on items, such as the relationship of folders or the location of identically named data within a file system. Sync Framework provides the NotifyingChangeApplier class to help process constraint conflicts.

For more information about constraint conflicts, see Detecting and Resolving Constraint Conflicts.

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 methods:

Understanding Constraint Conflicts

Constraint conflicts are detected by the destination provider during the change application phase of synchronization, typically in the SaveItemChange or SaveChangeWithChangeUnits method. When the destination provider detects a constraint conflict, it reports the conflict to the NotifyingChangeApplier object by using the RecordConstraintConflictForChangeUnit or RecordConstraintConflictForItem method. The change applier resolves the conflict according to either the collision conflict resolution policy set for the session, or the conflict resolution action set by the application for the specified conflict. The change applier then dispatches any necessary calls to the destination provider, such as SaveItemChange or SaveChangeWithChangeUnits, so that the destination provider can apply the resolved conflict to the destination replica. Typical resolutions of constraint conflicts include renaming the source item or destination item so that the constraint conflict no longer occurs, or merging the contents of the two items into a single item.

Build Requirements

Example

The example code in this topic shows how to enable the change applier to handle constraint conflicts, how to detect and report a constraint conflict during the change application phase of synchronization, how to set the resolution action by handling the ItemConstraint event in the synchronization application, and how to resolve the conflict by applying the resolution to the destination replica. The replica in this example stores contacts in a text file as a list of comma-separated values. The items to synchronize are the contacts that are contained in this file and the change units that are tracked are the fields within each contact. Contacts are uniquely identified in the contact store by a combination of the name and phone number fields. When a contact that has the same name and phone number is created locally on two different replicas, then when the replicas are synchronized a collision constraint conflict occurs.

Enabling the NotifyingChangeApplier Object to Process Constraint Conflicts

The following requirements must all be satisfied to enable the NotifyingChangeApplier object to successfully handle constraint conflicts.

Create an IConflictLogAccess Object

The change applier uses a conflict log to save temporary conflicts during synchronization so that the conflicts can be processed efficiently during the session. For replicas that do not otherwise store conflicts, Sync Framework provides the MemoryConflictLog class, which operates in memory and can be used to store temporary conflicts during the synchronization session. This example creates the in-memory conflict log when the session starts.

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

Pass the IConflictLogAccess Object to the NotifyingChangeApplier Object

The destination provider must call the ApplyChanges method during processing of its ProcessChangeBatch method. Be aware that the form of ApplyChanges that is called must accept a collision conflict resolution policy and an IConflictLogAccess object. This example calls ApplyChanges, specifying a collision conflict resolution policy of ApplicationDefined and passing the in-memory conflict log that was created in BeginSession.

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

Implement SaveConstraintConflict

The change applier calls the SaveConstraintConflict method of the INotifyingChangeApplierTarget2 interface to save temporary conflicts. This example adds the INotifyingChangeApplierTarget2 interface to the class that implements KnowledgeSyncProvider.

class ContactsProviderWithConstraintConflicts : KnowledgeSyncProvider
    , INotifyingChangeApplierTarget
    , INotifyingChangeApplierTarget2
    , IChangeDataRetriever

This example implements SaveConstraintConflict by using the MemoryConflictLog object to save temporary conflicts and by throwing an exception otherwise.

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

Implement TryGetDestinationVersion

The change applier must be able to get the version of a destination item that is in conflict. To supply this, the destination provider implements the TryGetDestinationVersion method of the INotifyingChangeApplierTarget interface. This method is optional when constraint conflicts are not reported; otherwise, it is required.

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

Reporting a Constraint Conflict

Constraint conflicts are detected by the destination provider during the change application phase of synchronization. This example uses change units, so constraint conflicts are reported in the SaveChangeWithChangeUnits method by calling RecordConstraintConflictForItem when a change action of Create causes an identity collision in the contact store.

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

The CanCreateContact method of the contact store that is called in the above example uses a private DetectIndexCollision method to detect whether there is an identity collision caused by the contact to create. An identity collision occurs when the contact to be created contains the same name and phone number fields as an already existing contact.

public bool CanCreateContact(ItemChange itemChange, string[] changeData, out ConstraintConflictReason reason, out SyncId conflictingItemId)
{
    bool canCreate = true;

    // Create a temporary contact and see if its index values conflict with any item already in the contact store.
    Contact newContact = new Contact(changeData);
    canCreate = !DetectIndexCollision(newContact, out conflictingItemId);
    if (!canCreate)
    {
        // An index collision occurred, so report a collision conflict.
        reason = ConstraintConflictReason.Collision;
    }
    else
    {
        // This value won't be used because canCreate is set to true in this case.
        reason = ConstraintConflictReason.Other;
    }

    return canCreate;
}
private bool DetectIndexCollision(Contact newContact, out SyncId conflictingItemId)
{
    bool collision = false;
    conflictingItemId = null;

    // Enumerate the contacts in the list and determine whether any have the same name and phone number
    // as the contact to create.
    IEnumerator<KeyValuePair<SyncId, Contact>> contactEnum = _ContactList.GetEnumerator();
    while (contactEnum.MoveNext())
    {
        if (contactEnum.Current.Value.IsIndexEqual(newContact))
        {
            // A conflicting item exists, so return its item ID and a value that indicates the collision.
            conflictingItemId = contactEnum.Current.Key;
            collision = true;
            break;
        }
    }

    return collision;
}

Setting the Resolution Action for a Constraint Conflict

When the destination provider specifies a collision conflict resolution policy of ApplicationDefined, the application must register a handler for the ItemConstraint event before it starts synchronization.

// Register to receive the ItemConstraint event from both providers. Only the destination provider actually fires
// this event during synchronization.
((KnowledgeSyncProvider)localProvider).DestinationCallbacks.ItemConstraint += new EventHandler<ItemConstraintEventArgs>(HandleItemConstraint);
((KnowledgeSyncProvider)remoteProvider).DestinationCallbacks.ItemConstraint += new EventHandler<ItemConstraintEventArgs>(HandleItemConstraint);

The ItemConstraint event handler determines how the conflict is resolved. This example displays the conflicting items to the user and asks how the conflict should be resolved.

void HandleItemConstraint(Object sender, ItemConstraintEventArgs args)
{
    if (ConstraintConflictReason.Collision == args.ConstraintConflictReason)
    {
        // Display the two items that are in conflict and solicit a resolution from the user.
        Contact srcContact = new Contact((string[])args.SourceChangeData);
        Contact destContact = new Contact((string[])args.DestinationChangeData);
        string msg = "Source change is " + srcContact.ToString() +
                   "\nDestination change is " + destContact.ToString() +
                   "\nClick Yes to rename the source change and apply it." +
                   "\nClick No to rename the destination item and apply the source change." +
                   "\nClick Cancel to delete the destination item and apply the source change.";
        ConstraintConflictDlg ccDlg = new ConstraintConflictDlg(msg);
        ccDlg.ShowDialog();

        // Set the resolution action based on the user's response.
        args.SetResolutionAction(ccDlg.Resolution);
    }
    else 
    {
        args.SetResolutionAction(ConstraintConflictResolutionAction.SaveConflict);
    }
}

Handling Constraint Conflict Resolutions

After the constraint conflict resolution action has been set by the application, the change applier makes any necessary changes to the metadata associated with the resolution, and dispatches a call to the destination provider so that it can apply the change to the destination replica. This example uses change units, so the change applier calls the SaveChangeWithChangeUnits method of the destination provider. The three possible resolutions that were presented to the user by the application are handled in this method.

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

Complete Provider Code

For a complete listing of the provider code used in this document, see Example Code for Reporting and Resolving Constraint Conflicts.

Next Steps

Next, you might want to implement a conflict log that can persist conflicts beyond the end of the synchronization session. For more information about creating a conflict log, see Logging and Managing Conflicts.

See Also

Concepts

Programming Common Standard Custom Provider Tasks
Detecting and Resolving Constraint Conflicts