1 using System.ComponentModel.DataAnnotations.Resources;
2 using System.Diagnostics.CodeAnalysis;
3 using System.Globalization;
4 
5 namespace System.ComponentModel.DataAnnotations {
6     /// <summary>
7     /// Used for specifying a range constraint
8     /// </summary>
9     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
10     [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "We want it to be accessible via method on parent.")]
11     [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want users to be able to extend this class")]
12     public class RangeAttribute : ValidationAttribute {
13         /// <summary>
14         /// Gets the minimum value for the range
15         /// </summary>
16         public object Minimum { get; private set; }
17 
18         /// <summary>
19         /// Gets the maximum value for the range
20         /// </summary>
21         public object Maximum { get; private set; }
22 
23         /// <summary>
24         /// Gets the type of the <see cref="Minimum"/> and <see cref="Maximum"/> values (e.g. Int32, Double, or some custom type)
25         /// </summary>
26         public Type OperandType { get; private set; }
27 
28         private Func<object, object> Conversion { get; set; }
29 
30         /// <summary>
31         /// Constructor that takes integer minimum and maximum values
32         /// </summary>
33         /// <param name="minimum">The minimum value, inclusive</param>
34         /// <param name="maximum">The maximum value, inclusive</param>
RangeAttribute(int minimum, int maximum)35         public RangeAttribute(int minimum, int maximum)
36             : this() {
37             this.Minimum = minimum;
38             this.Maximum = maximum;
39             this.OperandType = typeof(int);
40         }
41 
42         /// <summary>
43         /// Constructor that takes double minimum and maximum values
44         /// </summary>
45         /// <param name="minimum">The minimum value, inclusive</param>
46         /// <param name="maximum">The maximum value, inclusive</param>
RangeAttribute(double minimum, double maximum)47         public RangeAttribute(double minimum, double maximum)
48             : this() {
49             this.Minimum = minimum;
50             this.Maximum = maximum;
51             this.OperandType = typeof(double);
52         }
53 
54         /// <summary>
55         /// Allows for specifying range for arbitrary types. The minimum and maximum strings will be converted to the target type.
56         /// </summary>
57         /// <param name="type">The type of the range parameters. Must implement IComparable.</param>
58         /// <param name="minimum">The minimum allowable value.</param>
59         /// <param name="maximum">The maximum allowable value.</param>
RangeAttribute(Type type, string minimum, string maximum)60         public RangeAttribute(Type type, string minimum, string maximum)
61             : this() {
62             this.OperandType = type;
63             this.Minimum = minimum;
64             this.Maximum = maximum;
65         }
66 
RangeAttribute()67         private RangeAttribute()
68             : base(() => DataAnnotationsResources.RangeAttribute_ValidationError) {
69         }
70 
Initialize(IComparable minimum, IComparable maximum, Func<object, object> conversion)71         private void Initialize(IComparable minimum, IComparable maximum, Func<object, object> conversion) {
72             if (minimum.CompareTo(maximum) > 0) {
73                 throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.RangeAttribute_MinGreaterThanMax, maximum, minimum));
74             }
75 
76             this.Minimum = minimum;
77             this.Maximum = maximum;
78             this.Conversion = conversion;
79         }
80 
81         /// <summary>
82         /// Returns true if the value falls between min and max, inclusive.
83         /// </summary>
84         /// <param name="value">The value to test for validity.</param>
85         /// <returns><c>true</c> means the <paramref name="value"/> is valid</returns>
86         /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
87 #if !SILVERLIGHT
88         public
89 #else
90         internal
91 #endif
IsValid(object value)92         override bool IsValid(object value) {
93             // Validate our properties and create the conversion function
94             this.SetupConversion();
95 
96             // Automatically pass if value is null or empty. RequiredAttribute should be used to assert a value is not empty.
97             if (value == null) {
98                 return true;
99             }
100             string s = value as string;
101             if (s != null && String.IsNullOrEmpty(s)) {
102                 return true;
103             }
104 
105             object convertedValue = null;
106 
107             try {
108                 convertedValue = this.Conversion(value);
109             } catch (FormatException) {
110                 return false;
111             } catch (InvalidCastException) {
112                 return false;
113             } catch (NotSupportedException) {
114                 return false;
115             }
116 
117             IComparable min = (IComparable)this.Minimum;
118             IComparable max = (IComparable)this.Maximum;
119             return min.CompareTo(convertedValue) <= 0 && max.CompareTo(convertedValue) >= 0;
120         }
121 
122         /// <summary>
123         /// Override of <see cref="ValidationAttribute.FormatErrorMessage"/>
124         /// </summary>
125         /// <remarks>This override exists to provide a formatted message describing the minimum and maximum values</remarks>
126         /// <param name="name">The user-visible name to include in the formatted message.</param>
127         /// <returns>A localized string describing the minimum and maximum values</returns>
128         /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
FormatErrorMessage(string name)129         public override string FormatErrorMessage(string name) {
130             this.SetupConversion();
131 
132             return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, this.Minimum, this.Maximum);
133         }
134 
135         /// <summary>
136         /// Validates the properties of this attribute and sets up the conversion function.
137         /// This method throws exceptions if the attribute is not configured properly.
138         /// If it has once determined it is properly configured, it is a NOP.
139         /// </summary>
SetupConversion()140         private void SetupConversion() {
141             if (this.Conversion == null) {
142                 object minimum = this.Minimum;
143                 object maximum = this.Maximum;
144 
145                 if (minimum == null || maximum == null) {
146                     throw new InvalidOperationException(DataAnnotationsResources.RangeAttribute_Must_Set_Min_And_Max);
147                 }
148 
149                 // Careful here -- OperandType could be int or double if they used the long form of the ctor.
150                 // But the min and max would still be strings.  Do use the type of the min/max operands to condition
151                 // the following code.
152                 Type operandType = minimum.GetType();
153 
154                 if (operandType == typeof(int)) {
155                     this.Initialize((int)minimum, (int)maximum, v => Convert.ToInt32(v, CultureInfo.InvariantCulture));
156                 } else if (operandType == typeof(double)) {
157                     this.Initialize((double)minimum, (double)maximum, v => Convert.ToDouble(v, CultureInfo.InvariantCulture));
158                 } else {
159                     Type type = this.OperandType;
160                     if (type == null) {
161                         throw new InvalidOperationException(DataAnnotationsResources.RangeAttribute_Must_Set_Operand_Type);
162                     }
163                     Type comparableType = typeof(IComparable);
164                     if (!comparableType.IsAssignableFrom(type)) {
165                         throw new InvalidOperationException(
166                             String.Format(
167                                 CultureInfo.CurrentCulture,
168                                 DataAnnotationsResources.RangeAttribute_ArbitraryTypeNotIComparable,
169                                 type.FullName,
170                                 comparableType.FullName));
171                     }
172 
173 #if SILVERLIGHT
174                     Func<object, object> conversion = value => (value != null && value.GetType() == type) ? value : Convert.ChangeType(value, type, CultureInfo.CurrentCulture);
175                     IComparable min = (IComparable)conversion(minimum);
176                     IComparable max = (IComparable)conversion(maximum);
177 #else
178                     TypeConverter converter = TypeDescriptor.GetConverter(type);
179                     IComparable min = (IComparable)converter.ConvertFromString((string)minimum);
180                     IComparable max = (IComparable)converter.ConvertFromString((string)maximum);
181 
182                     Func<object, object> conversion = value => (value != null && value.GetType() == type) ? value : converter.ConvertFrom(value);
183 #endif // !SILVERLIGHT
184 
185                     this.Initialize(min, max, conversion);
186                 }
187             }
188         }
189     }
190 }
191