Wednesday, February 27, 2008

Guarding with extension methods

I came up with a little - as far as I'm concerned - cool library consisting of extension methods used to guard input parameters of public methods.

It's always a good idea to validate arguments of public methods and throw exceptions if the validation fails. If you require that a parameter is not null, throw an "ArgumentNullException" if the specified argument is null, this makes debugging so much easier and helps other developers that uses your API. To do this in all public methods is a painstaking and repetitive job but my little library makes it just a bit easier.

For example to validate that the string argument of the following method is not null or empty we'd have to write code something like this:

public static char Foo(string bar)
{
    if (bar == null)
        throw new ArgumentNullException("bar");
    if (bar.Length == 0)
        throw new ArgumentOutOfRangeException("bar");

    return bar[0];
}

Now with my extension methods you'd instead write the following:

public static char Foo(string bar)
{
    bar.ExceptionIfNullOrEmpty("bar");
    return bar[0];
}

We can perform other validations as well, for example, we can validate that a value is within a given range:

public static bool Foo(int i)
{
    // The parameter "i" must be within the range 0-10
    i.ExceptionIfOutOfRange(0, 10, "i"); 
}

By using generics and generic constraints the extension methods only appear on values of types they are applicable to. For example the method that checks if a value is within a specified range is only applicable to values of types that implements the IComparable(T)-interface.

public static void ExceptionIfOutOfRange<T>(this T value,
    T lowerBound, T upperBound, string argumentName) where T : IComparable<T>
{
    if (value.CompareTo(lowerBound) < 0 || value.CompareTo(upperBound) > 0)
    {
        throw new ArgumentOutOfRangeException(argumentName);
    }
}

This means that the method will only be shown in the Intellisense on values that it can be applied to:

image

image

Note that in the first case the variable "o" is of the type "object", and the only applicable method is the one that checks for null. In the second case "o" is of the type "int" and now there are two applicable methods, the one that checks if arguments that implements IComparable(T) are within a specified range and the one that checks if an index (int) is within a specified range. Since int is a value type it can't be null so the ExceptionIfNull(T)-method is not applicable, this is little piece of magic is accomplished by the generic constraint "where T : class" that set on the ExceptionIfNull(T)-method.

And here's the code:

#region Using Directives
using System;
using System.Globalization; 
#endregion

namespace Minimal
{
    /// <summary>
    /// Provides helper extension methods for conditional throwing of
    /// exceptions. Mainly used for guarding input parameters.
    /// </summary>
    public static class ThrowHelper
    {
        /// <summary>
        /// Throws an ArgumentNullException if the value is null.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value to check for null.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <exception cref="ArgumentNullException">The value is null.</exception>
        public static void ExceptionIfNull<T>(this T value, string argumentName) where T : class
        {
            if (value == null)
            {
                throw new ArgumentNullException(argumentName);
            }
        }

        /// <summary>
        /// Throws an ArgumentNullException if the value is null.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value to check for null.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <param name="message">A message for the exception.</param>
        /// <exception cref="ArgumentNullException">The value is null.</exception>
        public static void ExceptionIfNull<T>(this T value, string argumentName, string message) where T : class
        {
            if (value == null)
            {
                throw new ArgumentNullException(argumentName, message);
            }
        }

        /// <summary>
        /// Throws an ArgumentNullException if the specified value is null or
        /// throws an ArgumentOutOfRangeException if the specified value is an
        /// empty string.
        /// </summary>
        /// <param name="value">The string to check for null or empty.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <exception cref="ArgumentNullException">The value is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The value is an empty string.</exception>
        public static void ExceptionIfNullOrEmpty(this string value,
            string argumentName)
        {
            value.ExceptionIfNull("value");

            if (value.Length == 0)
            {
                throw new ArgumentOutOfRangeException(argumentName,
                    string.Format(CultureInfo.InvariantCulture,
                        "The length of the string '{0}' may not be 0.", argumentName ?? string.Empty));
            }
        }

        /// <summary>
        /// Throws an ArgumentOutOfRangeException if the specified value is not within the range
        /// specified by the lowerBound and upperBound parameters.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value to check that it's not out of range.</param>
        /// <param name="lowerBound">The lowest value that's considered being within the range.</param>
        /// <param name="upperBound">The highest value that's considered being within the range.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <exception cref="ArgumentOutOfRangeException">The value is not within the given range.</exception>
        public static void ExceptionIfOutOfRange<T>(this T value,
            T lowerBound, T upperBound, string argumentName) where T : IComparable<T>
        {
            if (value.CompareTo(lowerBound) < 0 || value.CompareTo(upperBound) > 0)
            {
                throw new ArgumentOutOfRangeException(argumentName);
            }
        }


        /// <summary>
        /// Throws an ArgumentOutOfRangeException if the specified value is not within the range
        /// specified by the lowerBound and upperBound parameters.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value to check that it's not out of range.</param>
        /// <param name="lowerBound">The lowest value that's considered being within the range.</param>
        /// <param name="upperBound">The highest value that's considered being within the range.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <param name="message">A message for the exception.</param>
        /// <exception cref="ArgumentOutOfRangeException">The value is not within the given range.</exception>
        public static void ExceptionIfOutOfRange<T>(this T value,
            T lowerBound, T upperBound, string argumentName, string message) where T : IComparable<T>
        {
            if (value.CompareTo(lowerBound) < 0 || value.CompareTo(upperBound) > 0)
            {
                throw new ArgumentOutOfRangeException(argumentName, message);
            }
        }

        /// <summary>
        /// Throws an IndexOutOfRangeException if the specified index is not within the range
        /// specified by the lowerBound and upperBound parameters.
        /// </summary>
        /// <param name="index">The index to check that it's not out of range.</param>
        /// <param name="lowerBound">The lowest value considered being within the range.</param>
        /// <param name="upperBound">The highest value considered being within the range.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <exception cref="IndexOutOfRangeException">The index is not within the given range.</exception>
        public static void ExceptionIfIndexOutOfRange(this int index,
            int lowerBound, int upperBound, string argumentName)
        {
            if (index < lowerBound || index > upperBound)
            {
                throw new IndexOutOfRangeException();
            }
        }

        /// <summary>
        /// Throws an IndexOutOfRangeException if the specified index is not within the range
        /// specified by the lowerBound and upperBound parameters.
        /// </summary>
        /// <param name="index">The index to check that it's not out of range.</param>
        /// <param name="lowerBound">The lowest value considered being within the range.</param>
        /// <param name="upperBound">The highest value considered being within the range.</param>
        /// <param name="argumentName">The name of the argument the value represents.</param>
        /// <exception cref="IndexOutOfRangeException">The index is not within the given range.</exception>
        public static void ExceptionIfIndexOutOfRange(this long index,
            long lowerBound, long upperBound, string argumentName)
        {
            if (index < lowerBound || index > upperBound)
            {
                throw new IndexOutOfRangeException();
            }
        }
    }
}

1 comment:

  1. Too bad after an hour I discovered extensions are not supported in VS2005 (at least not out of the box). Cool tool though.

    ReplyDelete