1 using System.ComponentModel.DataAnnotations.Resources;
2 using System.Globalization;
3 using System.Reflection;
4 
5 namespace System.ComponentModel.DataAnnotations {
6     /// <summary>
7     /// Validation attribute that executes a user-supplied method at runtime, using one of these signatures:
8     /// <para>
9     /// public static <see cref="ValidationResult"/> Method(object value) { ... }
10     /// </para>
11     /// <para>
12     /// public static <see cref="ValidationResult"/> Method(object value, <see cref="ValidationContext"/> context) { ... }
13     /// </para>
14     /// <para>
15     /// The value can be strongly typed as type conversion will be attempted.
16     /// </para>
17     /// </summary>
18     /// <remarks>
19     /// This validation attribute is used to invoke custom logic to perform validation at runtime.
20     /// Like any other <see cref="ValidationAttribute"/>, its <see cref="IsValid(object, ValidationContext)"/>
21     /// method is invoked to perform validation.  This implementation simply redirects that call to the method
22     /// identified by <see cref="Method"/> on a type identified by <see cref="ValidatorType"/>
23     /// <para>
24     /// The supplied <see cref="ValidatorType"/> cannot be null, and it must be a public type.
25     /// </para>
26     /// <para>
27     /// The named <see cref="Method"/> must be public, static, return <see cref="ValidationResult"/> and take at
28     /// least one input parameter for the value to be validated.  This value parameter may be strongly typed.
29     /// Type conversion will be attempted if clients pass in a value of a different type.
30     /// </para>
31     /// <para>
32     /// The <see cref="Method"/> may also declare an additional parameter of type <see cref="ValidationContext"/>.
33     /// The <see cref="ValidationContext"/> parameter provides additional context the method may use to determine
34     /// the context in which it is being used.
35     /// </para>
36     /// <para>
37     /// If the method returns <see cref="ValidationResult"/>.<see cref="ValidationResult.Success"/>, that indicates the given value is acceptable and validation passed.
38     /// Returning an instance of <see cref="ValidationResult"/> indicates that the value is not acceptable
39     /// and validation failed.
40     /// </para>
41     /// <para>
42     /// If the method returns a <see cref="ValidationResult"/> with a <c>null</c> <see cref="ValidationResult.ErrorMessage"/>
43     /// then the normal <see cref="ValidationAttribute.FormatErrorMessage"/> method will be called to compose the error message.
44     /// </para>
45     /// </remarks>
46     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)]
47     public sealed class CustomValidationAttribute : ValidationAttribute {
48         #region Member Fields
49 
50         private Type _validatorType;
51         private string _method;
52         private MethodInfo _methodInfo;
53         private bool _isSingleArgumentMethod;
54         private string _lastMessage;
55         private Type _valuesType;
56         private Lazy<string> _malformedErrorMessage;
57 #if !SILVERLIGHT
58         private Tuple<string, Type> _typeId;
59 #endif
60 
61         #endregion
62 
63         #region All Constructors
64 
65         /// <summary>
66         /// Instantiates a custom validation attribute that will invoke a method in the
67         /// specified type.
68         /// </summary>
69         /// <remarks>An invalid <paramref name="validatorType"/> or <paramref name="Method"/> will be cause
70         /// <see cref="IsValid(object, ValidationContext)"/>> to return a <see cref="ValidationResult"/>
71         /// and <see cref="ValidationAttribute.FormatErrorMessage"/> to return a summary error message.
72         /// </remarks>
73         /// <param name="validatorType">The type that will contain the method to invoke.  It cannot be null.  See <see cref="Method"/>.</param>
74         /// <param name="method">The name of the method to invoke in <paramref name="validatorType"/>.</param>
CustomValidationAttribute(Type validatorType, string method)75         public CustomValidationAttribute(Type validatorType, string method)
76             : base(() => DataAnnotationsResources.CustomValidationAttribute_ValidationError) {
77             this._validatorType = validatorType;
78             this._method = method;
79             _malformedErrorMessage = new Lazy<string>(CheckAttributeWellFormed);
80         }
81 
82         #endregion
83 
84         #region Properties
85         /// <summary>
86         /// Gets the type that contains the validation method identified by <see cref="Method"/>.
87         /// </summary>
88         public Type ValidatorType {
89             get {
90                 return this._validatorType;
91             }
92         }
93 
94         /// <summary>
95         /// Gets the name of the method in <see cref="ValidatorType"/> to invoke to perform validation.
96         /// </summary>
97         public string Method {
98             get {
99                 return this._method;
100             }
101         }
102 
103 #if !SILVERLIGHT
104         /// <summary>
105         /// Gets a unique identifier for this attribute.
106         /// </summary>
107         public override object TypeId {
108             get {
109                 if (_typeId == null) {
110                     _typeId = new Tuple<string, Type>(this._method, this._validatorType);
111                 }
112                 return _typeId;
113             }
114         }
115 #endif
116 
117         #endregion
118 
119         /// <summary>
120         /// Override of validation method.  See <see cref="ValidationAttribute.IsValid(object, ValidationContext)"/>.
121         /// </summary>
122         /// <param name="value">The value to validate.</param>
123         /// <param name="validationContext">A <see cref="ValidationContext"/> instance that provides
124         /// context about the validation operation, such as the object and member being validated.</param>
125         /// <returns>Whatever the <see cref="Method"/> in <see cref="ValidatorType"/> returns.</returns>
126         /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
IsValid(object value, ValidationContext validationContext)127         protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
128             // If attribute is not valid, throw an exeption right away to inform the developer
129             this.ThrowIfAttributeNotWellFormed();
130 
131             MethodInfo methodInfo = this._methodInfo;
132 
133             // If the value is not of the correct type and cannot be converted, fail
134             // to indicate it is not acceptable.  The convention is that IsValid is merely a probe,
135             // and clients are not expecting exceptions.
136             object convertedValue;
137             if (!this.TryConvertValue(value, out convertedValue)) {
138                 return new ValidationResult(String.Format(CultureInfo.CurrentCulture, Resources.DataAnnotationsResources.CustomValidationAttribute_Type_Conversion_Failed,
139                                                     (value != null ? value.GetType().ToString() : "null"), this._valuesType, this._validatorType, this._method));
140             }
141 
142             // Invoke the method.  Catch TargetInvocationException merely to unwrap it.
143             // Callers don't know Reflection is being used and will not typically see
144             // the real exception
145             try {
146                 // 1-parameter form is ValidationResult Method(object value)
147                 // 2-parameter form is ValidationResult Method(object value, ValidationContext context),
148                 object[] methodParams = this._isSingleArgumentMethod
149                                             ? new object[] { convertedValue }
150                                             : new object[] { convertedValue, validationContext };
151 
152                 ValidationResult result = (ValidationResult)methodInfo.Invoke(null, methodParams);
153 
154                 // We capture the message they provide us only in the event of failure,
155                 // otherwise we use the normal message supplied via the ctor
156                 this._lastMessage = null;
157 
158                 if (result != null) {
159                     this._lastMessage = result.ErrorMessage;
160                 }
161 
162                 return result;
163             } catch (TargetInvocationException tie) {
164                 if (tie.InnerException != null) {
165                     throw tie.InnerException;
166                 }
167 
168                 throw;
169             }
170         }
171 
172         /// <summary>
173         /// Override of <see cref="ValidationAttribute.FormatErrorMessage"/>
174         /// </summary>
175         /// <param name="name">The name to include in the formatted string</param>
176         /// <returns>A localized string to describe the problem.</returns>
177         /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
FormatErrorMessage(string name)178         public override string FormatErrorMessage(string name) {
179             // If attribute is not valid, throw an exeption right away to inform the developer
180             this.ThrowIfAttributeNotWellFormed();
181 
182             if (!string.IsNullOrEmpty(this._lastMessage)) {
183                 return String.Format(CultureInfo.CurrentCulture, this._lastMessage, name);
184             }
185 
186             // If success or they supplied no custom message, use normal base class behavior
187             return base.FormatErrorMessage(name);
188         }
189 
190         /// <summary>
191         /// Checks whether the current attribute instance itself is valid for use.
192         /// </summary>
193         /// <returns>The error message why it is not well-formed, null if it is well-formed.</returns>
CheckAttributeWellFormed()194         private string CheckAttributeWellFormed() {
195             return this.ValidateValidatorTypeParameter() ?? this.ValidateMethodParameter();
196         }
197 
198         /// <summary>
199         /// Internal helper to determine whether <see cref="ValidatorType"/> is legal for use.
200         /// </summary>
201         /// <returns><c>null</c> or the appropriate error message.</returns>
ValidateValidatorTypeParameter()202         private string ValidateValidatorTypeParameter() {
203             if (this._validatorType == null) {
204                 return DataAnnotationsResources.CustomValidationAttribute_ValidatorType_Required;
205             }
206 
207             if (!this._validatorType.IsVisible) {
208                 return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Type_Must_Be_Public, this._validatorType.Name);
209             }
210 
211             return null;
212         }
213 
214         /// <summary>
215         /// Internal helper to determine whether <see cref="Method"/> is legal for use.
216         /// </summary>
217         /// <returns><c>null</c> or the appropriate error message.</returns>
ValidateMethodParameter()218         private string ValidateMethodParameter() {
219             if (String.IsNullOrEmpty(this._method)) {
220                 return DataAnnotationsResources.CustomValidationAttribute_Method_Required;
221             }
222 
223             // Named method must be public and static
224             MethodInfo methodInfo = this._validatorType.GetMethod(this._method, BindingFlags.Public | BindingFlags.Static);
225             if (methodInfo == null) {
226                 return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Not_Found, this._method, this._validatorType.Name);
227             }
228 
229             // Method must return a ValidationResult
230             if (methodInfo.ReturnType != typeof(ValidationResult)) {
231                 return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Must_Return_ValidationResult, this._method, this._validatorType.Name);
232             }
233 
234             ParameterInfo[] parameterInfos = methodInfo.GetParameters();
235 
236             // Must declare at least one input parameter for the value and it cannot be ByRef
237             if (parameterInfos.Length == 0 || parameterInfos[0].ParameterType.IsByRef) {
238                 return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Signature, this._method, this._validatorType.Name);
239             }
240 
241             // We accept 2 forms:
242             // 1-parameter form is ValidationResult Method(object value)
243             // 2-parameter form is ValidationResult Method(object value, ValidationContext context),
244             this._isSingleArgumentMethod = (parameterInfos.Length == 1);
245 
246             if (!this._isSingleArgumentMethod) {
247                 if ((parameterInfos.Length != 2) || (parameterInfos[1].ParameterType != typeof(ValidationContext))) {
248                     return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Signature, this._method, this._validatorType.Name);
249                 }
250             }
251 
252             this._methodInfo = methodInfo;
253             this._valuesType = parameterInfos[0].ParameterType;
254             return null;
255         }
256 
257         /// <summary>
258         /// Throws InvalidOperationException if the attribute is not valid.
259         /// </summary>
ThrowIfAttributeNotWellFormed()260         private void ThrowIfAttributeNotWellFormed() {
261             string errorMessage = _malformedErrorMessage.Value;
262             if (errorMessage != null) {
263                 throw new InvalidOperationException(errorMessage);
264             }
265         }
266 
267         /// <summary>
268         /// Attempts to convert the given value to the type needed to invoke the method for the current
269         /// CustomValidationAttribute.
270         /// </summary>
271         /// <param name="value">The value to check/convert.</param>
272         /// <param name="convertedValue">If successful, the converted (or copied) value.</param>
273         /// <returns><c>true</c> if type value was already correct or was successfully converted.</returns>
TryConvertValue(object value, out object convertedValue)274         private bool TryConvertValue(object value, out object convertedValue) {
275             convertedValue = null;
276             Type t = this._valuesType;
277 
278             // Null is permitted for reference types or for Nullable<>'s only
279             if (value == null) {
280                 if (t.IsValueType && (!t.IsGenericType || t.GetGenericTypeDefinition() != typeof(Nullable<>))) {
281                     return false;
282                 }
283 
284                 return true;    // convertedValue already null, which is correct for this case
285             }
286 
287             // If the type is already legally assignable, we're good
288             if (t.IsAssignableFrom(value.GetType())) {
289                 convertedValue = value;
290                 return true;
291             }
292 
293             // Value is not the right type -- attempt a convert.
294             // Any expected exception returns a false
295             try {
296                 convertedValue = Convert.ChangeType(value, t, CultureInfo.CurrentCulture);
297                 return true;
298             } catch (FormatException) {
299                 return false;
300             } catch (InvalidCastException) {
301                 return false;
302             } catch (NotSupportedException) {
303                 return false;
304             }
305         }
306     }
307 }
308