using System.ComponentModel.DataAnnotations.Resources; using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace System.ComponentModel.DataAnnotations { /// /// Used for specifying a range constraint /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "We want it to be accessible via method on parent.")] [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want users to be able to extend this class")] public class RangeAttribute : ValidationAttribute { /// /// Gets the minimum value for the range /// public object Minimum { get; private set; } /// /// Gets the maximum value for the range /// public object Maximum { get; private set; } /// /// Gets the type of the and values (e.g. Int32, Double, or some custom type) /// public Type OperandType { get; private set; } private Func Conversion { get; set; } /// /// Constructor that takes integer minimum and maximum values /// /// The minimum value, inclusive /// The maximum value, inclusive public RangeAttribute(int minimum, int maximum) : this() { this.Minimum = minimum; this.Maximum = maximum; this.OperandType = typeof(int); } /// /// Constructor that takes double minimum and maximum values /// /// The minimum value, inclusive /// The maximum value, inclusive public RangeAttribute(double minimum, double maximum) : this() { this.Minimum = minimum; this.Maximum = maximum; this.OperandType = typeof(double); } /// /// Allows for specifying range for arbitrary types. The minimum and maximum strings will be converted to the target type. /// /// The type of the range parameters. Must implement IComparable. /// The minimum allowable value. /// The maximum allowable value. public RangeAttribute(Type type, string minimum, string maximum) : this() { this.OperandType = type; this.Minimum = minimum; this.Maximum = maximum; } private RangeAttribute() : base(() => DataAnnotationsResources.RangeAttribute_ValidationError) { } private void Initialize(IComparable minimum, IComparable maximum, Func conversion) { if (minimum.CompareTo(maximum) > 0) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.RangeAttribute_MinGreaterThanMax, maximum, minimum)); } this.Minimum = minimum; this.Maximum = maximum; this.Conversion = conversion; } /// /// Returns true if the value falls between min and max, inclusive. /// /// The value to test for validity. /// true means the is valid /// is thrown if the current attribute is ill-formed. internal override bool IsValid(object value) { // Validate our properties and create the conversion function this.SetupConversion(); // Automatically pass if value is null or empty. RequiredAttribute should be used to assert a value is not empty. if (value == null) { return true; } string s = value as string; if (s != null && String.IsNullOrEmpty(s)) { return true; } object convertedValue = null; try { convertedValue = this.Conversion(value); } catch (FormatException) { return false; } catch (InvalidCastException) { return false; } catch (NotSupportedException) { return false; } IComparable min = (IComparable)this.Minimum; IComparable max = (IComparable)this.Maximum; return min.CompareTo(convertedValue) <= 0 && max.CompareTo(convertedValue) >= 0; } /// /// Override of /// /// This override exists to provide a formatted message describing the minimum and maximum values /// The user-visible name to include in the formatted message. /// A localized string describing the minimum and maximum values /// is thrown if the current attribute is ill-formed. public override string FormatErrorMessage(string name) { this.SetupConversion(); return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, this.Minimum, this.Maximum); } /// /// Validates the properties of this attribute and sets up the conversion function. /// This method throws exceptions if the attribute is not configured properly. /// If it has once determined it is properly configured, it is a NOP. /// private void SetupConversion() { if (this.Conversion == null) { object minimum = this.Minimum; object maximum = this.Maximum; if (minimum == null || maximum == null) { throw new InvalidOperationException(DataAnnotationsResources.RangeAttribute_Must_Set_Min_And_Max); } // Careful here -- OperandType could be int or double if they used the long form of the ctor. // But the min and max would still be strings. Do use the type of the min/max operands to condition // the following code. Type operandType = minimum.GetType(); if (operandType == typeof(int)) { this.Initialize((int)minimum, (int)maximum, v => Convert.ToInt32(v, CultureInfo.InvariantCulture)); } else if (operandType == typeof(double)) { this.Initialize((double)minimum, (double)maximum, v => Convert.ToDouble(v, CultureInfo.InvariantCulture)); } else { Type type = this.OperandType; if (type == null) { throw new InvalidOperationException(DataAnnotationsResources.RangeAttribute_Must_Set_Operand_Type); } Type comparableType = typeof(IComparable); if (!comparableType.IsAssignableFrom(type)) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, DataAnnotationsResources.RangeAttribute_ArbitraryTypeNotIComparable, type.FullName, comparableType.FullName)); } Func conversion = value => (value != null && value.GetType() == type) ? value : Convert.ChangeType(value, type, CultureInfo.CurrentCulture); IComparable min = (IComparable)conversion(minimum); IComparable max = (IComparable)conversion(maximum); this.Initialize(min, max, conversion); } } } } }