NRules:在带有自定义基类的Rule上使用DSL扩展的问题

时间:2018-11-05 16:56:55

标签: c# extension-methods nrules

我正在使用NRules定义规则,这些规则都继承自公共基类,而该基类本身又继承自Rule

当我使用DSL扩展名插入一个包裹匹配对象的新事实时,传递给扩展方法的匹配对象似乎是null

这是一个独立的示例,应该可以演示该问题。我正在使用xUnit测试框架来定义两个规则,每个规则具有相同的测试。第一个通过,第二个失败。

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);
        }
    }
}

问题是为什么带有DSL扩展名的第二条规则不能正常工作?我做错什么了,怎么解决?

1 个答案:

答案 0 :(得分:1)

使用NRules DSL时要注意的第一件事是在规则中声明匹配变量并绑定到该变量时会发生什么:

Apple apple = null;
When()
    .Match(() => apple);

实际上没有任何值分配给该变量。它被捕获为一个表达式树,并提取其名称,并用于以后查找引用同一变量的其他表达式。然后,引擎将这些引用替换为实际匹配的事实。 例如:

Then()
    .Do(ctx => ctx.Insert(new Basket(apple)));

这里的“ apple”是与When子句相同的apple变量,因此NRules会识别出该错误并正确地将表达式拼接在一起。

提取扩展方法时,将变量命名为“ fruit”:

public static IRightHandSideExpression AddToBasket(this IRightHandSideExpression rhs, IFruit fruit)
{
    return rhs.Do(ctx => ctx.Insert(new Basket(fruit)));
}

由于“水果”和“苹果”不匹配,因此引擎不再将其识别为同一事实引用。

因此,解决方案1是使用与声明相同的方式来命名变量:

public static class DslExtensions
{
    public static IRightHandSideExpression AddToBasket(this IRightHandSideExpression rhs, IFruit apple)
    {
        return rhs.Do(ctx => ctx.Insert(new Basket(apple)));
    }
}

显然这是不理想的,因为您依赖于变量的匹配命名。 由于NRules根据表达式树进行操作,因此构建通用扩展方法的更好方法是也可以根据表达式树来编写它,而不再依赖于变量命名。

因此,解决方案2是使用lambda表达式编写扩展方法。

public class TestRuleWithExtension : RuleBase
{
    public override void Define()
    {
        base.Define();

        Apple apple = null;
        When()
            .Match(() => apple);

        Then()
            .AddToBasket(() => apple);
    }
}

public static class DslExtensions
{
    public static IRightHandSideExpression AddToBasket(this IRightHandSideExpression rhs, Expression<Func<IFruit>> alias)
    {
        var context = Expression.Parameter(typeof(IContext), "ctx");

        var ctor = typeof(Basket).GetConstructor(new[] {typeof(IFruit)});
        var newBasket = Expression.New(ctor, alias.Body);

        var action = Expression.Lambda<Action<IContext>>(
            Expression.Call(context, nameof(IContext.Insert), null, newBasket), 
            context);
        return rhs.Do(action);
    }
}

请注意,AddToBasket(() => apple)现在捕获lambda表达式,该表达式随后被提取并用于扩展方法的实现中。然后,我使用某种表达式魔术构建了一个与您拥有的表达式等效的lambda表达式,但是这次不依赖任何特定的变量命名。