Using DotNetRules to verify, map, and change your objects






4.87/5 (7 votes)
The DotNetRules Library is a .NET rule engine that applies policies to objects without the need to call each Policy manually from code, but with one simple call.
Introduction
As a software developer, I went through a lot of situations where I had to validate conditions on an existing object. With the coming of domain driven design I found myself ending up with dozens of lines of fluent validation code that made my clean domain object look like spaghetti code all over again. Using a rule engine is fun, but most of the engines out there required a lot of perquisites and all I wanted was to write some code, so I started my own lightweight engine.
The DotNetRules library is an approach to apply policies to objects without the need to call each Policy manually from code, but with one simple call. Using this approach you can deploy your policies in an external library and deploy them independently of the core application.
Flow
So what is the setup then?
Your application calls the Executor with an object for which you want to apply the policies, and the Executor will then invoke all policies that match your object. The policies can reside either in the same library as your application, or in an external one.
A Policy
A policy is a class that follows this schema:
- It
has a
PolicyAttribute
which acts as aPolicyDescriptor
, registering the types it is used for and gets information about Policy Chaining. - It
implements one of the
PolicyBase
classes, which take care of creating the context and the subjects for the policy: - It has the following interfaces that encapsulate the logic:
- void Establish (0-1) – If implemented can be used to establish required policy context.
- bool Given (1-X) – Used to create the condition(s) that have to be met to apply the policy.
- void Then(1-X) – The Actions that will be executed when the conditions are met.
- void Finalize (0-1) – Can be used to clean up after the policy has finished.
Let’s look at our first example. Open a new console project and create a
class LegacyDomainObject
with a property Version
of
type string. Then create a second class ExamplePolicy
and copy and paste the following source:
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();
};
}
If this reminds you of the Gherkin language, you are not far off. It is based on Gherkin, and I call it Ghetin (which is an acronym for “Gherkin this is not”).
The
PolicyAttribute
gives us the information that we are creating a Policy for the
TargetDomainObject
.
PolicyBase
will automatically create and initialize our Subject with the required type.
Inside the Given-Case we validate our requirement. If this requirement is met, we then throw an exception.
Let’s
execute and test the policy. Create a new instance of your TargetDomainObject
,
set version to “a”, and call the extension method ApplyPolicy
on your target.
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();
}
}
When you start the console application, the following text should show up:
> Exception! But don't panic, we were expecting that
So, what happened? The executor loaded all the policies that had a policy description matching the type to evaluate and applied them all. The policy we wrote threw an exception, thus we stranded in the catch-block.
A RelationshipPolicy
The DotNetRules comes with another base policy, the RelationPolicyBase
. This Policy allows you
to apply a policy based on two input objects. This allows you to write simple rules based on the parameters of the objects.
Imagine a setup where you have to import the data for your Domain from the legacy system we checked before. When some of the values change, you want to change them as well. Also, you want to write notifications for changes to the console.
To create something we can see we extend our LegacyDomainObject
, and create a new TargetDomainObject
. They look like this:
class TargetDomainObject
{
public string Body { get; set; }
public int Version { get; set; }
}
class LegacyDomainObject
{
public byte[] Body { get; set; }
public string Version { get; set; }
}
We now want
to update the body and version of the TargetDomainObject
if the version of the
LegacyDomainObject
has changed. Our Policy therefore would look like this:
[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);
}
It’s as easy to read as to write: Given the versions are not the same, then update the version and the body, and write our new values to the console. Well, as soon as our Policy is applied. To apply it we’ll have to extend the Main-function a bit.
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();
}
We start with the old “Check that it’s working when it’s incorrect” example from before. Then we change our LegacyDomainObject
to be more compliant to our expectations
(and we check that by calling “ApplyPolicies” on the object as well), after which we call “ApplyPoliciesFor” on the TargetDomainObject
, which will invoke
all policies for the TargetDomainObject
that have the LegacyDomainObject
as source.
What’s with ordering?
If nothing is specified, the Policies are executed ordered by name. You can however specify a “WaitFor” Type inside the PolicyAttribute
. That would look like:
[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?"); };
}
This Policy
will now wait for our ExampleRelated
Policy and start immediately after that.
I want to return something! Now!
And yes, you can! There is another keyword, with the hard-to-guess name “Return”. It is a generic delegate, and it will return whatever you like. Note that you can add only one Return delegate.
[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;
}
To get the value, you will have to call the single policy. Calling them all would give you many and more return values, and someday in the future there will be a LINQ-query that will give you access to each and every one, but right now that is Science Fiction.
So a “get that value”-piece of code would look like:
int result =
legacyItem.ApplyPoliciesFor<int, LegacyItem, TargetDomainObject>(
targetDomainObject, policies: new[] {typeof (PolicyWithReturnValue)});
Note: If you look at the result, you won’t see
an integer, but a funny looking “ExecutionTrace<int>
”. The
ExecutionTrace
itself contains some tracing information about
what actually happened when the policies were executed (see "Will it
test?"). This
ExecutionTrace
can be implicitly casted to the first generic type from your
“Apply”-function. Note that you will have to specify every type for the generic
method.
Will it test?
Luckily it
will. There are two ways to test it. The first is testing a single Policy using
the TestContext
class that comes with the framework. It gives you the option to
test whether a policy was fulfilled (to test your Given-clause), and of course
you can test the values after they were set. In Machine.Specification
that would look something like this:
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();
}
The second way is to test the complete flow of your policies. You can check the number of policies that were called as well as which policies were called and it what order.
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));
}
Can I select specific policies I want to apply
Sometimes, just wildly applying all policies may just not be what you want. For this you can specify the “policies”-parameter and specifying which policies you want to apply. For instance, you have an ASP.NET MVC page and want to use the same model for different cases even though you really just requires part of the model (yeah, it’s the “lazy-dev-solution”, but a great example), instead of writing your own Mapper, or even worst an inline mapping like so:
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";
You write your Mapping class in beautiful Ghetin-Language and your controller looks like this:
var orig = ProductService.Get(product.Id);
orig.ApplyPoliciesFor(product, policies: new[] { typeof(MapProductLegalPolicy),
typeof(MapProductReturnPolicy) });
I really don’t like the idea of all those policies getting applied automatically…
Well then, tell your policy you don’t want it to execute automatically. The
PolicyAttribute
has a property called “AutoExecute
” – set it to false and you will have to set the policy to execute it.
[Policy(typeof(TargetDomainObject), typeof(LegacyItem), AutoExecute= false)]
My policies are separated from my project. Am I going to die now?
Luckily there are no unhealthy side effects known when using DotNetRules. So you most probably won’t die from using it, no matter what.
To answer your first question, the one that was phrased like a statement: You can easily load Policies from external assemblies, because that’s what happening under the hood all the time. DotNetRules has to guess a location for the policies it should apply, and it does that by search the assembly of the subject. Therefore, whenever you are using an external library that has objects and the policies that are applied to them already embedded, you are fine without the need to consider anything else.
When you
want to load policies from a different assembly you can just add the parameter
policyLocation
like so:
var orig = ProductService.Get(product.Id);
orig.ApplyPoliciesFor(product,
policyLocation: typeof(MapProductLegalPolicy).Assembly);
My policies are not found!
That may be related to the chapter "My policies are separated from my project! Will I die now?".
The policies are by default searched in the Subject’s Assembly. That’s the
object that has the “Apply” on its back, or (if not the extension method is
called) is the first that is called. If you are unsure it does make sense to
specify the policyLocation
explicitly.
If the policy is still not applied, you
might want to check the “ExecutionTrace” object that is returned from every
Apply-Function. It contains a lot of information about the execution tree and
it has a property "CurrentAssembly
" that tells you which assembly was
searched for policies.
Use it with MVC
There is an extension for the DotNetRules. Cleverly it’s called “DotNetRules.Web.Mvc”.
It has the big advantage that it uses the ModelState
to log errors. To use it,
simply call ApplyPolicy(For)
at the ModelState
, like so:
ModelState.ApplyPoliciesFor(orig, product,
policies: new[] { typeof(MapProductLegalPolicy) });
if (!ModelState.IsValid)
{
return Edit(product.Id);
}
The MVC Extension will catch any errors and add them as ModelError
to the
State
. Note that the name of the that-function that has caused the exception will be used
as the name of the property of the Model.
So if your model looks like this:
class Model {
public string Value { get; set; }
}
Then your Policy's "that" should look like this:
Given invalid = () => string.IsNullOrEmpty(Subject.Value);
Then Value = () => throw new ArgumentNullOrEmpty("Value cannot be null or empty");
The rest is magic.
Extend it
So you’ve come to the point where all this is not enough? Not yet? Well, imaging yourself facing a problem where you’ll need three objects – A source, a target, and a transformation in the middle of it all. How would you do that? Well, you can’t. But you can extend the runtime. Create a new class and copy and paste the following content in it:
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; }
}
Based on that we can write a new Policy with all three properties:
[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);
}
We used transform here to hide the console and the conversion from the policy, which is a good thing; remember, the policies are supposed to be readable, not full of complex mapping logic, nobody really cares about when he or she wants to know what will happen when the objects are mapped.
You can also make this more readable by statically typing the properties in the policybase, but still there is no way around the attribute.
To execute
our custom policy, we have to call Execute.Apply
manually like so:
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);
And our custom, self-coded, threesome policy is applied.
Upcoming
- Better support for Exceptions when something is unexpected (i.e.,
value.ShouldBeNull()
). - Better support for MVC as soon as the Exceptions know for which property they were called.
- Somewhere in the future a Roslyn extension for Visual Studio that shows you all the policies that would be applied at that point.
Limitations
- Only the policies will be applied where exactly all types match (yet).
- Therefore you cannot
WaitFor
a policy with a different type signature.
Where to find the code?
This is an Open Source project, and you can find the complete sources at GitHub: https://github.com/MatthiasKainer/DotNetRules.
If you don't want to see the code, have no interest in improving this thing, and really just came here for a quick look on a rule engine and a want to try it, why don't just nuget it?
PM> Install-Package DotNetRules
or:
PM> Install-Package DotNetRules.Web.Mvc
for MVC Support.
History
- 2012-12-01 - Initial text.
- 2012-12-02 - More information about MVC's usage.