NRules: ошибка, связанная с использованием расширения DSL в правиле с настраиваемым базовым классом. - PullRequest
0 голосов
/ 05 ноября 2018

Я использую 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 голосов
/ 12 ноября 2018

Первое, на что нужно обратить внимание при использовании NRules DSL, это то, что происходит, когда вы объявляете переменную соответствия в правиле и связываетесь с ним:

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

Этой переменной фактически никогда не назначается значение. Он захватывается как дерево выражений, и его имя извлекается и используется для последующего поиска других выражений, ссылающихся на эту же переменную. Затем двигатель заменяет эти ссылки фактическим сопоставленным фактом. Например:

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

Здесь «яблоко» - это та же переменная яблока, что и в предложении When, поэтому 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 - написать метод расширения с использованием лямбда-выражений.

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) теперь захватывает лямбда-выражение, которое позже извлекается и используется в реализации метода расширения. С некоторой магией выражения я затем создал лямбда-выражение, эквивалентное тому, которое вы имели, но на этот раз не полагаясь на какое-либо конкретное именование переменных.

...