Sitecore Experience Platform

Merge contacts

Use the client.MergeContacts() method to merge an existing source contact into an existing target contact. Contact merge is primarily used by the Web Tracker when an anonymous contact with an ongoing session identifies themselves as a known contact.

Contacts are merged by the xConnect service layer after Submit() / SubmitAsync() has been called.

The following example demonstrates how to perform a merge operation:

Note

Marketing Automation enrollments are not merged. However, you can purge the source contact from all plans before merging. For more information, see Marketing Automation Operations API.

using System;
using System.Threading.Tasks;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;

namespace Documentation
{
    public class MergeContacts
    {
        // Async example
        public async void ExampleAsync()
        {
            using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
            {
                try
                {
                    // New known contact
                    var identifers = new ContactIdentifier[] { new ContactIdentifier("twitter", "myrtlesitecore", ContactIdentifierType.Known) };

                    var newContact = new Contact(identifers);

                    client.AddContact(newContact);

                    // Contact must be saved before a merge
                    await client.SubmitAsync();

                    // Reference to existing contact
                    var reference = new Sitecore.XConnect.ContactReference(Guid.Parse("B9814105-1F45-E611-82E6-34E6D7117DCB"));

                    Task<Sitecore.XConnect.Contact> contactTask = client.GetAsync<Sitecore.XConnect.Contact>(reference, new Sitecore.XConnect.ContactExpandOptions() { });

                    var existingContact = await contactTask;

                    // Data copied FROM existingContact TO newContact
                    client.MergeContacts(existingContact, newContact);

                    await client.SubmitAsync();

                }
                catch (XdbExecutionException ex)
                {
                    // Manage exceptions
                }
            }
        }

        // Sync example
        public void ExampleSync()
        {
            using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
            {
                try
                {
                    // New known contact
                    var identifers = new ContactIdentifier[] { new ContactIdentifier("twitter", "myrtlesitecore", ContactIdentifierType.Known) };

                    var newContact = new Contact(identifers);

                    client.AddContact(newContact);

                    // Contact must be saved before a merge
                    client.Submit();

                    // Reference to existing contact
                    var reference = new Sitecore.XConnect.ContactReference(Guid.Parse("B9814105-1F45-E611-82E6-34E6D7117DCB"));

                    Contact existingContact = client.Get<Sitecore.XConnect.Contact>(reference, new Sitecore.XConnect.ContactExpandOptions() { });

                    // Data copied FROM existingContact TO newContact
                    client.MergeContacts(existingContact, newContact);

                    client.Submit();

                }
                catch (XdbExecutionException ex)
                {
                    // Manage exceptions
                }
            }
        }
    }
}

Merge logic

The merge process is divided into two separate calls. The following occurs in the first call:

  • All value facets are copied from the source contact to the target contact except when the facet already exists on the target contact. For example, if the target contact already has PersonalInformation facet, it will not be overridden by the source contact’s PersonalInformation facet.

Note

xOptimization’s TestCombinations facet behaves differently - if an anonymous contact was exposed to a test combination and then identifies themselves partway through the session, the known contact’s test combinations facet will be overwritten.

  • Calculated facet merge handlers are executed.

  • Enrollments for anonymous contacts are abandoned, meaning:

    • The AutomationPlanEnrollmentCache facet is not merged

    • The AutomationPlanExit facet is not merged

  • All facet values are removed from the source contact.

  • All known identifiers are moved from the source contact to the target contact - anonymous identifiers are not moved. Without known identifiers, source contact becomes anonymous.

  • All interactions, including interaction facets, are copied from the source contact to the target contact. Interactions are not removed from the source contact.

  • A MergeInfo facet is added to the source contact with the following property values:

    • MergeDate - Date and time of merge operation

    • Obsolete - will be true

    • SuccessorContactId - the ID of the target contact

  • An anonymous identifier is added to the target contact. The identifier is the ID of the source contact and the source is defined by Sitecore.XConnect.Constants.MergeIdentifierSource

  • Facets and identifiers for the target and source contacts are updated in memory if the operation completes successfully.

At this point, all changes are saved and IDs are assigned to the new, copied interactions by the repository. The following occurs in the second call:

  • An InteractionMergeInfo facet is added to every interaction that belongs to the source contact, and includes the following key information:

    • SuccessorInteractionId - the ID of the interaction that replaces this interaction.

    • SuccessorContactId - the ID of the contact that owns the successor interaction.

    Note

    The InteractionMergeInfo facet is part of a separate merge model named Sitecore.XConnect.Collection.ContactMerge.Model. This model is not part of the core model, and does not exist on clients like Content Delivery and Content Management by default.

Search and data extraction behavior after merge in 9.0 Update 2 and later

The following applies to the obsoleted (source) contact after a merge:

  • Obsolete contacts are not removed from the xDB Collection database. However, obsolete contacts do not have any contact facets or known identifiers.

  • Interactions are not removed from the source contact during a merge. After a merge, there are two copies of the same interactions in the xDB Collection database. One set of copies belongs to the source contact, and one set of copies belongs to the target contact.

Data extraction behaves in the following way:

  • Contact data extraction does return obsoleted contacts.

  • Contact data extraction does return an obsoleted contact’s interactions if expand options are used.

  • Interaction data extraction does not return the same interaction twice.

    • If a merge occurs after interaction data extraction begins, the source contact’s copy of the interaction is returned.

    • If a merge occurs before interaction data extraction begins, the target contact’s copy of the interaction is returned.

  • If two contacts are merged whilst contact data extraction is ongoing, there is a risk that the source contact is returned before the merge occurs and the target contact is returned after the merge occurs. In this scenario, the same facet data is returned twice. If the contacts are merge before data extraction occurs, only the target contact will have facets.

Search and indexing behaves in the following way:

  • Obsolete contacts are only indexed if indexing of anonymous contacts is enabled.

  • Interactions belonging to obsolete contacts are not indexed. Interactions are copied from the source contact to the target contact. The target contact’s copies of the interactions are indexed.

  • Search can return obsolete contacts - for example, searching by last modified date might return some obsolete contacts. To exclude obsolete contacts, search for contacts where the Obsolete property of the MergeInfo facet is set to false.

Merge error logging

In 9.0 Update 2 and later, the entire merge operation batch is written to the xConnect log if one or both of the repository calls fails. The log message includes a list of operations, types, status, and data such as contact and interaction ID:

[Error] Batch operations:
    Operation type: Sitecore.XConnect.Operations.MergeContactsOperation, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.    Batch index: 0. Status: Failed.
    Operation type: Sitecore.XConnect.Service.MergeContactsInvoker+ContactMergeHandlerOperation, Sitecore.XConnect.Service, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null. Batch index: 1. Status: Succeeded.
    Operation type: Sitecore.XConnect.Operations.GetEntityOperation`1[[Sitecore.XConnect.Contact, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.     Batch index: 2. Status: Succeeded.
    Operation type: Sitecore.XConnect.Operations.GetEntityOperation`1[[Sitecore.XConnect.Contact, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.     Batch index: 3. Status: Succeeded.
    Operation type: Sitecore.XConnect.Operations.SetFacetOperation`1[[Sitecore.XConnect.Collection.Model.MergeInfo, Sitecore.XConnect.Collection.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.   Batch index: 4. Status: Succeeded.      Facet key: MergeInfo, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d652b}.
    Operation type: Sitecore.XConnect.Operations.AddContactIdentifierOperation, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.    Batch index: 5. Status: Succeeded.
    Operation type: Sitecore.XConnect.Operations.ClearFacetOperation, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.    Batch index: 6. Status: Succeeded.      Facet key: KeyBehaviorCache, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d652b}.
    Operation type: Sitecore.XConnect.Operations.ClearFacetOperation, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.    Batch index: 7. Status: Succeeded.      Facet key: EngagementMeasures, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d652b}.
    Operation type: Sitecore.XConnect.Operations.ClearFacetOperation, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.    Batch index: 8. Status: Succeeded.      Facet key: InteractionsCache, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d652b}.
    Operation type: Sitecore.XConnect.Service.Operations.CopyInteractionOperation, Sitecore.XConnect.Service, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.   Batch index: 9. Status: Succeeded.      Entity type: Interaction.    Entity ID: {288fa990-1a61-0000-0000-05383f9f1feb}.
    Operation type: Sitecore.XConnect.Service.Operations.SetInteractionMergeInfoOperation, Sitecore.XConnect.Service, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null. Batch index: 10. Status: Failed. Facet key: InteractionMergeInfo, facet target: Interaction. ID - {288fa990-1a61-0000-0000-05383f9db83e}.
    Operation type: Sitecore.XConnect.Service.Operations.CopyFacetOperation`1[[Sitecore.XConnect.Facet, Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect.Service, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.    Batch index: 11. Status: Succeeded.      Facet key: CustomValues, facet target: Interaction. ID - {288fa990-1a61-0000-0000-05383f9f1feb}.
    Operation type: Sitecore.XConnect.Operations.SetFacetOperation`1[[Sitecore.XConnect.Collection.Model.EngagementMeasures, Sitecore.XConnect.Collection.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.   Batch index: 12. Status: Succeeded.      Facet key: EngagementMeasures, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d6535}.
    Operation type: Sitecore.XConnect.Operations.SetFacetOperation`1[[Sitecore.XConnect.Collection.Model.Cache.InteractionsCache, Sitecore.XConnect.Collection.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.   Batch index: 13. Status: Succeeded.      Facet key: InteractionsCache, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d6535}.
    Operation type: Sitecore.XConnect.Operations.SetFacetOperation`1[[Sitecore.XConnect.Collection.Model.KeyBehaviorCache, Sitecore.XConnect.Collection.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Sitecore.XConnect, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.   Batch index: 14. Status: Succeeded.      Facet key: KeyBehaviorCache, facet target: Contact. ID - {288fa990-1a61-0000-0000-05383f9d6535}.
    Operation type: Sitecore.XConnect.Service.IdentifierOperationWithContactEntity, Sitecore.XConnect.Service, Version=0.0.1.0, Culture=neutral, PublicKeyToken=null.   Batch index: 15. Status: Succeeded.

Note

For privacy reasons, contact identifiers are not written to the log.

Custom merge handlers

You must create a calculated facet merge handler for calculated facets, as they may required custom merge logic. Value facets are merged automatically. However, you can create a merge handler for a value facet if that facet requires custom merge logic.

Get source and target contacts

Source and target contacts are linked by an identifier on the target contact and a facet on the source contact. Source contacts are not deleted during a merge.

The following example demonstrates how to retrieve the contact that an source contact was merged into:

using Sitecore.XConnect.Collection.Model;
using System;
using System.Threading.Tasks;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;

namespace Documentation
{
    public class GetTargetContacts
    {
        // Async example
        public async void ExampleAsync()
        {
            using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
            {
                try
                {
                    var sourceContactId = Guid.Parse("77f75056-5d96-4155-93e5-d206214d6a40");

                    // Get source contact - this is an obsolete contact that has been used
                    // in a merge operation
                    Task<Contact> contactTask = client.GetAsync<Contact>(new ContactReference(sourceContactId), new ContactExpandOptions());

                    Contact contact = await contactTask;

                    // Get the merge facet - you do not need to request this facet as part of the expand options
                    var mergeFacet = contact.GetFacet<MergeInfo>(MergeInfo.DefaultFacetKey);

                    if (mergeFacet != null)
                    {
                        // If the merge facet exists, use the SuccessorContactId to retrieve the target contact that
                        // the source contact was merged into
                        var targetContactTask = client.GetAsync<Contact>(new ContactReference(mergeFacet.SuccessorContactId), new ContactExpandOptions());

                        var targetContact = await targetContactTask;
                    }
                }
                catch (XdbExecutionException ex)
                {
                    // Manage exceptions
                }
            }
        }

        // Sync example
        public void ExampleSync()
        {
            using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
            {
                try
                {
                    var sourceContactId = Guid.Parse("77f75056-5d96-4155-93e5-d206214d6a40");

                    // Get source contact - this is an obsolete contact that has been used
                    // in a merge operation
                    Contact contact = client.Get<Contact>(new ContactReference(sourceContactId), new ContactExpandOptions());

                    // Get the merge facet - you do not need to request this facet as part of the expand options
                    var mergeFacet = contact.GetFacet<MergeInfo>(MergeInfo.DefaultFacetKey);

                    if (mergeFacet != null)
                    {
                        // If the merge facet exists, use the SuccessorContactId to retrieve the target contact that
                        // the source contact was merged into
                        var targetContact = client.GetAsync<Contact>(new ContactReference(mergeFacet.SuccessorContactId), new ContactExpandOptions());
                    }
                }
                catch (XdbExecutionException ex)
                {
                    // Manage exceptions
                }
            }
        }
    }
}

The following example demonstrates how to retrieve the contact or contacts that have previously been used as the source contact in a merge operation for a particular contact:

using Sitecore.XConnect.Collection.Model;
using Sitecore.XConnect.Operations;
using System;
using System.Threading.Tasks;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using System.Linq;
using System.Collections.Generic;

namespace Documentation
{
    public class GetMergedContacts
    {
        // Async example
        public async void ExampleAsync()
        {
            using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
            {
                try
                {
                    // Get contact
                    Task<Contact> contactTask = client.GetAsync<Contact>(new IdentifiedContactReference("twitter", "myrtlesitecore"), new ContactExpandOptions());

                    Contact contact = await contactTask;

                    // Get all identifiers pointing to source contacts that were merged into this contact
                    var mergeIdentifiers = contact.Identifiers.Where(x => x.Source == Sitecore.XConnect.Constants.MergeIdentifierSource);

                    if (mergeIdentifiers.Any())
                    {
                        // Build a list of ContactReference objects from the merge identifiers - the identifier
                        // string is the ID of a source contact
                        var mergedContactReferences = new List<ContactReference>();

                        foreach (var mergeIdentifier in mergeIdentifiers)
                        {
                            Guid id = Guid.Empty;

                            if (Guid.TryParse(mergeIdentifier.Identifier, out id))
                            {
                                ContactReference reference = new ContactReference(id);

                                mergedContactReferences.Add(reference);
                            }
                        }

                        // Get all contacts that were previously merged into this contact
                        var contactsTask = client.GetAsync<Contact>(mergedContactReferences.ToArray(), new ContactExpandOptions());

                        var contacts = await contactsTask;
                    }

                }
                catch (XdbExecutionException ex)
                {
                    // Manage exceptions
                }
            }
        }

        // Sync example
        public void ExampleSync()
        {
            using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
            {

                try
                {
                    // Get contact
                    Contact contact = client.Get<Contact>(new IdentifiedContactReference("twitter", "myrtlesitecore"), new ContactExpandOptions());

                    // Get all identifiers pointing to source contacts that were merged into this contact
                    var mergeIdentifiers = contact.Identifiers.Where(x => x.Source == Sitecore.XConnect.Constants.MergeIdentifierSource);

                    if (mergeIdentifiers.Any())
                    {
                        // Build a list of ContactReference objects from the merge identifiers - the identifier
                        // string is the ID of a source contact
                        var mergedContactReferences = new List<ContactReference>();

                        foreach (var mergeIdentifier in mergeIdentifiers)
                        {
                            Guid id = Guid.Empty;

                            if (Guid.TryParse(mergeIdentifier.Identifier, out id))
                            {
                                ContactReference reference = new ContactReference(id);

                                mergedContactReferences.Add(reference);
                            }
                        }

                        // Get all contacts that were previously merged into this contact
                        var contacts = client.Get<Contact>(mergedContactReferences.ToArray(), new ContactExpandOptions());
                    }

                }
                catch (XdbExecutionException ex)
                {
                    // Manage exceptions
                }
            }
        }
    }
}

Restrictions and recommended practices

The following restrictions apply to the contact merge operation:

  • If a contact has already been used as a source - in other words, it is anonymous and has a MergeInfo facet - it cannot be used as the source contact in another merge operation. You will get an exception.

  • Both contacts must be saved to xConnect before - this means that you must call client.SubmitAsync() before attempting to merge. You cannot mix AddContact and Merge in the same batch.

  • You cannot add an identifier to the source contact in the same batch a performing a merge. If you need to add an identifier to the source contact, you must submit that batch and perform the merge in a subsequent batch.

  • You cannot merge a known contact (source) into an anonymous contact (target) - the known contact must be the target. You can, however, merge known into known and anonymous into anonymous.

Be wary of creating long chains of merged contacts. This can happen if you continually merge known into known or anonymous into anonymous:

bad-merge1.png