Working with Reusable Rules in Web Rule
Web Rule comes with a powerful feature that allows you to reuse any evaluation type rule in any other rule as if it were a simple field of System.Boolean type.
Imagine if your organization used a simple condition of Age is greater or equal to 21 in hundreds of its business rules. Obviously, if the value of 21 changes tomorrow, your organization would need to edit, test and re-deploy all those rules with the new value. Having that condition as a reusable rule allows for much greater flexibility because any change to that rule is instantly adapted by all other rules that may use it.
For example, consider the following source object:
using System;
using CodeEffects.Rule.Attributes;
namespace TestProject
{
public class Patient
{
public string Name { get; set; }
public Gender Gender { get; set; }
[Field(DisplayName = "Date of Birth",
DateTimeFormat = "MMMM dd, yyyy")]
public DateTime? DOB { get; set; }
[Method("Age", "Returns age of the patient")]
public int GetAge()
{
if(this.DOB == null) throw new Exception("DOB is not set");
else return DateTime.Now.Year - ((DateTime)this.DOB).Year;
}
// C-tor
public Patient()
{
this.Gender = Site.Gender.Unknown;
}
}
public enum Gender
{
Male,
Female,
[ExcludeFromEvaluation]
Unknown
}
}
If we were to register Web Rule on a web page with this Patient class as a source object, we could create an evaluation type rule that checks if the patient is 21 or older, name that rule "Legal Age" and save it. Later, we could reuse that rule in other rules as if it were a field of bool type called "Legal Age". Let's go through this process, step by step. First, let's create that Legal Age rule using the Rule Editor:

Then let's save that rule. In ASP.NET, we need to handle the RuleEditor.SaveRule event that is raised every time the rule author clicks the Save button (not shown on the above screenshot):
private void SaveRule(object sender, SaveEventArgs e)
{
if(this.ruleControl.IsEmpty || !this.ruleControl.IsValid)
{
this.lblInfo.Text = "The rule is either empty or invalid.";
return;
}
Storage.SaveRule(e);
Storage.AddRuleToCollection(
this.ruleControl.ToolBarRules, e.RuleID, e.RuleName, e.RuleDescription);
Storage.SortRules(this.ruleControl.ToolBarRules);
if (e.IsEvaluationTypeRule)
Storage.AddRuleToCollection(
this.ruleControl.ContextMenuRules,
e.RuleID, e.RuleName, e.RuleDescription);
this.lblInfo.Text = "Rule was saved successfully";
}
The Storage.AddRuleToCollection static method used in the above sample looks like this:
public static void AddRuleToCollection(ICollection<MenuItem> rules,
string id, string name, string description)
{
// Create a new collection if the param is null
if (rules == null) rules = new List<MenuItem>();
// Check if the rule already exists in the collection
MenuItem item = ((List<MenuItem>)rules)
.Find(delegate(MenuItem el) { return el.ID == id; });
// Create a new rule or update the existing one
if (item == null)
rules.Add(new MenuItem(id, name, description));
else
{
item.DisplayName = name;
item.Description = description;
}
}
The code samples are part of the Default.aspx page from the ASP.NET demo project. Download and run this project to see the full implementation of reusable rules. In MVC, the same process looks like this (the MVC demo project is downloadable from the same link):
// "ruleEditor" is the ID of the RuleEditor instance declared in the view.
// The name of the action parameter must match that ID, even though RuleEditor
// and RuleModel are two different types. This is MVC's default behavior.
[HttpPost]
public ActionResult Save(RuleModel ruleEditor)
{
// At this point the rule model doesn't know which type to use as its source object
// We need to "bind" the source type to the rule model
ruleEditor.BindSource(typeof(Patient));
// Add the rule model to the ViewBag object
ViewBag.Rule = ruleEditor;
// Make sure that the Rule Area is not empty and the current rule is valid
if (ruleEditor.IsEmpty() || !ruleEditor.IsValid())
{
ViewBag.Message = "The rule is empty or invalid";
return View("Index");
}
try
{
// Save the rule
this.storage.SaveRule(ruleEditor.Id, ruleEditor.GetRuleXml());
// Get all rules for Tool Bar and context menus and save it in the bag
this.LoadMenuRules();
ViewBag.Message = "The rule was saved successfully";
}
catch (InvalidOperationException ex)
{
ViewBag.Message = ex.Message;
}
return View("Index");
}
In both cases, we added our new rule to the collection of items in the Rules menu and fields context menu. That allows us to select the new rule from the Rules menu for editing (more about adding items to the Rules menu can be found in the Toolbar topic)...

... or select it from the context menu to use as a field in any other new or existing rule:

Notice that the Help String displays the description of our rule every time the mouse hovers over its name in either menu. It does so because we added that description in the very first step above. This feature is helpful if there are many items in the menu or if multiple people work on the same project.
So, now we can reuse our Legal Age rule in any other rule that uses the same source object:

This powerful feature allows you to easily encapsulate common logic into separate rules and reuse them across your entire organization.
IMPORTANT! If used carelessly, reusable rules can lead to a nasty thing called "circular references". Imagine the following three business rules:
-
Rule # 1
Check if Name is "John" and Rule # 3 is False
-
Rule # 2
Check if Email contains ".com" and Rule # 1 is True
-
Rule # 3
Check if Zip code is "12345" or Rule # 2 is True
It's easy to see the circular references in these rules. What makes it worse is the fact that rule author cannot see that by using Rule # 1 in the Rule # 2 (s)he actually creates a recursion.
Web Rule comes equipped with excellent features that help developers to avoid recursions. For instance, it does not allow rule authors to reuse the rule inside of itself while this rule is being edited - the Rule Editor simply won't display rule's name in the context menu even if developer adds it to the menu's item collection. Web Rule can also check for circular references during the rule validation. But it does this only if it's able to get Rule XML of the referenced rule and check it for inner references of the rules that it has examined so far. You can force Web Rule to perform such check by supplying it the GetRuleDelegate - a method that gets a rule ID and returns rule's XML. You do that differently for each pattern.
In ASP.NET, set the value of RuleEditor.GetRuleDelegate:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.ruleControl.GetRuleDelegate = Storage.LoadRuleXml;
}
In MVC, pass a GetRuleDelegate to the RuleModel.IsValid() method:
[HttpPost]
public ActionResult Save(RuleModel ruleEditor)
{
ruleEditor.BindSource(typeof(Patient));
// Make sure that the Rule Area is not empty and the current rule is valid
if(ruleEditor.IsEmpty() || !ruleEditor.IsValid(StorageService.LoadRuleXml))
{
ViewBag.Message = "The rule is empty or invalid";
return View("Index");
}
// The rest of the action is omitted
}
(Both code samples were taken from our demo projects.)
In both cases Web Rule will perform the recursion check during the rule validation because it knows how to get the referenced rule if it finds one in the current rule. Web Rule checks not only the initial rule but all referenced rules, rules referenced in those referenced rules, and so on. This is done by calling the same routine recursively. Don't supply the delegate if you absolutely sure that the current rule does not reference any other rules.
If recursion is detected, the entire initial rule is marked invalid and returned back to the client.
|