使用 DotNetRules 验证、映射和更改你的对象






4.87/5 (7投票s)
DotNetRules 库是一个 .NET 规则引擎,它无需从代码中手动调用每个策略即可将策略应用于对象,只需一个简单的调用。
引言
作为一名软件开发者,我曾遇到过很多需要验证现有对象上条件的情况。随着领域驱动设计的到来,我发现自己最终写了数十行流畅的验证代码,这让我的干净的领域对象再次看起来像意大利面条代码。使用规则引擎很有趣,但大多数现有引擎都需要很多前提条件,而我想要的只是写一些代码,所以我开始了自己的轻量级引擎。
DotNetRules 库是一种将策略应用于对象的方法,无需从代码中手动调用每个策略,只需一个简单的调用。使用这种方法,您可以将策略部署在外部库中,并独立于核心应用程序进行部署。
流程
那么设置是什么呢?
您的应用程序使用您想要应用策略的对象调用 Executor,然后 Executor 将调用所有匹配您对象的策略。策略可以驻留在与您的应用程序相同的库中,也可以驻留在外部库中。
策略
策略是一个遵循此模式的类:
- 它有一个
PolicyAttribute
,它充当PolicyDescriptor
,注册它所使用的类型,并获取有关策略链接的信息。 - 它实现了
PolicyBase
类之一,该类负责创建策略的上下文和主体。 - 它具有封装逻辑的以下接口:
- void Establish (0-1) – 如果实现,可用于建立所需的策略上下文。
- bool Given (1-X) – 用于创建应用策略必须满足的条件。
- void Then(1-X) – 当满足条件时将执行的操作。
- void Finalize (0-1) – 可用于在策略完成后进行清理。
让我们看我们的第一个例子。打开一个新的控制台项目,创建一个名为 LegacyDomainObject
的类,其中包含一个类型为 string 的 Version
属性。然后创建第二个类 ExamplePolicy
,并复制粘贴以下源代码:
using DotNetRules.Runtime;
[Policy(typeof(LegacyDomainObject))]
internal class APolicyForASingleObject : PolicyBase<LegacyDomainObject>
{
Given versionIsNotANumber = () => {
int i;
return !int.TryParse(Subject.Version, out i);
};
Then throwAnInvalidStateException = () => {
throw new InvalidStateException();
};
}
如果这让你想起 Gherkin 语言,你就离真相不远了。它基于 Gherkin,我称之为 Ghetin(它是“Gherkin this is not”的缩写)。
PolicyAttribute
提供了我们正在为 TargetDomainObject
创建策略的信息。
PolicyBase
将自动创建并初始化我们具有所需类型的 Subject。
在 Given-Case 中,我们验证我们的需求。如果满足此需求,我们就抛出异常。
让我们执行并测试策略。创建一个 TargetDomainObject
的新实例,将 version 设置为“a”,然后在目标上调用扩展方法 ApplyPolicy
。
class Program
{
static void Main()
{
try
{
new LegacyDomainObject { Version = "a" }.ApplyPolicies();
Console.WriteLine("That was unexpected");
}
catch (Exception e0)
{
Console.WriteLine("Exception! But don't panic, we were expecting that");
}
Console.ReadKey();
}
}
当您启动控制台应用程序时,应显示以下文本:
> 异常!但不要惊慌,我们预料到了
那么,发生了什么?Executor 加载了所有具有匹配要评估类型的策略描述符的策略,并将它们全部应用了。我们编写的策略抛出了异常,因此我们落入了 catch-block。
关系策略
DotNetRules 附带另一个基类策略,即 RelationPolicyBase
。此策略允许您基于两个输入对象应用策略。这允许您根据对象的参数编写简单的规则。
设想一种场景,您需要从我们之前检查的旧系统中导入域数据。当某些值更改时,您也想更改它们。此外,您想将更改通知写入控制台。
为了创建一些我们可以看到的东西,我们扩展 LegacyDomainObject
,并创建一个新的 TargetDomainObject
。它们看起来是这样的:
class TargetDomainObject
{
public string Body { get; set; }
public int Version { get; set; }
}
class LegacyDomainObject
{
public byte[] Body { get; set; }
public string Version { get; set; }
}
我们现在希望在 LegacyDomainObject
的版本发生变化时更新 TargetDomainObject
的 body 和 version。因此,我们的策略将如下所示:
[Policy(typeof(TargetDomainObject), typeof(LegacyDomainObject))]
class ExampleRelated : RelationPolicyBase<LegacyDomainObject, TargetDomainObject>
{
Given versionsAreNotTheSame = () =>
Convert.ToInt32(Source.Version) != Target.Version;
Then updateTheVersion = () => Target.Version = Convert.ToInt32(Source.Version);
Then updateTheBody = () => Target.Body = Encoding.UTF8.GetString(Source.Body);
Finally writeToConsole = () =>
Console.WriteLine("Object was updated. Version = {0}, Body = {1}",
Target.Version, Target.Body);
}
它易于阅读和编写:鉴于版本不相同,然后更新版本和 body,并将我们的新值写入控制台。好吧,一旦我们的策略被应用。要应用它,我们必须稍微扩展 Main 函数。
static void Main()
{
var legacyDomainObject = new LegacyDomainObject { Version = "a" };
var targetDomainObject = new TargetDomainObject();
legacyDomainObject.Body = new byte[]
{ 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100 };
legacyDomainObject.Version = "1";
legacyDomainObject.ApplyPolicies();
targetDomainObject.ApplyPoliciesFor(legacyDomainObject);
Console.ReadKey();
}
我们从之前旧的“检查不正确时是否有效”的例子开始。然后,我们将 LegacyDomainObject
更改为更符合我们的预期(我们通过调用对象上的“ApplyPolicies”来检查这一点),然后我们调用 TargetDomainObject
上的“ApplyPoliciesFor”,这将调用 TargetDomainObject
的所有策略,并将 LegacyDomainObject
作为源。
排序呢?
如果未指定,策略将按名称排序执行。但是,您可以在 PolicyAttribute
中指定一个“WaitFor”类型。它看起来是这样的:
[Policy(typeof(TargetDomainObject),
typeof(LegacyDomainObject),
WaitFor = typeof(ExampleRelated))]
class WaitingPolicy : RelationPolicyBase<LegacyDomainObject, TargetDomainObject>
{
Given theVersionsAreStillNotTheSame = () =>
Convert.ToInt32(Source.Version) != Target.Version;
Then throwWtfException = () => { throw new Exception("wtf?"); };
}
此策略现在将等待我们的 ExampleRelated
策略,并在之后立即开始。
我想返回一些东西!现在!
是的,你可以!还有一个关键字,名称很难猜,叫做“Return”。它是一个通用的委托,它将返回任何你想要的东西。请注意,您只能添加一个 Return 委托。
[Policy(typeof(TargetDomainObject), typeof(LegacyItem))]
class PolicyWithReturnValue : RelationPolicyBase<LegacyItem, TargetDomainObject>
{
Given isTrue = () => !Source.Number.Equals(Target.Integer.ToString());
Then convertTheStringToNumber = () =>
{
Target.Integer = Convert.ToInt32(Source.Number);
};
Return<int> @return = () => Target.Integer;
}
要获取该值,您必须调用单个策略。调用所有策略将为您提供许多返回值,将来有一天会有 LINQ 查询,让您可以访问每一个,但现在那只是科幻小说。
所以“获取该值”的代码看起来是这样的:
int result =
legacyItem.ApplyPoliciesFor<int, LegacyItem, TargetDomainObject>(
targetDomainObject, policies: new[] {typeof (PolicyWithReturnValue)});
注意:如果你看结果,你不会看到一个整数,而是一个看起来很有趣的“ExecutionTrace<int>
”。ExecutionTrace
本身包含一些关于策略执行时实际发生情况的跟踪信息(请参阅“它会测试吗?”)。此 ExecutionTrace
可以隐式转换为“Apply”函数中的第一个泛型类型。请注意,您必须为泛型方法指定所有类型。
它会测试吗?
幸运的是,它会。有两种方法可以测试它。第一种是使用框架附带的 TestContext
类来测试单个策略。它为您提供了测试策略是否已满足(测试您的 Given 子句)的选项,当然,您也可以测试设置后的值。在 Machine.Specification
中,它看起来像这样:
class When_the_values_are_the_same
{
static TestContext _testContext;
static LegacyItem _legacyItem;
static TargetDomainObject _targetDomainObject;
Establish context = () =>
{
_testContext = new TestContext(typeof(VersionPolicy));
_legacyItem = new LegacyItem {Version = "1"};
_targetDomainObject = new TargetDomainObject { Version = 1 };
};
Because of = () => _testContext.Execute(_legacyItem, _targetDomainObject);
It should_not_fullfill_the_condition = () =>
_testContext.WasConditionFullfilled().ShouldBeFalse();
}
第二种方法是测试策略的完整流程。您可以检查被调用的策略数量,以及被调用的策略及其顺序。
class When_two_values_are_different
{
static ExecutionTrace _result;
static LegacyItem _legacyItem;
static TargetDomainObject _targetDomainObject;
Establish context = () =>
{
_legacyItem =
new LegacyItem { Text = "text", Number = "100" };
_targetDomainObject =
new TargetDomainObject { StringArray = new string[0], Integer = 0 };
};
Because of = () => _result = Executor.Apply(_legacyItem, _targetDomainObject);
It should_have_executed_two_policies = () => _result.Called.ShouldEqual(2);
It should_have_executed_the_ADependendPolicy = () =>
_result.By.Any(_ => _ == typeof(WaitingPolicy)).ShouldBeTrue();
It should_have_executed_the_ExamplePolicy = () =>
_result.By.Any(_ => _ == typeof(ExamplePolicy)).ShouldBeTrue();
It should_have_executed_the_ExamplePolicy_first =
() => _result.By.Peek().ShouldEqual(typeof(ExamplePolicy));
}
我可以选择我想应用的特定策略吗?
有时,仅仅随意应用所有策略可能不是您想要的。为此,您可以指定“policies”参数并指定您想要应用的策略。例如,您有一个 ASP.NET MVC 页面,并希望为不同的情况使用相同的模型,即使您实际上只需要模型的一部分(是的,这是“懒惰开发人员解决方案”,但这是一个很好的例子),而不是编写自己的映射器,甚至更糟的是内联映射,如下所示:
var orig = ProductService.Get(product.Id);
if (string.IsNullOrEmpty(product.Returns))
throw new ArgumentNullException("product.Returns");
if (string.IsNullOrEmpty(product.TC))
throw new ArgumentNullException("product.TC");
orig.Returns = product.Returns.ToSafeHtml();
orig.TC = product.TC.ToSafeHtml();
view = "EditLegal";
您用漂亮的 Ghetin 语言编写您的映射类,您的控制器看起来像这样:
var orig = ProductService.Get(product.Id);
orig.ApplyPoliciesFor(product, policies: new[] { typeof(MapProductLegalPolicy),
typeof(MapProductReturnPolicy) });
我真的很不喜欢所有这些策略自动应用的这个想法……
那么,告诉您的策略您不希望它自动执行。PolicyAttribute
有一个名为“AutoExecute
”的属性——将其设置为 false,您将不得不设置策略来执行它。
[Policy(typeof(TargetDomainObject), typeof(LegacyItem), AutoExecute= false)]
我的策略与我的项目分离。我这就要死了吗?
幸运的是,使用 DotNetRules 没有已知的健康副作用。因此,无论如何,您很可能不会因此而死亡。
为了回答您的第一个问题,那个被表述为陈述的问题:您可以轻松地从外部程序集中加载策略,因为这正是幕后一直在发生的事情。DotNetRules 必须猜测它应该应用的策略的位置,它通过搜索主体的程序集来做到这一点。因此,无论何时使用嵌入了对象及其应用的策略的外部库,您都可以正常使用,而无需考虑其他任何事情。
当您想从不同的程序集中加载策略时,您可以添加 policyLocation
参数,如下所示:
var orig = ProductService.Get(product.Id);
orig.ApplyPoliciesFor(product,
policyLocation: typeof(MapProductLegalPolicy).Assembly);
找不到我的策略!
这可能与“我的策略与我的项目分离!我这就要死了吗?”这一章有关。
默认情况下,策略会在主体的程序集中搜索。那是“Apply”对象所在的程序集,或者(如果调用的不是扩展方法)是第一个被调用的。如果您不确定,明确指定 policyLocation
是有意义的。
如果策略仍未应用,您可能需要检查从每个 Apply 函数返回的“ExecutionTrace”对象。它包含有关执行树的大量信息,并且有一个“CurrentAssembly
”属性,告诉您正在搜索哪个程序集来查找策略。
与 MVC 一起使用
有一个 DotNetRules 的扩展。它被巧妙地称为“DotNetRules.Web.Mvc”。它的主要优点是它使用 ModelState
来记录错误。要使用它,只需在 ModelState
上调用 ApplyPolicy(For)
,如下所示:
ModelState.ApplyPoliciesFor(orig, product,
policies: new[] { typeof(MapProductLegalPolicy) });
if (!ModelState.IsValid)
{
return Edit(product.Id);
}
MVC 扩展将捕获任何错误并将其作为 ModelError
添加到 State
。请注意,导致异常的 that 函数的名称将用作 Model 的属性名称。
所以,如果您的模型看起来像这样:
class Model {
public string Value { get; set; }
}
那么您的策略的“that”应该看起来像这样:
Given invalid = () => string.IsNullOrEmpty(Subject.Value);
Then Value = () => throw new ArgumentNullOrEmpty("Value cannot be null or empty");
剩下的就是魔法了。
扩展它
所以,你已经达到了这一切都不够的地步?还没有?嗯,想象一下你面临一个问题,你需要三个对象——一个源,一个目标,以及中间的转换。你会怎么做?嗯,你做不到。但你可以扩展运行时。创建一个新类并将以下内容复制粘贴到其中:
public class RelationAndTransformPolicyBase<TSource, TTransform, TTarget> : BasePolicy
{
public static TSource Source { get; set; }
public static TTransform Transform { get; set; }
public static TTarget Target { get; set; }
}
基于此,我们可以编写一个具有所有三个属性的新策略:
[Policy(typeof(TargetDomainObject),
typeof(LegacyDomainObject),
typeof(TransformObject))]
class ThreesomePolicy : RelationAndTransformPolicyBase<LegacyDomainObject,
TransformObject, TargetDomainObject>
{
Given legacyAndDomainAreNotEqual = () => Transform.AreEqual(Source, Target);
Then updateTheVersion = () => Target.Version = Transform.IntifyVersion(Source);
Then updateTheBody = () => Target.Body = Transform.StringifyBody(Source);
Finally writeToConsole = () => Transform.Print(Target);
}
我们在这里使用 transform 来隐藏策略中的控制台和转换,这是一个好主意;请记住,策略应该易于阅读,而不是充满复杂的映射逻辑,当人们想知道对象映射时会发生什么时,没有人真正关心。
您还可以通过静态类型化 policybase 中的属性来提高可读性,但仍然无法绕过属性。
要执行我们的自定义策略,我们必须手动调用 Execute.Apply
,如下所示:
var legacyDomainObject = new LegacyDomainObject {Version = "a"};
var targetDomainObject = new TargetDomainObject();
var transformer = new TransformObject();
legacyDomainObject.Body = new byte[] {72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100};
legacyDomainObject.Version = "1";
legacyDomainObject.ApplyPolicies();
Executor.Apply(legacyDomainObject, targetDomainObject, transformer);
然后,我们的自定义、自编码的三方策略就应用了。
即将推出
- 当发生意外情况时(例如
value.ShouldBeNull()
),可以更好地支持异常。 - 当异常知道它们是为什么属性调用时,可以更好地支持 MVC。
- 将来,Visual Studio 将有一个 Roslyn 扩展,可以显示当时将要应用的所有策略。
限制
- 只有当所有类型都完全匹配时(目前)才会应用策略。
- 因此,您不能
WaitFor
具有不同类型签名的策略。
代码在哪里可以找到?
这是一个开源项目,您可以在 GitHub 上找到完整的源代码: https://github.com/MatthiasKainer/DotNetRules。
如果您不想看代码,对改进这件事不感兴趣,只是想快速了解一下规则引擎并想尝试一下,为什么不直接 nuget 它呢?
PM> Install-Package DotNetRules
或
PM> Install-Package DotNetRules.Web.Mvc
用于 MVC 支持。
历史
- 2012-12-01 - 初始文本。
- 2012-12-02 - 关于 MVC 用法的更多信息。