Sitecore Email Experience Manager (EXM), Importing Contacts and Custom Facets – Part 3

In Sitecore Email Experience Manager (EXM), Importing Contacts and Custom Facets – Part 1, I showed you how you can import contacts from a CSV file into a list in Sitecore. In Sitecore Email Experience Manager (EXM), Importing Contacts and Custom Facets – Part 2, I demonstrated how you can add to the list of out of the box supported fields when importing contacts into Sitecore. If you haven’t read these as yet, best to start with them first.

In Sitecore Email Experience Manager (EXM), Importing Contacts and Custom Facets – Part 3, I’m going to share with you how you can create your own custom facets and populate them for your contacts as you import them.

To do this, you are going to need to complete the following tasks:

  • Create a custom facet that will describe the additional data you wish to store for your contacts
  • Build a model for your custom facet that will be used by Xdb to store your custom facet data
  • Serialize your custom model and publish it so that Xdb will recognise your model
  • Create a custom Import mapper class that will be used by the Import process to read your custom fields from the import data and write it to your custom facet
  • Update the Import map configuration so that you can map the fields of your CSV file on import
  • Create the required Sitecore configuration settings for your facet and facet import mapper
  • Deploy the updated/new files to Sitecore and XConnect

Create a custom facet

We are going to store Employee information for each of our contacts. To support that, the first thing we are going to need to do is to create our own custom Facet.

Using Sitecore Helix principles and guidelines, I’ve created a new Feature project in Visual Studio that will contain all the custom code to support our custom EXM requirements. In this project, I’ve started with adding a new Facet class called EmployeeInfo:

using System;
using Sitecore.XConnect;

namespace Aceik.Feature.EXM.Models
{
    public class EmployeeInfo : Facet
    {
        public const string DefaultFacetKey = "EmployeeInfo";
        public string EmployeeId { get; set; }
        public string Department { get; set; }
        public DateTime? StartDate { get; set; }
        public string EmployeeType { get; set; }
    }
}

My EmployeeInfo facet, contains 4 properties that I want to be able to populate for my contacts:

  • EmployeeId
  • Department
  • StartDate
  • EmployeeType

Build a model for your custom Facet

The next thing I need to do is to create an XdbModel for my custom Facet. This is required to be deployed into our XConnect instance and will allow XConnect to know about our custom Facet and be able to store the Facet details for each contact.

using Sitecore.XConnect;
using Sitecore.XConnect.Schema;

namespace Aceik.Feature.EXM.Models
{
    public class EmployeeModel
    {
        public static XdbModel Model { get; } = EmployeeModel.BuildModel();

        private static XdbModel BuildModel()
        {
            XdbModelBuilder modelBuilder = new XdbModelBuilder("EmployeeModel", new XdbModelVersion(1, 0));

            modelBuilder.ReferenceModel(Sitecore.XConnect.Collection.Model.CollectionModel.Model);
            modelBuilder.DefineFacet<Contact, EmployeeInfo>(EmployeeInfo.DefaultFacetKey);

            return modelBuilder.BuildModel();
        }
    }
}

Note: Pay attention to the XdbModelBuilder parameters. This instructs the model with our custom Facet details.

We now need to run a build on our new project in Visual Studio so that the assembly can be generated with our classes.

Serlialize your custom model

XConnect requires a JSON representation of your XdbModel. We will now create a console application that can be used to generated the required JSON file.

First, Add a new project to your solution in Visual Studio, selecting to use the Console App template.

You will need to add a reference to your Feature project into the Console application as well as Nuget reference to the Sitecore.XConnect package.

Update the program.cs file to look like below:

using Sitecore.XConnect.Serialization;
using System.IO;

namespace Aceik.Feature.EXM.Serialize
{
    class Program
    {
        static void Main(string[] args)
        {
            // File name will be "Aceik.Feature.Mail.Models.EmployeeModel, 1.0.json
            var json = XdbModelWriter.Serialize(Models.EmployeeModel.Model);
            File.WriteAllText("Aceik.Feature.EXM.Models." + Models.EmployeeModel.Model.FullName + ".json", json);
        }
    }
}

Build and execute the console application.

As per the code comments, this will generate a file called “Aceik.Feature.Mail.Models.EmployeeModel, 1.0.json”. This will vary based on your custom model name and versioning you assigned in your XdbModel.

Note: If after deploying your model to XConnect, you need to update it, you will need to update the XdbModel version and deploy the updated JSON file before XConnect will recognise the new properties of your custom facet.

Create a custom import mapper

When a contact is imported via CSV, various ImportMapper classes are executed to process the contact and to populate the various Facets with the data from the CSV.

To populate our custom Facet, we need to create our own custom ImportMapper class to do this.

To process the contacts employee fields, I created my custom import mapper class called EmployeeInfoFacetMapper.

using System;
using System.Globalization;
using Sitecore.Diagnostics;
using Sitecore.ListManagement.Import;
using Sitecore.ListManagement.XConnect.Web.Import;
using Sitecore.XConnect;
using Aceik.Feature.EXM.Models;

namespace Aceik.Feature.EXM.Import
{
    public class EmployeeInfoFacetMapper : IFacetMapper
    {
        public EmployeeInfoFacetMapper() : this(EmployeeInfo.DefaultFacetKey)
        {
        }

        public EmployeeInfoFacetMapper(string facetName)
        {
            Assert.ArgumentNotNull(facetName, nameof(facetName));
            this.FacetName = facetName;
        }

        public string FacetName { get; }

        public MappingResult Map(
          string facetKey,
          Facet source,
          ContactMappingInfo mappings,
          string[] data)
        {
            if (facetKey != this.FacetName)
                return new NoMatch(facetKey);

            var facet = source as EmployeeInfo ?? new EmployeeInfo();

            // EmployeeInfo Properties
            facet.EmployeeId = SetTextField(FacetName, mappings, data, "EmployeeId");
            facet.Department = SetTextField(FacetName, mappings, data, "Department");
            facet.StartDate = SetDateTimeField(FacetName, mappings, data, "StartDate", "dd/MM/yyyy");
            facet.EmployeeType = SetTextField(FacetName, mappings, data, "EmployeeType");

            return new FacetMapped(facetKey, facet);
        }

        private string SetTextField(string facetName, ContactMappingInfo mappings, string[] data, string fieldName) => mappings.GetValue($"{facetName}_{fieldName}", data);

        private DateTime? SetDateTimeField(string facetName, ContactMappingInfo mappings, string[] data, string fieldName, string dateFormat)
        {
            DateTime? fieldValue = null;
            var fieldText = mappings.GetValue($"{facetName}_{fieldName}", data);
            DateTime fieldDate;

            if (DateTime.TryParseExact(fieldText, dateFormat, new CultureInfo("en-AU"), DateTimeStyles.None, out fieldDate))
                fieldValue = fieldDate.ToUniversalTime();

            return fieldValue;
        }
    }
}

This class is instantiated for each contact. First, it verifies that the data being passed to it is for the EmployeeInfo facet. Once that is confirmed, it populates the EmployeeInfo facet properties from the CSV data that is passed into it.

As you can see, I’ve used a couple of private methods to extract and set the string and DateTime values passed in.

Notice that when extracting the values from the mappings data passed in, it uses a pattern to extract the data for the correct field: “{facetName}_{fieldName}”. This pattern matches the Import mappings that we will setup in the next step.

Import Mapping Fields

As per Part 2, we need to configure the custom Import Mapping fields. Once again, switch to the Core database in Sitecore and navigate to this path: “/sitecore/client/Applications/List Manager/Dialogs/ImportWizardDialog/PageSettings/TabControl Parameters/Map/ImportModel”.

Select one of the existing mapping fields and duplicate it giving it a name of “Employee Info – Employee Id”. Populate the FieldName and DataField fields as per below. Note that the DataField value is the value used in the Import Mapper classes to identify the field so these need to be carefully assigned to match your custom Import Mapper class.

Do the same 3 more times to create the custom import mapping fields for:

  • Employee Info – Department
  • Employee Info – Start Date
  • Employee Info – Employee Type

Create the Sitecore configuration

In your project, you will need to create configuration files for the following purposes:

  • Patch the ListManagement.Import.FacetsToMap setting so that it includes your custom Facet name
  • Add your custom FacetMapper to the list of Import Facet Mappers defined in Sitecore
  • Add your schema to the list of schemas defined for XConnect
  • Add your projects path to the Layer configuration so that your configuration files are recognized

In my EXM Helix project, I’ve added the following configuration files:

  • ListManagement.config
  • sc.Aceik.Feature.EXM.Models.EmployeeModel.xml
  • Layers.config
ListManagement.config
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <settings>
      <setting name="ListManagement.Import.FacetsToMap" set:value="Emails|Personal|Addresses|EmployeeInfo" />
    </settings>

    <import>
      <facetMapper type="Sitecore.ListManagement.XConnect.Web.Import.CompositeFacetMapperCollection, Sitecore.ListManagement.XConnect.Web">
        <param resolve="true" type="Sitecore.Abstractions.BaseLog, Sitecore.Kernel"/>
        <facetMappers hint="list:Add">
          <facetMapper type="Aceik.Feature.EXM.Import.EmployeeInfoFacetMapper, Aceik.Feature.EXM" />
        </facetMappers>
      </facetMapper>
    </import>

    <xconnect>
      <runtime type="Sitecore.XConnect.Client.Configuration.RuntimeModelConfiguration,Sitecore.XConnect.Client.Configuration">
        <schemas hint="list:AddModelConfiguration">
          <!-- value of 'name' property must be unique -->
          <schema name="employeemodel" type="Sitecore.XConnect.Client.Configuration.StaticModelConfiguration,Sitecore.XConnect.Client.Configuration" patch:after="schema[@name='collectionmodel']">
            <param desc="modeltype">Aceik.Feature.EXM.Models.EmployeeModel, Aceik.Feature.EXM</param>
          </schema>
        </schemas>
      </runtime>
    </xconnect>

  </sitecore>
</configuration>
sc.Aceik.Feature.EXM.Models.EmployeeModel.xml
<?xml version="1.0" encoding="utf-8"?>
<Settings>
  <Sitecore>
    <XConnect>
      <Services>
        <XConnect.Client.Configuration>
          <Options>
            <Models>
              <EmployeeModel>
                <TypeName>Aceik.Feature.EXM.Models.EmployeeModel, Aceik.Feature.EXM</TypeName>
                <PropertyName>Model</PropertyName>
              </EmployeeModel>
            </Models>
          </Options>
        </XConnect.Client.Configuration>
      </Services>
    </XConnect>
  </Sitecore>
</Settings>
Layers.config
<?xml version="1.0" encoding="utf-8"?>
<layers>
  <layer name="Sitecore" includeFolder="/App_Config/Sitecore/">
    <loadOrder>
      <add path="ContentSearch.Azure" type="Folder" />
      <add path="Reporting" type="Folder" />
      <add path="Marketing.Xdb.Sql.Common" type="Folder" />
      <add path="CMS.Core" type="Folder" />
      <add path="AntiCSRFModule" type="Folder" />
      <add path="Contact.Enrichment.Services.Client" type="Folder" />
      <add path="ContentSearch" type="Folder" />
      <add path="Buckets" type="Folder" />
      <add path="DeviceDetection.Client" type="Folder" />
      <add path="DetectionServices.Location" type="Folder" />
      <add path="ItemWebApi" type="Folder" />
      <add path="Owin.Authentication" type="Folder" />
      <add path="Owin.Authentication.IdentityServer" type="Folder" />
      <add path="TransientFaultHandling" type="Folder" />
      <add path="Messaging" type="Folder" />
      <add path="Processing.Tasks.Messaging.Xmgmt" type="Folder" />
      <add path="Update" type="Folder" />
      <add path="XConnect.Client.Configuration" type="Folder" />
      <add path="Marketing.Xdb.MarketingAutomation.Operations" type="Folder" />
      <add path="Marketing.Xdb.MarketingAutomation.Reporting" type="Folder" />
      <add path="Marketing.Xdb.ReferenceData.Core" type="Folder" />
      <add path="Marketing.Xdb.ReferenceData.Client" type="Folder" />
      <add path="Marketing.Operations.xMgmt" type="Folder" />
      <add path="Marketing.Segmentation.xMgmt" type="Folder" />
      <add path="Marketing.Xdb.MarketingAutomation.Locators" type="Folder" />
      <add path="Marketing.Xdb.ReferenceData.Service" type="Folder" />
      <add path="Marketing.Xdb.ReferenceData.SqlServer" type="Folder" />
      <add path="Marketing.Operations.Xdb.ReferenceData" type="Folder" />
      <add path="Marketing.xDB" type="Folder" />
      <add path="Mvc" type="Folder" />
      <add path="Services.Client" type="Folder" />
      <add path="ExperienceContentManagement.Administration" type="Folder" />
      <add path="Speak.Integration" type="Folder" />
      <add path="Marketing.Tracking" type="Folder" />
      <add path="Tracking.Web.MVC" type="Folder" />
      <add path="Tracking.Web.RobotDetection" type="Folder" />
      <add path="Marketing.Assets" type="Folder" />
      <add path="Marketing.Xdb.MarketingAutomation.Tracking" type="Folder" />
      <add path="SPEAK" type="Folder" />
      <add path="Speak.Applications" type="Folder" />
      <add path="LaunchPad" type="Folder" />
      <add path="Experience Editor" type="Folder" />
      <add path="ContentTagging" type="Folder" />
      <add path="ListManagement" type="Folder" />
      <add path="Marketing.Client" type="Folder" />
      <add path="MVC.ExperienceEditor" type="Folder" />
      <add path="MVC.DeviceSimulator" type="Folder" />
      <add path="Personalization" type="Folder" />
      <add path="ContentTesting" type="Folder" />
      <add path="ExperienceProfile" type="Folder" />
      <add path="ExperienceExplorer" type="Folder" />
      <add path="SPEAK.Components" type="Folder" />
      <add path="ExperienceAnalytics" type="Folder" />
      <add path="CampaignCreator" type="Folder" />
      <add path="ExperienceForms" type="Folder" />
      <add path="ExperienceForms" type="Folder" />
      <add path="FederatedExperienceManager" type="Folder" />
      <add path="Marketing.Automation.Client" type="Folder" />
      <add path="Marketing.Automation.ActivityDescriptors.Client" type="Folder" />
      <add path="EmailExperience" type="Folder" />
      <add path="PathAnalyzer" type="Folder" />
      <add path="UpdateCenter" type="Folder" />
    </loadOrder>
  </layer>
  <layer name="Modules" includeFolder="/App_Config/Modules/" />
  <layer name="Custom" includeFolder="/App_Config/Include/">
    <loadOrder>
      <add path="Foundation" type="Folder" />
      <add path="Feature" type="Folder" />
      <add path="Project" type="Folder" />
    </loadOrder>
  </layer>

  <layer name="Aceik" includeFolder="/App_Config/Aceik/">
    <loadOrder>
      <add path="Foundation" type="Folder" />
      <add path="Feature" type="Folder" />
      <add path="Project" type="Folder" />
    </loadOrder>
  </layer>

  <layer name="Environment" includeFolder="/App_Config/Environment/" />
</layers>

Deploy to Sitecore and XConnect

Deploy the EXM helix project to Sitecore using your usual method. In my case, I used the publish option in Visual Studio and deployed the files to the website folder for my Sitecore instance.

To deploy the files to XConnect, this was not so simple. I manually deployed the following files to these locations in my XConnect instance:

  • sc.Aceik.Feature.EXM.Models.EmployeeModel.xml => \App_Data\jobs\continuous\AutomationEngine\App_Data\Config\sitecore
  • Aceik.Feature.EXM.dll => \App_Data\jobs\continuous\AutomationEngine
  • Aceik.Feature.EXM.Models.EmployeeModel, 1.0.json => \App_Data\Models

After deploying the files to XConnect, you will need to go and restart the XConnect services:

  • Marketing Automation Service
  • Processing Engine Service
  • Index Worker

Test the Import

Now that you have deployed the code and configuration changes to both Sitecore and XConnect, applied the Import Mapper configuration changes, you can now import your “Employees” from a CSV file. I have amended the CSV used in the Part 2 adding in the additional Employee specific columns:

email, firstname, middlename, lastname, gender, jobtitle, address1, address2, city, postcode, state, country,Employee ID,Department,Start Date,Employee Type
alberte@gmail.com,Albert,Basil,Einstein,Male,Scientist,Level 12,100 New York Lane, New York,25438, NY, US,FT0195,Science,23/01/1869,Fulltime
blaise@gmail.com,Blaise,Patricia,Pascal,Male,Mathematician,125 The Pond,,Paris,7321AE,PA, FR,CA0125,Mathematics,08/10/1915,Casual
cherschel@yahoo.com,Caroline,Jenny,Herschel,Female,Astronomer,34 The Strait,,Hanover,123BCF,Lower Saxony, DE,PT4567,Astronomy,05/05/1979,Permament Part-time
dorothyhodgkin@live.com,Dorothy,Anne,Hodgkin,Female,Chemist,87 Downing Street,,Westminister,SW1A,London,UK,FT0099,Science,13/08/1940,Fulltime
edmond145@facebook.com,Edmond,William,Halley,Male,Astronomer,114 ChittingHam Way,,Greenwich,SW1A,Kent,UK,CA0087,Astronomy,01/10/1985,Casual

I then imported these “Employees” using the same process as previously, with the only difference being that I now mapped the additional Employee fields.

Import from CSV file
Imported Employees

As you can see the Employees imported in the same manner as previously. Now, when I open the details of one of the Employees, I see no difference to before.

Contact Details

In the last and final part of this series, I’ll show you how to update the Experience Profile with a new Tab that you can use to display your custom Facet details. Click here for Part 4.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

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

Google photo

You are commenting using your Google 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 )

Connecting to %s

%d bloggers like this: