Hacking Sitecore and Life one pipeline at a time!

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!

 

Categorised in: Sitecore, Technology

5 Responses »

  1. Hi Pete, thanks for flagging this out. Your approach sounds good. I was thinking to replace with ‘listManagement.mapContactFacetsOnImport’ pipeline with IContactFacetMapping provider to make things simpler. You would just simply pass it as an argument to contactProcessingProvider in Sitecore.ListManager, the same way operationManager and setManager are passed in. Alternatively you can of course just override the MapContact method itself and pass a custom version of contactProcessingProvider, but then – this seems to be as a reusable piece of code, which can be applied at different places (see further below).
    However, your approach feels more elegant and provides more flexibility as I can simply just add a new processor to support mapping additional facets – with my approach you would have to override “default” ContactFacetMapping provider – feels a bit heavier and clunkier. (just a note, if anyone goes that path)

    One thing, though – there’s also ProcessContactsDataProvider (configured in Sitecore.ListManager.config). This particular provider seems to be better “equipped” as its MapContact method at least maps “Personal” and “Emails\Preferred” facets. The provider is used when creating contacts in the List Manager UI. Unfortunately it’s pretty hard-coded to the default options of creating contacts with First Name, Last Name and email address set. Having your solution implemented here would allow to support customization of the UI as well.

    I also notice you’re not concerned with mapping of any other facets, besides your custom ones – is that just for the sake of brevity, here?

    One last thing – did you report this to Sitecore and if so, is there a hotfix?

    Thanks a lot,
    Martin

    Liked by 1 person

    • Big Comment, thanks for the feedback. There’s probably a number of ways this can be fixed, but I felt the pipelined approach provided some neat flexibility, plus I like pipelines. The ContactFacetMapper provider still needs to know about custom facets, but it might work just fine.

      So the ProcessContactsDataProvider is only called for a specific use case. When using the CSV import in ListManager, Sitecore doesn’t call that provider and instead calls only the ContactProcessingProvider, which could also be a bug, I don’t know. That is why I target this provider directly in this blog post. But you still have to account for the custom facets anyways, and that particular provider is a little harder to override.

      Yeah, so the default facets were less of a concern, and so I only highlighted the custom facets. You can easily add in a processor to handle any other types of facets, including the OOTB facets. My vision for that pipeline is that you’d probably configure 1 processor per facet to keep things tidy, which matches other patterns that Sitecore employs elsewhere.

      I did report this to Sitecore twice over the last two years, and have not seen a hotfix come out. However, that might be because in Sitecore 9, this entire process was overhauled and the issue no longer exists. In Sitecore 9, the XConnectContactImporter.Import() method handles existing Contacts correctly and uses the FacetMapper like you’d expect.

      Liked by 1 person

      • Thnx, Pete for taking time to read my reply and elaborate further.

        We started to use Sitecore 9 and I must say (as u pointed out) – things are much better implemented and the whole Contact “story” is finally (mostly) working as you would expect it.

        Cheers,
        Martin

        Liked by 1 person

  2. Hi Pete,

    after some back and forth conversation with Sitecore support, they came back that this is by design. Their official response: “List Manager import was designed for initial import of contacts only, but not for continuously update of the existing contact facets. List Manager import always overrides the contact with all facets, and you have to create your custom code to manipulate or / and add the facets incrementally.” and they ain’t budging.
    This I find a bit strange, as in the world of martech, the integration between different systems is a key and Sitecore shouldn’t assume that it’s the ultimate owner/source of the data, but rather an ecosystem player. In our experience, there are lots of integration that involves manual (re-)import of updated data. Another good example is pre-processing or buying your contact data using 3rd party tools.

    Cheers,
    Martin

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 1,237 other subscribers

Blog Stats

  • 138,854 hits
Follow Sitecore Hacker on WordPress.com
Sitecore® and Own the Experience® are registered trademarks of Sitecore Corporation A/S in the U.S. and other countries.  This website is independent of Sitecore Corporation, and is not affiliated with or sponsored by Sitecore Corporation.