I am using NRules to define rules that all inherit from a common base class, which itself inherits from Rule
.
When I use a DSL extension to insert a new fact that wraps a matched object, it seems that the matched object passed to the extension method is null
.
Here's a self-contained example that should demonstrate the problem. I am using the xUnit
test framework to define two rules, each with identical tests. The first one passes, the second one fails.
using NRules;
using NRules.Fluent;
using NRules.Fluent.Dsl;
using Xunit;
using System.Linq;
using System.Reflection;
namespace IntegrationTests.Engine
{
// A simple domain model
public interface IFruit { }
public class Apple : IFruit { }
public class Basket
{
public Basket(IFruit apple)
{
MyApple = apple;
}
public IFruit MyApple { get; private set; }
}
// A base class for the rules
public abstract class RuleBase : Rule
{
public override void Define()
{
// Empty
}
}
// The first rule, which does not use the extension:
public class TestRule : RuleBase
{
public override void Define()
{
base.Define();
Apple a = null;
When()
.Match(() => a);
Then()
.Do(ctx => ctx.Insert(new Basket(a)));
}
}
// The second rule, which uses an extension to add a new fact
public class TestRuleWithExtension : RuleBase
{
public override void Define()
{
base.Define();
Apple apple = null;
When()
.Match(() => apple);
Then()
.AddToBasket(apple);
}
}
// The DSL extension
public static class DslExtensions
{
public static IRightHandSideExpression AddToBasket(this IRightHandSideExpression rhs, IFruit fruit)
{
return rhs.Do(ctx => ctx.Insert(new Basket(fruit)));
}
}
// The tests
public class ExtensionTest
{
// This one tests the first rule and passes
[Fact]
public void TestInsert()
{
//Load rules
var repository = new RuleRepository();
repository.Load(x => x
.From(Assembly.GetExecutingAssembly())
.Where(rule => rule.Name.EndsWith("TestRule")));
//Compile rules
var factory = repository.Compile();
//Create a working session
var session = factory.CreateSession();
//Load domain model
var apple = new Apple();
//Insert facts into rules engine's memory
session.Insert(apple);
//Start match/resolve/act cycle
session.Fire();
// Query for inserted facts
var bananas = session.Query<Basket>().FirstOrDefault();
// Assert that the rule has been applied
Assert.Equal(apple, bananas.MyApple);
}
// This one tests the second rule, and fails
[Fact]
public void TestInsertWithExtension()
{
//Load rules
var repository = new RuleRepository();
repository.Load(x => x
.From(Assembly.GetExecutingAssembly())
.Where(rule => rule.Name.EndsWith("TestRuleWithExtension")));
//Compile rules
var factory = repository.Compile();
//Create a working session
var session = factory.CreateSession();
//Load domain model
var apple = new Apple();
//Insert facts into rules engine's memory
session.Insert(apple);
//Start match/resolve/act cycle
session.Fire();
// Query for inserted facts
var bananas = session.Query<Basket>().FirstOrDefault();
// Assert that the rule has been applied
Assert.Equal(apple, bananas.MyApple);
}
}
}
The question is why does the second rule with the DSL extension not work properly? Am I doing something wrong and how can I fix it?
First thing to note with the NRules DSL is what happens when you declare a match variable in a rule and bind to it:
No value is actually ever assigned to this variable. It is captured as an expression tree, and its name is extracted, and used to later find other expressions referencing the same variable. The engine then replaces those references with the actual matched fact. For instance:
Here "apple" is the same apple variable form the When clause, so NRules recognizes that and correctly stitches the expressions together.
When you extracted an extension method, you named the variable "fruit":
The engine no longer recognizes this as the same fact reference, since "fruit" and "apple" don't match.
So, fix #1 is to just name the variable the same way as the declaration:
Obviously this is not ideal, since you are relying on matching naming of variable. Since NRules operates in terms of the expression trees, a better way to build a generic extension method would be to also write it in terms on expression trees, and no longer depend on the variable naming.
So, fix #2 is to write the extension method using lambda expressions.
Note that
AddToBasket(() => apple)
now captures the lambda expression, which is later extracted and used in the implementation of the extension method. With some expression magic I then constructed a lambda expression equivalent to the one you had, but this time not relying on any specific variable naming.