Nullifying Null

One of the more annoying problems with  building rules that are also code is that you have to deal with code related issues. One of the more common ones is NullReferenceException.

For example, let us say that we have the following rule:

when Order.Amount > 10 and Customer.IsPreferred:
      ApplyDiscount 5.precent 

We also support a mode in which a customer can create an order without actually registering on the site (anonymous checkout).

In this scenario, the Customer property is null. We can rewrite the rule to look like this:

when Order.Amount > 10 and Customer is not null and Customer.IsPreferred:
ApplyDiscount 5.precent

But I think that this is extremely ugly. We can also decide to return a default instance of the customer when it is not there, but here I want to show you another way to handle this. We define the rule as invalid when Customer is not there, so it should not be run. The question is how we can know that.

The dirty way is to do something like this:

var referencesCustomer = File.ReadAllText(ruleName).Contains("Customer");
if(referencesCustomer && Customer == null)
   return;

If you gagged when seeing this code, that is a good sign. Let us solve this properly. First, we want some help from the compiler, so let us inspect the when() meta method that we have seen in the previous post a little closer.

[Meta]
public static ExpressionStatement when(Expression condition, BlockExpression action)
{
	var ctor = new BlockExpression();

	var conditionFunc = new Block();
	conditionFunc.Add(new ReturnStatement(condition));

	ctor.Body.Add(
		new BinaryExpression(
			BinaryOperatorType.Assign,
			new ReferenceExpression("Condition"),
			new BlockExpression(conditionFunc)
			)
		);

	Expression serialize = new CodeSerializer().Serialize(condition);
	Builder.Revisit(serialize);

	ctor.Body.Add(
		new BinaryExpression(
			BinaryOperatorType.Assign,
			new ReferenceExpression("ConditionExpression"),
			serialize
			)
		);

	ctor.Body.Add(
		new BinaryExpression(
			BinaryOperatorType.Assign,
			new ReferenceExpression("Action"),
			action
			)
		);

	return new ExpressionStatement(
		new MethodInvocationExpression(
			ctor
			));
}

We take the cal to the when method and transform it to the following code:

delegate
{
	Condition = () => Order.Amount > 10 && Customer.IsPreferred;
	ConditionExpression = (Expression<Func<bool>>)() => Order.Amount > 10 && Customer.IsPreferred;
	Action = delegate
	{
		// not interesting for this post
	};
}();

I am translating to C# 3.0 here in order to make it easier to grasp the concept. The real code is in Boo, of course, and is more interesting. The most fascinating concept here is the use of CodeSerializer, which will turn the condition that we passed into an AST that we can access at runtime. I tried to simulate that by doing an explicit cast to expression tree, which would give similar result in C#).

Having the AST of the code at runtime, even if we don't want to change it (a totally different concept) is incredibly powerful. In this case, we are going to use this to detect when we are referencing a null property and marking the rule as invalid.

Here is the code:

public void Evaluate()
{
	var references = new List<string>();
	new InlineVisitor
	{
		OnReferenceExpression = r => references.Add(r.Name);
	}.Visit(ConditionExpression);
	if(references.Contains("Customer") && Customer == null)
		return;// rule invalid
	if(Condition())
		Action();
}

This is a very simple example of how you can add smarts to the way that your code behaves. This technique is the foundation for a whole host of options. I am using similar approaches for adaptive rules and for auditable actions. Fun stuff, if I say so myself.

Print | posted on Wednesday, September 03, 2008 1:50 AM

Feedback


Gravatar

# re: Nullifying Null 9/3/2008 3:06 AM Snafu918

While the solution you posted may be succinct it is far from simple for anyone new to .net. I've only been doing c# .net for a year but I've been programming for 9 years so I think I can say you would need to comment the hell out of that code.


Gravatar

# re: Nullifying Null 9/3/2008 3:42 AM Ayende Rahien

Paul,
I makes some assumptions about you being able to understand AST, I admit.


Gravatar

# re: Nullifying Null 9/3/2008 3:54 AM pb

Fine as long as you don't need a rule that reacts to it being null and doing something...


Gravatar

# re: Nullifying Null 9/3/2008 4:04 AM Ayende Rahien

pb,
That is actually easy.

OnIsExpression => x => isExpressions.Add(x.Name)

Now you just check if it is there.


Gravatar

# re: Nullifying Null 9/3/2008 4:59 AM pb

I get trying to make it easier to read, but this kind of hides something somewhat important, that you'd have to figure out when your rules weren't doing what you thought. I think I'd rather see either

1) Just add a "not Invalid(Customer)" to the beginning of each when

2) a rule about rules, such as:

for_all_whens
return_false_if Invalid(Customer)

so it was all clear what was going on. That seems possible given the code you've posted so far. Maybe you're just trying to do this as a simple example of a more complicated concept you have going on though.


Gravatar

# re: Nullifying Null 9/3/2008 5:03 AM Ayende Rahien

pb,
While the second is possible (and is mostly what I did today), I strongly recommend against it. Trying to understand how it works is hard.

1) is an eye sore.

It is easier to say that if Customer is null, the rule is not evaluated. (To be rather more exact, it is known that if customer is null, the rule evaluate to false).
Since this applies to just about any property, where there can be many, it is simpler overall, I think.


Gravatar

# re: Nullifying Null 9/3/2008 9:00 AM Roger Alsing

Why not simply use the null object pattern in this case?

I cant say I use null object much myself, but in this kind of scenario it might be a more robust approach, making the life for a rule writer easier.

Just because you don't have a anonymous customer in your persistent store doesnt mean you can't expose one to the rule engine.


Gravatar

# re: Nullifying Null 9/3/2008 9:05 AM Roger Alsing

Also, are you exposing your "real" domain model to the DSL?

So that the DSL could invoke code in the domain that is not supposed to?

Or are you transforming a subset of the domain into some sort of readonly mini DM for the rule engine?


Gravatar

# re: Nullifying Null 9/3/2008 9:14 AM Andrey Shchekin

I think 'null' (UnregisteredCustomer) object may be easier in this case (with IsRegistered false to check that if something depends on fact of having a known Customer).


Gravatar

# re: Nullifying Null 9/3/2008 2:37 PM Ayende Rahien

Roger,
Yes, I did pointed that one out. That is one option to deal with that, but it is not always a good option.


Gravatar

# re: Nullifying Null 9/3/2008 2:38 PM Ayende Rahien

Roger,
I am doing both. Exposing the domain directly to the DSL make it easier to work with it.
On the other hand, it hurt versioning and the fluidity of the language.


Gravatar

# re: Nullifying Null 9/3/2008 4:54 PM Roger Alsing

Ok I must be blind, because I didn't see that you pointed out null object the first time I read it.

Either way.
I'd say that there are two major benefits for using null object here.

1) The person that performs an anonymous checkout isby definition a "customer" since he is buying from you.
So it would be extremely odd to not model this into the domain.

2) Nullifying rules because of state is not very obvious for the reader of the DSL script.
This breaks the rule of least surprise (IMO)

Even if the intent is to lessen the burden on the DSL writer, I'd guess that this will be more error prone because people could make incorrect assumptions on state and expect that certain blocks of code have been executed when they have infact just been ignored.


Gravatar

# re: Nullifying Null 9/7/2008 9:11 AM Joe

Roger, he pointed it out here:

"We can also decide to return a default instance of the customer when it is not there,...."

That's the nullable pattern.


Gravatar

# re: Nullifying Null 9/7/2008 10:32 PM Neil

I tend to agree with Roger that the null value pattern fits nicely... after all an anonymous customer is still a (non-preferred) customer and your domain should reflect that. Out of curiosity, for what reasons is the null value pattern not a good option?


Gravatar

# re: Nullifying Null 9/8/2008 1:06 AM Ayende Rahien

Because it doesn't always work.
Null is a missing value, in which case the rule is invalid.
NullValue pattern implies that it should still work, which is not always the case.

As a simple example, how would you handle a rule that give 2% discounts for customers after 6 years ?

Comments have been closed on this topic.