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.
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 is
a class that follows this schema:
- It
has a
PolicyAttribute
which acts as a PolicyDescriptor
, 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.
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.
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.
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));
}
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) });
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);
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.
- 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.
- 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.