LibrarySites.Banner

Validate Fields with the Rules Engine of the Sitecore ASP.NET CMS

This blog post describes a prototype for a solution that you can use to validate field values using the rules engine in the Sitecore ASP.NET web Content Management System (CMS) and Customer Engagement Platform (CEP). This post combines two of my favorite Sitecore infrastructure components: validation and the rules engine.

To use the rules engine to validate fields, we need a class that represents a field validation processing context to pass through the rules engine. This will expose the ID of the field and the value of the field through the FieldID and FieldValue properties, and also provides a ValidationMessage for text to show the user. The setter for the FieldValue property expands deltas (such as layout deltas) before validation.

namespace Sitecore.Sharedsource.Rules.Validators
{
  using System;
  
  using S = Sitecore; // for @adc_sitecore and @cassidydotdk
  
  public class FieldValidatorsRuleContext : S.Rules.Validators.ValidatorsRuleContext
  {
    private string fieldValue;
  
    public FieldValidatorsRuleContext(
      S.Data.Items.Item item,
      S.Data.ID fieldId,
      S.Data.Validators.BaseValidator validator)
    {
      this.Item = item;
      this.FieldID = fieldId;
      this.Validator = validator;
      this.Result = S.Data.Validators.ValidatorResult.Valid;
      this.Text = string.Empty;
    }
  
    public S.Data.ID FieldID { get; set; }
  
    public string ValidationMessage { get; set; }
  
    public string FieldValue
    {
      get
      {
        return this.fieldValue;
      }
  
      set
      {
        S.Diagnostics.Assert.IsNotNull(this.Item, "Item");
        S.Diagnostics.Assert.IsNotNull(this.FieldID, "FieldID");
  
        // expand layout deltas
        if (this.Item.Template.StandardValues != null
          && this.Item.ID != this.Item.Template.StandardValues.ID
          && S.Xml.Patch.XmlPatchUtils.IsXmlPatch(value))
        {
          this.fieldValue = S.Data.Fields.XmlDeltas.ApplyDelta(
            this.Item.Fields[this.FieldID].GetStandardValue(),
            value);
        }
        else
        {
          this.fieldValue = value;
        }
      }
    }
  }
}

Next we need the action to set the validation result. It might be possible to reuse the action implemented previously for item validation rules for field validation rules, but I modified the code to support the ValidationMessage property in the rules context.

namespace Sitecore.Sharedsource.Rules.Validators.Actions
{
  using System;
  
  using S = Sitecore;
  
  public class SetValidatorResult<T> : S.Rules.Actions.RuleAction<T>
    where T : S.Rules.Validators.ValidatorsRuleContext
  {
    public string ValidatorResult { get; set; }
  
    public string Text { get; set; }
  
    public override void Apply(T ruleContext)
    {
      S.Diagnostics.Assert.ArgumentNotNull(ruleContext, "ruleContext");
      S.Diagnostics.Assert.IsNotNullOrEmpty(
        this.ValidatorResult,
        "ValidatorResult");
      S.Data.Items.Item resultItem = ruleContext.Item.Database.GetItem(
        this.ValidatorResult);
      S.Diagnostics.Assert.IsNotNull(resultItem, "resultItem");
      S.Data.Validators.ValidatorResult resultValue = (S.Data.Validators.ValidatorResult)Enum.Parse(
        typeof(S.Data.Validators.ValidatorResult), 
        resultItem.Name, 
        true);
  
      if (ruleContext.Result < resultValue)
      {
        ruleContext.Result = resultValue;
      }
  
      S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext context =
        ruleContext as S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext;
  
      if (!string.IsNullOrEmpty(this.Text))
      {
        ruleContext.Text += "\n" + this.Text;
      }
      else if (context != null && !string.IsNullOrEmpty(context.ValidationMessage))
      {
        ruleContext.Text += "\n" + context.ValidationMessage;
      }
      else if (ruleContext.Parameters["Validation Message"] != null)
      {
        ruleContext.Text += "\n" + ruleContext.Parameters["Validation Message"];
      }
      else
      {
        ruleContext.Text = "\nFailed rules-based validation";
      }
  
      if (!string.IsNullOrEmpty(ruleContext.Text))
      {
        ruleContext.Text = ruleContext.Text.TrimStart('\n');
      }
    }
  }
}

To register the action in the Content Editor:

  1. Navigate to the /sitecore/system/Settings/Rules/Validation Rules/Actions item (create folders as needed)
  2. Insert an item named Set Validation Result using the System/Rules/Action data template.
  3. Enter the following in the Text field in the Data section:
    set the validation result to [validatorresult,validatorresult,,specific result]
  4. Enter the type signature of the action in the Type field in the Script section, for example:
    Sitecore.Sharedsource.Rules.Validators.Actions.SetValidatorResult,Sitecore.Sharedsource

The action actually exposes a Text property for hard-coding validation message. To use that, change the text in the action definition item to:

set the validation result to [validatorresult,validatorresult,,specific result] with message [text,,,value]

For the CMS user to select a validation result for the action, this solution uses the macro developed for item validation with the rules engine (linked in the Resources section at the end of this page).

We need a field validator that can instantiate a rules context and invoke the rules. Some notes about this logic follow a little more explanation in this post.

namespace Sitecore.Sharedsource.Data.Validators.FieldValidators
{
  using System;
  using System.Runtime.Serialization;
 
  using S = Sitecore;
 
  [Serializable]
  public class FieldRulesValidator : S.Data.Validators.StandardValidator
  {
    private S.Data.Items.Item item;
 
    private S.Data.Fields.Field field;
 
    public FieldRulesValidator(
      SerializationInfo info,
      StreamingContext context)
      : base(info, context)
    {
    }
 
    public FieldRulesValidator()
    {
    }
 
    public override string Name
    {
      get
      {
        return "Field Validation Rules Validator";
      }
    }
 
    protected override S.Data.Validators.ValidatorResult Evaluate()
    {
      this.item = this.GetItem();
      S.Diagnostics.Assert.IsNotNull(this.item, "item");
      this.field = this.GetField();
 
      if (this.field == null)
      {
        S.Diagnostics.Log.Warn(
          this + " : field " + this.FieldID + " does not exist in " + this.GetItem().Paths.FullPath,
          this);
        return S.Data.Validators.ValidatorResult.Valid;
      }
 
      S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext> rules =
        new S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>();
      this.AddRules(this.GetFieldRules(), rules);
      this.AddRules(this.GetFieldTypeRules(), rules);
      this.AddRules(this.GetFieldTypeChildRules(), rules);
      this.AddRules(this.GetValidatorRules(), rules);
      this.AddRules(this.GetValidatorChildRules(), rules);
 
      if (rules.Count < 1)
      {
        return S.Data.Validators.ValidatorResult.Valid;
      }
 
      S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext ruleContext =
        new S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext(this.item, this.field.ID, this);
 
      // in a Sitecore user interface, validate the potentially-unsaved value
      // otherwise validate the saved value
      ruleContext.FieldValue = this.ControlValidationValue;
 
      if (string.IsNullOrEmpty(this.ControlToValidate))
      {
        ruleContext.FieldValue = this.item[this.FieldID];
      }
 
      rules.Run(ruleContext);
 
      if (ruleContext.Result == S.Data.Validators.ValidatorResult.Valid)
      {
        return ruleContext.Result;
      }
 
      this.Text = this.GetText(
        ruleContext.Text,
        new string[] { this.Name });
      return this.GetFailedResult(ruleContext.Result);
    }
 
    // return CriticalError or FatalError or the Page Editor will not invoke this validator
    protected override S.Data.Validators.ValidatorResult GetMaxValidatorResult()
    {
      return this.GetFailedResult(S.Data.Validators.ValidatorResult.FatalError);
    }
 
    // two arguments of the same type; be careful with argument order!
    private void AddRules(
      S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext> addThese,
      S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext> toThose)
    {
      if (addThese != null && addThese.Count > 0)
      {
        toThose.AddRange(addThese.Rules);
      }
    }
 
    // rules defined by the Rule field in the definition item for the field type
    // (the grandchild of the data template)
    private S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext> GetFieldRules()
    {
      S.Data.Items.Item fieldItem = this.item.Database.GetItem(
        this.field.ID);
      S.Diagnostics.Assert.IsNotNull(fieldItem, "fieldItem");
 
      if (fieldItem.Fields["Rule"] != null
        && !string.IsNullOrEmpty(fieldItem.Fields["Rule"].Value))
      {
        return S.Rules.RuleFactory.GetRules<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>(
          fieldItem.Fields["Rule"]);
      }
 
      return null;
    }
 
    // rules defined by the validation rules item for the field type
    // /sitecore/system/Settings/Validation Rules/Field Types/<field type>
    private S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext> GetFieldTypeRules()
    {
      S.Data.Items.Item fieldType = this.item.Database.GetItem(
        "/sitecore/system/Settings/Validation Rules/Field Types/" + this.GetField().TypeKey);
 
      if (fieldType != null
        && fieldType.Fields["Rule"] != null
        && !string.IsNullOrEmpty(fieldType.Fields["Rule"].Value))
      {
        return S.Rules.RuleFactory.GetRules<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>(
          fieldType.Fields["Rule"]);
      }
 
      return null;
    }
 
    // rules defined by the children of the validation rules item for the field type
    // /sitecore/system/Settings/Validation Rules/Field Types/<field type>
    private S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext> GetFieldTypeChildRules()
    {
      S.Data.Items.Item fieldType = this.item.Database.GetItem(
        "/sitecore/system/Settings/Validation Rules/Field Types/" + this.GetField().TypeKey);
 
      if (fieldType != null
        && fieldType.HasChildren)
      {
        return S.Rules.RuleFactory.GetRules<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>(
          fieldType,
          "Rule");
      }
 
      return null;
    }
 
    // rules defined by the Rule field of the validator definition item
    // /sitecore/system/Settings/Validation Rules/Field Rules/Rules Engine Rules
    private S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>
      GetValidatorRules()
    {
      S.Diagnostics.Assert.ArgumentNotNull(this.ValidatorID, "ValidatorID");
      S.Data.Items.Item validatorItem = this.item.Database.GetItem(this.ValidatorID);
      S.Diagnostics.Assert.IsNotNull(validatorItem, "validatorItem");
 
      if (validatorItem.Fields["Rule"] != null)
      {
        S.Rules.RuleList<S.Rules.Validators.ValidatorsRuleContext> rules =
          new S.Rules.RuleList<S.Rules.Validators.ValidatorsRuleContext>();
        return S.Rules.RuleFactory.GetRules<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>(
          validatorItem.Fields["Rule"]);
      }
 
      return null;
    }
 
    // rules defined by the Rule field in the children of the validator definition item
    // /sitecore/system/Settings/Validation Rules/Field Rules/Field Rules
    private S.Rules.RuleList<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>
      GetValidatorChildRules()
    {
      S.Diagnostics.Assert.ArgumentNotNull(this.ValidatorID, "ValidatorID");
      S.Data.Items.Item validatorItem = this.item.Database.GetItem(this.ValidatorID);
      S.Diagnostics.Assert.IsNotNull(validatorItem, "validatorItem");
 
      if (validatorItem.HasChildren)
      {
        S.Rules.RuleList<S.Rules.Validators.ValidatorsRuleContext> rules =
          new S.Rules.RuleList<S.Rules.Validators.ValidatorsRuleContext>();
        return S.Rules.RuleFactory.GetRules<S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext>(
          validatorItem,
          "Rule");
      }
 
      return null;
    }
  }
}

To register the validator in the Content Editor:

  1. Navigate to the /sitecore/system/Settings/Validation Rules/Field Rules item or one of its descendants (for example, create a folder named Custom).
  2. Insert an item named Field Validation Rules using the System/Validation/Validation Rule data template.
  3. Enter Field Validation Rules for the Title field in the Text section.
  4. Enter Apply field validation rules for the Description field in the Text section.
  5. Enter the type signature (namespace.classname,assembly) in the Type field in the Data section, for example:
    Sitecore.Sharedsource.Data.Validators.FieldValidators.FieldRulesValidator,Sitecore.Sharedsource

I think the easiest way to apply field validation rules would be to use a field of type Rules in the field definition items. I created the User Defined/Field Validation Rules data template containing a single field named Rule of type Rules. Because data template fields are one of the few types of items that do not support all of the properties defined by the standard template, I set the base template for this field to the null ID {00000000-0000-0000-0000-000000000000}. I am not sure this is necessary (I have a case open with support), but it could prevent some kind of infinite recursion. Unfortunately, it makes my Rules field appear only when showing standard fields. I then updated base templates for the  System/Templates/Template field template to include the User Defined/Field Validation Rules template.

This validator actually lets you define field validation rules in a number of places:

  • In the field definition item (by adding a field named Rules as described previously).
  • In a validation definition item for the field type. For example, for layout details, this would be the /sitecore/system/Settings/Validation Rules/Field Types/layout item. You may need to create definition items for field types for which no validation exists using the System/Validation/Field Type Validation Rules data template, and you should add a base template to that template to include the field named Rule of type Rules to the System/Templates/Template field data template used for field definition items)
  • In children of that validation definition item, which should contain a field named Rule of type Rules.
  • In the validator definition item (in which case you need to add a base template containing a field named Rule of type Rules to the System/Validation/Validation Rule data template for validator definition items.
  • In children of the validator definition item (those children should contain a field named Rule of type Rules).

Set the Source property of any such Rules field to /sitecore/system/Settings/Rules/Validation Rules so that the relevant conditions and actions will appear when working with these specific fields. There are actually more options, such as defining reusable validation rules in some place, and then selecting those rules in specific field definition items, but this post is already to long and codey.

To create useful field validation rules, we probably need conditions that operate on fields. For example, maybe certain fields should never contain their standard values – we need a condition that checks if the field contains its standard value.

namespace Sitecore.Sharedsource.Rules.Validators.Conditions
{
  using Assert = Sitecore.Diagnostics.Assert;
 
  using S = Sitecore;
 
  public class FieldContainsStandardValue<T> :
    S.Rules.Conditions.OperatorCondition<T>
    where T : S.Sharedsource.Rules.Validators.FieldValidatorsRuleContext
  {
    protected override bool Execute(T ruleContext)
    {
      Assert.ArgumentNotNull(ruleContext, "ruleContext");
      Assert.ArgumentNotNull(ruleContext.Item, "ruleContext.Item");
      Assert.ArgumentNotNull(ruleContext.FieldValue, "ruleContext.FieldValue");
      Assert.ArgumentNotNull(ruleContext.FieldID, "ruleContext.FieldID");
      S.Data.Fields.Field field = ruleContext.Item.Fields[ruleContext.FieldID];
      Assert.IsNotNull(field, "field: " + ruleContext.FieldID);
 
      if (ruleContext.Item.Template.StandardValues == null
        || ruleContext.Item.Template.StandardValues.ID == ruleContext.Item.ID
        || ruleContext.FieldValue != field.GetStandardValue())
      {
        return false;
      }
 
      string message = S.Globalization.Translate.Text(
        "The field {0} cannot contain its standard value.");
      ruleContext.ValidationMessage = string.Format(
        message,
        string.IsNullOrEmpty(field.Title) ? field.Name : field.Title);
      return true;
    }
  }
}

Now we can create the /sitecore/system/Settings/Rules/Validation Rules/Conditions folder and set its insert options to include the System/Rules/Condition data template. Then create an item named Field Contains Standard Value using the System/Rules/Condition data template. Enter when the field contains its standard value for the Text field in the Data section and enter the type signature of the condition in the Type field of the Script section, for example:

Sitecore.Sharedsource.Rules.Validators.Conditions.FieldContainsStandardValue,Sitecore.Sharedsource

Now  we can create field validation rules in any of the ways described earlier. For example, I created a rule in the Sample/Sample Item/Data/Title field:

screen capture of field validaiton rule

Here is the result in the Content Editor:

screen capture of field validation message in the Content Editor

Here is the result in the Page Editor:

screen capture of field validation message in the Page Editor

Remember that the Page Editor only applies validators for which the GetMaxValidatorResult() method returns fatal or critical, and does not validate layout details without some customization (see the following links).

Resources