File: src\Shared\Data.Validation\LengthAttribute.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Compliance.Redaction\Microsoft.Extensions.Compliance.Redaction.csproj (Microsoft.Extensions.Compliance.Redaction)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
 
#pragma warning disable CA1716
namespace Microsoft.Shared.Data.Validation;
#pragma warning restore CA1716
 
/// <summary>
/// Specifies the minimum length of any <see cref="IEnumerable"/> or <see cref="string"/> objects.
/// </summary>
/// <remarks>
/// The standard <see cref="MinLengthAttribute" /> supports only non generic <see cref="Array"/> or <see cref="string"/> typed objects
/// on .NET Framework, while <see cref="System.Collections.Generic.ICollection{T}"/> type is supported only on .NET Core.
/// See issue here <see href="https://github.com/dotnet/runtime/issues/23288"/>.
/// This attribute aims to allow validation of all these objects in a consistent manner across target frameworks.
/// </remarks>
#if !SHARED_PROJECT
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
#endif
 
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
 
internal sealed class LengthAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum allowed length of the collection or string.
    /// </summary>
    public int MinimumLength { get; }
 
    /// <summary>
    /// Gets the maximum allowed length of the collection or string.
    /// </summary>
    public int? MaximumLength { get; }
 
    /// <summary>
    /// Gets or sets a value indicating whether the length validation should exclude the <see cref="MinimumLength"/> and <see cref="MaximumLength"/> values.
    /// </summary>
    /// <value>
    /// The default value is <c>false</c>.
    /// </value>
    public bool Exclusive { get; set; }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    [RequiresUnreferencedCode("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")]
    public LengthAttribute(int minimumLength)
    {
        MinimumLength = minimumLength;
        MaximumLength = null;
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    /// <param name="maximumLength">
    /// The maximum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    [RequiresUnreferencedCode("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")]
    public LengthAttribute(int minimumLength, int maximumLength)
    {
        MinimumLength = minimumLength;
        MaximumLength = maximumLength;
    }
 
    /// <summary>
    /// Validates that a given value is in range.
    /// </summary>
    /// <remarks>
    /// This method returns <c>true</c> if the <paramref name = "value" /> is <see langword="null"/>.
    /// It is assumed the <see cref = "RequiredAttribute" /> is used if the value may not be <see langword="null"/>.
    /// </remarks>
    /// <param name="value">The value to validate.</param>
    /// <param name="validationContext">Additional context for this validation.</param>
    /// <returns>A value indicating success or failure.</returns>
    /// <exception cref="ArgumentOutOfRangeException"><see cref="MinimumLength"/> is less than zero or if it is greater than <see cref="MaximumLength"/>.</exception>
    /// <exception cref="InvalidOperationException">The validated type is not supported.</exception>
    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The ctor is marked with RequiresUnreferencedCode.")]
    protected override ValidationResult IsValid(object? value, ValidationContext? validationContext)
    {
        if (MinimumLength < 0)
        {
            throw new InvalidOperationException($"{nameof(LengthAttribute)} requires a minimum length >= 0 (see field {validationContext.GetDisplayName()})");
        }
 
        if (MaximumLength.HasValue && MinimumLength >= MaximumLength)
        {
            throw new InvalidOperationException($"{nameof(LengthAttribute)} requires the minimum length to be less than maximum length (see field {validationContext.GetDisplayName()})");
        }
 
        // Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null.
        if (value == null)
        {
            return ValidationResult.Success!;
        }
 
        int count;
        switch (value)
        {
            case string s:
                count = s.Length;
                break;
 
            case ICollection c:
                count = c.Count;
                break;
 
            case IEnumerable e:
                count = 0;
                foreach (var item in e)
                {
                    count++;
                }
 
                break;
 
            default:
                var property = GetCountProperty(value);
                if (property != null && property.CanRead && property.PropertyType == typeof(int))
                {
                    count = (int)property.GetValue(value)!;
                }
                else
                {
                    throw new InvalidOperationException($"{nameof(LengthAttribute)} is not supported for fields of type {value.GetType()} (see field {validationContext.GetDisplayName()})");
                }
 
                break;
        }
 
        return Validate(count, validationContext);
    }
 
    [RequiresUnreferencedCode("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")]
    private static PropertyInfo? GetCountProperty(object value) => value.GetType().GetRuntimeProperty("Count");
 
    private ValidationResult Validate(int count, ValidationContext? validationContext)
    {
        bool result;
 
        if (MaximumLength.HasValue)
        {
            // Minimum and maximum length validation.
            result = Exclusive
                ? count > MinimumLength && count < MaximumLength
                : count >= MinimumLength && count <= MaximumLength;
        }
        else
        {
            // Minimum length validation only.
            result = Exclusive
                ? count > MinimumLength
                : count >= MinimumLength;
        }
 
        if (!result)
        {
            if (!string.IsNullOrEmpty(ErrorMessage) || !string.IsNullOrEmpty(ErrorMessageResourceName))
            {
                return new ValidationResult(FormatErrorMessage(validationContext.GetDisplayName()), validationContext.GetMemberName());
            }
 
            var exclusiveString = Exclusive ? "exclusive " : string.Empty;
            var orEqualString = Exclusive ? string.Empty : "or equal ";
            var validationMessage = MaximumLength.HasValue
                ? $"The field {validationContext.GetDisplayName()} length must be in the {exclusiveString}range [{MinimumLength}..{MaximumLength}]."
                : $"The field {validationContext.GetDisplayName()} length must be greater {orEqualString}than {MinimumLength}.";
 
            return new ValidationResult(validationMessage, validationContext.GetMemberName());
        }
 
        return ValidationResult.Success!;
    }
}