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;
}
}
}
}