Skip to content

Discoverable RangeValidation

One aspect of writing code that has always bothered me is how to communicate legal values. Sure, if the type that a Property accepts is of your own making then the onus is on the one who created the instance to make sure it's a legal value. But so many Properties are value types and not every value is valid. Now, you say that's why we have validators, it's their job to check the validity and tell the user what they did wrong. Fine. That's the UI element for communicating the problem. But what always seemed wrong was that in your UI code you specified what values were legal. And if the use of business level logic in UI code wasn't bad enough, then how about the question of where the valid values came from anyway? Did the programmer inately know what was valid or did the values come from the documentation? Neither allows programatic discovery.

Attributes for discoverability

Now, maybe i'm late to the party and this has been obvious to everyone else, but at least in my exposure to .NET programming Attributes have only cropped up occasionally--when the platform provided or required them (primarily Xml Serialization). But I'm quickly coming around to creating my own. They're an easy way to attach all sorts of meta data that would usually be buried in a documentation paragraph.

Anyway, the other day I was creating an object model that included a number of numeric properties with limited legal ranges. So I a) documented this range limitation and _b)_created a helper class that would range check my value and throw ArgumentOutOfRangeException as needed. Standard stuff. Then I exposed the class via a PropertyGrid which got me add Design Time support via attributes.

If you haven't ever created a User Control or Custom Control for others to use, you may not be familiar with these attributes. Once you drag your own Control into a form all your public properties become visible as properties in the Property window under Misc. This isn't always desirable or at least not self-explanatory. To help make your controls easier to use, you can use the [Browsable(false)] to mark your property as inaccessible for design time support. If you do want to use them at design time, Description and Category are two important attributes for making your Control more accessible, providing a description and a proper Category label for your property, respectively. In general, there are a lot of useful Attributes in System.ComponentModel for Design Time support.

Adding these Attributes, I decided that the same mechanism could be used to communicate valid values for any concerned party to divine. It looks like this had already been considered by MS, but only as part of Powershell, with the ValidateRange Attribute. So I wrote my own interpretation, complete with static helper to perform validation. This last part makes for maintainable code, but i'm not sure of performance overhead introduced, so use with caution.

The implementation is type specific and shown here as an Int range Validator. Unfortunately classes deriving from Attribute can't be Generic. Another nuisance is that Nullable types are not valid as parameters for Attributes.

namespace Droog.Validation
{
  [AttributeUsage(AttributeTargets.Property)]
  public class ValidateIntRange : Attribute
  {
    /// <summary>
    /// Must be used from within the set part of the Property.
    /// It divines the Caller to perform validation.
    /// </summary>
    /// <param name="value"></param>
    static public void Validate(int value)
    {
      StackTrace trace = new StackTrace();
      StackFrame frame = trace.GetFrame(1);
      MethodBase methodBase = frame.GetMethod();
      // there has to be a better way to get PropertyInfo from methodBase
      PropertyInfo property
        = methodBase.DeclaringType.GetProperty(methodBase.Name.Substring(4));
      ValidateIntRange rangeValidator = null;
      try
      {
        // we make the assumption that if the caller is using
        // this method, they defined the attribute
        rangeValidator
          = (ValidateIntRange)property.GetCustomAttributes(typeof(ValidateIntRange), false)[0];
      }
      catch (Exception e)
      {
        throw new InvalidOperationException(
          "Cannot call Validate if the ValidateIntRange Attribute is not defined", e);
      }
      rangeValidator.Validate(value, property);
    }

    int? min = null;
    int? max = null;

    /// <summary>
    /// Validation with both an upper and lower bound
    /// </summary>
    /// <param name="min"></param>
    /// <param name="max"></param>
    public ValidateIntRange(int min, int max)
    {
      this.min = min;
      this.max = max;
    }

    /// <summary>
    /// Validation with only upper or lower bound.
    /// Must also specify named parameter Min or Max
    /// </summary>
    public ValidateIntRange()
    {
    }

    public bool HasMin
    {
      get { return (min == null) ? false : true; }
    }

    public bool HasMax
    {
      get { return (max == null) ? false : true; }
    }

    public int Min
    {
      get { return (int)min; }
      set { min = value; }
    }

    public int Max
    {
      get { return (int)max; }
      set { max = value; }
    }

    private void Validate(int value, PropertyInfo property)
    {
      if (max != null && value > max)
      {
        throw new ArgumentOutOfRangeException(
          property.Name,
          "Value cannot be greater than " + max);
      }
      else if (min != null && value < min)
      {
        throw new ArgumentOutOfRangeException(
          property.Name,
          "Value cannot be less than " + min);
      }
    }
  }
}

To properly take advantage of this Attribute, we must both attach the attribute to a property and call the Validation method:

/// <summary>
/// Valid from 0 to 10
/// </summary>
[ValidateIntRange(0, 10)]
public int Rating
{
  get { return rating; }
  set
  {
    ValidateIntRange.Validate(value);
    rating = value;
  }
}

/// <summary>
/// Valid from 0 to Int.MaxValue
/// </summary>
[ValidateIntRange(Min = 0)]
public int Positive
{
  get { return positive; }
  set
  {
    ValidateIntRange.Validate(value);
    positive = value;
  }
}

Voila, Properties with range validation and anyone using this class can programatically determine the valid range and use that information in their UI, etc.

Update:

Turns out I was a bit quick on the trigger. Attributes are even more limited than I thought in the type of data that they accept. The moment i tried to create decimal version of the above, I got stuck. So what I will probably do is create ValidateRange which takes strings and then uses Reflection on the caller to determine what type the values should be cast to. It'll be more flexible as well, as I'll need only one ValidateRange instead of one per numeric type.