alt text

Photo by Melanie Deziel on Unsplash

In this blog post, I will share how to create custom personalization and segmentation rules in Sitecore.  Personalization rules are used to create conditional renderings to personalize and control contacts’ experiences. Segmentation rules are used in ListManager to add segments to lists. Also can be used as a condition in Marketing Automation.

Personalization Rules

First, we need to create a new Tag, that will serve as a section in Rule Editor, under /sitecore/system/Settings/Rules/Definitions/Tags:

alt text

Then, we need to create a new Element Folder with the same name under /sitecore/system/Settings/Rules/Definitions/Elements:

alt text

Under this new Element Folder, there is a Default item under Tags, we need to select our custom Tag here. We need to do the same for /sitecore/system/Settings/Rules/Conditional Renderings/Tags/Default. So that we can use this item while content editing. After that, we can start adding new rules under Element Folder:

alt text

Text is what user is going to see when they opened Rule Editor. The brackets contain four comma separated positional parameters:

  1. operatorid, accountname: The name of the property that the parameter sets in the .NET class that implements the condition
  2. StringOperator: Which user interface to activate when user clicks, could be anything available under /sitecore/system/Settings/Rules/Definitions/Macros or could be an empty string
  3. URL paramaters for user interface activated by previous macro parameter, empty in our cases since we are using String Operators
  4. compares to, specific value: The text that is displayed if user does not select any value for the parameter

String Operator (defined under /sitecore/system/Settings/Rules/Definitions/String Operators):

alt text

accountname becomes the parameter in the condition:

public string AccountName { get; set; }

Then we use our parameter name ‘accountname’ that we are going to use for comparison in the code.  We created these conditions for both custom facets and also Sitecore’s own Contact Facets. To be able to use the custom facets, we need to load them in the session. As described here link

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <dataAdapterManager defaultProvider="xconnect">
      <providers>
        <add name="xconnect" type="Sitecore.Analytics.XConnect.DataAccess.XConnectDataAdapterProvider, Sitecore.Analytics.XConnect">
          <facets hint="raw:AddFacet">
            <facet facetKey="YourNewFacetKey"/>
          </facets>
        </add>
      </providers>
    </dataAdapterManager>
  </sitecore>
</configuration>

For Sitecore-defined facets, we don’t need to do this step since they are already included. Here is the example code for the custom facet as a condition:

public class WhenContactAccountNameComparesToCondition<T> : StringOperatorCondition<T> where T : RuleContext
    {
        public string AccountName { get; set; }

        protected override bool Execute(T ruleContext)
        {
            var contact = Tracker.Current?.Contact;

            if (contact == null || contact.IdentificationLevel == ContactIdentificationLevel.Anonymous)
            {
                Log.Info(this.GetType() + ": contact is null or anonymous", this);
                return false;
            }

            var xConnectFacet = Tracker.Current.Contact.GetFacet<Sitecore.Analytics.XConnect.Facets.IXConnectFacets>("XConnectFacets");
            if (xConnectFacet != null)
            {
                if (xConnectFacet.Facets != null && xConnectFacet.Facets.ContainsKey(ExtendedContact.DefaultFacetKey))
                {
                    ExtendedContact extendedContact = xConnectFacet.Facets[ExtendedContact.DefaultFacetKey] as ExtendedContact;
                    if (!string.IsNullOrEmpty(extendedContact?.AccountName))
                    {
                        return this.Compare(extendedContact.AccountName, AccountName);
                    }
                }
            }

            return false;
        }
    }

First, the custom condition needs to inherit StringOperatorCondition and implement Execute function.  In our implementation, if a user has a value for any facet, that means it’s identified so after checking if the contact is null, we needed to add a check for identification.

Accessing custom facets in session

After loading custom facets into the session, we need to access them. As Sitecore describes here link, we used IXConnectFacets to fetch the facets. Since in Sitecore 9.3 and later, legacy facet classes such as IContactPersonalInfo are no longer available. 

So instead of reaching a facet like the following as described here link, we must reach like this:

var xConnectFacet = Tracker.Current.Contact.GetFacet<Sitecore.Analytics.XConnect.Facets.IXConnectFacets>("XConnectFacets");
if (xConnectFacet != null)
{
      if (xConnectFacet.Facets != null && xConnectFacet.Facets.ContainsKey(EmailAddressList.DefaultFacetKey))
      {
         EmailAddressList addressList = xConnectFacet.Facets[EmailAddressList.DefaultFacetKey] as EmailAddressList;
      }
}

Segmentation Rules

We can also create segmentation rules, but they need to be created differently than personalization rules and extend different classes. 

Sitecore already has detailed documentation about how to create one using a custom facet here link

But there are a few things that need to be mentioned that are not included in the documentation.  This example uses string comparison to compare custom facet values:

public class CrmAccountNameComparesToCondition : ICondition, IContactSearchQueryFactory
{
    public string AccountName { get; set; }
    public StringOperationType Comparison { get; set; }

    // Evaluates condition for single contact
    public bool Evaluate(IRuleExecutionContext context)
    {
        var contact = context.Fact<Contact>();

        return Comparison.Evaluate(contact.GetFacet<ExtendedContact>(ExtendedContact.DefaultFacetKey)?.AccountName, AccountName);
    }

    // Evaluates contact in a search context
    // IMPORTANT: Use InteractionsCache() facet rather than contact.Interactions as some search providers do not provide joins.
    public Expression<Func<Contact, bool>> CreateContactSearchQuery(IContactSearchQueryContext context)
    {
        return contact => Comparison.Evaluate(contact.GetFacet<ExtendedContact>(ExtendedContact.DefaultFacetKey).AccountName, AccountName);
    }
}

This example just checks if contact has a value at all:

public class ContactFirstNameNotNullOrEmpty : ICondition, IContactSearchQueryFactory
{
    public bool Evaluate(IRuleExecutionContext context)
    {
        var facet = context.Fact<Contact>().Personal();
        if (facet == null)
        {
            return false;
        }
        return !string.IsNullOrEmpty(facet.FirstName);
    }

    public Expression<Func<Contact, bool>> CreateContactSearchQuery(IContactSearchQueryContext context)
    {
        return (Expression<Func<Contact, bool>>)(contact => contact.Personal().FirstName != null && contact.Personal().FirstName != string.Empty);
    }
}

The first thing to mention here is, Evaluate function is used by MAT, CreateContactSearchQuery function is used by ListManager.

Check interactions

In Sitecore documentation, there is one more check for the search query:

&& contact.InteractionsCache().InteractionCaches.Any()

This code snippet checks if the contact has any interactions. While this might be helpful for some cases, it also means that if we import the contacts from a list, these contacts will not be shown in the List Manager when we segment them, since they do not have interactions yet.

YourLinqIsTooStrongException

If we do not want to use a comparison and rather check if the facet is null or empty, we can use the second code snippet. But as you can see instead of using the following code:

string.IsNullOrEmpty(contact.Personal().FirstName)

we have to keep it simple and use the following:

contact.Personal().FirstName != null && contact.Personal().FirstName != string.Empty

This is because the Sitecore predicate engine is throwing an error ‘YourLinqIsTooStrongException’ in Rule Editor and basically wants us to write simplified code here.

There is a list of methods/operators from Sitecore available here link

PII Sensitive Data

List Manager relies on data being available on the xDB index. For some facets such as Personal Information, there are sensitive data, therefore not available in xDB indexes by default. These are FirstName, LastName, MiddleName, Nickname, and Birthdate. Sitecore marks them as PIISensitive and can be found on Sitecore.XConnect.Collection.Model. 

If we like to see the search results depending on PIISensitive data, we can follow the following documentation from Sitecore link

Note that this setting is different in an on-premise environment. Also, rebuilding the xDB search index is needed.