File: ModelBinding\Validation\ValidationVisitor.cs
Web Access
Project: src\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
 
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
 
/// <summary>
/// A visitor implementation that interprets <see cref="ValidationStateDictionary"/> to traverse
/// a model object graph and perform validation.
/// </summary>
public class ValidationVisitor
{
    private readonly ValidationStack _currentPath;
    private int? _maxValidationDepth;
 
    /// <summary>
    /// Creates a new <see cref="ValidationVisitor"/>.
    /// </summary>
    /// <param name="actionContext">The <see cref="ActionContext"/> associated with the current request.</param>
    /// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/>.</param>
    /// <param name="validatorCache">The <see cref="ValidatorCache"/> that provides a list of <see cref="IModelValidator"/>s.</param>
    /// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
    /// <param name="validationState">The <see cref="ValidationStateDictionary"/>.</param>
    public ValidationVisitor(
        ActionContext actionContext,
        IModelValidatorProvider validatorProvider,
        ValidatorCache validatorCache,
        IModelMetadataProvider metadataProvider,
        ValidationStateDictionary? validationState)
    {
        ArgumentNullException.ThrowIfNull(actionContext);
        ArgumentNullException.ThrowIfNull(validatorProvider);
        ArgumentNullException.ThrowIfNull(validatorCache);
 
        Context = actionContext;
        ValidatorProvider = validatorProvider;
        Cache = validatorCache;
 
        MetadataProvider = metadataProvider;
        ValidationState = validationState;
 
        ModelState = actionContext.ModelState;
        _currentPath = new ValidationStack();
    }
 
    /// <summary>
    /// The model validator provider.
    /// </summary>
    protected IModelValidatorProvider ValidatorProvider { get; }
 
    /// <summary>
    /// The model metadata provider.
    /// </summary>
    protected IModelMetadataProvider MetadataProvider { get; }
 
    /// <summary>
    /// The validator cache.
    /// </summary>
    protected ValidatorCache Cache { get; }
 
    /// <summary>
    /// The action context.
    /// </summary>
    protected ActionContext Context { get; }
 
    /// <summary>
    /// The model state.
    /// </summary>
    protected ModelStateDictionary ModelState { get; }
 
    /// <summary>
    /// The validation state.
    /// </summary>
    protected ValidationStateDictionary? ValidationState { get; }
 
    /// <summary>
    /// The container.
    /// </summary>
    protected object? Container { get; set; }
 
    /// <summary>
    /// The key.
    /// </summary>
    protected string? Key { get; set; }
 
    /// <summary>
    /// The model.
    /// </summary>
    protected object? Model { get; set; }
 
    /// <summary>
    /// The model metadata.
    /// </summary>
    protected ModelMetadata? Metadata { get; set; }
 
    /// <summary>
    /// The validation strategy.
    /// </summary>
    protected IValidationStrategy? Strategy { get; set; }
 
    /// <summary>
    /// Gets or sets the maximum depth to constrain the validation visitor when validating.
    /// <para>
    /// <see cref="ValidationVisitor"/> traverses the object graph of the model being validated. For models
    /// that are very deep or are infinitely recursive, validation may result in stack overflow.
    /// </para>
    /// <para>
    /// When not <see langword="null"/>, <see cref="Visit(ModelMetadata, string, object)"/> will throw if
    /// current traversal depth exceeds the specified value.
    /// </para>
    /// </summary>
    public int? MaxValidationDepth
    {
        get => _maxValidationDepth;
        set
        {
            if (value != null && value <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value));
            }
 
            _maxValidationDepth = value;
        }
    }
 
    /// <summary>
    /// Indicates whether validation of a complex type should be performed if validation fails for any of its children. The default behavior is false.
    /// </summary>
    public bool ValidateComplexTypesIfChildValidationFails { get; set; }
 
    /// <summary>
    /// Validates a object.
    /// </summary>
    /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
    /// <param name="key">The model prefix key.</param>
    /// <param name="model">The model object.</param>
    /// <returns><c>true</c> if the object is valid, otherwise <c>false</c>.</returns>
    public bool Validate(ModelMetadata metadata, string key, object model)
    {
        return Validate(metadata, key, model, alwaysValidateAtTopLevel: false);
    }
 
    /// <summary>
    /// Validates a object.
    /// </summary>
    /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
    /// <param name="key">The model prefix key.</param>
    /// <param name="model">The model object.</param>
    /// <param name="alwaysValidateAtTopLevel">If <c>true</c>, applies validation rules even if the top-level value is <c>null</c>.</param>
    /// <returns><c>true</c> if the object is valid, otherwise <c>false</c>.</returns>
    public virtual bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel)
        => Validate(metadata, key, model, alwaysValidateAtTopLevel, container: null);
 
    /// <summary>
    /// Validates a object.
    /// </summary>
    /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
    /// <param name="key">The model prefix key.</param>
    /// <param name="model">The model object.</param>
    /// <param name="alwaysValidateAtTopLevel">If <c>true</c>, applies validation rules even if the top-level value is <c>null</c>.</param>
    /// <param name="container">The model container.</param>
    /// <returns><c>true</c> if the object is valid, otherwise <c>false</c>.</returns>
    public virtual bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel, object? container)
    {
        if (container != null && metadata!.MetadataKind != ModelMetadataKind.Property)
        {
            throw new ArgumentException(Resources.FormatValidationVisitor_ContainerCannotBeSpecified(metadata.MetadataKind));
        }
 
        if (model == null && key != null && !alwaysValidateAtTopLevel)
        {
            var entry = ModelState[key];
 
            // Rationale: We might see the same model state key for two different objects and want to preserve any
            // known invalidity.
            if (entry != null && entry.ValidationState != ModelValidationState.Invalid)
            {
                entry.ValidationState = ModelValidationState.Valid;
            }
 
            return true;
        }
 
        // Container is non-null only when validation top-level properties. Start off by treating "container" as the "Model" instance.
        // Invoking StateManager.Recurse later in this invocation will result in it being correctly used as the container instance during the
        // validation of "model".
        Model = container;
        return Visit(metadata!, key, model);
    }
 
    /// <summary>
    /// Validates a single node in a model object graph.
    /// </summary>
    /// <returns><c>true</c> if the node is valid, otherwise <c>false</c>.</returns>
    protected virtual bool ValidateNode()
    {
        Debug.Assert(Key != null);
        Debug.Assert(Metadata != null);
        var state = ModelState.GetValidationState(Key);
 
        // Rationale: we might see the same model state key used for two different objects.
        // We want to run validation unless it's already known that this key is invalid.
        if (state != ModelValidationState.Invalid)
        {
            var validators = Cache.GetValidators(Metadata, ValidatorProvider);
 
            var count = validators.Count;
            if (count > 0)
            {
                var context = new ModelValidationContext(
                    Context,
                    Metadata!,
                    MetadataProvider,
                    Container,
                    Model);
 
                var results = new List<ModelValidationResult>();
                for (var i = 0; i < count; i++)
                {
                    results.AddRange(validators[i].Validate(context));
                }
 
                var resultsCount = results.Count;
                for (var i = 0; i < resultsCount; i++)
                {
                    var result = results[i];
                    var key = ModelNames.CreatePropertyModelName(Key, result.MemberName);
 
                    // It's OK for key to be the empty string here. This can happen when a top
                    // level object implements IValidatableObject.
                    ModelState.TryAddModelError(key, result.Message);
                }
            }
        }
 
        state = ModelState.GetFieldValidationState(Key);
        if (state == ModelValidationState.Invalid)
        {
            return false;
        }
        else
        {
            // If the field has an entry in ModelState, then record it as valid. Don't create
            // extra entries if they don't exist already.
            var entry = ModelState[Key];
            if (entry != null)
            {
                entry.ValidationState = ModelValidationState.Valid;
            }
 
            return true;
        }
    }
 
    /// <summary>
    /// Validate something in a model.
    /// </summary>
    /// <param name="metadata">The model metadata.</param>
    /// <param name="key">The key to validate.</param>
    /// <param name="model">The model to validate.</param>
    /// <see langword="true"/> if the specified model key is valid, otherwise <see langword="false"/>.
    /// <returns>Whether the the specified model key is valid.</returns>
    protected virtual bool Visit(ModelMetadata metadata, string? key, object? model)
    {
        RuntimeHelpers.EnsureSufficientExecutionStack();
 
        if (model != null && !_currentPath.Push(model))
        {
            // This is a cycle, bail.
            return true;
        }
 
        bool result;
        try
        {
            // Throws InvalidOperationException if the object graph is too deep
            result = VisitImplementation(ref metadata, ref key, model);
        }
        finally
        {
            _currentPath.Pop(model);
        }
        return result;
    }
 
    private bool VisitImplementation(ref ModelMetadata metadata, ref string? key, object? model)
    {
        if (MaxValidationDepth != null && _currentPath.Count > MaxValidationDepth)
        {
            // Non cyclic but too deep an object graph.
 
            string message;
            switch (metadata.MetadataKind)
            {
                case ModelMetadataKind.Property:
                    message = Resources.FormatValidationVisitor_ExceededMaxPropertyDepth(nameof(ValidationVisitor), MaxValidationDepth, metadata.Name, metadata.ContainerType);
                    break;
 
                default:
                    // Since the minimum depth is never 0, MetadataKind can never be Parameter. Consequently we only special case MetadataKind.Property.
                    message = Resources.FormatValidationVisitor_ExceededMaxDepth(nameof(ValidationVisitor), MaxValidationDepth, metadata.ModelType);
                    break;
            }
 
            message += " " + Resources.FormatValidationVisitor_ExceededMaxDepthFix(nameof(MvcOptions), nameof(MvcOptions.MaxValidationDepth));
            throw new InvalidOperationException(message)
            {
                HelpLink = "https://aka.ms/AA21ue1",
            };
        }
 
        var entry = GetValidationEntry(model);
        key = entry?.Key ?? key ?? string.Empty;
        metadata = entry?.Metadata ?? metadata;
        var strategy = entry?.Strategy;
 
        if (ModelState.HasReachedMaxErrors)
        {
            SuppressValidation(key);
            return false;
        }
        else if (entry != null && entry.SuppressValidation)
        {
            // Use the key on the entry, because we might not have entries in model state.
            SuppressValidation(entry.Key);
            return true;
        }
        // If the metadata indicates that no validators exist AND the aggregate state for the key says that the model graph
        // is not invalid (i.e. is one of Unvalidated, Valid, or Skipped) we can safely mark the graph as valid.
        else if (metadata.HasValidators == false &&
            ModelState.GetFieldValidationState(key) != ModelValidationState.Invalid)
        {
            if (metadata.BoundConstructor != null)
            {
                metadata.ThrowIfRecordTypeHasValidationOnProperties();
            }
 
            // No validators will be created for this graph of objects. Mark it as valid if it wasn't previously validated.
            var entries = ModelState.FindKeysWithPrefix(key);
            foreach (var item in entries)
            {
                if (item.Value.ValidationState == ModelValidationState.Unvalidated)
                {
                    item.Value.ValidationState = ModelValidationState.Valid;
                }
            }
 
            return true;
        }
 
        using (StateManager.Recurse(this, key ?? string.Empty, metadata, model, strategy!))
        {
            if (Metadata!.IsEnumerableType)
            {
                return VisitComplexType(DefaultCollectionValidationStrategy.Instance);
            }
 
            if (Metadata.IsComplexType)
            {
                return VisitComplexType(DefaultComplexObjectValidationStrategy.Instance);
            }
 
            return VisitSimpleType();
        }
    }
 
    /// <summary>
    /// Validate complex types, this covers everything VisitSimpleType does not i.e. both enumerations and complex types.
    /// </summary>
    /// <param name="defaultStrategy">The default validation strategy to use.</param>
    /// <returns><see langword="true" /> if valid, otherwise <see langword="false" />.</returns>
    protected virtual bool VisitComplexType(IValidationStrategy defaultStrategy)
    {
        var isValid = true;
 
        if (Model != null && Metadata!.ValidateChildren)
        {
            var strategy = Strategy ?? defaultStrategy;
            isValid = VisitChildren(strategy);
        }
        else if (Model != null)
        {
            // Suppress validation for the entries matching this prefix. This will temporarily set
            // the current node to 'skipped' but we're going to visit it right away, so subsequent
            // code will set it to 'valid' or 'invalid'
            SuppressValidation(Key!);
        }
 
        // Double-checking HasReachedMaxErrors just in case this model has no properties.
        // If validation has failed for any children, only validate the parent if ValidateComplexTypesIfChildValidationFails is true.
        if ((isValid || ValidateComplexTypesIfChildValidationFails) && !ModelState.HasReachedMaxErrors)
        {
            isValid &= ValidateNode();
        }
 
        return isValid;
    }
 
    /// <summary>
    /// Validate a simple type.
    /// </summary>
    /// <returns>True if valid.</returns>
    protected virtual bool VisitSimpleType()
    {
        if (ModelState.HasReachedMaxErrors)
        {
            SuppressValidation(Key!);
            return false;
        }
 
        return ValidateNode();
    }
 
    /// <summary>
    /// Validate all the child nodes using the specified strategy.
    /// </summary>
    /// <param name="strategy">The validation strategy.</param>
    /// <returns><see langword="true" /> if all children are valid, otherwise <see langword="false" />.</returns>
    protected virtual bool VisitChildren(IValidationStrategy strategy)
    {
        Debug.Assert(Metadata is not null && Key is not null && Model is not null);
 
        var isValid = true;
        var enumerator = strategy.GetChildren(Metadata, Key, Model);
        var parentEntry = new ValidationEntry(Metadata, Key, Model);
 
        while (enumerator.MoveNext())
        {
            var entry = enumerator.Current;
            var metadata = entry.Metadata;
            var key = entry.Key;
            if (metadata.PropertyValidationFilter?.ShouldValidateEntry(entry, parentEntry) == false)
            {
                SuppressValidation(key);
                continue;
            }
 
            isValid &= Visit(metadata, key, entry.Model);
        }
 
        return isValid;
    }
 
    /// <summary>
    /// Supress validation for a given key.
    /// </summary>
    /// <param name="key">The key to supress.</param>
    protected virtual void SuppressValidation(string key)
    {
        if (key == null)
        {
            // If the key is null, that means that we shouldn't expect any entries in ModelState for
            // this value, so there's nothing to do.
            return;
        }
 
        var entries = ModelState.FindKeysWithPrefix(key);
        foreach (var entry in entries)
        {
            if (entry.Value.ValidationState != ModelValidationState.Invalid)
            {
                entry.Value.ValidationState = ModelValidationState.Skipped;
            }
        }
    }
 
    /// <summary>
    /// Get the validation entry for the model.
    /// </summary>
    /// <param name="model">The model.</param>
    /// <returns>The validation state entry for the model.</returns>
    protected virtual ValidationStateEntry? GetValidationEntry(object? model)
    {
        if (model == null || ValidationState == null)
        {
            return null;
        }
 
        ValidationState.TryGetValue(model, out var entry);
        return entry;
    }
 
    /// <summary>
    /// State manager used for by <see cref="ValidationVisitor"/>.
    /// </summary>
    protected readonly struct StateManager : IDisposable
    {
        private readonly ValidationVisitor _visitor;
        private readonly object? _container;
        private readonly string _key;
        private readonly ModelMetadata _metadata;
        private readonly object _model;
        private readonly IValidationStrategy _strategy;
 
        /// <summary>
        /// Set up a state manager from a visitor.
        /// </summary>
        /// <param name="visitor">The visitor.</param>
        /// <param name="key">The key.</param>
        /// <param name="metadata">The metadata.</param>
        /// <param name="model">The model.</param>
        /// <param name="strategy">The strategy.</param>
        /// <returns>A StateManager setup for recursion.</returns>
        public static StateManager Recurse(
            ValidationVisitor visitor,
            string key,
            ModelMetadata metadata,
            object? model,
            IValidationStrategy strategy)
        {
            var recursifier = new StateManager(visitor, null);
 
            visitor.Container = visitor.Model;
            visitor.Key = key;
            visitor.Metadata = metadata;
            visitor.Model = model;
            visitor.Strategy = strategy;
 
            return recursifier;
        }
 
        /// <summary>
        /// Initialize a new <see cref="StateManager"/>.
        /// </summary>
        /// <param name="visitor">The visitor.</param>
        /// <param name="newModel">The model to validate.</param>
        public StateManager(ValidationVisitor visitor, object? newModel)
        {
            _visitor = visitor;
 
            _container = _visitor.Container;
            _key = _visitor.Key!;
            _metadata = _visitor.Metadata!;
            _model = _visitor.Model!;
            _strategy = _visitor.Strategy!;
        }
 
        /// <inheritdoc/>
        public void Dispose()
        {
            _visitor.Container = _container;
            _visitor.Key = _key;
            _visitor.Metadata = _metadata;
            _visitor.Model = _model;
            _visitor.Strategy = _strategy;
        }
    }
}