File: System\ComponentModel\DataAnnotations\Validator.cs
Web Access
Project: src\src\libraries\System.ComponentModel.Annotations\src\System.ComponentModel.Annotations.csproj (System.ComponentModel.Annotations)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
 
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 readonly 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>
        [RequiresUnreferencedCode("The Type of validationContext.ObjectType cannot be statically discovered.")]
        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.
            var propertyType = _store.GetPropertyType(validationContext);
            var propertyName = validationContext.MemberName!;
            EnsureValidPropertyType(propertyName, propertyType, value);
 
            var result = true;
            var breakOnFirstError = (validationResults == null);
 
            var attributes = _store.GetPropertyValidationAttributes(validationContext);
 
            foreach (var err in GetValidationErrors(value, validationContext, attributes, breakOnFirstError))
            {
                result = false;
 
                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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        public static bool TryValidateObject(
            object instance, ValidationContext validationContext, ICollection<ValidationResult>? validationResults) =>
            TryValidateObject(instance, validationContext, validationResults, validateAllProperties: false);
 
        /// <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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        public static bool TryValidateObject(object instance, ValidationContext validationContext,
            ICollection<ValidationResult>? validationResults, bool validateAllProperties)
        {
            ArgumentNullException.ThrowIfNull(instance);
 
            if (validationContext != null && instance != validationContext.ObjectInstance)
            {
                throw new ArgumentException(SR.Validator_InstanceMustMatchValidationContextInstance, nameof(instance));
            }
 
            var result = true;
            var breakOnFirstError = (validationResults == null);
 
            foreach (ValidationError err in GetObjectValidationErrors(instance, validationContext!, validateAllProperties, breakOnFirstError))
            {
                result = false;
 
                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.</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)
        {
            ArgumentNullException.ThrowIfNull(validationAttributes);
 
            var result = true;
            var breakOnFirstError = validationResults == null;
 
            foreach (
                var err in
                    GetValidationErrors(value, validationContext, validationAttributes, breakOnFirstError))
            {
                result = false;
 
                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>
        [RequiresUnreferencedCode("The Type of validationContext.ObjectType cannot be statically discovered.")]
        public static void ValidateProperty(object? value, ValidationContext validationContext)
        {
            // Throw if value cannot be assigned to this property.  That is not a validation exception.
            var propertyType = _store.GetPropertyType(validationContext);
            EnsureValidPropertyType(validationContext.MemberName!, propertyType, value);
 
            var attributes = _store.GetPropertyValidationAttributes(validationContext);
 
            List<ValidationError> errors = GetValidationErrors(value, validationContext, attributes, false);
            if (errors.Count > 0)
            {
                errors[0].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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        public static void ValidateObject(object instance, ValidationContext validationContext,
            bool validateAllProperties)
        {
            ArgumentNullException.ThrowIfNull(instance);
            ArgumentNullException.ThrowIfNull(validationContext);
 
            if (instance != validationContext.ObjectInstance)
            {
                throw new ArgumentException(SR.Validator_InstanceMustMatchValidationContextInstance, nameof(instance));
            }
 
            List<ValidationError> errors = GetObjectValidationErrors(instance, validationContext, validateAllProperties, false);
            if (errors.Count > 0)
            {
                errors[0].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.</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)
        {
            ArgumentNullException.ThrowIfNull(validationContext);
            ArgumentNullException.ThrowIfNull(validationAttributes);
 
            List<ValidationError> errors = GetValidationErrors(value, validationContext, validationAttributes, false);
            if (errors.Count > 0)
            {
                errors[0].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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        private static ValidationContext CreateValidationContext(object instance, ValidationContext validationContext)
        {
            Debug.Assert(validationContext != null);
 
            // Create a new context using the existing ValidationContext that acts as an IServiceProvider and contains our existing items.
            var 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 (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.IsInstanceOfType(value);
        }
 
        /// <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(SR.Format(SR.Validator_Property_Value_Wrong_Type, propertyName, propertyType),
                                            nameof(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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        private static List<ValidationError> GetObjectValidationErrors(object instance,
            ValidationContext validationContext, bool validateAllProperties, bool breakOnFirstError)
        {
            ArgumentNullException.ThrowIfNull(validationContext);
 
            Debug.Assert(instance != null);
 
            // Step 1: Validate the object properties' validation attributes
            List<ValidationError> errors = GetObjectPropertyValidationErrors(instance, validationContext, validateAllProperties, breakOnFirstError);
 
            // We only proceed to Step 2 if there are no errors
            if (errors.Count > 0)
            {
                return errors;
            }
 
            // Step 2: Validate the object's validation attributes
            var attributes = _store.GetTypeValidationAttributes(validationContext);
            errors.AddRange(GetValidationErrors(instance, validationContext, attributes, breakOnFirstError));
 
            // We only proceed to Step 3 if there are no errors
            if (errors.Count > 0)
            {
                return errors;
            }
 
            // Step 3: Test for IValidatableObject implementation
            if (instance is IValidatableObject validatable)
            {
                var results = validatable.Validate(validationContext);
 
                if (results != null)
                {
                    foreach (ValidationResult result in results)
                    {
                        if (result != ValidationResult.Success)
                        {
                            errors.Add(new ValidationError(null, instance, result));
                        }
                    }
                }
            }
 
            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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        private static List<ValidationError> GetObjectPropertyValidationErrors(object instance,
            ValidationContext validationContext, bool validateAllProperties, bool breakOnFirstError)
        {
            var properties = GetPropertyValues(instance, validationContext);
            var errors = new List<ValidationError>();
 
            foreach (var property in properties)
            {
                // get list of all validation attributes for this property
                var 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 first Required attribute
                    foreach (ValidationAttribute attribute in attributes)
                    {
                        if (attribute is RequiredAttribute reqAttr)
                        {
                            // 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
                            var validationResult = reqAttr.GetValidationResult(property.Value, property.Key);
                            if (validationResult != ValidationResult.Success)
                            {
                                errors.Add(new ValidationError(reqAttr, property.Value, validationResult!));
                            }
                            break;
                        }
                    }
                }
 
                if (breakOnFirstError && errors.Count > 0)
                {
                    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>
        [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
        private static List<KeyValuePair<ValidationContext, object?>> GetPropertyValues(object instance,
            ValidationContext validationContext)
        {
            var properties = TypeDescriptor.GetProperties(instance.GetType());
            var items = new List<KeyValuePair<ValidationContext, object?>>(properties.Count);
            foreach (PropertyDescriptor property in properties)
            {
                var context = CreateValidationContext(instance, validationContext);
                context.MemberName = property.Name;
                context.MemberType = property.PropertyType;
 
                if (_store.GetPropertyValidationAttributes(context).Any())
                {
                    items.Add(new KeyValuePair<ValidationContext, object?>(context, property.GetValue(instance)));
                }
            }
 
            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 List<ValidationError> GetValidationErrors(object? value,
            ValidationContext validationContext, IEnumerable<ValidationAttribute> attributes, bool breakOnFirstError)
        {
            ArgumentNullException.ThrowIfNull(validationContext);
 
            var errors = new List<ValidationError>();
            ValidationError? validationError;
 
            // Get the required validator if there is one and test it first, aborting on failure
            RequiredAttribute? required = null;
            foreach (ValidationAttribute attribute in attributes)
            {
                required = attribute as RequiredAttribute;
                if (required is not null)
                {
                    if (!TryValidate(value, validationContext, required, out validationError))
                    {
                        errors.Add(validationError);
                        return errors;
                    }
                    break;
                }
            }
 
            // 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,
            [NotNullWhen(false)] out ValidationError? validationError)
        {
            Debug.Assert(validationContext != null);
 
            var 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 sealed class ValidationError
        {
            private readonly object? _value;
            private readonly ValidationAttribute? _validationAttribute;
 
            internal ValidationError(ValidationAttribute? validationAttribute, object? value,
                ValidationResult validationResult)
            {
                _validationAttribute = validationAttribute;
                ValidationResult = validationResult;
                _value = value;
            }
 
            internal ValidationResult ValidationResult { get; }
 
            internal void ThrowValidationException() => throw new ValidationException(ValidationResult, _validationAttribute, _value);
        }
    }
}