File: System\ComponentModel\DataAnnotations\CustomValidationAttribute.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.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
 
namespace System.ComponentModel.DataAnnotations
{
    /// <summary>
    ///     Validation attribute that executes a user-supplied method at runtime, using one of these signatures:
    ///     <para>
    ///         public static <see cref="ValidationResult" /> Method(object value) { ... }
    ///     </para>
    ///     <para>
    ///         public static <see cref="ValidationResult" /> Method(object value, <see cref="ValidationContext" /> context) {
    ///         ... }
    ///     </para>
    ///     <para>
    ///         The value can be strongly typed as type conversion will be attempted.
    ///     </para>
    /// </summary>
    /// <remarks>
    ///     This validation attribute is used to invoke custom logic to perform validation at runtime.
    ///     Like any other <see cref="ValidationAttribute" />, its <see cref="IsValid(object, ValidationContext)" />
    ///     method is invoked to perform validation.  This implementation simply redirects that call to the method
    ///     identified by <see cref="Method" /> on a type identified by <see cref="ValidatorType" />
    ///     <para>
    ///         The supplied <see cref="ValidatorType" /> cannot be null, and it must be a public type.
    ///     </para>
    ///     <para>
    ///         The named <see cref="Method" /> must be public, static, return <see cref="ValidationResult" /> 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.
    ///     </para>
    ///     <para>
    ///         The <see cref="Method" /> may also declare an additional parameter of type <see cref="ValidationContext" />.
    ///         The <see cref="ValidationContext" /> parameter provides additional context the method may use to determine
    ///         the context in which it is being used.
    ///     </para>
    ///     <para>
    ///         If the method returns <see cref="ValidationResult" />.<see cref="ValidationResult.Success" />, that indicates
    ///         the given value is acceptable and validation passed.
    ///         Returning an instance of <see cref="ValidationResult" /> indicates that the value is not acceptable
    ///         and validation failed.
    ///     </para>
    ///     <para>
    ///         If the method returns a <see cref="ValidationResult" /> with a <c>null</c>
    ///         <see cref="ValidationResult.ErrorMessage" />
    ///         then the normal <see cref="ValidationAttribute.FormatErrorMessage" /> method will be called to compose the
    ///         error message.
    ///     </para>
    /// </remarks>
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method |
        AttributeTargets.Parameter, AllowMultiple = true)]
    public sealed class CustomValidationAttribute : ValidationAttribute
    {
        #region Member Fields
 
        private readonly Lazy<string?> _malformedErrorMessage;
        private bool _isSingleArgumentMethod;
        private string? _lastMessage;
        private MethodInfo? _methodInfo;
        private Type? _firstParameterType;
        private Tuple<string, Type>? _typeId;
 
        #endregion
 
        #region All Constructors
 
        /// <summary>
        ///     Instantiates a custom validation attribute that will invoke a method in the
        ///     specified type.
        /// </summary>
        /// <remarks>
        ///     An invalid <paramref name="validatorType" /> or <paramref name="method" /> will be cause
        ///     <see cref="IsValid(object, ValidationContext)" />> to return a <see cref="ValidationResult" />
        ///     and <see cref="ValidationAttribute.FormatErrorMessage" /> to return a summary error message.
        /// </remarks>
        /// <param name="validatorType">
        ///     The type that will contain the method to invoke.  It cannot be null.  See
        ///     <see cref="Method" />.
        /// </param>
        /// <param name="method">The name of the method to invoke in <paramref name="validatorType" />.</param>
        public CustomValidationAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type validatorType, string method)
            : base(() => SR.CustomValidationAttribute_ValidationError)
        {
            ValidatorType = validatorType;
            Method = method;
            _malformedErrorMessage = new Lazy<string?>(CheckAttributeWellFormed);
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        ///     Gets the type that contains the validation method identified by <see cref="Method" />.
        /// </summary>
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
        public Type ValidatorType { get; }
 
        /// <summary>
        /// Gets a unique identifier for this attribute.
        /// </summary>
        public override object TypeId => _typeId ??= new Tuple<string, Type>(Method, ValidatorType);
 
        /// <summary>
        ///     Gets the name of the method in <see cref="ValidatorType" /> to invoke to perform validation.
        /// </summary>
        public string Method { get; }
 
        public override bool RequiresValidationContext
        {
            get
            {
                // If attribute is not valid, throw an exception right away to inform the developer
                ThrowIfAttributeNotWellFormed();
                // We should return true when 2-parameter form of the validation method is used
                return !_isSingleArgumentMethod;
            }
        }
        #endregion
 
        /// <summary>
        ///     Override of validation method.  See <see cref="ValidationAttribute.IsValid(object, ValidationContext)" />.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">
        ///     A <see cref="ValidationContext" /> instance that provides
        ///     context about the validation operation, such as the object and member being validated.
        /// </param>
        /// <returns>Whatever the <see cref="Method" /> in <see cref="ValidatorType" /> returns.</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
        {
            // If attribute is not valid, throw an exception right away to inform the developer
            ThrowIfAttributeNotWellFormed();
 
            var methodInfo = _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 (!TryConvertValue(value, out convertedValue))
            {
                return new ValidationResult(SR.Format(SR.CustomValidationAttribute_Type_Conversion_Failed,
                                            (value != null ? value.GetType().ToString() : "null"),
                                            _firstParameterType,
                                            ValidatorType,
                                            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),
                var methodParams = _isSingleArgumentMethod
                    ? new object?[] { convertedValue }
                    : new[] { convertedValue, validationContext };
 
                var 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
                _lastMessage = null;
 
                if (result != null)
                {
                    _lastMessage = result.ErrorMessage;
                }
 
                return result;
            }
            catch (TargetInvocationException tie)
            {
                throw tie.InnerException!;
            }
        }
 
        /// <summary>
        ///     Override of <see cref="ValidationAttribute.FormatErrorMessage" />
        /// </summary>
        /// <param name="name">The name to include in the formatted string</param>
        /// <returns>A localized string to describe the problem.</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is malformed.</exception>
        public override string FormatErrorMessage(string name)
        {
            // If attribute is not valid, throw an exception right away to inform the developer
            ThrowIfAttributeNotWellFormed();
 
            if (!string.IsNullOrEmpty(_lastMessage))
            {
                return string.Format(CultureInfo.CurrentCulture, _lastMessage, name);
            }
 
            // If success or they supplied no custom message, use normal base class behavior
            return base.FormatErrorMessage(name);
        }
 
        /// <summary>
        ///     Checks whether the current attribute instance itself is valid for use.
        /// </summary>
        /// <returns>The error message why it is not well-formed, null if it is well-formed.</returns>
        private string? CheckAttributeWellFormed() => ValidateValidatorTypeParameter() ?? ValidateMethodParameter();
 
        /// <summary>
        ///     Internal helper to determine whether <see cref="ValidatorType" /> is legal for use.
        /// </summary>
        /// <returns><c>null</c> or the appropriate error message.</returns>
        private string? ValidateValidatorTypeParameter()
        {
            if (ValidatorType == null)
            {
                return SR.CustomValidationAttribute_ValidatorType_Required;
            }
 
            if (!ValidatorType.IsVisible)
            {
                return SR.Format(SR.CustomValidationAttribute_Type_Must_Be_Public, ValidatorType.Name);
            }
 
            return null;
        }
 
        /// <summary>
        ///     Internal helper to determine whether <see cref="Method" /> is legal for use.
        /// </summary>
        /// <returns><c>null</c> or the appropriate error message.</returns>
        private string? ValidateMethodParameter()
        {
            if (string.IsNullOrEmpty(Method))
            {
                return SR.CustomValidationAttribute_Method_Required;
            }
 
            // Named method must be public and static
            var methodInfo = ValidatorType.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .SingleOrDefault(m => string.Equals(m.Name, Method, StringComparison.Ordinal));
            if (methodInfo == null)
            {
                return SR.Format(SR.CustomValidationAttribute_Method_Not_Found, Method, ValidatorType.Name);
            }
 
            // Method must return a ValidationResult or derived class
            if (!typeof(ValidationResult).IsAssignableFrom(methodInfo.ReturnType))
            {
                return SR.Format(SR.CustomValidationAttribute_Method_Must_Return_ValidationResult, Method, 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 SR.Format(SR.CustomValidationAttribute_Method_Signature, Method, ValidatorType.Name);
            }
 
            // We accept 2 forms:
            // 1-parameter form is ValidationResult Method(object value)
            // 2-parameter form is ValidationResult Method(object value, ValidationContext context),
            _isSingleArgumentMethod = (parameterInfos.Length == 1);
 
            if (!_isSingleArgumentMethod)
            {
                if ((parameterInfos.Length != 2) || (parameterInfos[1].ParameterType != typeof(ValidationContext)))
                {
                    return SR.Format(SR.CustomValidationAttribute_Method_Signature, Method, ValidatorType.Name);
                }
            }
 
            _methodInfo = methodInfo;
            _firstParameterType = parameterInfos[0].ParameterType;
            return null;
        }
 
        /// <summary>
        ///     Throws InvalidOperationException if the attribute is not valid.
        /// </summary>
        private void ThrowIfAttributeNotWellFormed()
        {
            string? errorMessage = _malformedErrorMessage.Value;
            if (errorMessage != null)
            {
                throw new InvalidOperationException(errorMessage);
            }
        }
 
        /// <summary>
        ///     Attempts to convert the given value to the type needed to invoke the method for the current
        ///     CustomValidationAttribute.
        /// </summary>
        /// <param name="value">The value to check/convert.</param>
        /// <param name="convertedValue">If successful, the converted (or copied) value.</param>
        /// <returns><c>true</c> if type value was already correct or was successfully converted.</returns>
        private bool TryConvertValue(object? value, out object? convertedValue)
        {
            convertedValue = null;
            var expectedValueType = _firstParameterType!;
 
            // Null is permitted for reference types or for Nullable<>'s only
            if (value == null)
            {
                if (expectedValueType.IsValueType
                    && (!expectedValueType.IsGenericType
                        || expectedValueType.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 (expectedValueType.IsInstanceOfType(value))
            {
                convertedValue = value;
                return true;
            }
 
            // Value is not the right type -- attempt a convert.
            // Any expected exception returns a false
            try
            {
                convertedValue = Convert.ChangeType(value, expectedValueType, CultureInfo.CurrentCulture);
                return true;
            }
            catch (FormatException)
            {
                return false;
            }
            catch (InvalidCastException)
            {
                return false;
            }
            catch (NotSupportedException)
            {
                return false;
            }
        }
    }
}