File: System\ComponentModel\DataAnnotations\RangeAttribute.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;
 
namespace System.ComponentModel.DataAnnotations
{
    /// <summary>
    ///     Used for specifying a range constraint
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
        AllowMultiple = false)]
    public class RangeAttribute : ValidationAttribute
    {
        /// <summary>
        ///     Constructor that takes integer minimum and maximum values
        /// </summary>
        /// <param name="minimum">The minimum value, inclusive</param>
        /// <param name="maximum">The maximum value, inclusive</param>
        public RangeAttribute(int minimum, int maximum)
            : base(populateErrorMessageResourceAccessor: false)
        {
            Minimum = minimum;
            Maximum = maximum;
            OperandType = typeof(int);
            ErrorMessageResourceAccessor = GetValidationErrorMessage;
        }
 
        /// <summary>
        ///     Constructor that takes double minimum and maximum values
        /// </summary>
        /// <param name="minimum">The minimum value, inclusive</param>
        /// <param name="maximum">The maximum value, inclusive</param>
        public RangeAttribute(double minimum, double maximum)
            : base(populateErrorMessageResourceAccessor: false)
        {
            Minimum = minimum;
            Maximum = maximum;
            OperandType = typeof(double);
            ErrorMessageResourceAccessor = GetValidationErrorMessage;
        }
 
        /// <summary>
        ///     Allows for specifying range for arbitrary types. The minimum and maximum strings
        ///     will be converted to the target type.
        /// </summary>
        /// <param name="type">The type of the range parameters. Must implement IComparable.</param>
        /// <param name="minimum">The minimum allowable value.</param>
        /// <param name="maximum">The maximum allowable value.</param>
        [RequiresUnreferencedCode("Generic TypeConverters may require the generic types to be annotated. For example, NullableConverter requires the underlying type to be DynamicallyAccessedMembers All.")]
        public RangeAttribute(
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
            string minimum,
            string maximum)
            : base(populateErrorMessageResourceAccessor: false)
        {
            OperandType = type;
            Minimum = minimum;
            Maximum = maximum;
            ErrorMessageResourceAccessor = GetValidationErrorMessage;
        }
 
        /// <summary>
        ///     Gets the minimum value for the range
        /// </summary>
        public object Minimum { get; private set; }
 
        /// <summary>
        ///     Gets the maximum value for the range
        /// </summary>
        public object Maximum { get; private set; }
 
        /// <summary>
        ///     Specifies whether validation should fail for values that are equal to <see cref="Minimum"/>.
        /// </summary>
        public bool MinimumIsExclusive { get; set; }
 
        /// <summary>
        ///     Specifies whether validation should fail for values that are equal to <see cref="Maximum"/>.
        /// </summary>
        public bool MaximumIsExclusive { get; set; }
 
        /// <summary>
        ///     Gets the type of the <see cref="Minimum" /> and <see cref="Maximum" /> values (e.g. Int32, Double, or some custom
        ///     type)
        /// </summary>
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
        public Type OperandType { get; }
 
        /// <summary>
        /// Determines whether string values for <see cref="Minimum"/> and <see cref="Maximum"/> are parsed in the invariant
        /// culture rather than the current culture in effect at the time of the validation.
        /// </summary>
        public bool ParseLimitsInInvariantCulture { get; set; }
 
        /// <summary>
        /// Determines whether any conversions necessary from the value being validated to <see cref="OperandType"/> as set
        /// by the <c>type</c> parameter of the <see cref="RangeAttribute(Type, string, string)"/> constructor are carried
        /// out in the invariant culture rather than the current culture in effect at the time of the validation.
        /// </summary>
        /// <remarks>This property has no effects with the constructors with <see cref="int"/> or <see cref="double"/>
        /// parameters, for which the invariant culture is always used for any conversions of the validated value.</remarks>
        public bool ConvertValueInInvariantCulture { get; set; }
 
        private Func<object, object?>? Conversion { get; set; }
 
        private void Initialize(IComparable minimum, IComparable maximum, Func<object, object?> conversion)
        {
            int cmp = minimum.CompareTo(maximum);
            if (cmp > 0)
            {
                throw new InvalidOperationException(SR.Format(SR.RangeAttribute_MinGreaterThanMax, maximum, minimum));
            }
            else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive))
            {
                throw new InvalidOperationException(SR.RangeAttribute_CannotUseExclusiveBoundsWhenTheyAreEqual);
            }
 
            Minimum = minimum;
            Maximum = maximum;
            Conversion = conversion;
        }
 
        /// <summary>
        ///     Returns true if the value falls between min and max, inclusive.
        /// </summary>
        /// <param name="value">The value to test for validity.</param>
        /// <returns><c>true</c> means the <paramref name="value" /> is valid</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
        public override bool IsValid(object? value)
        {
            // Validate our properties and create the conversion function
            SetupConversion();
 
            // Automatically pass if value is null or empty. RequiredAttribute should be used to assert a value is not empty.
            if (value is null or string { Length: 0 })
            {
                return true;
            }
 
            object? convertedValue;
 
            try
            {
                convertedValue = Conversion!(value);
            }
            catch (FormatException)
            {
                return false;
            }
            catch (InvalidCastException)
            {
                return false;
            }
            catch (NotSupportedException)
            {
                return false;
            }
 
            var min = (IComparable)Minimum;
            var max = (IComparable)Maximum;
            return
                (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) &&
                (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0);
        }
 
        /// <summary>
        ///     Override of <see cref="ValidationAttribute.FormatErrorMessage" />
        /// </summary>
        /// <remarks>This override exists to provide a formatted message describing the minimum and maximum values</remarks>
        /// <param name="name">The user-visible name to include in the formatted message.</param>
        /// <returns>A localized string describing the minimum and maximum values</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
        public override string FormatErrorMessage(string name)
        {
            SetupConversion();
 
            return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, Minimum, Maximum);
        }
 
        /// <summary>
        ///     Validates the properties of this attribute and sets up the conversion function.
        ///     This method throws exceptions if the attribute is not configured properly.
        ///     If it has once determined it is properly configured, it is a NOP.
        /// </summary>
        private void SetupConversion()
        {
            if (Conversion == null)
            {
                object minimum = Minimum;
                object maximum = Maximum;
 
                if (minimum == null || maximum == null)
                {
                    throw new InvalidOperationException(SR.RangeAttribute_Must_Set_Min_And_Max);
                }
 
                // Careful here -- OperandType could be int or double if they used the long form of the ctor.
                // But the min and max would still be strings.  Do use the type of the min/max operands to condition
                // the following code.
                Type operandType = minimum.GetType();
 
                if (operandType == typeof(int))
                {
                    Initialize((int)minimum, (int)maximum, v => Convert.ToInt32(v, CultureInfo.InvariantCulture));
                }
                else if (operandType == typeof(double))
                {
                    Initialize((double)minimum, (double)maximum,
                        v => Convert.ToDouble(v, CultureInfo.InvariantCulture));
                }
                else
                {
                    Type type = OperandType;
                    if (type == null)
                    {
                        throw new InvalidOperationException(SR.RangeAttribute_Must_Set_Operand_Type);
                    }
                    Type comparableType = typeof(IComparable);
                    if (!comparableType.IsAssignableFrom(type))
                    {
                        throw new InvalidOperationException(SR.Format(SR.RangeAttribute_ArbitraryTypeNotIComparable,
                                                            type.FullName,
                                                            comparableType.FullName));
                    }
 
                    TypeConverter converter = GetOperandTypeConverter();
                    IComparable min = (IComparable)(ParseLimitsInInvariantCulture
                        ? converter.ConvertFromInvariantString((string)minimum)!
                        : converter.ConvertFromString((string)minimum))!;
                    IComparable max = (IComparable)(ParseLimitsInInvariantCulture
                        ? converter.ConvertFromInvariantString((string)maximum)!
                        : converter.ConvertFromString((string)maximum))!;
 
                    Func<object, object?> conversion;
                    if (ConvertValueInInvariantCulture)
                    {
                        conversion = value => value.GetType() == type
                            ? value
                            : converter.ConvertFrom(null, CultureInfo.InvariantCulture, value);
                    }
                    else
                    {
                        conversion = value => value.GetType() == type ? value : converter.ConvertFrom(value);
                    }
 
                    Initialize(min, max, conversion);
                }
            }
        }
 
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
            Justification = "The ctor that allows this code to be called is marked with RequiresUnreferencedCode.")]
        private TypeConverter GetOperandTypeConverter() =>
            TypeDescriptor.GetConverter(OperandType);
 
        private string GetValidationErrorMessage()
        {
            return (MinimumIsExclusive, MaximumIsExclusive) switch
            {
                (false, false) => SR.RangeAttribute_ValidationError,
                (true, false) => SR.RangeAttribute_ValidationError_MinExclusive,
                (false, true) => SR.RangeAttribute_ValidationError_MaxExclusive,
                (true, true) => SR.RangeAttribute_ValidationError_MinExclusive_MaxExclusive,
            };
        }
    }
}