Fact update question

342 views
Skip to first unread message

Andreas Nilsson

unread,
Feb 17, 2014, 11:53:12 AM2/17/14
to nrules...@googlegroups.com
Hi, i have been trying out this lib for a while and have a question. Its about how to update the facts properly. The structure i use i very much like the One-To-Many relation Order-OrderRow, where Order has a list of OrderRow. When i make changes to the OrderRow object and have rules matching only the Order fact it seems to me like i have to update the Order object in the session to. It seems to me a Little bit strange that when i update a fact i must also beware of how it relates to other entities. Below is a testcase that hopefully proves my Point.
 
Have i gotten this all wrong?
 
Regards Andreas
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NRules.IntegrationTests.TestAssets;
using NUnit.Framework;
namespace NRules.IntegrationTests.TestAssets
{
 public class MyFactType
 {
  public string TestProperty { get; set; }
  public List<FactType1> Children { get; set; }
 }
}
namespace NRules.IntegrationTests.TestRules
{
 public class MyRule : BaseRule
 {
  public override void Define()
  {
   When()
    .Exists<MyFactType>(model =>
     model.Children[0].TestProperty == "true" &&
     model.Children[1].TestProperty == "true" &&
     model.Children[2].TestProperty == "true");
   Then()
    .Do(ctx => Notifier.RuleActivated())
    .Do(ctx => Console.WriteLine("AN3 Fired!"));
  }
 }
}
namespace NRules.IntegrationTests.TestRules
{
 [TestFixture]
 public class MyTest : BaseRuleTestFixture
 {
  [Test]
  public void Test1()
  {
   FactType1 c1 = new FactType1() { TestProperty = "false" };
   FactType1 c2 = new FactType1() { TestProperty = "false" };
   FactType1 c3 = new FactType1() { TestProperty = "false" };
   MyFactType model = new MyFactType() { TestProperty = "Model", Children = new List<FactType1>() { c1, c2, c3 } };
   
   Session.Insert(model);
   Session.Fire();
   AssertDidNotFire<MyRule>();
   c1.TestProperty = "true";
   c2.TestProperty = "true";
   c3.TestProperty = "true";
   Session.Update(model);  // Why does this work?
   Session.Fire();
   AssertFiredOnce<MyRule>();
  }
  [Test]
  public void Test2()
  {
   FactType1 c1 = new FactType1() { TestProperty = "false" };
   FactType1 c2 = new FactType1() { TestProperty = "false" };
   FactType1 c3 = new FactType1() { TestProperty = "false" };
   MyFactType model = new MyFactType() { TestProperty = "Model", Children = new List<FactType1>() { c1, c2, c3 } };
   Session.Insert(model);
   Session.Insert(c1);
   Session.Insert(c2);
   Session.Insert(c3);
   Session.Fire();
   AssertDidNotFire<MyRule>();
   c1.TestProperty = "true";
   c2.TestProperty = "true";
   c3.TestProperty = "true";
   Session.Update(c1);
   Session.Update(c2);
   Session.Update(c3);
   Session.Fire();    // Why does this not work?
   AssertFiredOnce<MyRule>();
  }
  protected override void SetUpRules()
  {
   SetUpRule<MyRule>();
  }
 }
}

Sergiy Nikolayev

unread,
Feb 17, 2014, 7:37:45 PM2/17/14
to nrules...@googlegroups.com
Hi Andreas

To understand why it behaves the way it does, let's clarify what we call a "fact" in rules engine terminology. A fact is an object whose state is stored in rules engine's working memory, and that's used in the rules patterns. Now, in the case you provided, the model (MyFactType) is a "fact" (since the rule matches on that object), but collection of children (FactType1) are not "facts", since they are simply a property on the model. So, in essence, the rules engine is not aware of any one-to-many relationship. It just knows about the "one" part of that "one-to-many", and that is why the update on the parent works, but update on children doesn't (In fact, your tests would pass even if you didn't assert the children at all, since it is the parent that matters).

To make the rules engine aware of the one-to-many relationship, the rules must be expressed in terms of both parent and children. The DSL in the current version of the rules engine may not be flexible enough to express all such scenarios, but here is an example of rewriting the rule in a way that your tests will work as you intended:

    public class MyRule : BaseRule
    {
        public override void Define()
        {
            MyFactType parent = null;
            FactType1 child1 = null;
            FactType1 child2 = null;
            FactType1 child3 = null;

            When()
             .Match<FactType1>(() => child1, x => x.TestProperty == "true")
             .Match<FactType1>(() => child2, x => x.TestProperty == "true")
             .Match<FactType1>(() => child3, x => x.TestProperty == "true")
             .Match<MyFactType>(() => parent, model =>
              model.Children[0] == child1 &&
              model.Children[1] == child2 &&
              model.Children[2] == child3);
            Then()
             .Do(ctx => Notifier.RuleActivated())
             .Do(ctx => Console.WriteLine("AN3 Fired!"));
        }
    } 

Let me know if this is not clear, or if you have other questions.

Andreas Nilsson

unread,
Sep 2, 2014, 3:38:58 AM9/2/14
to nrules...@googlegroups.com
Hi again, from time to time i pick this up and try to figure out how it's working, i have another question for you, hope thats ok :-)

This time its a simple rule engine, solve x = p + q ( p and q are required to calculate), i have 3 rules, p required, q required and the calculation, my facts are simple classes, name and value properties.

Here are the rules:

public class Property
{
public int Value { get; set; }
public string Name { get; set; }
}

public class PRequired : Rule
{
public override void Define()
{
Property property = null;
When()
.Match<Property>(() => property, p => p.Name == "P" && p.Value == 0);
Then()
.Do(context => Action1(context));
}

private void Action1(IContext context)
{
Console.WriteLine("P required");
}
}

public class QRequired : Rule
{
public override void Define()
{
Property property = null;
When()
.Match<Property>(() => property, p => p.Name == "Q" && p.Value == 0);
Then()
.Do(context => Action1(context));
}

private void Action1(IContext context)
{
Console.WriteLine("Q required");
}
}

public class XCalculation : Rule
{
public override void Define()
{
Property property_p = null;
Property property_q = null;
Property property_x = null;
When()
.Match<Property>(() => property_p, p => p.Name == "P" && p.Value != 0)
.Match<Property>(() => property_q, q => q.Name == "Q" && q.Value != 0)
.Match<Property>(() => property_x, x => x.Name == "X");
Then()
.Do(context => Action1(context, property_p, property_q, property_x));
}

private void Action1(IContext context, Property p, Property q, Property x)
{
x.Value = p.Value + q.Value;
context.Update(x);
Console.WriteLine("X = " + x.Value);
}
}

The idea is that when i start i put zero in p and q, the required rules fires ok, then i update the facts with values and the calculation fires, works fine, however if i continue and update P or Q i would like the calculation rule to fire again but it doesn't, is this something i can accomplish some how? Here's the program:

// Load rules
var ruleRepository = new RuleRepository();
ruleRepository.Load("Ruleset", r => r.From( new []{ typeof(PRequired), typeof(QRequired), typeof(XCalculation) }));
// Compile rules
var factory = ruleRepository.Compile();

// Create a working session
var session = factory.CreateSession();
Property p = new Property() { Name = "P", Value = 0};
Property q = new Property() { Name = "Q", Value = 0 };
Property x = new Property() { Name = "X", Value = 0 };
session.Insert(p);
session.Insert(q);
session.Insert(x);

session.Fire();

Console.WriteLine("Update P to 10");
p.Value = 10;
session.Update(p);
session.Fire();

Console.WriteLine("Update Q to 20");
q.Value = 20;
session.Update(q);
session.Fire();
// Calculation is done here

Console.WriteLine("Update P to 100");
p.Value = 100;
session.Update(p);
session.Fire();
// No calculation here!!!

Console.ReadKey();


Regards Andreas

Sergiy Nikolayev

unread,
Sep 3, 2014, 11:47:44 PM9/3/14
to nrules...@googlegroups.com
Hi Andreas!

To understand why rules engine does what it does, you need to understand how rete algorithm works (which is a matching algorithm used in most rules engines, including NRules). I highly recommend this article, as it gives an easy to read explanation: http://techondec.wordpress.com/2011/03/14/rete-algorithm-demystified-part-2/

Pertaining to your scenario, when you insert or update facts, the engine evaluates all relevant conditions and keeps track of which ones matched. Once all conditions for a given rule and a given set of facts matched, the rule is activated (with the matching set of facts as inputs) and fires. Updating the fact that already matched the rule does not really do anything (since the rule is already activated).
If, however, you updated the fact that it no longer matches the condition (i.e. in your case if you set it to zero), the activation would get deleted. If you then updated it again, so that it matches the rule, the rule would get activated (and would fire) again. You could also do this explicitly by retracting and the re-inserting the fact.
I also want to note that method "Fire" does not fire all matching rules. When facts match rules and rules are getting activated, they are placed on a queue (simply speaking). When you call method "Fire" all activated rules are fired off of that queue. If you call the "Fire" again, and the queue is empty, nothing happens, so the rule needs to get back on the queue (activated again) in order to fire.

Console.WriteLine("Update Q to 20");
q
.Value = 20;
session
.Update(q);
session
.Fire();
// Calculation is done here

Console.WriteLine("Update P to 100");
p
.Value = 100;

session
.Retract(p);
session
.Insert(p);
session
.Fire();
// Calculation is done here again

If you are looking for a calculation engine that keeps calculated fields in sync (similar to how Excel does it with its formulas) then rules engine is not going to give you that out of the box. You can trick it into doing this, by either doing Retract/Insert, like I showed you above, or by some other techniques, but in essence you have to code it. 

Let me know if I answered your question.
Sergiy

Sergiy Nikolayev

unread,
Feb 9, 2015, 12:35:48 AM2/9/15
to nrules...@googlegroups.com
Hi Andreas,

I know it's been a while, but I wanted to follow up in case this is still important to you.
It appeared that it actually was a defect with NRules that rules did not re-activate when corresponding facts were updated (see Issue43).
This was addressed in the release v0.3.1, which is now available in the nuget feed.
Reply all
Reply to author
Forward
0 new messages