|
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Resources;
using System.Globalization;
using System.Linq;
using System.Reflection;
namespace System.ComponentModel.DataAnnotations {
/// <summary>
/// Helper class to validate objects, properties and other values using their associated <see cref="ValidationAttribute"/>
/// custom attributes.
/// </summary>
public static class Validator {
private static ValidationAttributeStore _store = ValidationAttributeStore.Instance;
/// <summary>
/// Tests whether the given property value is valid.
/// </summary>
/// <remarks>
/// This method will test each <see cref="ValidationAttribute"/> associated with the property
/// identified by <paramref name="validationContext"/>. If <paramref name="validationResults"/> is non-null,
/// this method will add a <see cref="ValidationResult"/> to it for each validation failure.
/// <para>
/// If there is a <see cref="RequiredAttribute"/> found on the property, it will be evaluated before all other
/// validation attributes. If the required validator fails then validation will abort, adding that single
/// failure into the <paramref name="validationResults"/> when applicable, returning a value of <c>false</c>.
/// </para>
/// <para>
/// If <paramref name="validationResults"/> is null and there isn't a <see cref="RequiredAttribute"/> failure,
/// then all validators will be evaluated.
/// </para>
/// </remarks>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">Describes the property member to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult"/>s for the failures.</param>
/// <returns><c>true</c> if the value is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentException">
/// When the <see cref="ValidationContext.MemberName"/> of <paramref name="validationContext"/> is not a valid property.
/// </exception>
public static bool TryValidateProperty(object value, ValidationContext validationContext, ICollection<ValidationResult> validationResults) {
// Throw if value cannot be assigned to this property. That is not a validation exception.
Type propertyType = _store.GetPropertyType(validationContext);
string propertyName = validationContext.MemberName;
EnsureValidPropertyType(propertyName, propertyType, value);
bool result = true;
bool breakOnFirstError = (validationResults == null);
IEnumerable<ValidationAttribute> attributes = _store.GetPropertyValidationAttributes(validationContext);
foreach (ValidationError err in GetValidationErrors(value, validationContext, attributes, breakOnFirstError)) {
result = false;
if (validationResults != null) {
validationResults.Add(err.ValidationResult);
}
}
return result;
}
/// <summary>
/// Tests whether the given object instance is valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute"/>s attached to the object instance's type. It also
/// checks to ensure all properties marked with <see cref="RequiredAttribute"/> are set. It does not validate the
/// property values of the object.
/// <para>
/// If <paramref name="validationResults"/> is null, then execution will abort upon the first validation
/// failure. If <paramref name="validationResults"/> is non-null, then all validation attributes will be
/// evaluated.
/// </para>
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be <c>null</c>.</param>
/// <param name="validationContext">Describes the object to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult"/>s for the failures.</param>
/// <returns><c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance"/> is null.</exception>
/// <exception cref="ArgumentException">When <paramref name="instance"/> doesn't match the
/// <see cref="ValidationContext.ObjectInstance"/>on <paramref name="validationContext"/>.</exception>
public static bool TryValidateObject(object instance, ValidationContext validationContext, ICollection<ValidationResult> validationResults) {
return TryValidateObject(instance, validationContext, validationResults, false /*validateAllProperties*/);
}
/// <summary>
/// Tests whether the given object instance is valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute"/>s attached to the object instance's type. It also
/// checks to ensure all properties marked with <see cref="RequiredAttribute"/> are set. If <paramref name="validateAllProperties"/>
/// is <c>true</c>, this method will also evaluate the <see cref="ValidationAttribute"/>s for all the immediate properties
/// of this object. This process is not recursive.
/// <para>
/// If <paramref name="validationResults"/> is null, then execution will abort upon the first validation
/// failure. If <paramref name="validationResults"/> is non-null, then all validation attributes will be
/// evaluated.
/// </para>
/// <para>
/// For any given property, if it has a <see cref="RequiredAttribute"/> that fails validation, no other validators
/// will be evaluated for that property.
/// </para>
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult"/>s for the failures.</param>
/// <param name="validateAllProperties">If <c>true</c>, also evaluates all properties of the object (this process is not
/// recursive over properties of the properties).</param>
/// <returns><c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance"/> is null.</exception>
/// <exception cref="ArgumentException">When <paramref name="instance"/> doesn't match the
/// <see cref="ValidationContext.ObjectInstance"/>on <paramref name="validationContext"/>.</exception>
public static bool TryValidateObject(object instance, ValidationContext validationContext, ICollection<ValidationResult> validationResults, bool validateAllProperties) {
if (instance == null) {
throw new ArgumentNullException("instance");
}
if (validationContext != null && instance != validationContext.ObjectInstance) {
throw new ArgumentException(Resources.DataAnnotationsResources.Validator_InstanceMustMatchValidationContextInstance, "instance");
}
bool result = true;
bool breakOnFirstError = (validationResults == null);
foreach (ValidationError err in GetObjectValidationErrors(instance, validationContext, validateAllProperties, breakOnFirstError)) {
result = false;
if (validationResults != null) {
validationResults.Add(err.ValidationResult);
}
}
return result;
}
/// <summary>
/// Tests whether the given value is valid against a specified list of <see cref="ValidationAttribute"/>s.
/// </summary>
/// <remarks>
/// This method will test each <see cref="ValidationAttribute"/>s specified . If
/// <paramref name="validationResults"/> is non-null, this method will add a <see cref="ValidationResult"/>
/// to it for each validation failure.
/// <para>
/// If there is a <see cref="RequiredAttribute"/> within the <paramref name="validationAttributes"/>, it will
/// be evaluated before all other validation attributes. If the required validator fails then validation will
/// abort, adding that single failure into the <paramref name="validationResults"/> when applicable, returning a
/// value of <c>false</c>.
/// </para>
/// <para>
/// If <paramref name="validationResults"/> is null and there isn't a <see cref="RequiredAttribute"/> failure,
/// then all validators will be evaluated.
/// </para>
/// </remarks>
/// <param name="value">The value to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult"/>s for the failures.</param>
/// <param name="validationAttributes">The list of <see cref="ValidationAttribute"/>s to validate this <paramref name="value"/> against.</param>
/// <returns><c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
public static bool TryValidateValue(object value, ValidationContext validationContext, ICollection<ValidationResult> validationResults, IEnumerable<ValidationAttribute> validationAttributes) {
bool result = true;
bool breakOnFirstError = validationResults == null;
foreach (ValidationError err in GetValidationErrors(value, validationContext, validationAttributes, breakOnFirstError)) {
result = false;
if (validationResults != null) {
validationResults.Add(err.ValidationResult);
}
}
return result;
}
/// <summary>
/// Throws a <see cref="ValidationException"/> if the given property <paramref name="value"/> is not valid.
/// </summary>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators. It cannot be <c>null</c>.</param>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
/// <exception cref="ValidationException">When <paramref name="value"/> is invalid for this property.</exception>
public static void ValidateProperty(object value, ValidationContext validationContext) {
// Throw if value cannot be assigned to this property. That is not a validation exception.
Type propertyType = _store.GetPropertyType(validationContext);
EnsureValidPropertyType(validationContext.MemberName, propertyType, value);
IEnumerable<ValidationAttribute> attributes = _store.GetPropertyValidationAttributes(validationContext);
ValidationError err = GetValidationErrors(value, validationContext, attributes, false).FirstOrDefault();
if (err != null) {
err.ThrowValidationException();
}
}
/// <summary>
/// Throws a <see cref="ValidationException"/> if the given <paramref name="instance"/> is not valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute"/>s attached to the object's type.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators. It cannot be <c>null</c>.</param>
/// <exception cref="ArgumentNullException">When <paramref name="instance"/> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
/// <exception cref="ArgumentException">When <paramref name="instance"/> doesn't match the
/// <see cref="ValidationContext.ObjectInstance"/> on <paramref name="validationContext"/>.</exception>
/// <exception cref="ValidationException">When <paramref name="instance"/> is found to be invalid.</exception>
public static void ValidateObject(object instance, ValidationContext validationContext) {
ValidateObject(instance, validationContext, false /*validateAllProperties*/);
}
/// <summary>
/// Throws a <see cref="ValidationException"/> if the given object instance is not valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute"/>s attached to the object's type.
/// If <paramref name="validateAllProperties"/> is <c>true</c> it also validates all the object's properties.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators. It cannot be <c>null</c>.</param>
/// <param name="validateAllProperties">If <c>true</c>, also validates all the <paramref name="instance"/>'s properties.</param>
/// <exception cref="ArgumentNullException">When <paramref name="instance"/> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
/// <exception cref="ArgumentException">When <paramref name="instance"/> doesn't match the
/// <see cref="ValidationContext.ObjectInstance"/> on <paramref name="validationContext"/>.</exception>
/// <exception cref="ValidationException">When <paramref name="instance"/> is found to be invalid.</exception>
public static void ValidateObject(object instance, ValidationContext validationContext, bool validateAllProperties) {
if (instance == null) {
throw new ArgumentNullException("instance");
}
if (validationContext == null) {
throw new ArgumentNullException("validationContext");
}
if (instance != validationContext.ObjectInstance) {
throw new ArgumentException(Resources.DataAnnotationsResources.Validator_InstanceMustMatchValidationContextInstance, "instance");
}
ValidationError err = GetObjectValidationErrors(instance, validationContext, validateAllProperties, false).FirstOrDefault();
if (err != null) {
err.ThrowValidationException();
}
}
/// <summary>
/// Throw a <see cref="ValidationException"/> if the given value is not valid for the <see cref="ValidationAttribute"/>s.
/// </summary>
/// <remarks>
/// This method evaluates the <see cref="ValidationAttribute"/>s supplied until a validation error occurs,
/// at which time a <see cref="ValidationException"/> is thrown.
/// <para>
/// A <see cref="RequiredAttribute"/> within the <paramref name="validationAttributes"/> will always be evaluated first.
/// </para>
/// </remarks>
/// <param name="value">The value to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object being tested.</param>
/// <param name="validationAttributes">The list of <see cref="ValidationAttribute"/>s to validate against this instance.</param>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
/// <exception cref="ValidationException">When <paramref name="value"/> is found to be invalid.</exception>
public static void ValidateValue(object value, ValidationContext validationContext, IEnumerable<ValidationAttribute> validationAttributes) {
if (validationContext == null) {
throw new ArgumentNullException("validationContext");
}
ValidationError err = GetValidationErrors(value, validationContext, validationAttributes, false).FirstOrDefault();
if (err != null) {
err.ThrowValidationException();
}
}
/// <summary>
/// Creates a new <see cref="ValidationContext"/> to use to validate the type or a member of
/// the given object instance.
/// </summary>
/// <param name="instance">The object instance to use for the context.</param>
/// <param name="validationContext">An parent validation context that supplies an <see cref="IServiceProvider"/>
/// and <see cref="ValidationContext.Items"/>.</param>
/// <returns>A new <see cref="ValidationContext"/> for the <paramref name="instance"/> provided.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
internal static ValidationContext CreateValidationContext(object instance, ValidationContext validationContext) {
if (validationContext == null) {
throw new ArgumentNullException("validationContext");
}
// Create a new context using the existing ValidationContext that acts as an IServiceProvider and contains our existing items.
ValidationContext context = new ValidationContext(instance, validationContext, validationContext.Items);
return context;
}
/// <summary>
/// Determine whether the given value can legally be assigned into the specified type.
/// </summary>
/// <param name="destinationType">The destination <see cref="Type"/> for the value.</param>
/// <param name="value">The value to test to see if it can be assigned as the Type indicated by <paramref name="destinationType"/>.</param>
/// <returns><c>true</c> if the assignment is legal.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="destinationType"/> is null.</exception>
private static bool CanBeAssigned(Type destinationType, object value) {
if (destinationType == null) {
throw new ArgumentNullException("destinationType");
}
if (value == null) {
// Null can be assigned only to reference types or Nullable or Nullable<>
return !destinationType.IsValueType ||
(destinationType.IsGenericType && destinationType.GetGenericTypeDefinition() == typeof(Nullable<>));
}
// Not null -- be sure it can be cast to the right type
return destinationType.IsAssignableFrom(value.GetType());
}
/// <summary>
/// Determines whether the given value can legally be assigned to the given property.
/// </summary>
/// <param name="propertyName">The name of the property.</param>
/// <param name="propertyType">The type of the property.</param>
/// <param name="value">The value. Null is permitted only if the property will accept it.</param>
/// <exception cref="ArgumentException"> is thrown if <paramref name="value"/> is the wrong type for this property.</exception>
private static void EnsureValidPropertyType(string propertyName, Type propertyType, object value) {
if (!CanBeAssigned(propertyType, value)) {
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.Validator_Property_Value_Wrong_Type, propertyName, propertyType), "value");
}
}
/// <summary>
/// Internal iterator to enumerate all validation errors for the given object instance.
/// </summary>
/// <param name="instance">Object instance to test.</param>
/// <param name="validationContext">Describes the object type.</param>
/// <param name="validateAllProperties">if <c>true</c> also validates all properties.</param>
/// <param name="breakOnFirstError">Whether to break on the first error or validate everything.</param>
/// <returns>A collection of validation errors that result from validating the <paramref name="instance"/> with
/// the given <paramref name="validationContext"/>.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance"/> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
/// <exception cref="ArgumentException">When <paramref name="instance"/> doesn't match the
/// <see cref="ValidationContext.ObjectInstance"/> on <paramref name="validationContext"/>.</exception>
private static IEnumerable<ValidationError> GetObjectValidationErrors(object instance, ValidationContext validationContext, bool validateAllProperties, bool breakOnFirstError) {
if (instance == null) {
throw new ArgumentNullException("instance");
}
if (validationContext == null) {
throw new ArgumentNullException("validationContext");
}
// Step 1: Validate the object properties' validation attributes
List<ValidationError> errors = new List<ValidationError>();
errors.AddRange(GetObjectPropertyValidationErrors(instance, validationContext, validateAllProperties, breakOnFirstError));
// We only proceed to Step 2 if there are no errors
if (errors.Any()) {
return errors;
}
// Step 2: Validate the object's validation attributes
IEnumerable<ValidationAttribute> attributes = _store.GetTypeValidationAttributes(validationContext);
errors.AddRange(GetValidationErrors(instance, validationContext, attributes, breakOnFirstError));
#if !SILVERLIGHT
// We only proceed to Step 3 if there are no errors
if (errors.Any()) {
return errors;
}
// Step 3: Test for IValidatableObject implementation
IValidatableObject validatable = instance as IValidatableObject;
if (validatable != null) {
IEnumerable<ValidationResult> results = validatable.Validate(validationContext);
foreach (ValidationResult result in results.Where(r => r != ValidationResult.Success)) {
errors.Add(new ValidationError(null, instance, result));
}
}
#endif
return errors;
}
/// <summary>
/// Internal iterator to enumerate all the validation errors for all properties of the given object instance.
/// </summary>
/// <param name="instance">Object instance to test.</param>
/// <param name="validationContext">Describes the object type.</param>
/// <param name="validateAllProperties">If <c>true</c>, evaluates all the properties, otherwise just checks that
/// ones marked with <see cref="RequiredAttribute"/> are not null.</param>
/// <param name="breakOnFirstError">Whether to break on the first error or validate everything.</param>
/// <returns>A list of <see cref="ValidationError"/> instances.</returns>
private static IEnumerable<ValidationError> GetObjectPropertyValidationErrors(object instance, ValidationContext validationContext, bool validateAllProperties, bool breakOnFirstError) {
ICollection<KeyValuePair<ValidationContext, object>> properties = GetPropertyValues(instance, validationContext);
List<ValidationError> errors = new List<ValidationError>();
foreach (KeyValuePair<ValidationContext, object> property in properties) {
// get list of all validation attributes for this property
IEnumerable<ValidationAttribute> attributes = _store.GetPropertyValidationAttributes(property.Key);
if (validateAllProperties) {
// validate all validation attributes on this property
errors.AddRange(GetValidationErrors(property.Value, property.Key, attributes, breakOnFirstError));
} else {
// only validate the Required attributes
RequiredAttribute reqAttr = attributes.FirstOrDefault(a => a is RequiredAttribute) as RequiredAttribute;
if (reqAttr != null) {
// Note: we let the [Required] attribute do its own null testing,
// since the user may have subclassed it and have a deeper meaning to what 'required' means
ValidationResult validationResult = reqAttr.GetValidationResult(property.Value, property.Key);
if (validationResult != ValidationResult.Success) {
errors.Add(new ValidationError(reqAttr, property.Value, validationResult));
}
}
}
if (breakOnFirstError && errors.Any()) {
break;
}
}
return errors;
}
/// <summary>
/// Retrieves the property values for the given instance.
/// </summary>
/// <param name="instance">Instance from which to fetch the properties.</param>
/// <param name="validationContext">Describes the entity being validated.</param>
/// <returns>A set of key value pairs, where the key is a validation context for the property and the value is its current
/// value.</returns>
/// <remarks>Ignores indexed properties.</remarks>
private static ICollection<KeyValuePair<ValidationContext, object>> GetPropertyValues(object instance, ValidationContext validationContext) {
#if SILVERLIGHT
IEnumerable<PropertyInfo> properties = from property in instance.GetType().GetProperties()
where !property.GetIndexParameters().Any()
select property;
List<KeyValuePair<ValidationContext, object>> items = new List<KeyValuePair<ValidationContext, object>>(properties.Count());
foreach (PropertyInfo property in properties) {
ValidationContext context = CreateValidationContext(instance, validationContext);
context.MemberName = property.Name;
if (_store.GetPropertyValidationAttributes(context).Any()) {
items.Add(new KeyValuePair<ValidationContext, object>(context, property.GetValue(instance, null)));
}
}
#else
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(instance);
List<KeyValuePair<ValidationContext, object>> items = new List<KeyValuePair<ValidationContext, object>>(properties.Count);
foreach (PropertyDescriptor property in properties) {
ValidationContext context = CreateValidationContext(instance, validationContext);
context.MemberName = property.Name;
if (_store.GetPropertyValidationAttributes(context).Any()) {
items.Add(new KeyValuePair<ValidationContext, object>(context, property.GetValue(instance)));
}
}
#endif
return items;
}
/// <summary>
/// Internal iterator to enumerate all validation errors for an value.
/// </summary>
/// <remarks>
/// If a <see cref="RequiredAttribute"/> is found, it will be evaluated first, and if that fails,
/// validation will abort, regardless of the <paramref name="breakOnFirstError"/> parameter value.
/// </remarks>
/// <param name="value">The value to pass to the validation attributes.</param>
/// <param name="validationContext">Describes the type/member being evaluated.</param>
/// <param name="attributes">The validation attributes to evaluate.</param>
/// <param name="breakOnFirstError">Whether or not to break on the first validation failure. A
/// <see cref="RequiredAttribute"/> failure will always abort with that sole failure.</param>
/// <returns>The collection of validation errors.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
private static IEnumerable<ValidationError> GetValidationErrors(object value, ValidationContext validationContext, IEnumerable<ValidationAttribute> attributes, bool breakOnFirstError) {
if (validationContext == null) {
throw new ArgumentNullException("validationContext");
}
List<ValidationError> errors = new List<ValidationError>();
ValidationError validationError;
// Get the required validator if there is one and test it first, aborting on failure
RequiredAttribute required = attributes.FirstOrDefault(a => a is RequiredAttribute) as RequiredAttribute;
if (required != null) {
if (!TryValidate(value, validationContext, required, out validationError)) {
errors.Add(validationError);
return errors;
}
}
// Iterate through the rest of the validators, skipping the required validator
foreach (ValidationAttribute attr in attributes) {
if (attr != required) {
if (!TryValidate(value, validationContext, attr, out validationError)) {
errors.Add(validationError);
if (breakOnFirstError) {
break;
}
}
}
}
return errors;
}
/// <summary>
/// Tests whether a value is valid against a single <see cref="ValidationAttribute"/> using the <see cref="ValidationContext"/>.
/// </summary>
/// <param name="value">The value to be tested for validity.</param>
/// <param name="validationContext">Describes the property member to validate.</param>
/// <param name="attribute">The validation attribute to test.</param>
/// <param name="validationError">The validation error that occurs during validation. Will be <c>null</c> when the return value is <c>true</c>.</param>
/// <returns><c>true</c> if the value is valid.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext"/> is null.</exception>
private static bool TryValidate(object value, ValidationContext validationContext, ValidationAttribute attribute, out ValidationError validationError) {
if (validationContext == null) {
throw new ArgumentNullException("validationContext");
}
ValidationResult validationResult = attribute.GetValidationResult(value, validationContext);
if (validationResult != ValidationResult.Success) {
validationError = new ValidationError(attribute, value, validationResult);
return false;
}
validationError = null;
return true;
}
/// <summary>
/// Private helper class to encapsulate a ValidationAttribute with the failed value and the user-visible
/// target name against which it was validated.
/// </summary>
private class ValidationError {
internal ValidationError(ValidationAttribute validationAttribute, object value, ValidationResult validationResult) {
this.ValidationAttribute = validationAttribute;
this.ValidationResult = validationResult;
this.Value = value;
}
internal object Value { get; set; }
internal ValidationAttribute ValidationAttribute { get; set; }
internal ValidationResult ValidationResult { get; set; }
internal void ThrowValidationException() {
throw new ValidationException(this.ValidationResult, this.ValidationAttribute, this.Value);
}
}
}
}
|