Print

Introducing CuttingEdge.Conditions

CuttingEdge.Conditions is a library that helps developers to write pre- and postcondition validations in their .NET 3.5 code base. Writing these validations is easy and it improves the readability and maintainability of code.

Download: The CuttingEdge.Conditions library and source code can be downloaded from CodePlex.com. Visit the homepage at conditions.codeplex.com or go directly to the releases tab.

Warning: This post is based on a pre-release of CuttingEdge.Conditions. While most of the concepts and behavior of the library is the same, the final release has some changes, of which most noticeably is the removal of the extension method behavior of the Requires() and Ensures() methods. Please note that the following syntax isn’t supported anymore: c.Requires().IsNotNull(). Instead the proposed syntax is Condition.Requires(c).IsNotNull(). Please keep that in mind while reading this article.

Writing precondition validations raises the quality of code. Code with validations is easer to understand and allows developers to find bugs faster, mostly during development instead of during debugging. Writing precondition validations however has always been the poor relation in programming. It takes time to write it and many developers I worked with (even the ones I respect) skipped writing them.

Skipping precondition validations will lead to code that is more difficult to use and is likely to be misused. It allows developers to pass invalid method arguments, which results in unexpected program behavior and those awful NullReferenceExceptions from deep down the call stack. It leads to an higher amount of bugs and thus more time spent debugging.

The CuttingEdge.Conditions library is an attempt to lower the barrier of writing precondition validations and to make code more readable, thus resulting in better code, less bugs, and shorter development cycles.

To understand how CuttingEdge.Conditions tries to achieve this, let us first have a look at some code we might write on a daily basis. Here is an example of precondition validations, the old fashion way:

void TheOldFashionWay(int id, IEnumerable<int> col, 
DayOfWeek day)
{
if (id < 1)
{
throw new ArgumentOutOfRangeException("id",
String.Format("id should be greater " +
"than 0. The actual value is {0}.", id));
}

if (col == null)
{
throw new ArgumentNullException("col",
"collection should not be empty");
}

if (col.Count() == 0)
{
throw new ArgumentException(
"collection should not be empty", "col");
}

if (day >= DayOfWeek.Monday &&
day <= DayOfWeek.Friday)
{
throw new InvalidEnumArgumentException(
String.Format("day should be between " +
"Monday and Friday. The actual value " +
"is {0}.", day));
}

// Do method work
}

That’s an awful amount of code for a few simple validations! Here’s how it looks would CuttingEdge.Conditions be adopted:

void TheConditionsWay(int id, IEnumerable<int> col, 
DayOfWeek day)
{
id.Requires("id").IsGreaterThan(0);
col.Requires("col").IsNotEmpty();
day.Requires("day").IsInRange(DayOfWeek.Monday, DayOfWeek.Friday);

// Do method work
}

That’s quite different, isn’t it? It’s not only far less code; it’s also very readable. And please note that both methods have exactly the same contract; Both methods throw exactly the same exceptions!

Besides these normal precondition checks, CuttingEdge.Conditions enables you to do postcondition checks as well. Unlike a precondition, the violation of a postcondition has purely an internal cause. It can be considered a bug. Throwing an ArgumentException in that case would clearly confuse the developer using that code. Because of this difference, CuttingEdge.Conditions will always throw a PostconditionException on a violation of a postcondition.

Here is an example of a postcondition check:

public ICollection PostconditionExample()
{
Type type = typeof(Collection<int>);
object obj = Activator.CreateInstance(type);

obj.Ensures("obj")
.IsNotNull()
.IsOfType(typeof(ICollection));

return (ICollection)obj;
}

The postcondition example shows two interesting things. Firstly, The Ensures extension method is used to start a postcondition validation. Secondly, method calls can be chained in a fluent manner as shown with the IsNotNull and IsOfType methods.

The API

The CuttingEdge.Conditions API has many validation methods that easily cover 98% of your validation needs. There are currently 88 extension methods for 51 different checks. The API can be divided in seven groups:

  • Entry point methods
  • Null check methods
  • Type check methods
  • Comparison checks
  • Collection checks
  • String checks
  • Evaluations

Below I will list all methods that are in the current beta 1 release of CuttingEdge.Conditions. The number of methods will possibly grow over time, and please comment here or on Codeplex if you think there are validations missing. I will consider adding them to the library. Also note that it’s easy for you to extend the API with your own methods, by simply placing extension methods in your own project.

Entry point methods

Entry point methods are used to start the validation. These extension methods can be called on each and every variable, of each and every type. There are currently 2 methods:

  • Requires (2 overloads)
  • Ensures (3 overloads)

The Requires extension methods can be used to write preconditions. It will throw an ArgumentException or one of it’s descendants on failure. The Ensures extension methods can be used to write postconditions. It will throw an PostconditionException.

Null check methods

Null checks can be used if arguments require a value or require no value. These checks can be performed on reference types and Nullable<T> structures. There are two methods:

  • IsNotNull (2 overloads)
  • IsNull (2 overloads)
Type check methods

There are two methods that can be used for a type checking:

  • IsOfType
  • IsNotOfType
Comparison checks

Comparison checks validate arguments to be equal to some other value or to be in a given range. They can be performed on Nullable<T> structures and all types that implement IComparable. There are currently 12 methods:

  • IsEqualTo (3 overloads)
  • IsNotEqualTo (3 overloads)
  • IsGreaterThan (3 overloads)
  • IsNotGreaterThan (3 overloads)
  • IsGreaterOrEqual (3 overloads)
  • IsNotGreaterOrEqual (3 overloads)
  • IsInRange (3 overloads)
  • IsNotInRange (3 overloads)
  • IsLessThan (3 overloads)
  • IsNotLessThan (3 overloads)
  • IsLessOrEqual (3 overloads)
  • IsNotLessOrEqual (3 overloads)
Collection checks

Collection checks can be used on types that (at least) implement IEnumerable. There are currently 18 methods:

  • Contains (2 overloads)
  • DoesNotContain (2 overloads)
  • ContainsAll (2 overloads)
  • DoesNotContainAll (2 overloads)
  • ContainsAny (2 overloads)
  • DoesNotContainAny (2 overloads)
  • IsEmpty
  • IsNotEmpty
  • HasLength
  • DoesNotHaveLength
  • IsLongerThan
  • IsNotLongerThan
  • IsLongerOrEqual
  • IsNotLongerOrEqual
  • IsShorterThan
  • IsNotShorterThan
  • IsShorterOrEqual
  • IsNotShorterOrEqual
String checks

There is a separate group of methods that allow validation of strings. There are currently 16 methods:

  • Contains
  • DoesNotContain
  • StartsWith (2 overloads)
  • DoesNotStartWith (2 overloads)
  • EndsWith (2 overloads)
  • DoesNotEndWith (2 overloads)
  • HasLength
  • DoesNotHaveLength
  • IsEmpty
  • IsNotEmpty
  • IsNullOrEmpty
  • IsNotNullOrEmpty
  • IsLongerThan
  • IsLongerOrEqual
  • IsShorterThan
  • IsShorterOrEqual
Evaluations

For all checks that just can’t be done with the methods mentioned above, the Evaluate method overloads are the solution. The first overload checks a Boolean argument and throws when it equals false. The second overload runs a specified Expression that returns a Boolean. This allows the developer to define a lambda expression. These two overloads allow to literally express any pre- or postcondition you want. There is one method:

  • Evaluate (2 overloads)

Here are two examples of the use of Evaluate:

// Evaluate with boolean
s.Requires("s").Evaluate(
s.StartsWith("hello") || s.EndsWith("world"));
// Evaluate using a lambda expression
s.Requires("s").Evaluate((str) =>
str.StartsWith("hello") || str.EndsWith("world"));

The two examples look quite a lot, but they’re actually quite different. The first example uses a boolean argument and the evaluation is very fast. However, it lacks a good exception message on failure:
s should be valid. The actual value is 'world hello'. Parameter name: s

The overload with the lambda expression is the exact opposite. It has to compile the given expression on every call and it therefore shouldn’t be used in performance sensitive parts of your code. But when used, the thrown exception has a very descriptive exception message:
'(str.StartsWith("hello") || str.EndsWith("world"))' should hold for s. The actual value is 'world hello'. Parameter name: s

Designed Behavior

Warning: This post is based on a pre-release of CuttingEdge.Conditions. While most of the concepts and behavior of the library is the same, the final release has some changes, of which most noticeably is the removal of the extension method behavior of the Requires() and Ensures() methods. Please note that the following syntax isn’t supported anymore: c.Requires().IsNotNull(). Instead the proposed syntax is Condition.Requires(c).IsNotNull(). Please keep that in mind while reading this article.

The CuttingEdge.Conditions API has been designed very carefully and I hope it behaves in the most intuitive way (you can more read about the design process and some backgrounds here). Below are some design decisions I made and the consequences of the code a developer has to write. Please let me know if this behavior is odd and needs to change.

A null value is considered smaller than any non null value.

This is consistent with the way object are validated within the .NET framework. The following lines of code show this behavior:

int? i = null;
// Next check will pass, because null is
// smaller than Int32.MinValue.
i.Requires().IsLessThan(Int32.MinValue);
A collection argument that holds a null reference is considered to contain zero elements.

This behavior may seem odd to users in the first place. A user of the library would possibly expect the API to throw an ArgumentNullException when a null reference is checked. There are however some cases, where throwing an exception would not be the expected behavior and this would therefore lead to an inconsistent API. Besides that, choosing null references to always fail, would limit the usefulness of the API. A user may find that null references are valid values in some cases. i.e., the user could define a precondition as follows: “a collection should have less than five elements and may be a null reference”. He wouldn’t be able to express this precondition using the collection methods of the API. He would be forced to use the Evaluate mechanism, which is clearly less readable and doesn’t throw a descriptive exception message.

The following example shows the designed behavior:

IEnumerable c = null;
// Both HasLength and IsEmpty checks will
// pass, because c equals null.
c.Requires().HasLength(0);
c.Requires().IsEmpty();
// IsShorterThan will pass, because c is
// clearly shorter than 5 characters.
c.Requires().IsShorterThan(5);
// When c equals null, it doesn't contain
// any elements, so we'd expect
// the next lines to pass.
c.Requires().DoesNotContain("value");
c.Requires().DoesNotContainAny(new string[] { "a", "b" });

When a null reference is not a valid value, a developer can use the IsNotNull() method. This is not needed in cases where even a null reference is an invalid state. The following example shows some examples:

// Use IsNotNull() to check for null.
c.Requires().IsNotNull().IsShorterThan(5);
// IsNotEmpty() will throw an ArgumentNullException
// when c equals null and an ArgumentException
// when c is empty but not null.
c.Requires().IsNotEmpty();
A checked collection is considered to hold all of the specified values when that list of values contains no elements.

A call to ContainsAll method will always succeed when the specified list of values is empty. The following example shows this behavior:

Collection<int> c =
new Collection<int> { 1, 2, 3, 4, 5 };
// All checks will pass.
c.Requires().ContainsAll(new int[] { 1, 2 });
c.Requires().ContainsAll(new int[] { 1 });
c.Requires().ContainsAll(new int[0] { });
c.Requires().ContainsAll(null);
A checked collection is considered to hold not any of the specified values when that set contains no elements.

A call to ContainsAny will always fail when the specified list of values is empty. The following example shows this behavior:

Collection<int> c = new Collection<int> { 1, 2, 3, 4, 5 };
// Next two checks will pass.
c.Requires().ContainsAny(new int[] { 1, 9 });
c.Requires().ContainsAny(new int[] { 1 });
// Next two checks will fail, because the
// specified lists are empty.
c.Requires().ContainsAny(new int[0] { });
c.Requires().ContainsAny(null);
A null string is considered to have a length of 0 characters.

The story here is about the same as with the collections described earlier. The following example shows this behavior:

string s = null;
// Next check passes.
s.Requires().HasLength(0);
s.Requires().IsShorterThan(5);
s.Requires().IsLongerThan(-1);
// You should use IsEmpty() or IsNotNull()
// if null is not a valid value.
s.Requires().IsEmpty();
s.Requires().IsNotNull().HasLength(0);
A null string and an empty string are not considered equal and there are various methods to check.

The following example shows this behavior:

string s = null;
// The following checks will fail.
s.Requires().IsEqualTo(String.Empty);
s.Requires().IsEmpty();
// The following checks will pass.
s.Requires().IsNullOrEmpty();
s.Requires().IsNull();
A null string is only contained within another string if it also is a null string. This is valid for methods like StartsWith, EndsWith and Contains.

This simply is how the String methods of .NET work. Implementing it differently would not only be pretty difficult, but users wouldn’t expect this behavior. The following example shows this behavior:

string s = null;
// All checks below will fail.
s.Requires().Contains(String.Empty);
String.Empty.Requires().Contains(null);
String.Empty.Requires().EndsWith(null);
Checking a null reference to be of a certain type will always succeed. The same is valid for the reverse operation.

Warning: The designed behavior has changed. Contrary to what the text below describes, in the final release a call to IsOfType with a null argument will always fail, and a call to IsNotOfType with a null argument will now always succeed. The code below is counterintuitive, and the final API mimics the behavior of the C# 'is' operator.

Here we see the same issue as we’ve seen before with the collections and strings. The user may decide a null reference is valid. If not, he will have to add a IsNotNull check to his code. The following example shows this behavior:
object o = null;
// Both checks below will pass, because o equals null.
o.Requires().IsOfType(typeof(string));
o.Requires().IsNotOfType(typeof(object));
// Use IsNotNull() when null is not a valid value.
o.Requires().IsNotNull().IsOfType(typeof(string));
o.Requires().IsNotNull().IsNotOfType(typeof(object));
Wrapping it up

Again I want to say that this release is a beta and future releases will change, hopefully based on your feedback. In future posts I’ll write about possible issues and give more examples of the use of the framework. Please download the library, give it a try and feedback about it!

Happy validating ;-)

- .NET General, C#, CuttingEdge.Conditions, LINQ - seven comments / No trackbacks - §

The code samples on my weblog are colorized using javascript, but you disabled javascript (for my website) on your browser. If you're interested in viewing the posted code snippets in color, please enable javascript.

seven comments:

i love it, saved me a ton of time, thanks.

:)
buaziz - 27 07 08 - 10:18

Hi,
thanks for this usefull library. I have one question, though: I have often the requirement for two arrays to have the same length. If I use array1.Requires("array1").HasLength(array2.Length) to check this condition, the exception I get is not very satisfying for me (e.g. "array1 should contain 3 elements, array1 contains currently 4 elements"). Is there another way of checking the dimensions of two arrays, or is there a possibility to pass a custom exception message?
multani - 14 09 08 - 13:31

Hi Multani,

Thank you for your interest in CuttingEdge.Conditions.

I'm afraid you're out of luck with the current (beta 1) release of CuttingEdge.Conditions. There is no possibility to pass a custom Exception message on requires. I'm working on a next version and that version will have better support for customizing the exception messages, but it will take a couple of weeks before it will be released.

In the meantime I'm afraid you have to write your precondition like this:
if (array1.Length != array2.Length) throw new ArgumentException("...", "array1");
Of course you can still use the library to do your null checks :-)
Steven (URL) - 14 09 08 - 19:17

Hi Steven,

thank you for your answer. Nice to hear that you are planning to support custom exception messages. I'm looking forward to see the next edition of CuttingEdge.Conditions...
Multani - 16 09 08 - 17:21

Thanks for the great library!
Patrick Wolf (URL) - 25 02 09 - 00:47

Hey,
I tried to implement you example:
id.Requires("id").IsGreaterThan(0);
But my compiler errors: Error 1 'int' does not contain a definition for 'Requires' and no extension method 'Requires' accepting a first argument of type 'int' could be found (are you missing a using directive or an assembly reference?)

What works fine is the following: CuttingEdge.Conditions.Condition.Requires(id, "id").IsGreaterThan(0);

I set the correct reference and also the correct using CuttingEdge.Conditions;

What I'm doing wrong?
Helmut Meyer - 17 12 09 - 18:11

Hi Helmut,

The code snippets in this blog post are based on a pre-release of CuttingEdge.Conditions. A lot has changed after this post and in the final release. I decided to remove the extension method behavior of the Requires() method. Therefore, statements such as ‘id.Requires()’ will not compile anymore. The supported way of writing validations is Condition.Requires(id, "id").IsGreaterThan(0), as you already found out the hard way. You can read more on why I changed this, and what you should do if you want that fluent syntax back here: http://www.cuttingedge.it/blogs/steven/p..

I'll put a warning in this blog post to warn people about the changed behavior. Thank you for noticing.
Steven (URL) - 17 12 09 - 19:29


No trackbacks:

Trackback link:

Please enable javascript to generate a trackback url


  
Remember personal info?

/

Before sending a comment, you have to answer correctly a simple question everyone knows the answer to. This completely baffles automated spam bots.
 

  (Register your username / Log in)

Notify:
Hide email:

Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.