Bug: Contact Facets Overwritten on CSV Import

Blog Series: Unlocking xDB – Vol: #4  
Referenced Sitecore Version: Sitecore 8.1U3 – 8.2

In this installment of Unlocking xDB, I’m exposing a bug that I have come across during my travels of customizing the Contact Facets while utilizing them through the List Manager’s CSV Import process.

In a nutshell, Sitecore has configured a ContactProcessingProvider whose primary purpose is for mapping contact facets to new contacts coming into the Contact Repository. This method, which is highlighted in the image below has two arguments coming into it: incomingContact and existingContact.

mapcontact

The bug is actually the fact that the method completely ignores the existing contact even though I have revealed through debugging, that an appropriate existing contact does, in fact, get provided to this method. The MapContacts() method is called via the ProcessContacts() method in the ContactProcessingProviderBase. ProcessContacts is the method that is utilized during the Bulk Importer process and is responsible for identifying the number of contacts updated, created, and not imported due to issues.

processcontact

Where this becomes a problem, is that the contactTemplate variable is what is used to create/overwrite a contact. Because the MapContact() method ignores the existing contact, the value of the contact template is that of only the values from the CSV file.

This is why custom contact facet values get overwritten when a CSV is uploaded. This can affect any contact that is uploaded via a CSV which becomes a huge issue for people who rely on this for EXM.

As with anything, there are multiple ways that someone might be able to fix this. In my opinion, because facets can be customized, I wanted to provide a solution that is extendable if other contact facets are utilized. So why not introduce a new pipeline?

The Pipelined Solution

This solution is comprised of the following elements:

  • A new Provider classed based off of ContactProcessingProvider that overrides MapContact().
  • A new pipeline with associated arguments to support the mapping of existing values.
  • A new pipeline processor that is specific to a custom contact facet. In this example, and building off of the previous facet in this series, Marketing Data is used.
  • Last but not least, the patch configuration to enable this new provider and pipeline.

PipelinedContactProcessingProvider

In the following code snippet, I have created a new class called PipelinedContactProcessingProvider that is based on ContactProcessingProvider. Besides the constructor method, this class consists of one overridden method for MapContact.

The workflow I’m using is that if an existing contact is provided through the argument, then call the new pipeline listManagement.mapContactFacetsOnImport passing in the new arguments which are the ContactTemplateDestination, which ultimately becomes the ContactTemplate, and the ExistingContact which is the contact from the existing contact argument.

using Sitecore.Analytics.Tracking;
using Sitecore.ListManagement.Analytics.Data;
using Sitecore.Pipelines;

namespace SitecoreHacker.Sandbox.CustomSitecore.Facets
{
    public class PipelinedContactProcessingProvider : ContactProcessingProvider
    {
        public PipelinedContactProcessingProvider(ContactRepositoryBase contactRepository,
            IContactTemplateFactory contactTemplateFactory,
            BulkOperationManager<IContactTemplate, KnownContactSet, IContactUpdateResult> operationManager,
            IWorkItemSetManager<KnownContactSet, IContactTemplate> setManager)
            : base(contactRepository, contactTemplateFactory, operationManager, setManager)
        {
        }

        protected override IContactTemplate MapContact(IContactTemplate contactSource, Contact existingContact)
        {

            if (existingContact != null)
            {
                var mapContactArgs = new MapContactFacetsOnImportArgs
                {
                    ContactTemplateDestination = contactSource,
                    ExistingContact = existingContact
                };

                //I Made this a Pipeline so that I could extend it in the future without mucking with this method.
                //The thought process is that I would create a processor for each custom facet (or OOTB facet)
                //that I want to carry over
                CorePipeline.Run("listManagement.mapContactFacetsOnImport", mapContactArgs);
                return mapContactArgs.ContactTemplateDestination;
            }

            return contactSource;
        }
    }
}

Map Contact Facets On Import Pipeline Args

This is a pretty simple method here, defining the pipeline args needed to support the new pipeline referenced in the method above.

using Sitecore.Analytics.Model.Entities;
using Sitecore.Analytics.Tracking;
using Sitecore.Pipelines;

namespace SitecoreHacker.Sandbox.CustomSitecore.Pipelines.ListManager.MapContactsOnImport
{
    public class MapContactFacetsOnImportArgs : PipelineArgs
    {
        public Contact ExistingContact { get; set; }
        public IContactTemplate ContactTemplateDestination { get; set; }
    }
}

Example Pipeline Processor – Marketing Data Facet Map

For my example facet, I’m evaluating the value of the incoming contact (source) and checking if the value is null. If the value is null, then this field was not included as part of the CSV import, so I want to use whatever value from the existing contact that might be present (which might include a null value. If the value is NOT null, this is what I consider an updated value and I want to retain it.

I make whatever adjustments are needed, and the values are passed back through the pipeline arguments. If I had additional contact facets (including valuable information that might have been captured and placed into the Out of the Box facets) then I would create additional pipeline processors for those.

using SitecoreHacker.Sandbox.CustomSitecore.Facets;
using Sitecore.Diagnostics;

namespace SitecoreHacker.Sandbox.CustomSitecore.Pipelines.ListManager.MapContactsOnImport
{
    public class MapContactMarketingProfile
    {
        public virtual void Process(MapContactFacetsOnImportArgs args)
        {
            Assert.ArgumentNotNull(args.ExistingContact, "ContactTemplateDestination");
            // This is the Existing Contact Facet
            var facet = args.ExistingContact.GetFacet<IMarketingData>("Marketing Data");

            //This is the Contact Template generated from the CSV Upload, and contains data from the CSV
            var source = args.ContactTemplateDestination.GetFacet<IMarketingData>("Marketing Data");

            //Here, make decisions based on which data to use.  In this example, I'm checking the CSV value.
            //If the CSV value is NULL (meaning there was no mapped field from the uploader), then I automatically use
            //whatever value is in the existing contact facet.
            //If it's not null, in this example, then I'm making the assumption, that I WANT to use the data from the CSV file.
            //You can make other decision points here too, based on business logic.
            source.Channel_Type = source.Channel_Type ?? facet.Channel_Type;
            source.Company_Crm_Id = source.Company_Crm_Id ?? facet.Company_Crm_Id;
            source.Company_Name = source.Company_Name ?? facet.Company_Name;
            source.Crm_ID = source.Crm_ID ?? facet.Crm_ID;
            source.Promotional_Code = source.Promotional_Code ?? facet.Promotional_Code;
            source.Region = source.Region ?? facet.Region;
            source.Type = source.Type ?? facet.Type;
        }
    }
}

Patch Configurations to Enable Solution

I have split this out into two patch configs. One to adjust the Contact Processing provider, and the other to create and enable the new pipeline.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <!-- PATCH THIS CONFIG to Use the Pipelined Contact Processing Provider Above -->
    <!--  CONTACTS PROCESSING PROVIDER           Performs the communication with current data engine when processing contacts.     -->
    <contactProcessingProvider type="SitecoreHacker.Sandbox.CustomSitecore.Facets.PipelinedContactProcessingProvider, SitecoreHacker.Sandbox">
    <!-- <contactProcessingProvider type="Sitecore.ListManagement.Analytics.Data.ContactProcessingProvider, Sitecore.ListManagement.Analytics"> -->
      <param ref="/sitecore/contactRepository" />
      <param ref="/sitecore/model/entities/contact/template" />
      <param name="operationManager" type="Sitecore.Analytics.Data.Bulk.Contact.ContactBulkUpdateManager, Sitecore.Analytics" />
      <param name="setManager" type="Sitecore.Analytics.Data.Bulk.Contact.KnownContactSetManager, Sitecore.Analytics" />
    </contactProcessingProvider>
  </sitecore>
</configuration>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<pipelines>
      			<listManagement.mapContactFacetsOnImport>
        <processor type="SitecoreHacker.Sandbox.CustomSitecore.Pipelines.ListManager.MapContactsOnImport.MapContactMarketingProfile, SitecoreHacker.Sandbox" />
      </listManagement.mapContactFacetsOnImport>
    </pipelines>
	</sitecore>
</configuration>

Summary

As mentioned, there are probably a number of different ways that this problem can be solved. I hope that by highlighting the issue, however, others that may have encountered the same issue I have can find a solid workaround based off of my research. Please feel free to reach out to me if you have identified a better method or find this solution helpful!

 

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s