Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

Using DotNetRules to verify, map, and change your objects

Rate me:
Please Sign up or sign in to vote.
4.87/5 (7 votes)
5 Mar 2013CPOL9 min read 16.9K   20   5
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? 

Image 1

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 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: 

C#
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.

C#
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: 

C#
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:  

C#
[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.  

C#
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:

C#
[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.

C#
[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:

C#
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: 

C#
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.

C#
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:

C#
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:

C#
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.

C#
[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:

C#
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:

C#
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: 

C#
class Model {
    public string Value { get; set; }
} 

Then your Policy's "that" should look like this:  

C#
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: 

C#
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:

C#
[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: 

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) AutoScout24 GmbH
Austria Austria
Born, raised and killed by skynet I stopped worrying and started to code my own skynet whose sole purpose it will be to revenge my death by running over the terminator with a bobby car

Comments and Discussions

 
QuestionNo Code Download? Pin
FatCatProgrammer5-Mar-13 6:18
FatCatProgrammer5-Mar-13 6:18 
AnswerRe: No Code Download? Pin
Dmitry Mukalov5-Mar-13 6:45
Dmitry Mukalov5-Mar-13 6:45 
github
AnswerTroll Pin
DelphiCoder5-Dec-12 13:04
DelphiCoder5-Dec-12 13:04 
GeneralMy vote of 2 Pin
abdurahman ibn hattab2-Dec-12 20:36
abdurahman ibn hattab2-Dec-12 20:36 
GeneralRe: My vote of 2 Pin
Matthias Kainer2-Dec-12 21:59
professionalMatthias Kainer2-Dec-12 21:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.