using System.ComponentModel.DataAnnotations.Resources; using System.Globalization; using System.Reflection; namespace System.ComponentModel.DataAnnotations { /// /// Validation attribute that executes a user-supplied method at runtime, using one of these signatures: /// /// public static Method(object value) { ... } /// /// /// public static Method(object value, context) { ... } /// /// /// The value can be strongly typed as type conversion will be attempted. /// /// /// /// This validation attribute is used to invoke custom logic to perform validation at runtime. /// Like any other , its /// method is invoked to perform validation. This implementation simply redirects that call to the method /// identified by on a type identified by /// /// The supplied cannot be null, and it must be a public type. /// /// /// The named must be public, static, return and take at /// least one input parameter for the value to be validated. This value parameter may be strongly typed. /// Type conversion will be attempted if clients pass in a value of a different type. /// /// /// The may also declare an additional parameter of type . /// The parameter provides additional context the method may use to determine /// the context in which it is being used. /// /// /// If the method returns ., that indicates the given value is acceptable and validation passed. /// Returning an instance of indicates that the value is not acceptable /// and validation failed. /// /// /// If the method returns a with a null /// then the normal method will be called to compose the error message. /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)] public sealed class CustomValidationAttribute : ValidationAttribute { #region Member Fields private Type _validatorType; private string _method; private MethodInfo _methodInfo; private bool _isSingleArgumentMethod; private string _lastMessage; private Type _valuesType; private Lazy _malformedErrorMessage; #endregion #region All Constructors /// /// Instantiates a custom validation attribute that will invoke a method in the /// specified type. /// /// An invalid or will be cause /// > to return a /// and to return a summary error message. /// /// The type that will contain the method to invoke. It cannot be null. See . /// The name of the method to invoke in . public CustomValidationAttribute(Type validatorType, string method) : base(() => DataAnnotationsResources.CustomValidationAttribute_ValidationError) { this._validatorType = validatorType; this._method = method; _malformedErrorMessage = new Lazy(CheckAttributeWellFormed); } #endregion #region Properties /// /// Gets the type that contains the validation method identified by . /// public Type ValidatorType { get { return this._validatorType; } } /// /// Gets the name of the method in to invoke to perform validation. /// public string Method { get { return this._method; } } #endregion /// /// Override of validation method. See . /// /// The value to validate. /// A instance that provides /// context about the validation operation, such as the object and member being validated. /// Whatever the in returns. /// is thrown if the current attribute is malformed. protected override ValidationResult IsValid(object value, ValidationContext validationContext) { // If attribute is not valid, throw an exeption right away to inform the developer this.ThrowIfAttributeNotWellFormed(); MethodInfo methodInfo = this._methodInfo; // If the value is not of the correct type and cannot be converted, fail // to indicate it is not acceptable. The convention is that IsValid is merely a probe, // and clients are not expecting exceptions. object convertedValue; if (!this.TryConvertValue(value, out convertedValue)) { return new ValidationResult(String.Format(CultureInfo.CurrentCulture, Resources.DataAnnotationsResources.CustomValidationAttribute_Type_Conversion_Failed, (value != null ? value.GetType().ToString() : "null"), this._valuesType, this._validatorType, this._method)); } // Invoke the method. Catch TargetInvocationException merely to unwrap it. // Callers don't know Reflection is being used and will not typically see // the real exception try { // 1-parameter form is ValidationResult Method(object value) // 2-parameter form is ValidationResult Method(object value, ValidationContext context), object[] methodParams = this._isSingleArgumentMethod ? new object[] { convertedValue } : new object[] { convertedValue, validationContext }; ValidationResult result = (ValidationResult)methodInfo.Invoke(null, methodParams); // We capture the message they provide us only in the event of failure, // otherwise we use the normal message supplied via the ctor this._lastMessage = null; if (result != null) { this._lastMessage = result.ErrorMessage; } return result; } catch (TargetInvocationException tie) { if (tie.InnerException != null) { throw tie.InnerException; } throw; } } /// /// Override of /// /// The name to include in the formatted string /// A localized string to describe the problem. /// is thrown if the current attribute is malformed. public override string FormatErrorMessage(string name) { // If attribute is not valid, throw an exeption right away to inform the developer this.ThrowIfAttributeNotWellFormed(); if (!string.IsNullOrEmpty(this._lastMessage)) { return String.Format(CultureInfo.CurrentCulture, this._lastMessage, name); } // If success or they supplied no custom message, use normal base class behavior return base.FormatErrorMessage(name); } /// /// Checks whether the current attribute instance itself is valid for use. /// /// The error message why it is not well-formed, null if it is well-formed. private string CheckAttributeWellFormed() { return this.ValidateValidatorTypeParameter() ?? this.ValidateMethodParameter(); } /// /// Internal helper to determine whether is legal for use. /// /// null or the appropriate error message. private string ValidateValidatorTypeParameter() { if (this._validatorType == null) { return DataAnnotationsResources.CustomValidationAttribute_ValidatorType_Required; } if (!this._validatorType.IsVisible) { return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Type_Must_Be_Public, this._validatorType.Name); } return null; } /// /// Internal helper to determine whether is legal for use. /// /// null or the appropriate error message. private string ValidateMethodParameter() { if (String.IsNullOrEmpty(this._method)) { return DataAnnotationsResources.CustomValidationAttribute_Method_Required; } // Named method must be public and static MethodInfo methodInfo = this._validatorType.GetMethod(this._method, BindingFlags.Public | BindingFlags.Static); if (methodInfo == null) { return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Not_Found, this._method, this._validatorType.Name); } // Method must return a ValidationResult if (methodInfo.ReturnType != typeof(ValidationResult)) { return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Must_Return_ValidationResult, this._method, this._validatorType.Name); } ParameterInfo[] parameterInfos = methodInfo.GetParameters(); // Must declare at least one input parameter for the value and it cannot be ByRef if (parameterInfos.Length == 0 || parameterInfos[0].ParameterType.IsByRef) { return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Signature, this._method, this._validatorType.Name); } // We accept 2 forms: // 1-parameter form is ValidationResult Method(object value) // 2-parameter form is ValidationResult Method(object value, ValidationContext context), this._isSingleArgumentMethod = (parameterInfos.Length == 1); if (!this._isSingleArgumentMethod) { if ((parameterInfos.Length != 2) || (parameterInfos[1].ParameterType != typeof(ValidationContext))) { return String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.CustomValidationAttribute_Method_Signature, this._method, this._validatorType.Name); } } this._methodInfo = methodInfo; this._valuesType = parameterInfos[0].ParameterType; return null; } /// /// Throws InvalidOperationException if the attribute is not valid. /// private void ThrowIfAttributeNotWellFormed() { string errorMessage = _malformedErrorMessage.Value; if (errorMessage != null) { throw new InvalidOperationException(errorMessage); } } /// /// Attempts to convert the given value to the type needed to invoke the method for the current /// CustomValidationAttribute. /// /// The value to check/convert. /// If successful, the converted (or copied) value. /// true if type value was already correct or was successfully converted. private bool TryConvertValue(object value, out object convertedValue) { convertedValue = null; Type t = this._valuesType; // Null is permitted for reference types or for Nullable<>'s only if (value == null) { if (t.IsValueType && (!t.IsGenericType || t.GetGenericTypeDefinition() != typeof(Nullable<>))) { return false; } return true; // convertedValue already null, which is correct for this case } // If the type is already legally assignable, we're good if (t.IsAssignableFrom(value.GetType())) { convertedValue = value; return true; } // Value is not the right type -- attempt a convert. // Any expected exception returns a false try { convertedValue = Convert.ChangeType(value, t, CultureInfo.CurrentCulture); return true; } catch (FormatException) { return false; } catch (InvalidCastException) { return false; } catch (NotSupportedException) { return false; } } } }