using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Resources; using System.Globalization; using System.Linq; using System.Reflection; namespace System.ComponentModel.DataAnnotations { /// /// Base class for all validation attributes. /// Override to implement validation logic. /// /// /// The properties and are used to provide /// a localized error message, but they cannot be set if is also used to provide a non-localized /// error message. /// public abstract class ValidationAttribute : Attribute { #region Member Fields private string _errorMessage; private Func _errorMessageResourceAccessor; private string _errorMessageResourceName; private Type _errorMessageResourceType; private bool _isCallingOverload; private object _syncLock = new object(); #endregion #region All Constructors /// /// Default constructor for any validation attribute. /// /// This constructor chooses a very generic validation error message. /// Developers subclassing ValidationAttribute should use other constructors /// or supply a better message. /// protected ValidationAttribute() : this(() => DataAnnotationsResources.ValidationAttribute_ValidationError) { } /// /// Constructor that accepts a fixed validation error message. /// /// A non-localized error message to use in . protected ValidationAttribute(string errorMessage) : this(() => errorMessage) { } /// /// Allows for providing a resource accessor function that will be used by the /// property to retrieve the error message. An example would be to have something like /// CustomAttribute() : base( () => MyResources.MyErrorMessage ) {}. /// /// The that will return an error message. protected ValidationAttribute(Func errorMessageAccessor) { // If null, will later be exposed as lack of error message to be able to construct accessor this._errorMessageResourceAccessor = errorMessageAccessor; } #endregion #region Protected Properties /// /// Gets the localized error message string, coming either from , or from evaluating the /// and pair. /// protected string ErrorMessageString { get { this.SetupResourceAccessor(); return this._errorMessageResourceAccessor(); } } /// /// A flag indicating whether a developer has customized the attribute's error message by setting any one of /// ErrorMessage, ErrorMessageResourceName, or ErrorMessageResourceType. /// internal bool CustomErrorMessageSet { get; private set; } /// /// A flag indicating that the attribute requires a non-null to perform validation. /// Base class returns false. Override in child classes as appropriate. /// public virtual bool RequiresValidationContext { get { return false; } } #endregion #region Public Properties /// /// Gets or sets and explicit error message string. /// /// /// This property is intended to be used for non-localizable error messages. Use /// and for localizable error messages. /// public string ErrorMessage { get { return this._errorMessage; } set { this._errorMessage = value; this._errorMessageResourceAccessor = null; this.CustomErrorMessageSet = true; } } /// /// Gets or sets the resource name (property name) to use as the key for lookups on the resource type. /// /// /// Use this property to set the name of the property within /// that will provide a localized error message. Use for non-localized error messages. /// public string ErrorMessageResourceName { get { return this._errorMessageResourceName; } set { this._errorMessageResourceName = value; this._errorMessageResourceAccessor = null; this.CustomErrorMessageSet = true; } } /// /// Gets or sets the resource type to use for error message lookups. /// /// /// Use this property only in conjunction with . They are /// used together to retrieve localized error messages at runtime. /// Use instead of this pair if error messages are not localized. /// /// public Type ErrorMessageResourceType { get { return this._errorMessageResourceType; } set { this._errorMessageResourceType = value; this._errorMessageResourceAccessor = null; this.CustomErrorMessageSet = true; } } #endregion #region Private Methods /// /// Validates the configuration of this attribute and sets up the appropriate error string accessor. /// This method bypasses all verification once the ResourceAccessor has been set. /// /// is thrown if the current attribute is malformed. private void SetupResourceAccessor() { if (this._errorMessageResourceAccessor == null) { string localErrorMessage = this._errorMessage; bool resourceNameSet = !string.IsNullOrEmpty(this._errorMessageResourceName); bool errorMessageSet = !string.IsNullOrEmpty(localErrorMessage); bool resourceTypeSet = this._errorMessageResourceType != null; // Either ErrorMessageResourceName or ErrorMessage may be set, but not both. // The following test checks both being set as well as both being not set. if (resourceNameSet == errorMessageSet) { throw new InvalidOperationException(DataAnnotationsResources.ValidationAttribute_Cannot_Set_ErrorMessage_And_Resource); } // Must set both or neither of ErrorMessageResourceType and ErrorMessageResourceName if (resourceTypeSet != resourceNameSet) { throw new InvalidOperationException(DataAnnotationsResources.ValidationAttribute_NeedBothResourceTypeAndResourceName); } // If set resource type (and we know resource name too), then go setup the accessor if (resourceNameSet) { this.SetResourceAccessorByPropertyLookup(); } else { // Here if not using resource type/name -- the accessor is just the error message string, // which we know is not empty to have gotten this far. this._errorMessageResourceAccessor = delegate { // We captured error message to local in case it changes before accessor runs return localErrorMessage; }; } } } private void SetResourceAccessorByPropertyLookup() { if (this._errorMessageResourceType != null && !string.IsNullOrEmpty(this._errorMessageResourceName)) { #if SILVERLIGHT var property = this._errorMessageResourceType.GetProperty(this._errorMessageResourceName, BindingFlags.Public | BindingFlags.Static); #else var property = this._errorMessageResourceType.GetProperty(this._errorMessageResourceName, BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic); if (property != null) { MethodInfo propertyGetter = property.GetGetMethod(true /*nonPublic*/); // We only support internal and public properties if (propertyGetter == null || (!propertyGetter.IsAssembly && !propertyGetter.IsPublic)) { // Set the property to null so the exception is thrown as if the property wasn't found property = null; } } #endif if (property == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, DataAnnotationsResources.ValidationAttribute_ResourceTypeDoesNotHaveProperty, this._errorMessageResourceType.FullName, this._errorMessageResourceName)); } if (property.PropertyType != typeof(string)) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, DataAnnotationsResources.ValidationAttribute_ResourcePropertyNotStringType, property.Name, this._errorMessageResourceType.FullName)); } this._errorMessageResourceAccessor = delegate { return (string)property.GetValue(null, null); }; } else { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.ValidationAttribute_NeedBothResourceTypeAndResourceName)); } } #endregion #region Protected & Public Methods /// /// Formats the error message to present to the user. /// /// The error message will be re-evaluated every time this function is called. /// It applies the (for example, the name of a field) to the formated error message, resulting /// in something like "The field 'name' has an incorrect value". /// /// Derived classes can override this method to customize how errors are generated. /// /// /// The base class implementation will use to obtain a localized /// error message from properties within the current attribute. If those have not been set, a generic /// error message will be provided. /// /// /// The user-visible name to include in the formatted message. /// The localized string describing the validation error /// is thrown if the current attribute is malformed. public virtual string FormatErrorMessage(string name) { return String.Format(CultureInfo.CurrentCulture, this.ErrorMessageString, name); } /// /// Gets the value indicating whether or not the specified is valid /// with respect to the current validation attribute. /// /// Derived classes should not override this method as it is only available for backwards compatibility. /// Instead, implement . /// /// /// /// The preferred public entry point for clients requesting validation is the method. /// /// The value to validate /// true if the is acceptable, false if it is not acceptable /// is thrown if the current attribute is malformed. /// is thrown when neither overload of IsValid has been implemented /// by a derived class. /// internal virtual bool IsValid(object value) { lock (this._syncLock) { if (this._isCallingOverload) { throw new NotImplementedException(DataAnnotationsResources.ValidationAttribute_IsValid_NotImplemented); } else { this._isCallingOverload = true; try { return this.IsValid(value, null) == null; } finally { this._isCallingOverload = false; } } } } /// /// Protected virtual method to override and implement validation logic. /// /// The value to validate. /// A instance that provides /// context about the validation operation, such as the object and member being validated. /// /// When validation is valid, . /// /// When validation is invalid, an instance of . /// /// /// is thrown if the current attribute is malformed. /// is thrown when /// has not been implemented by a derived class. /// protected virtual ValidationResult IsValid(object value, ValidationContext validationContext) { lock (this._syncLock) { if (this._isCallingOverload) { throw new NotImplementedException(DataAnnotationsResources.ValidationAttribute_IsValid_NotImplemented); } else { this._isCallingOverload = true; try { ValidationResult result = ValidationResult.Success; if (!this.IsValid(value)) { string[] memberNames = validationContext.MemberName != null ? new string[] { validationContext.MemberName } : null; result = new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName), memberNames); } return result; } finally { this._isCallingOverload = false; } } } } /// /// Tests whether the given is valid with respect to the current /// validation attribute without throwing a /// /// /// If this method returns , then validation was successful, otherwise /// an instance of will be returned with a guaranteed non-null /// . /// /// The value to validate /// A instance that provides /// context about the validation operation, such as the object and member being validated. /// /// When validation is valid, . /// /// When validation is invalid, an instance of . /// /// /// is thrown if the current attribute is malformed. /// When is null. /// is thrown when /// has not been implemented by a derived class. /// public ValidationResult GetValidationResult(object value, ValidationContext validationContext) { if (validationContext == null) { throw new ArgumentNullException("validationContext"); } ValidationResult result = this.IsValid(value, validationContext); // If validation fails, we want to ensure we have a ValidationResult that guarantees it has an ErrorMessage if (result != null) { bool hasErrorMessage = (result != null) ? !string.IsNullOrEmpty(result.ErrorMessage) : false; if (!hasErrorMessage) { string errorMessage = this.FormatErrorMessage(validationContext.DisplayName); result = new ValidationResult(errorMessage, result.MemberNames); } } return result; } #if !SILVERLIGHT /// /// Validates the specified and throws if it is not. /// /// The overloaded is the recommended entry point as it /// can provide additional context to the being validated. /// /// /// This base method invokes the method to determine whether or not the /// is acceptable. If returns false, this base /// method will invoke the to obtain a localized message describing /// the problem, and it will throw a /// /// The value to validate /// The string to be included in the validation error message if is not valid /// is thrown if returns false. /// /// is thrown if the current attribute is malformed. public void Validate(object value, string name) { if (!this.IsValid(value)) { throw new ValidationException(this.FormatErrorMessage(name), this, value); } } #endif /// /// Validates the specified and throws if it is not. /// /// This method invokes the method /// to determine whether or not the is acceptable given the . /// If that method doesn't return , this base method will throw /// a containing the describing the problem. /// /// The value to validate /// Additional context that may be used for validation. It cannot be null. /// is thrown if /// doesn't return . /// /// is thrown if the current attribute is malformed. /// is thrown when /// has not been implemented by a derived class. /// public void Validate(object value, ValidationContext validationContext) { if (validationContext == null) { throw new ArgumentNullException("validationContext"); } ValidationResult result = this.GetValidationResult(value, validationContext); if (result != null) { // Convenience -- if implementation did not fill in an error message, throw new ValidationException(result, this, value); } } #endregion } }