Merge contacts
From Sitecore version 10.1, the contact merge logic has changed. Profile scores merge from the source to target based on the latest SourceInteractionStartDateTime
. See Contact behavior profile.
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:
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 ContactExecutionOptions(new 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 ContactExecutionOptions(new 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’sPersonalInformation
facet.
Experience Optimization’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 betrue
-
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.
NoteThe
InteractionMergeInfo
facet is part of a separate merge model namedSitecore.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 theMergeInfo
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.
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 ContactExecutionOptions(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 ContactExecutionOptions(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 ContactExecutionOptions(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 ContactExecutionOptions(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 ContactExecutionOptions(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 ContactExecutionOptions(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 ContactExecutionOptions(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 ContactExecutionOptions(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 mixAddContact
andMerge
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.
-
You cannot merge an anonymous contact (source) into an anonymous contact (target) if the target contact is already merged into other contact. If you attempt this operation, X-Connect returns an exception to prevent circular merges between anonymous contacts.
We highly recommend running the contact merge operation as a single operation per batch. This process ensures there are no other operations that retrieve or modify any contact used in the merge operation. It also prevents inappropriate usage of merge contacts API that might lead to circular merges between contacts.
Be wary of creating long chains of merged contacts. This can happen if you continually merge known into known or anonymous into anonymous: