Discoverable RangeValidation (revisited)
The RangeValidation code I created turned out to be of limited usefulness because Attributes are pretty picky about what they can take as arguments. This meant that I couldn't even create the decimal
version, which was the original motivator. Revisiting the design I refactored the range validation code into a generic Attribute paired with a Helper class using generics to be type-safe. This also changed the usage to the following:
/// <summary>
/// Valid from 0 to 10
/// </summary>
[ValidateRange(0, 10)]
public decimal Rating
{
get { return rating; }
set
{
ValidateRange<decimal>.Validate(value);
rating = value;
}
}
/// <summary>
/// Valid from 0 to Int.MaxValue
/// </summary>
[ValidateRange(Min = 0)]
public int Positive
{
get { return positive; }
set
{
ValidateRange<int>.Validate(value);
positive = value;
}
}
The ValidateRange
class now simply takes the values and stores them as objects, leaving interpretation to the generic class ValidateRange<T>
.
[AttributeUsage(AttributeTargets.Property)]
public class ValidateRange : Attribute
{
object min = null;
object max = null;
/// <summary>
/// Validation with both an upper and lower bound
/// </summary>
/// <param name="min"></param>
/// <param name="max"></param>
public ValidateRange(object min, object 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 ValidateRange()
{
}
public object Min
{
get { return min; }
set { min = value; }
}
public object Max
{
get { return max; }
set { max = value; }
}
}
The generic class ValidateRange<T>
still has the static Validate
accessor which has to be used in the property that is tagged with the ValidateRange
attribute, since it divines the property from the StackTrace
. But given a PropertyInfo
instance, the class can also be instantiated independently, so that Validation code can inspect the range requirements and communicate them to the enduser, rather than only reacting to the exceptions thrown by the Property in question.`` ```
public class ValidateRange<T> where T : IComparable
{
/// <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(T 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));
ValidateRange<T> validator = new ValidateRange<T>(property);
validator.CheckRange(value);
}
bool hasMin = false;
bool hasMax = false;
T min;
T max;
PropertyInfo property;
public ValidateRange(PropertyInfo property)
{
this.property = property;
ValidateRange validationAttribute = null;
try
{
// we make the assumption that if the caller is using
// this object, they defined the attribute on the passed property
validationAttribute
= (ValidateRange)property.GetCustomAttributes(typeof(ValidateRange), false)[0];
}
catch (Exception e)
{
throw new InvalidOperationException(
"ValidateRange Attribute not defined", e);
}
if (validationAttribute.Min != null)
{
hasMin = true;
min = (T)Convert.ChangeType(validationAttribute.Min, min.GetType());
}
if (validationAttribute.Max != null)
{
hasMax = true;
max = (T)Convert.ChangeType(validationAttribute.Max, max.GetType());
}
}
public bool HasMin
{
get { return hasMin; }
}
public bool HasMax
{
get { return hasMax; }
}
public T Min
{
get { return min; }
}
public T Max
{
get { return max; }
}
private void CheckRange(T value)
{
if (HasMax && value.CompareTo(max) == 1)
{
throw new ArgumentOutOfRangeException(
property.Name,
"Value cannot be greater than " + max);
}
else if (HasMin && value.CompareTo(min) == -1)
{
throw new ArgumentOutOfRangeException(
property.Name,
"Value cannot be less than " + min);
}
}
}