Personalization using Sitecore Rules

The most compelling reason to use Sitecore over any other CMS in my opinion is personalization. Very personalized experiences can be achieved describing your customers in terms of rule conditions and creating rules that target that customers particular traits. For example, if you are building a site for a company that supplies website hosting for a monthly fee you can build a rule that looks like the following:
That example uses conditional renderings. The idea here that we making sure not to ask for money unless your due date coming up soon, if the bill is paid up-to-date we recognize that, and their can be scenarios for a credit, passed due, discounts on additional services, and maybe a default that tells something about tips and tricks to with SEO to get more people to their hosted site.  The most impressive part about this is that once the rules are set up, engineering is out of the picture. Designers and business folks can tinker with the rules and observe the results on a daily basis no deployments are needed, just push publish!

I have heard complaints that for some pages there are too many places where designers would have to repeat the same rules over and over, but using conditional renderings on each component is not the only way to use Sitecore rule.  In some cases I would suggest swapping out the entire content item based on rules.  In that case I like to set my content tree up like this:

The way this is implemented is the page item has an insert option for a personalized page which is a template derived from the page template and another template "personalizable". That template simply looks like this:
Take notice the field source sets the hideaction property to true.  This is because we are onlt looking for a true or false about weather the item is valid for the current request.  If it is we can choose to swap the page item out for a more appropriate item.  Here is an example implementation (I am using GlassMapper in this example but it is not required:



   1:   
   2:      using System;
   3:      using System.Collections.Generic;
   4:      using System.Linq;
   5:   
   6:      using Glass.Mapper.Sc;
   7:   
   8:      using Sitecore;
   9:      using Sitecore.Data;
  10:      using Sitecore.Data.Fields;
  11:      using Sitecore.Data.Items;
  12:      using Sitecore.Data.Managers;
  13:      using Sitecore.Diagnostics;
  14:      using Sitecore.Rules;
  15:      /// <summary>
  16:      ///     ExtensionMethods for Content Models.
  17:      /// </summary>
  18:      public static class ExtensionMethods
  19:      {
  20:          /// <summary>
  21:          ///     Determines whether the specified model has rules.
  22:          /// </summary>
  23:          /// <param name="model">The model.</param>
  24:          /// <param name="context">The Sitecore context.</param>
  25:          /// <returns><c>ture</c> if the specified model has rules, otherwise <c>false</c>.</returns>
  26:          public static bool HasAtLeastOnRule([NotNull] this IGlassBase model, [NotNull] ISitecoreContext context)
  27:          {
  28:              if (model == null) throw new ArgumentNullException("model");
  29:              if (context == null) throw new ArgumentNullException("context");
  30:              var ruleList = GetRules(model, context);
  31:              return ruleList != null && ruleList.Rules != null && ruleList.Rules.Any();
  32:          }
  33:   
  34:          /// <summary>
  35:          /// Determines whether the specified model has a rule that is tru.
  36:          /// </summary>
  37:          /// <param name="model">The model.</param>
  38:          /// <param name="context">The context.</param>
  39:          /// <returns></returns>
  40:          public static bool HasARuleThatIsTrue([NotNull] this IGlassBase model, [NotNull] ISitecoreContext context)
  41:          {
  42:              if (model == null) throw new ArgumentNullException("model");
  43:              if (context == null) throw new ArgumentNullException("context");
  44:              var ruleList = GetRules(model, context);
  45:              return HasARuleThatMatches(ruleList);
  46:          }
  47:   
  48:          /// <summary>
  49:          ///     Personalizes the specified context item model by executing rules of children items.
  50:          /// </summary>
  51:          /// <typeparam name="T">Glass mapped type.</typeparam>
  52:          /// <param name="model">The model.</param>
  53:          /// <param name="context">The context.</param>
  54:          /// <returns>The personalized model.</returns>
  55:          [NotNull]
  56:          public static T Personalize<T>([NotNull] this T model, [NotNull]ISitecoreContext context) where T : class, IGlassBase
  57:          {
  58:              if (model == null) throw new ArgumentNullException("model");
  59:              if (context == null) throw new ArgumentNullException("context");
  60:              var item = GetItem(model, context);
  61:              if (item == null || !item.HasChildren) return model;
  62:              var children = GetPersonalizableItems(item);
  63:              var personalizedItem =
  64:                  children.FirstOrDefault(c => HasARuleThatMatches(c.Fields["{13A108E6-FCD1-4E3F-84A9-086469360B88}"]));
  65:              return personalizedItem == null ? model : context.GetItem<T>(personalizedItem.ID.Guid);
  66:          }
  67:   
  68:          /// <summary>
  69:          /// Gets the rules for the item if it has any.
  70:          /// </summary>
  71:          /// <param name="model">The model.</param>
  72:          /// <param name="context">The context.</param>
  73:          /// <returns>Sitecore Rule List.</returns>
  74:          [CanBeNull]
  75:          private static RuleList<RuleContext> GetRules([NotNull] IGlassBase model, [NotNull] ISitecoreService context)
  76:          {
  77:              if (context.Database == null) return null;
  78:              var item = context.Database.GetItem(new ID(model.Id));
  79:              return GetRules(item);
  80:          }
  81:   
  82:          /// <summary>
  83:          ///     Gets the rules for the item.
  84:          /// </summary>
  85:          /// <param name="item">The item.</param>
  86:          /// <returns>Sitecore Rule List.</returns>
  87:          [CanBeNull]
  88:          private static RuleList<RuleContext> GetRules([CanBeNull] BaseItem item)
  89:          {
  90:              if (item == null || item.Fields == null || !item.Fields.Contains("{13A108E6-FCD1-4E3F-84A9-086469360B88}")) return null;
  91:              return GetRules(item.Fields["{13A108E6-FCD1-4E3F-84A9-086469360B88}"]);
  92:          }
  93:   
  94:          /// <summary>
  95:          ///     Gets the rules.
  96:          /// </summary>
  97:          /// <param name="field">The field.</param>
  98:          /// <returns>Sitecore Rule List.</returns>
  99:          [CanBeNull]
 100:          private static RuleList<RuleContext> GetRules([CanBeNull] Field field)
 101:          {
 102:              return field == null ? null : RuleFactory.GetRules<RuleContext>(field);
 103:          }
 104:   
 105:          /// <summary>
 106:          ///     Gets the Sitecore item represented by the glass model.
 107:          /// </summary>
 108:          /// <param name="model">The model.</param>
 109:          /// <param name="context">The context.</param>
 110:          /// <returns>the Sitecore item</returns>
 111:          [CanBeNull]
 112:          private static Item GetItem([NotNull] IGlassBase model, [NotNull] ISitecoreService context)
 113:          {
 114:              return context.Database == null 
 115:                  ? null 
 116:                  : context.Database.GetItem(new ID(model.Id));
 117:          }
 118:   
 119:          /// <summary>
 120:          ///     Gets the personizable items (childen with Rules Field).
 121:          /// </summary>
 122:          /// <param name="item">The item.</param>
 123:          /// <returns>Collection of items.</returns>
 124:          /// <exception cref="System.ArgumentNullException">item</exception>
 125:          [NotNull]
 126:          private static IEnumerable<Item> GetPersonalizableItems(Item item)
 127:          {
 128:              if (item == null) throw new ArgumentNullException("item");
 129:              var templateId = item.TemplateID;
 130:              foreach (var child in item.Children ?? Enumerable.Empty<Item>())
 131:              {
 132:                  if (child == null || child.Fields == null) continue;
 133:                  var template = TemplateManager.GetTemplate(child);
 134:                  if (template == null || !child.Fields.Contains("{13A108E6-FCD1-4E3F-84A9-086469360B88}")) continue;
 135:                  if (template.ID == templateId) yield return child;
 136:                  if (template.BaseIDs != null && template.BaseIDs.Contains(templateId)) yield return child;
 137:              }
 138:          }
 139:   
 140:          /// <summary>
 141:          ///     Determines whether is rule true from the specified rule field.
 142:          /// </summary>
 143:          /// <param name="ruleField">The rule field.</param>
 144:          /// <returns><c>true</c> if rules evaluate to true; otherwise <c>false</c>.</returns>
 145:          private static bool HasARuleThatMatches(Field ruleField)
 146:          {
 147:              var rules = GetRules(ruleField);
 148:              return HasARuleThatMatches(rules);
 149:          }
 150:   
 151:          /// <summary>
 152:          ///     Determines whether is rule true from the specified rule list.
 153:          /// </summary>
 154:          /// <param name="rules">The rule list.</param>
 155:          /// <returns><c>true</c> if rules evaluate to true; otherwise <c>false</c>.</returns>
 156:          private static bool HasARuleThatMatches(RuleList<RuleContext> rules)
 157:          {
 158:              var ruleContext = new RuleContext();
 159:              var stack = new RuleStack();
 160:              if (rules == null || rules.Rules == null) return false;
 161:              // ReSharper disable PossibleNullReferenceException
 162:              foreach (var rule in rules.Rules.Where(rule => rule.Condition != null))
 163:              {
 164:                  try
 165:                  {
 166:                      rule.Condition.Evaluate(ruleContext, stack);
 167:                      if (ruleContext.IsAborted) continue;
 168:                      if (stack.Count <= 0 || !(bool)stack.Pop()) continue;
 169:                      return true;
 170:                  }
 171:                  catch (Exception e)
 172:                  {
 173:                      Log.Error(e.Message, e, typeof(ExtensionMethods));
 174:                  }
 175:              }
 176:              // ReSharper restore PossibleNullReferenceException
 177:              return false;
 178:          }
 179:      }

Then to get this to execute on every page request we would create the following pipeline processor:

   1:   using System;
   2:   
   3:      using Sitecore;
   4:      using Sitecore.Data.Items;
   5:      using Sitecore.Diagnostics;
   6:      using Sitecore.Pipelines;
   7:   
   8:      using Glass.Mapper.Sc;
   9:   
  10:      /// <summary>
  11:      ///     Segments the page level items.
  12:      /// </summary>
  13:      public class PageLevelSegmentationProcessor : ISitecorePipelineProcessor<PipelineArgs>
  14:      {
  15:          /// <summary>
  16:          ///     Processes the specified arguments (or sometimes Sitecore request).
  17:          /// </summary>
  18:          /// <param name="args">The arguments.</param>
  19:          public void Process(PipelineArgs args)
  20:          {
  21:              if (Context.Item == null || Context.PageMode.IsPageEditor || Context.PageMode.IsPreview) return;
  22:              if (this.ItemIsASegmentedItem(Context.Item))
  23:              {
  24:                  Context.Item = null;
  25:                  return;
  26:              }
  27:              this.ExecuteSegmentation();
  28:          }
  29:   
  30:          /// <summary>
  31:          ///     Executes the segmentation pipeline against the current item.
  32:          /// </summary>
  33:          private void ExecuteSegmentation()
  34:          {
  35:              try
  36:              {
  37:                  var context = new SitecoreContext();
  38:                  var page = context.CreateType<PageItem>(Context.Item);
  39:                  var segmented = page.Personalize(context);
  40:                  Context.Item = context.ResolveItem(segmented);
  41:              }
  42:              catch (Exception e)
  43:              {
  44:                  Log.Error(String.Concat("Segmentation failed for item ", Context.Item.ID), e, this);
  45:              }
  46:          }
  47:   
  48:          /// <summary>
  49:          ///     Determines if the current item is a segmented item.
  50:          /// </summary>
  51:          /// <param name="item">The item.</param>
  52:          /// <returns><c>true</c> if the current item is a segmented item, otherwise <c>false</c>.</returns>
  53:          private bool ItemIsASegmentedItem(Item item)
  54:          {
  55:              return item.Fields["Rule"] != null;
  56:          }
  57:      }

Then I put the following config into the Content Delivery server:


   1:  <?xml version="1.0"?>
   2:  <configuration  xmlns:patch="http://www.sitecore.net/xmlconfig/">
   3:    <sitecore>
   4:      <pipelines>
   5:        <httpRequestBegin>
   6:                  <processor type="BissTalk.Pipelines.PageLevelSegmentationProcessor, BissTalk"
   7:                    patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
   8:        </httpRequestBegin>
   9:        <mvc.buildPageDefinition>
  10:          <processor type="BissTalk.Pipelines.PageLevelSegmentationProcessor, BissTalk" patch:before="*" />
  11:        </mvc.buildPageDefinition>
  12:      </pipelines>
  13:    </sitecore>
  14:  </configuration>

Now we can really start doing some wild personalization.  As you do you will discover that very often we will still need to repeat the same sets of rules over and over again.  For example if you look at the above rule "when the balance due amount is greater than 0 and where bill is due in less than 5 days." When you find that you or other designers are repeating the same sets of rules over and over again you should consider creating a rule item that contains a collection of other rules.  Here is how you can do that:


   1:      using System;
   2:      using System.Collections.Concurrent;
   3:      using System.Linq;
   4:      using System.Web;
   5:   
   6:      using Sitecore;
   7:      using Sitecore.Data;
   8:      using Sitecore.Data.Fields;
   9:      using Sitecore.Rules;
  10:      using Sitecore.Rules.Conditions;
  11:   
  12:      /// <summary>
  13:      /// </summary>
  14:      /// <seealso cref="Sitecore.Rules.RuleContext" />
  15:      public class CurrentUserMatches<T> : WhenCondition<T> where T : RuleContext
  16:      {
  17:          /// <summary>
  18:          ///     Gets or sets the item identifier.
  19:          /// </summary>
  20:          /// <value>
  21:          ///     The item identifier.
  22:          /// </value>
  23:          public string ItemId { get; set; }
  24:   
  25:          private ConcurrentDictionary<Guid, bool> Cache
  26:          {
  27:              get
  28:              {
  29:                  const string KEY = "BissTalk.Rules.CurrentUserMatches";
  30:                  if (HttpContext.Current == null) return new ConcurrentDictionary<Guid, bool>();
  31:                  var cache = HttpContext.Current.Items.Contains(KEY)
  32:                                  ? HttpContext.Current.Items[KEY] as ConcurrentDictionary<Guid, bool>
  33:                                  : null;
  34:                  if (cache == null) HttpContext.Current.Items.Add(KEY, cache = new ConcurrentDictionary<Guid, bool>());
  35:                  return cache;
  36:              }
  37:          }
  38:   
  39:          /// <summary>
  40:          ///     Executes the specified rule context.
  41:          /// </summary>
  42:          /// <param name="ruleContext">The rule context.</param>
  43:          /// <returns>
  44:          ///     <c>True</c>, if the condition succeeds, otherwise <c>false</c>.
  45:          /// </returns>
  46:          protected override bool Execute(T ruleContext)
  47:          {
  48:              Guid itemId;
  49:              if (Context.Database == null || !Guid.TryParse(ItemId, out itemId)) return false;
  50:              bool result;
  51:              if (Cache.TryGetValue(itemId, out result)) return result;
  52:              var item = Context.Database.GetItem(new ID(itemId));
  53:              if (item == null || item.Fields["{13A108E6-FCD1-4E3F-84A9-086469360B88}"] == null) return false;
  54:              result = GetConditionResult(ruleContext, item.Fields["{13A108E6-FCD1-4E3F-84A9-086469360B88}"], itemId);
  55:              Cache.TryAdd(itemId, result);
  56:              return result;
  57:          }
  58:   
  59:          /// <summary>
  60:          /// Gets the condition result.
  61:          /// </summary>
  62:          /// <param name="ruleContext">The rule context.</param>
  63:          /// <param name="ruleField">The rule field.</param>
  64:          /// <param name="itemId">The item identifier.</param>
  65:          /// <returns></returns>
  66:          private bool GetConditionResult(RuleContext ruleContext, Field ruleField, Guid itemId)
  67:          {
  68:              var stack = new RuleStack();
  69:              foreach (
  70:                  var rule in RuleFactory.GetRules<RuleContext>(ruleField).Rules.Where(rule => rule.Condition != null))
  71:              {
  72:                  rule.Condition.Evaluate(ruleContext, stack);
  73:                  if (ruleContext.IsAborted) continue;
  74:                  if (stack.Count <= 0 || !(bool)stack.Pop()) continue;
  75:                  Cache.TryAdd(itemId, true);
  76:                  return true;
  77:              }
  78:              return false;
  79:          }
  80:      }

Now I can organize things into reusable sets of rules. Then get really fancy and have rulesets that contain other rulesets The end result can look something like this:

Now go out and do cool things with personalization and have fun!

1 comment

1 comment :

  1. This is really helpful.. great article which I missed to read for almost 2 years :(

    ReplyDelete