File: RuntimeValidatableParameterInfoResolver.cs
Web Access
Project: src\aspnetcore\src\Validation\src\Microsoft.Extensions.Validation.csproj (Microsoft.Extensions.Validation)
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using System.Linq;
using System.Reflection;
using System.Security.Claims;

namespace Microsoft.Extensions.Validation;

internal sealed class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver
{
    // TODO: the implementation currently relies on static discovery of types.
    public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
    {
        validatableInfo = null;
        return false;
    }

    public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
    {
        if (parameterInfo.Name == null)
        {
            throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
        }

        // Skip method parameter if it or its type are annotated with SkipValidationAttribute.
        if (parameterInfo.GetCustomAttribute<SkipValidationAttribute>() != null ||
            parameterInfo.ParameterType.GetCustomAttribute<SkipValidationAttribute>() != null)
        {
            validatableInfo = null;
            return false;
        }

        var validationAttributes = parameterInfo
            .GetCustomAttributes<ValidationAttribute>()
            .ToArray();

        // If there are no validation attributes and this type is not a complex type
        // we don't need to validate it. Complex types without attributes are still
        // validatable because we want to run the validations on the properties.
        if (validationAttributes.Length == 0 && !IsComplexType(parameterInfo.ParameterType))
        {
            validatableInfo = null;
            return false;
        }

        var displayNameInfo = ResolveDisplayInfo(parameterInfo);

        validatableInfo = new RuntimeValidatableParameterInfo(
            parameterType: parameterInfo.ParameterType,
            name: parameterInfo.Name,
            displayNameInfo: displayNameInfo,
            validationAttributes: validationAttributes
        );
        return true;
    }

    private static DisplayNameInfo? ResolveDisplayInfo(ParameterInfo parameterInfo)
    {
        var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
        if (displayAttribute is { ResourceType: not null, Name: not null })
        {
            // Resource-based display name from [Display(ResourceType = ..., Name = ...)] is the
            // canonical localized source; the IValidationLocalizer is intentionally bypassed.
            // The DisplayAttribute instance is retained for the lifetime of the resolver, mirroring
            // the source-generator's static accessor design.
            return new ParameterReflectionDisplayName(displayAttribute);
        }

        if (displayAttribute?.Name is not null)
        {
            // Literal name from [Display(Name = "...")].
            return new LiteralDisplayName(displayAttribute.Name);
        }

        var displayNameAttribute = parameterInfo.GetCustomAttribute<DisplayNameAttribute>();
        if (displayNameAttribute is not null)
        {
            // Literal name from [DisplayName("...")].
            return new LiteralDisplayName(displayNameAttribute.DisplayName);
        }

        return null;
    }

    internal sealed class RuntimeValidatableParameterInfo(
        Type parameterType,
        string name,
        DisplayNameInfo? displayNameInfo,
        ValidationAttribute[] validationAttributes) :
            ValidatableParameterInfo(parameterType, name, displayNameInfo)
    {
        protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;

        private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
    }

    private sealed class LiteralDisplayName(string literal) : DisplayNameInfo
    {
        public override string? GetDisplayName(ValidateContext context, string memberName, Type? declaringType)
        {
            var localizer = context.ValidationOptions.Localizer;
            if (localizer is null)
            {
                return literal;
            }

            // The literal acts as both the lookup key for the localizer AND the fallback display
            // name when the localizer can't translate.
            return localizer.ResolveDisplayName(new DisplayNameLocalizationContext
            {
                DeclaringType = declaringType,
                DisplayName = literal,
                MemberName = memberName,
            }) ?? literal;
        }
    }

    private sealed class ParameterReflectionDisplayName(DisplayAttribute attribute) : DisplayNameInfo
    {
        public override string? GetDisplayName(ValidateContext context, string memberName, Type? declaringType)
            => attribute.GetName();
    }

    private static bool IsComplexType(Type type)
    {
        // Skip primitives, enums, common built-in types, and types that are specially
        // handled by RDF/RDG that don't need validation if they don't have attributes
        if (type.IsPrimitive ||
            type.IsEnum ||
            type == typeof(string) ||
            type == typeof(decimal) ||
            type == typeof(DateTime) ||
            type == typeof(DateTimeOffset) ||
            type == typeof(TimeOnly) ||
            type == typeof(DateOnly) ||
            type == typeof(TimeSpan) ||
            type == typeof(Guid) ||
            type == typeof(ClaimsPrincipal) ||
            type == typeof(CancellationToken) ||
            type == typeof(Stream) ||
            type == typeof(PipeReader))
        {
            return false;
        }

        // Check if the underlying type in a nullable is valid
        if (Nullable.GetUnderlyingType(type) is { } nullableType)
        {
            return IsComplexType(nullableType);
        }

        // Complex types include both reference types (classes) and value types (structs, record structs)
        // that aren't in the exclusion list above
        return type.IsClass || type.IsValueType;
    }
}