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