3

我正在使用 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 扩展的第二条规则不能正常工作?我做错了什么,我该如何解决?

4

1 回答 1

3

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

引擎不再将其识别为相同的事实引用,因为“fruit”和“apple”不匹配。

因此,修复 #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 表达式,但这次不依赖于任何特定的变量命名。

于 2018-11-11T22:44:04.467 回答