NRules: ошибка, связанная с использованием расширения DSL в правиле с настраиваемым базовым классом.
Я использую 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 ответ
Первое, на что нужно обратить внимание при использовании NRules DSL, - это то, что происходит, когда вы объявляете переменную соответствия в правиле и связываетесь с ним:
Apple apple = null;
When()
.Match(() => apple);
На самом деле значение этой переменной никогда не присваивается. Он захватывается как дерево выражений, а его имя извлекается и используется для последующего поиска других выражений, ссылающихся на эту же переменную. Затем двигатель заменяет эти ссылки фактическим сопоставленным фактом. Например:
Then()
.Do(ctx => ctx.Insert(new Basket(apple)));
Здесь "apple" - это та же самая переменная 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)
теперь захватывает лямбда-выражение, которое позже извлекается и используется в реализации метода расширения. С некоторой магией выражения я затем создал лямбда-выражение, эквивалентное тому, которое вы имели, но на этот раз не полагаясь на какое-либо конкретное именование переменных.