c# - NRules:在具有自定义基类的规则上使用 DSL 扩展的问题

标签 c# extension-methods nrules

我正在使用 NRules 来定义所有继承自公共(public)基类的规则,该基类本身继承自 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扩展的第二条规则不能正常工作?我做错了什么吗?我该如何解决?

最佳答案

对于 NRules DSL,首先要注意的是当您在规则中声明匹配变量并绑定(bind)到它时会发生什么:

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

关于c# - NRules:在具有自定义基类的规则上使用 DSL 扩展的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53158857/

相关文章:

aspnetboilerplate - ABP 中 NRules 的属性注入(inject)

c# - 每 10% 输入一个代码分支,直到 100%

c# - 具有多列的 Linq 高级查询

c# - 哪些 System.Drawing 类算作 GDI 对象?

c# - 将注册表中的版本号转换为System.Version?

c# - 如何使用扩展方法将附加数据与现有对象相关联?

c# - 当 linq2db 不知道如何转换给定的表达式,即 Split(char) 到 SQL 时,我如何告诉它如何转换?

c# - 将标志转换为 IEnumerable<Enum> 的扩展方法,反之 (C#)