3

I'm trying to create a marketing automation EXM campaign with custom-made tokens in the e-mails. To this end, we're importing contacts from a CSV into the xDB and reading them with a custom dispatcher and token map.

I followed the steps outlined in the documentation and I've succeeded to get my tokens to show up in the EXM editor and tokens that have standard facets as a datasource are filled correctly. I've also validated imported contacts to have correctly filled facet values. However my tokens that should be filled with those values from my custom facets remain empty.

I strongly suspect this has something to do with my implementation of either the Dispatcher or TokenMap, but both the documentation and my personal experience with this piece of Sitecore are simply not sufficient to come to a conclusion I'm afraid. Can anyone help me out?

For context, below are excerpts from my Facet, DispatchTask and TokenMapper respecively:

[FacetKey(FacetKeys.PolicyHolder)]
public class PolicyHolder : Facet
{
    public const string DefaultFacetKey = FacetKeys.PolicyHolder;

    public string PolicyHolderId { get; set; }
    public string InsertionCapital { get; set; }
    public string InsertionLower { get; set; }
}

public static class MaturityMailFacetExtensions
{
    public static PolicyHolder GetPolicyHolder(this Contact contact)
    {
        return contact.GetFacet<PolicyHolder>();
    }

    public static PersonalInformation GetPolicyHolderPersonalInformation(this Contact contact)
    {
        return contact.GetFacet<PersonalInformation>();
    }
}

public class MarketingAutomationDispatchTask : DispatchTask
{
    private IContactService _contactService;
    public MarketingAutomationDispatchTask(ShortRunningTaskPool taskPool, IRecipientValidator recipientValidator, IContactService contactService, EcmDataProvider dataProvider, ItemUtilExt itemUtil, IEventDataService eventDataService, IDispatchManager dispatchManager, EmailAddressHistoryManager emailAddressHistoryManager, IRecipientManagerFactory recipientManagerFactory, SentMessageManager sentMessageManager)
        : base(taskPool, recipientValidator, contactService, dataProvider, itemUtil, eventDataService, dispatchManager, emailAddressHistoryManager, recipientManagerFactory, sentMessageManager)
    {
        _contactService = contactService;
    }

    protected override IReadOnlyCollection<IEntityLookupResult<Contact>> GetContacts(List<DispatchQueueItem> dispatchQueueItems)
    {
        return _contactService.GetContacts(dispatchQueueItems.Select(x => x.ContactIdentifier),
            FacetKeys.MaturityMailModelFacets);
    }
}

public class MarketingAutomationTokenMap : DefaultRecipientPropertyTokenMap
{
    protected static readonly MethodInfo GetPolicyHolderPersonalInformation = typeof(MaturityMailFacetExtensions).GetMethod(nameof(MaturityMailFacetExtensions.GetPolicyHolderPersonalInformation), new[] { typeof(Contact) });
    protected static readonly MethodInfo GetPolicyHolder = typeof(MaturityMailFacetExtensions).GetMethod(nameof(MaturityMailFacetExtensions.GetPolicyHolder), new[] { typeof(Contact) });

    static MarketingAutomationTokenMap()
    {
        if (TokenBindings == null)
        {
            TokenBindings = new Dictionary<Token, RecipientPropertyTokenBinding>();
        }

        // These work
        var customTokenBinding = RecipientPropertyTokenBinding.Build<PersonalInformation>(new Token(MarketingAutomationTokens.InitialsPolicyHolder), p => p.FirstName, GetPolicyHolderPersonalInformation);
        TokenBindings.Add(customTokenBinding.Token, customTokenBinding);

        customTokenBinding = RecipientPropertyTokenBinding.Build<PersonalInformation>(new Token(MarketingAutomationTokens.LastNamePolicyHolder), p => p.LastName, GetPolicyHolderPersonalInformation);
        TokenBindings.Add(customTokenBinding.Token, customTokenBinding);

        customTokenBinding = RecipientPropertyTokenBinding.Build<PersonalInformation>(new Token(MarketingAutomationTokens.GenderPolicyHolder), p => p.Gender, GetPolicyHolderPersonalInformation);
        TokenBindings.Add(customTokenBinding.Token, customTokenBinding);

        // These don't
        customTokenBinding = RecipientPropertyTokenBinding.Build<PolicyHolder>(new Token(MarketingAutomationTokens.LastNamePrefixCapitalCasePolicyHolder), p => p.InsertionCapital, GetPolicyHolder);
        TokenBindings.Add(customTokenBinding.Token, customTokenBinding);

        customTokenBinding = RecipientPropertyTokenBinding.Build<PolicyHolder>(new Token(MarketingAutomationTokens.LastNamePrefixLowerCasePolicyHolder), p => p.InsertionLower, GetPolicyHolder);
        TokenBindings.Add(customTokenBinding.Token, customTokenBinding);
    }
}

Lastly, the config that puts it all together:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <exm>
            <dispatchTask type="Feature.ExmTokenReplacer.MarketingAutomationDispatchTask, Feature.ExmTokenReplacer" patch:instead="*[@type='Sitecore.EmailCampaign.Cm.Dispatch.DispatchTask, Sitecore.EmailCampaign.Cm']">
                <param ref="exm/dispatchFailedTaskPool"/>
                <param desc="recipientValidator" ref="exm/recipientValidator" />
                <param desc="contactService" ref="exm/contactService" />
                <param desc="dataProvider" ref="exm/dataProvider" />
                <param desc="itemUtil" ref="exm/itemUtil" />
                <param desc="eventDataService" ref="exm/eventDataService" />
                <param desc="dispatchManager" ref="exm/dispatchManager" />
                <param desc="emailAddressHistoryManager" ref="exm/emailAddressHistoryManager" />
                <param desc="recipientManagerFactory" ref="exm/recipientManagerFactory" />
                <param desc="sentHistoryManager" ref="exm/sentHistoryManager" />
            </dispatchTask>
        </exm>
        <recipients>
            <recipientPropertyTokenMap type="Feature.ExmTokenReplacer.MarketingAutomationTokenMap, Feature.ExmTokenReplacer" singleInstance="true" patch:instead="*[@type='Sitecore.Modules.EmailCampaign.Core.Personalization.DefaultRecipientPropertyTokenMap, Sitecore.EmailCampaign']" />
        </recipients>
    </sitecore>
</configuration>

EDIT: As requested, here's the PolicyHolder JSON that's deployed in the IndexWorker and XConnect app_data/model folders:

{
  "Name": "MaturityMailModel",
  "Version": "1.0",
  "References": [
    {
      "Name": "XConnect",
      "Version": "1.0"
    },
    {
      "Name": "Sitecore.XConnect.Collection.Model",
      "Version": "9.0"
    }
  ],
  "Types": {
    "Foundation.MarketingAutomation.Models.Facets.PolicyHolder": {
      "Type": "Facet",
      "BaseType": "Sitecore.XConnect.Facet",
      "ClrType": "Foundation.MarketingAutomation.Models.Facets.PolicyHolder, Foundation.MarketingAutomation.Models, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "Properties": {
        "PolicyHolderId": {
          "Type": "String"
        },
        "InsertionCapital": {
          "Type": "String"
        },
        "InsertionLower": {
          "Type": "String"
        }
      }
    },
    (etc...)
  },
  "Facets": [
    {
      "Target": "Contact",
      "Name": "PolicyHolder",
      "Type": "Foundation.MarketingAutomation.Models.Facets.PolicyHolder"
    },
    (etc...)
  ]
}
  • Hey Mark, did you copy the json files for your model to xconnect ? – Vlad Iobagiu Apr 3 '18 at 14:44
  • do you get an exception on dispatch task like UnknownFacetException? – Vlad Iobagiu Apr 3 '18 at 14:46
  • Hi @VladIobagiu , xConnect is properly configured, no exceptions or missing data there. At some point I had the UnknownFacetException on the DispatchTask, but all the right JSON, DLL and config files are in place now. I've validated the import into xDB, it's the extraction that's giving me headaches :) – Mark-Danney Apr 3 '18 at 14:53
  • 1
    Can you add PolicyHolder facet json which is added on xconnect folder : _xconnect\App_data\Models ? – Vlad Iobagiu Apr 3 '18 at 15:23
  • 2
    @Mark-Danney Have you deployed your custom model to both places according to this documentation? doc.sitecore.net/developers/xp/xconnect/xconnect-model/… . I have a feeling that you missed out search indexer and that's why tokens are not replaced with values... – Peter Procházka Apr 3 '18 at 15:40
0

The solution to this problem in the end was frustratingly simple; name your DefaultFacetKey the same as the class name.

The FacetKeys.PolicyHolder value was "VgzPolicyHolder", but when resolving the TokenMap expression somewhere deep down there's a TypeHelpers class in the xConnect namespace. That tries to get the value of the FacetKey attribute but somehow failed. When it does, it defaults to the classname.

After renaming my facetkey to "PolicyHolder" all was well again.

| improve this answer | |
  • Glad you got this fixed. As a point of information, no one would've been able to figure that out since only snippets and not the entire solution (including constants) were provided. Not saying that to be that guy, but it's one of the reasons why you see us always asking for more code. Glad it's fixed! I think the requirement for naming that facet key the same are mentioned in the doc (doc.sitecore.net/developers/xp/xconnect/xconnect-model/facets/…) but not highlighted in importance. In fact, it's the first sentence. – Pete Navarra Apr 6 '18 at 13:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.