NRules:在具有自定义基础 class 的规则上使用 DSL 扩展的问题
NRules: Issue using DSL extension on Rule with custom base class
我正在使用 NRules 来定义所有继承自公共基础 class 的规则,该基础本身继承自 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,首先要注意的是当您在规则中声明匹配变量并绑定到它时会发生什么:
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 表达式,与您的那个表达式等效,但这次不依赖于任何特定的变量命名。
我正在使用 NRules 来定义所有继承自公共基础 class 的规则,该基础本身继承自 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,首先要注意的是当您在规则中声明匹配变量并绑定到它时会发生什么:
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 表达式,与您的那个表达式等效,但这次不依赖于任何特定的变量命名。