P3.NET

Fluent Argument Validation

Quick, what is the syntax for ArgumentException? How about ArgumentOutOfRangeException? Did you know the arguments are swapped between the two? In one case the parameter name comes first while the other has the message first. Getting this wrong is so easy that it happens a lot.

Fluent APIs have become very popular for a variety of reasons the least of which is that they are easy to read. By combining a simple instance class with extension methods then they become easy to discover and extend as well. One of the pain points for many developers is argument validation. Everyone knows that you should validate arguments but such code tends to be a lot of boilerplate code that is either not maintained or simply wrong.

In this post I will introduce a system for fluent argument validation that we use at my company. The code is not necessarily any better than what you might already be writing. It is shorter, easier to read and easier to extend at the cost of slight performance overhead at runtime. Given the advent of LINQ code everywhere this is probably not an issue for most code.

Representing Arguments

In order to generate meaningful exceptions we need to first capture information about the argument. The three pieces of information we have are: name, type and value. We will create a simple Argument class to represent the argument definition. If we later want to capture additional information we can store it here. The only thing we really need is the argument name so we’ll pass that as a parameter to the constructor.

public class Argument
{
   public Argument ( string name )
   {
      Name = name ?? "";
   }

   public string Name { get; private set; }
}

To capture the value we’ll use a generic type.

public class Argument<T>
{
   public Argument ( string name, T value )
   {
      Name = name ?? "";

      Value = value;
   }

   public string Name { get; private set; }

   public T Value { get; private set; }
}

Notice that it does not derive from Argument. To make the code easier to read we will expose a method off of Argument that creates an instance of the generic instance.

public class Argument
{
   ...

   public ArgumentConstraint<T> WithValue<T> ( T value )
   {
      return new ArgumentConstraint<T>(new Argument<T>(Name, value));
   }
}

//Example usage
var arg = new Argument("arg1").WithValue(2);

As with most fluent interfaces we need to create a starting point into the API but still allow for extensibility. In that regard we will create a static class that exposes a couple of methods to create the argument. While someone can create the instance using new, the fluent API reads better.

public static class Verify
{
   public static Argument Argument ( string name )
   {
      return new Argument(name);
   }

   public static ArgumentConstraint<T> Argument<T> ( string name, T value )
   {
      return new ArgumentConstraint<T>(new Argument<T>(name, value));
   }
}

And how it might be used in a function.

void Foo ( string name, int id )
{
   Verify.Argument("name").WithValue("First").SomeCondition();

   Verify.Argument("id", id).SomeCondition();

   ...
}

Argument Constraints

In the previous code the Argument class was not used directly but rather as a parameter to an argument constraint. As mentioned earlier, Argument represents the argument definition. When using the API the argument constraint is where the real work is. An argument constraint separates the definition from the constraints (conditions) to be verified. The base constraint class, which is generic, provides a couple of basic constraints using function delegates. In each case the constraint tests whether the condition is true or not and throws an ArgumentException if it is not. If the condition is true then a new constraint is returned.

public class ArgumentConstraint<T>
{
   public ArgumentConstraint ( Argument<T> argument )
   {
      if (argument == null)
         throw new ArgumentNullException("argument");

      Argument = argument;
   }

   public Argument<T> Argument { get; private set; }

   public AndArgumentConstraint<T> Is ( Func<T, bool> condition )
   {
      return Is(condition, "Argument is invalid.");
   }

   public AndArgumentConstraint<T> Is ( Func<T, bool> condition, string message, params object[] args )
   {
      if (condition == null)
         throw new ArgumentNullException("condition");

      if (!condition(m_argument.Value))
         throw new ArgumentException(String.Format(message, args), Argument.Name);

      return new AndArgumentConstraint<T>(this);
   }

   public AndArgumentConstraint<T> IsNot ( Func<T, bool> condition )
   {
      return IsNot(condition, "Argument is invalid.");
   }

   public AndArgumentConstraint<T> IsNot ( Func<T, bool> condition, string message, params object[] args )
   {
      if (condition == null)
         throw new ArgumentNullException("condition");

      if (condition(m_argument.Value))
         throw new ArgumentException(String.Format(message, args), Argument.Name);

      return new AndArgumentConstraint<T>(this);
   }
}

The AndArgumentConstraint type is a wrapper type that simply allows chaining constraints together in a fluent manner.

public class AndArgumentConstraint<T>
{
   public AndArgumentConstraint ( ArgumentConstraint<T> parent )
   {
      if (parent == null)
         throw new ArgumentNullException("parent");

      Parent = parent;
   }

   public ArgumentConstraint<T> And { get; private set; }
}

Here’s how you might use constraints.

void Foo ( string name, int id )
{
   Verify.Argument("name").WithValue(name)
             .IsNot(x => String.IsNullOrEmpty(x))
             .And.IsNot(x => x == "Bob");
   ...
}

Adding New Constraints

The base constraints are simply stepping stones to creating type-focused constraints. For example we can now create a series of string constraints that ensure that a string argument is not null or empty.

public static class StringArgumentConstraintExtensions
{
   public static AndArgumentConstraint<string> IsNotNullOrEmpty ( this ArgumentConstraint<string> source )
   {
      if (source.Argument.Value == null)
         throw new ArgumentNullException(source.Argument.Name);

      if (source.Argument.Value.Length == 0)
         throw new ArgumentException("String cannot be empty.", source.Argument.Name);

      return new AndArgumentConstraint<string>(source);
   }
}

//Example usage
void Foo ( string name )
{
   Verify.Argument("name").WithValue(name).IsNotNullOrEmpty();
   ...
}

We can even generalize for things like IComparable.

public static class ComparableArgumentConstraintExtensions
{
   public static AndArgumentConstraint<T> IsGreaterThan<T> ( this ArgumentConstraint<T> source, T value )
         where T : IComparable<T>
   {
      return IsGreaterThan<T>(source, value, "Value must be greater than {0}", value);
   }
}

//Example usage
void Foo ( int id )
{
   Verify.Argument("id").WithValue(id).IsGreaterThan(0);

   ...
}

We can specialize this even further for numeric values.

public static class IntegralArgumentConstraintExtensions
{
  public static AndArgumentConstraint<int> IsGreaterThanZero ( this ArgumentConstraint<int> source )
   {
      return source.IsGreaterThan(0, "Value must be greater than 0", value);
   }
}

//Example usage
void Foo ( int id )
{
   Verify.Argument("id").WithValue(id).IsGreaterThanZero();
   ...
}

Customizing Messages

For the most part argument exception messages are pretty generic and can be generated without any assistances. For the cases where you want to generate a more specific message then the extension methods allow you to specify a message and optional arguments. The custom message will override the default argument message that would have been used.

When creating a constraint the argument definition is available through the Argument property of the constraint. This allows a constraint to use the argument definition, like the name, in messaging.

Extending an Argument Definition

Beyond adding new constraints there are other places where the code can be extended. One area is with the argument definition itself. As it stands now a constraint is returned from the Verify methods which prevent extending the definition. What should probably happen is that Verify should return the argument definition. This would allow extensions to the definition if new information is to be captured. But at some point we still need to switch from a definition to a constraint. A new method (i.e. Is, IsNot) would need to be added to the Argument class that converts it to a constraint. This makes it clear when the transition occurs but it introduces a wrinkle when adding multiple constraints as the transition methods would need to be added there as well (for consistency). Still it is probably a worthwhile change to make. Perhaps I’ll update the code to do that at some point.

Download the Code