File: ValidatableTypeInfo.cs
Web Access
Project: src\src\Validation\src\Microsoft.Extensions.Validation.csproj (Microsoft.Extensions.Validation)
// 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.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
 
namespace Microsoft.Extensions.Validation;
 
/// <summary>
/// Contains validation information for a type.
/// </summary>
[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
public abstract class ValidatableTypeInfo : IValidatableInfo
{
    private readonly int _membersCount;
    private readonly List<Type> _superTypes;
 
    /// <summary>
    /// Creates a new instance of <see cref="ValidatableTypeInfo"/>.
    /// </summary>
    /// <param name="type">The type being validated.</param>
    /// <param name="members">The members that can be validated.</param>
    protected ValidatableTypeInfo(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
        IReadOnlyList<ValidatablePropertyInfo> members)
    {
        Type = type;
        Members = members;
        _membersCount = members.Count;
        _superTypes = type.GetAllImplementedTypes();
    }
 
    /// <summary>
    /// Gets the validation attributes for this member.
    /// </summary>
    /// <returns>An array of validation attributes to apply to this member.</returns>
    protected abstract ValidationAttribute[] GetValidationAttributes();
 
    /// <summary>
    /// The type being validated.
    /// </summary>
    internal Type Type { get; }
 
    /// <summary>
    /// The members that can be validated.
    /// </summary>
    internal IReadOnlyList<ValidatablePropertyInfo> Members { get; }
 
    /// <inheritdoc />
    public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken)
    {
        if (value == null)
        {
            return;
        }
 
        // Check if we've exceeded the maximum depth
        if (context.CurrentDepth >= context.ValidationOptions.MaxDepth)
        {
            throw new InvalidOperationException(
                $"Maximum validation depth of {context.ValidationOptions.MaxDepth} exceeded at '{context.CurrentValidationPath}' in '{Type.Name}'. " +
                "This is likely caused by a circular reference in the object graph. " +
                "Consider increasing the MaxDepth in ValidationOptions if deeper validation is required.");
        }
 
        var originalPrefix = context.CurrentValidationPath;
        var originalErrorCount = context.ValidationErrors?.Count ?? 0;
 
        try
        {
            // First validate direct members
            await ValidateMembersAsync(value, context, cancellationToken);
 
            var actualType = value.GetType();
 
            // Then validate inherited members
            foreach (var superTypeInfo in GetSuperTypeInfos(actualType, context))
            {
                await superTypeInfo.ValidateMembersAsync(value, context, cancellationToken);
            }
 
            // If any property-level validation errors were found, return early
            if (context.ValidationErrors is not null && context.ValidationErrors.Count > originalErrorCount)
            {
                return;
            }
 
            // Validate type-level attributes
            ValidateTypeAttributes(value, context);
 
            // If any type-level attribute errors were found, return early
            if (context.ValidationErrors is not null && context.ValidationErrors.Count > originalErrorCount)
            {
                return;
            }
 
            // Finally validate IValidatableObject if implemented
            ValidateValidatableObjectInterface(value, context);
        }
        finally
        {
            context.CurrentValidationPath = originalPrefix;
        }
    }
 
    private async Task ValidateMembersAsync(object? value, ValidateContext context, CancellationToken cancellationToken)
    {
        var originalPrefix = context.CurrentValidationPath;
 
        for (var i = 0; i < _membersCount; i++)
        {
            try
            {
                await Members[i].ValidateAsync(value, context, cancellationToken);
 
            }
            finally
            {
                context.CurrentValidationPath = originalPrefix;
            }
        }
    }
 
    private void ValidateTypeAttributes(object? value, ValidateContext context)
    {
        var validationAttributes = GetValidationAttributes();
        var errorPrefix = context.CurrentValidationPath;
 
        for (var i = 0; i < validationAttributes.Length; i++)
        {
            var attribute = validationAttributes[i];
            var result = attribute.GetValidationResult(value, context.ValidationContext);
            if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
            {
                // Create a validation error for each member name that is provided
                foreach (var memberName in result.MemberNames)
                {
                    var key = string.IsNullOrEmpty(errorPrefix) ? memberName : $"{errorPrefix}.{memberName}";
                    context.AddOrExtendValidationError(memberName, key, result.ErrorMessage, value);
                }
 
                if (!result.MemberNames.Any())
                {
                    // If no member names are specified, then treat this as a top-level error
                    context.AddOrExtendValidationError(string.Empty, errorPrefix, result.ErrorMessage, value);
                }
            }
        }
    }
 
    private void ValidateValidatableObjectInterface(object? value, ValidateContext context)
    {
        if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable)
        {
            // Important: Set the DisplayName to the type name for top-level validations
            // and restore the original validation context properties
            var originalDisplayName = context.ValidationContext.DisplayName;
            var originalMemberName = context.ValidationContext.MemberName;
            var errorPrefix = context.CurrentValidationPath;
 
            // Set the display name to the class name for IValidatableObject validation
            context.ValidationContext.DisplayName = Type.Name;
            context.ValidationContext.MemberName = null;
 
            var validationResults = validatable.Validate(context.ValidationContext);
            foreach (var validationResult in validationResults)
            {
                if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null)
                {
                    // Create a validation error for each member name that is provided
                    foreach (var memberName in validationResult.MemberNames)
                    {
                        var key = string.IsNullOrEmpty(errorPrefix) ? memberName : $"{errorPrefix}.{memberName}";
                        context.AddOrExtendValidationError(memberName, key, validationResult.ErrorMessage, value);
                    }
 
                    if (!validationResult.MemberNames.Any())
                    {
                        // If no member names are specified, then treat this as a top-level error
                        context.AddOrExtendValidationError(string.Empty, string.Empty, validationResult.ErrorMessage, value);
                    }
                }
            }
 
            // Restore the original validation context properties
            context.ValidationContext.DisplayName = originalDisplayName;
            context.ValidationContext.MemberName = originalMemberName;
        }
    }
 
    private IEnumerable<ValidatableTypeInfo> GetSuperTypeInfos(Type actualType, ValidateContext context)
    {
        foreach (var superType in _superTypes.Where(t => t.IsAssignableFrom(actualType)))
        {
            if (context.ValidationOptions.TryGetValidatableTypeInfo(superType, out var found)
                && found is ValidatableTypeInfo superTypeInfo)
            {
                yield return superTypeInfo;
            }
        }
    }
}