Issues syncing contacts from Dynamics To XConnect

Dynamics 2

When working on a project that aimed to sync contacts back and forth between Sitecore and Dynamics, we came across some issues syncing back and forth. This article will highlight those issues and show the approach we used to overcome them.

Full code showing the changes below in detail can be found in the Sitecore to Dynamics repository on github.

Issue 1: Dynamics => XConnect - Existing Contact

During the import, the out of the box pipeline tries to find an existing contact using “Dynamics.ContactId” identifier. Contacts intially created in Sitecore wont have this identifier, so nothing is found and a new Contact is created, even if they have the same email as another.

Solution

Override ResolveXConnectContactByIdentifierStepProcessor.FindExistingObject() and add logic to try and find the contact using email, if first approach didnt work.

In addition to this, we need to manually add the “Dynamics.ContactId” to the existing contact. Without that identifier, when we sync the contact back to Dynamics, it will create a new record if its missing.

protected override object FindExistingObject(...)
{
    var contact = base.FindExistingObject(client, clientHelper, identifierValue, pipelineStep, pipelineContext, logger);

    if (contact == null)
    {
        ...

        try
        {
            var dynamicsId = ReadDynamicsId(plugin,pipelineStep, pipelineContext, logger);
            var email = ReadEmail(plugin, pipelineStep, pipelineContext, logger);
            var reference = new IdentifiedContactReference("website", email);

            Contact contact2 = client.Get<Sitecore.XConnect.Contact>(reference, new ContactExecutionOptions(contactExpandOptions));
            if (contact2 != null && !string.IsNullOrWhiteSpace(dynamicsId))
            {
                ContactIdentifier dynamicsIdIdentifier = new ContactIdentifier("Dynamics.ContactId", dynamicsId, ContactIdentifierType.Known);

                var checkExistsReference = new IdentifiedContactReference("Dynamics.ContactId", dynamicsId);
                Contact checkExistsContact = client.Get<Sitecore.XConnect.Contact>(checkExistsReference, new ContactExecutionOptions(contactExpandOptions));

                if (checkExistsContact == null)
                {
                    client.AddContactIdentifier(contact2, dynamicsIdIdentifier);
                }
            }
            contact = contact2;
        }
        catch (XdbExecutionException ex)
        {
...
        }
    }
    return contact;
}

NB. If a new contact is synced to dynamics, without having ‘being imported over’. Then it creates a new contact, but also back fills the “Dynamics.ContactId” identifier with the value of the new contact record.

Issue 2: Dynamics => XConnect - New Contact

If a contact is imported from Dynamics using the OTB process with email aaa@aaa.com. Then a new contact comes to the website and IdentifysAs() using the same email address. Then a duplicate contact with the same email address is created. This is because the the import only adds “Dynamics.ContactId” identifier.

Solution

You need to manually add any identifiers that you would normally use within your site. In our case, we needed to manually add a new “website“ identifier to every new contact that is created in xDB as a result of the Dynamics import:

[source:”website”, value:”aaa@aaa.com”, ContactIdentifierType.Known]

Then, when the IdentifysAs() method is called, it searches for any contact with the same source and value. If found, then a merge process begins. Without that identifier multiple Contacts are created.

public override object CreateNewObject(...)
{
    ...

    var identifiers = new List<ContactIdentifier>();
    ...
    ContactIdentifier item = new ContactIdentifier(identifierSource, identifierValue, contactIdentifierType);

    if (item != null) identifiers.Add(item);
    ...
    var email = ReadEmail(plugin, pipelineStep, pipelineContext, logger);
    ContactIdentifier emailIdentifier = new ContactIdentifier("website", email, ContactIdentifierType.Known);

    if (emailIdentifier != null) identifiers.Add(emailIdentifier);


    return new ContactModel
    {
        ContactIdentifiers = identifiers
    };
}
...
protected virtual string ReadEmail(...)
{
    XConnectContactIdentifierSettings xConnectContactIdentifierSettings = pipelineStep.GetPlugin<XConnectContactIdentifierSettings>();
    object fromPipelineContext = this.GetObjectFromPipelineContext(xConnectContactIdentifierSettings.ContactIdentificationLevelObjectLocation, pipelineContext, logger);
    if (fromPipelineContext != null)
    {
        var emailValueAccessor = plugin.EmailValueAccessor;
        if (emailValueAccessor != null)
        {
            IValueReader valueReader = emailValueAccessor.ValueReader;
            if (valueReader != null)
            {
                DataAccessContext context = new DataAccessContext();
                ReadResult readResult = valueReader.Read(fromPipelineContext, context);
                if (readResult != null && readResult.WasValueRead)
                {
                    return readResult.ReadValue as string;
                }
            }
        }
    }

    return null;
}
protected virtual string ReadDynamicsId(...)
{
    XConnectContactIdentifierSettings xConnectContactIdentifierSettings = pipelineStep.GetPlugin<XConnectContactIdentifierSettings>();
    object fromPipelineContext = this.GetObjectFromPipelineContext(xConnectContactIdentifierSettings.ContactIdentificationLevelObjectLocation, pipelineContext, logger);
    if (fromPipelineContext != null)
    {
        var dynamicsIdValueAccessor = plugin.DynamicsIdValueAccessor;
        if (dynamicsIdValueAccessor != null)
        {
            IValueReader valueReader = dynamicsIdValueAccessor.ValueReader;
            if (valueReader != null)
            {
                DataAccessContext context = new DataAccessContext();
                ReadResult readResult = valueReader.Read(fromPipelineContext, context);
                if (readResult != null && readResult.WasValueRead)
                {
                    var result = readResult.ReadValue.ToString(); ;
                    return result;
                }
            }
        }
    }

    return null;
}

To support the above, you will also need to create a supporting custom converter as below:

namespace XXX.PipelineSteps
{
    [SupportedIds(new string[] { "{TEMPLATE IF OF CustomResolveXConnectContactStepPROCESSOR }" })]
    public class CustomResolveXConnectContactStepConverter : ResolveXConnectContactByIdentifierStepConverter
    {
        public const string EmailValueAccessor = "EmailValueAccessor";
        public const string DynamicsIdValueAccessor = "DynamicsIdValueAccessor"; 

        public CustomResolveXConnectContactStepConverter(IItemModelRepository repository) : base(repository)
        {
        }

        protected override void AddPlugins(ItemModel source, PipelineStep pipelineStep)
        {
            base.AddPlugins(source, pipelineStep);

            ContactFallbackResolverSettings fallbackPlugin = new ContactFallbackResolverSettings()
            {
                EmailValueAccessor = this.ConvertReferenceToModel<IValueAccessor>(source, EmailValueAccessor),
                DynamicsIdValueAccessor = this.ConvertReferenceToModel<IValueAccessor>(source, DynamicsIdValueAccessor),
            };
            pipelineStep.AddPlugin<ContactFallbackResolverSettings>(fallbackPlugin);

        }
    }
}
public class ContactFallbackResolverSettings : Sitecore.DataExchange.IPlugin
{
    public Sitecore.DataExchange.DataAccess.IValueAccessor EmailValueAccessor { get; set; }
    public Sitecore.DataExchange.DataAccess.IValueAccessor DynamicsIdValueAccessor { get; set; }
}

Then make the following changes

  1. Create new pipeline step template - using Resolve Contact by Identifier from xConnect Pipeline step as base template:
    /sitecore/templates/Data Exchange/Providers/xConnect/Pipeline Steps/Extended Resolve Contact by Identifier from xConnect Pipeline Step
  2. Add two new fields:
    1. EmailValueAccessor | Droptree
    2. DynamicsIdValueAccessor ! Droptree
  3. Duplicate Resolve Contact Model by Dynamics Id from xConnect and Change its template type to the new one you just created (by doing it this way you keep all values)
    /sitecore/system/Data Exchange/POC/Pipelines/Dynamics Contacts to xConnect Sync Pipelines/Process Single Contact from Dynamics Pipeline/Resolve Contact Model by Dynamics Id from xConnect
  4. Populate EmailValueAccessor: Email Address on Dynamics Contact
    /sitecore/system/Data Exchange/POC/Data Access/Value Accessor Sets/Providers/Dynamics/Dynamics Contact/Email Address on Dynamics Contact
  5. Populate DynamicsIdValueAccessor: Contact Id on Dynamics Contact
    /sitecore/system/Data Exchange/POC/Data Access/Value Accessor Sets/Providers/Dynamics/Dynamics Contact/Contact Id on Dynamics Contact
  6. Finally update Converter Type and Processor Type to use the new classes that we created.
    i.e.XXX.PipelineSteps.CustomResolveXConnectContactStepConverter, XXX XXX.PipelineSteps.CustomResolveXConnectContactStepProcessor, XXX
  7. Run the pipeline batch (clear last run to get all records)
  8. Debug - you can stick break points in each of the new functions to see whats happening

Issue 3: XConnect => Dynamics - New Contact

When syncing new contacts from XConnect to Dynamics, the OTB process uses the “Dynamics.ContactId” identifier to find an existing contact in Dynamics. If the contact has recently been created in Sitecore, then it wont have that identifier. So a new contact will be created in Dynamics, even if a contact already exists there with the same details.

Solution

If the OTB process fails to find a contact, conduct a secondary search using which ever details your business rules dictate (i.e. search by first name + last name + email).

Override the ResolveEntityStepProcessor:

public override object FindExistingObject(string identifierValue, PipelineStep pipelineStep, PipelineContext pipelineContext, ILogger logger)
{
    Entity entity;
    entity = (Entity)base.FindExistingObject(identifierValue, pipelineStep, pipelineContext, logger);

    if (entity == null)
    {
        ...
        var email = ReadEmail(plugin, pipelineStep, pipelineContext, logger);
        var firstName = ReadFirstName(plugin, pipelineStep, pipelineContext, logger);
        var lastName = ReadLastName(plugin, pipelineStep, pipelineContext, logger);
        ...
        Endpoint endpoint = base.GetEndpoint(pipelineStep, pipelineContext, logger);
        string crmConnectionString = endpoint.GetPlugin<OrganizationEndpointSettings>().CrmConnectionString;
        CrmServiceClient crmServiceClient = new CrmServiceClient(crmConnectionString);
        Sitecore.DataExchange.Providers.DynamicsCrm.ResolveEntities.ResolveEntitySettings resolveEntitySettings = GetResolveEntitySettings(pipelineStep, pipelineContext, logger);
        ColumnSet columnSet = (resolveEntitySettings.ReadAllAttributes ? new ColumnSet(allColumns: true) : new ColumnSet(resolveEntitySettings.AttributesToRead.ToArray()));

        if (!columnSet.Columns.Contains("firstname")) columnSet.Columns.Add("firstname");
        if (!columnSet.Columns.Contains("lastname")) columnSet.Columns.Add("lastname");
        if (!columnSet.Columns.Contains("emailaddress1")) columnSet.Columns.Add("emailaddress1");

        QueryExpression query = new QueryExpression("contact")
        {
            ColumnSet = columnSet,
            Criteria = new FilterExpression(LogicalOperator.And)
            {
                Conditions =
                {
                    new ConditionExpression("emailaddress1", ConditionOperator.Equal, email),
                    new ConditionExpression("firstname", ConditionOperator.Equal, firstName),
                    new ConditionExpression("lastname", ConditionOperator.Equal, lastName)
                }
            }
        };
        try
        {
            EntityCollection results = crmServiceClient.RetrieveMultiple(query);
            if (results.Entities.Count > 0)
            {
                entity = results.Entities.First();
            }
            else
            {
                entity = null;
            }
        }
        catch (Exception)
        {
            entity = null;
        }
        base.SetRepositoryStatusSettings((entity != null) ? RepositoryObjectStatus.Exists : RepositoryObjectStatus.DoesNotExist, pipelineContext);
    }
    return entity;
}

Leave a Reply

Your email address will not be published. Required fields are marked *