File: ModelBinding\ModelStateDictionary.cs
Web Access
Project: src\src\Mvc\Mvc.Abstractions\src\Microsoft.AspNetCore.Mvc.Abstractions.csproj (Microsoft.AspNetCore.Mvc.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Shared;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding;
 
/// <summary>
/// Represents the state of an attempt to bind values from an HTTP Request to an action method, which includes
/// validation information.
/// </summary>
[DebuggerDisplay("Entries = {Count}, IsValid = {IsValid}")]
[DebuggerTypeProxy(typeof(ModelStateDictionaryDebugView))]
public class ModelStateDictionary : IReadOnlyDictionary<string, ModelStateEntry?>
{
    // Make sure to update the doc headers if this value is changed.
    /// <summary>
    /// The default value for <see cref="MaxAllowedErrors"/> of <c>200</c>.
    /// </summary>
    public static readonly int DefaultMaxAllowedErrors = 200;
 
    // internal for testing
    internal const int DefaultMaxRecursionDepth = 32;
 
    private const char DelimiterDot = '.';
    private const char DelimiterOpen = '[';
 
    private readonly ModelStateNode _root;
    private int _maxAllowedErrors;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
    /// </summary>
    public ModelStateDictionary()
        : this(DefaultMaxAllowedErrors)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
    /// </summary>
    public ModelStateDictionary(int maxAllowedErrors)
        : this(maxAllowedErrors, maxValidationDepth: DefaultMaxRecursionDepth, maxStateDepth: DefaultMaxRecursionDepth)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
    /// </summary>
    private ModelStateDictionary(int maxAllowedErrors, int maxValidationDepth, int maxStateDepth)
    {
        MaxAllowedErrors = maxAllowedErrors;
        MaxValidationDepth = maxValidationDepth;
        MaxStateDepth = maxStateDepth;
        var emptySegment = new StringSegment(buffer: string.Empty);
        _root = new ModelStateNode(subKey: emptySegment)
        {
            Key = string.Empty
        };
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ModelStateDictionary"/> class by using values that are copied
    /// from the specified <paramref name="dictionary"/>.
    /// </summary>
    /// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
    public ModelStateDictionary(ModelStateDictionary dictionary)
        : this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors,
              dictionary?.MaxValidationDepth ?? DefaultMaxRecursionDepth,
              dictionary?.MaxStateDepth ?? DefaultMaxRecursionDepth)
    {
        ArgumentNullException.ThrowIfNull(dictionary);
 
        Merge(dictionary);
    }
 
    /// <summary>
    /// Root entry for the <see cref="ModelStateDictionary"/>.
    /// </summary>
    public ModelStateEntry Root => _root;
 
    /// <summary>
    /// Gets or sets the maximum allowed model state errors in this instance of <see cref="ModelStateDictionary"/>.
    /// Defaults to <c>200</c>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// <see cref="ModelStateDictionary"/> tracks the number of model errors added by calls to
    /// <see cref="AddModelError(string, Exception, ModelMetadata)"/> or
    /// <see cref="TryAddModelError(string, Exception, ModelMetadata)"/>.
    /// Once the value of <c>MaxAllowedErrors - 1</c> is reached, if another attempt is made to add an error,
    /// the error message will be ignored and a <see cref="TooManyModelErrorsException"/> will be added.
    /// </para>
    /// <para>
    /// Errors added via modifying <see cref="ModelStateEntry"/> directly do not count towards this limit.
    /// </para>
    /// </remarks>
    public int MaxAllowedErrors
    {
        get
        {
            return _maxAllowedErrors;
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);
 
            _maxAllowedErrors = value;
        }
    }
 
    /// <summary>
    /// Gets a value indicating whether or not the maximum number of errors have been
    /// recorded.
    /// </summary>
    /// <remarks>
    /// Returns <c>true</c> if a <see cref="TooManyModelErrorsException"/> has been recorded;
    /// otherwise <c>false</c>.
    /// </remarks>
    public bool HasReachedMaxErrors => ErrorCount >= MaxAllowedErrors;
 
    /// <summary>
    /// Gets the number of errors added to this instance of <see cref="ModelStateDictionary"/> via
    /// <see cref="M:AddModelError"/> or <see cref="M:TryAddModelError"/>.
    /// </summary>
    public int ErrorCount { get; private set; }
 
    /// <inheritdoc />
    public int Count { get; private set; }
 
    /// <summary>
    /// Gets the key sequence.
    /// </summary>
    public KeyEnumerable Keys => new KeyEnumerable(this);
 
    /// <inheritdoc />
    IEnumerable<string> IReadOnlyDictionary<string, ModelStateEntry?>.Keys => Keys;
 
    /// <summary>
    /// Gets the value sequence.
    /// </summary>
    public ValueEnumerable Values => new ValueEnumerable(this);
 
    /// <inheritdoc />
    IEnumerable<ModelStateEntry> IReadOnlyDictionary<string, ModelStateEntry?>.Values => Values;
 
    /// <summary>
    /// Gets a value that indicates whether any model state values in this model state dictionary is invalid or not validated.
    /// </summary>
    public bool IsValid
    {
        get
        {
            var state = ValidationState;
            return state == ModelValidationState.Valid || state == ModelValidationState.Skipped;
        }
    }
 
    /// <inheritdoc />
    public ModelValidationState ValidationState => GetValidity(_root, currentDepth: 0) ?? ModelValidationState.Valid;
 
    /// <inheritdoc />
    public ModelStateEntry? this[string key]
    {
        get
        {
            ArgumentNullException.ThrowIfNull(key);
 
            TryGetValue(key, out var entry);
            return entry;
        }
    }
 
    // Flag that indicates if TooManyModelErrorException has already been added to this dictionary.
    private bool HasRecordedMaxModelError { get; set; }
 
    internal int? MaxValidationDepth { get; set; }
 
    internal int? MaxStateDepth { get; set; }
 
    /// <summary>
    /// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
    /// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
    /// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
    /// recorded instead.
    /// </summary>
    /// <remarks>
    /// This method allows adding the <paramref name="exception"/> to the current <see cref="ModelStateDictionary"/>
    /// when <see cref="ModelMetadata"/> is not available or the exact <paramref name="exception"/>
    /// must be maintained for later use (even if it is for example a <see cref="FormatException"/>).
    /// Where <see cref="ModelMetadata"/> is available, use <see cref="AddModelError(string, Exception, ModelMetadata)"/> instead.
    /// </remarks>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
    /// <param name="exception">The <see cref="Exception"/> to add.</param>
    /// <returns>
    /// <c>True</c> if the given error was added, <c>false</c> if the error was ignored.
    /// See <see cref="MaxAllowedErrors"/>.
    /// </returns>
    public bool TryAddModelException(string key, Exception exception)
    {
        ArgumentNullException.ThrowIfNull(key);
        ArgumentNullException.ThrowIfNull(exception);
 
        if ((exception is InputFormatterException || exception is ValueProviderException)
           && !string.IsNullOrEmpty(exception.Message))
        {
            // InputFormatterException, ValueProviderException is a signal that the message is safe to expose to clients
            return TryAddModelError(key, exception.Message);
        }
 
        if (ErrorCount >= MaxAllowedErrors - 1)
        {
            EnsureMaxErrorsReachedRecorded();
            return false;
        }
 
        AddModelErrorCore(key, exception);
        return true;
    }
 
    /// <summary>
    /// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
    /// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
    /// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
    /// recorded instead.
    /// </summary>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
    /// <param name="exception">The <see cref="Exception"/> to add. Some exception types will be replaced with
    /// a descriptive error message.</param>
    /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
    public void AddModelError(string key, Exception exception, ModelMetadata metadata)
    {
        ArgumentNullException.ThrowIfNull(key);
        ArgumentNullException.ThrowIfNull(exception);
        ArgumentNullException.ThrowIfNull(metadata);
 
        TryAddModelError(key, exception, metadata);
    }
 
    /// <summary>
    /// Attempts to add the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/>
    /// instance that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
    /// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
    /// recorded instead.
    /// </summary>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
    /// <param name="exception">The <see cref="Exception"/> to add. Some exception types will be replaced with
    /// a descriptive error message.</param>
    /// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
    /// <returns>
    /// <c>True</c> if the given error was added, <c>false</c> if the error was ignored.
    /// See <see cref="MaxAllowedErrors"/>.
    /// </returns>
    public bool TryAddModelError(string key, Exception exception, ModelMetadata metadata)
    {
        ArgumentNullException.ThrowIfNull(key);
        ArgumentNullException.ThrowIfNull(exception);
        ArgumentNullException.ThrowIfNull(metadata);
 
        if (ErrorCount >= MaxAllowedErrors - 1)
        {
            EnsureMaxErrorsReachedRecorded();
            return false;
        }
 
        if (exception is FormatException || exception is OverflowException)
        {
            // Convert FormatExceptions and OverflowExceptions to Invalid value messages.
            TryGetValue(key, out var entry);
 
            // Not using metadata.GetDisplayName() or a single resource to avoid strange messages like
            // "The value '' is not valid." (when no value was provided, not even an empty string) and
            // "The supplied value is invalid for Int32." (when error is for an element or parameter).
            var messageProvider = metadata.ModelBindingMessageProvider;
 
            var name = metadata.DisplayName ?? metadata.PropertyName;
            string errorMessage;
            if (entry == null && name == null)
            {
                errorMessage = messageProvider.NonPropertyUnknownValueIsInvalidAccessor();
            }
            else if (entry == null)
            {
                errorMessage = messageProvider.UnknownValueIsInvalidAccessor(name!);
            }
            else if (name == null)
            {
                errorMessage = messageProvider.NonPropertyAttemptedValueIsInvalidAccessor(entry.AttemptedValue!);
            }
            else
            {
                errorMessage = messageProvider.AttemptedValueIsInvalidAccessor(entry.AttemptedValue!, name);
            }
 
            return TryAddModelError(key, errorMessage);
        }
        else if ((exception is InputFormatterException || exception is ValueProviderException)
            && !string.IsNullOrEmpty(exception.Message))
        {
            // InputFormatterException, ValueProviderException is a signal that the message is safe to expose to clients
            return TryAddModelError(key, exception.Message);
        }
 
        AddModelErrorCore(key, exception);
        return true;
    }
 
    /// <summary>
    /// Adds the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.Errors"/> instance
    /// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
    /// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
    /// recorded instead.
    /// </summary>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
    /// <param name="errorMessage">The error message to add.</param>
    public void AddModelError(string key, string errorMessage)
    {
        ArgumentNullException.ThrowIfNull(key);
        ArgumentNullException.ThrowIfNull(errorMessage);
 
        TryAddModelError(key, errorMessage);
    }
 
    /// <summary>
    /// Attempts to add the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.Errors"/>
    /// instance that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
    /// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
    /// recorded instead.
    /// </summary>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
    /// <param name="errorMessage">The error message to add.</param>
    /// <returns>
    /// <c>True</c> if the given error was added, <c>false</c> if the error was ignored.
    /// See <see cref="MaxAllowedErrors"/>.
    /// </returns>
    public bool TryAddModelError(string key, string errorMessage)
    {
        ArgumentNullException.ThrowIfNull(key);
        ArgumentNullException.ThrowIfNull(errorMessage);
 
        if (ErrorCount >= MaxAllowedErrors - 1)
        {
            EnsureMaxErrorsReachedRecorded();
            return false;
        }
 
        var modelState = GetOrAddNode(key);
        Count += !modelState.IsContainerNode ? 0 : 1;
        modelState.ValidationState = ModelValidationState.Invalid;
        modelState.MarkNonContainerNode();
        modelState.Errors.Add(errorMessage);
 
        ErrorCount++;
        return true;
    }
 
    /// <summary>
    /// Returns the aggregate <see cref="ModelValidationState"/> for items starting with the
    /// specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The key to look up model state errors for.</param>
    /// <returns>Returns <see cref="ModelValidationState.Unvalidated"/> if no entries are found for the specified
    /// key, <see cref="ModelValidationState.Invalid"/> if at least one instance is found with one or more model
    /// state errors; <see cref="ModelValidationState.Valid"/> otherwise.</returns>
    public ModelValidationState GetFieldValidationState(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var item = GetNode(key);
        return GetValidity(item, currentDepth: 0) ?? ModelValidationState.Unvalidated;
    }
 
    /// <summary>
    /// Returns <see cref="ModelValidationState"/> for the <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The key to look up model state errors for.</param>
    /// <returns>Returns <see cref="ModelValidationState.Unvalidated"/> if no entry is found for the specified
    /// key, <see cref="ModelValidationState.Invalid"/> if an instance is found with one or more model
    /// state errors; <see cref="ModelValidationState.Valid"/> otherwise.</returns>
    public ModelValidationState GetValidationState(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        if (TryGetValue(key, out var validationState))
        {
            return validationState.ValidationState;
        }
 
        return ModelValidationState.Unvalidated;
    }
 
    /// <summary>
    /// Marks the <see cref="ModelStateEntry.ValidationState"/> for the entry with the specified
    /// <paramref name="key"/> as <see cref="ModelValidationState.Valid"/>.
    /// </summary>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to mark as valid.</param>
    public void MarkFieldValid(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var modelState = GetOrAddNode(key);
        if (modelState.ValidationState == ModelValidationState.Invalid)
        {
            throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset);
        }
 
        Count += !modelState.IsContainerNode ? 0 : 1;
        modelState.MarkNonContainerNode();
        modelState.ValidationState = ModelValidationState.Valid;
    }
 
    /// <summary>
    /// Marks the <see cref="ModelStateEntry.ValidationState"/> for the entry with the specified <paramref name="key"/>
    /// as <see cref="ModelValidationState.Skipped"/>.
    /// </summary>
    /// <param name="key">The key of the <see cref="ModelStateEntry"/> to mark as skipped.</param>
    public void MarkFieldSkipped(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var modelState = GetOrAddNode(key);
        if (modelState.ValidationState == ModelValidationState.Invalid)
        {
            throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset_ToSkipped);
        }
 
        Count += !modelState.IsContainerNode ? 0 : 1;
        modelState.MarkNonContainerNode();
        modelState.ValidationState = ModelValidationState.Skipped;
    }
 
    /// <summary>
    /// Copies the values from the specified <paramref name="dictionary"/> into this instance, overwriting
    /// existing values if keys are the same.
    /// </summary>
    /// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
    public void Merge(ModelStateDictionary dictionary)
    {
        if (dictionary == null)
        {
            return;
        }
 
        foreach (var source in dictionary)
        {
            var target = GetOrAddNode(source.Key);
            Count += !target.IsContainerNode ? 0 : 1;
            ErrorCount += source.Value.Errors.Count - target.Errors.Count;
            target.Copy(source.Value);
            target.MarkNonContainerNode();
        }
    }
 
    /// <summary>
    /// Sets the of <see cref="ModelStateEntry.RawValue"/> and <see cref="ModelStateEntry.AttemptedValue"/> for
    /// the <see cref="ModelStateEntry"/> with the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The key for the <see cref="ModelStateEntry"/> entry.</param>
    /// <param name="rawValue">The raw value for the <see cref="ModelStateEntry"/> entry.</param>
    /// <param name="attemptedValue">
    /// The values of <paramref name="rawValue"/> in a comma-separated <see cref="string"/>.
    /// </param>
    public void SetModelValue(string key, object? rawValue, string? attemptedValue)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var modelState = GetOrAddNode(key);
        Count += !modelState.IsContainerNode ? 0 : 1;
        modelState.RawValue = rawValue;
        modelState.AttemptedValue = attemptedValue;
        modelState.MarkNonContainerNode();
    }
 
    /// <summary>
    /// Sets the value for the <see cref="ModelStateEntry"/> with the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The key for the <see cref="ModelStateEntry"/> entry</param>
    /// <param name="valueProviderResult">
    /// A <see cref="ValueProviderResult"/> with data for the <see cref="ModelStateEntry"/> entry.
    /// </param>
    public void SetModelValue(string key, ValueProviderResult valueProviderResult)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        // Avoid creating a new array for rawValue if there's only one value.
        object? rawValue;
        if (valueProviderResult == ValueProviderResult.None)
        {
            rawValue = null;
        }
        else if (valueProviderResult.Length == 1)
        {
            rawValue = valueProviderResult.Values[0];
        }
        else
        {
            rawValue = valueProviderResult.Values.ToArray();
        }
 
        SetModelValue(key, rawValue, valueProviderResult.ToString());
    }
 
    /// <summary>
    /// Clears <see cref="ModelStateDictionary"/> entries that match the key that is passed as parameter.
    /// </summary>
    /// <param name="key">The key of <see cref="ModelStateDictionary"/> to clear.</param>
    public void ClearValidationState(string key)
    {
        // If key is null or empty, clear all entries in the dictionary
        // else just clear the ones that have key as prefix
        var entries = FindKeysWithPrefix(key ?? string.Empty);
        foreach (var entry in entries)
        {
            entry.Value.Errors.Clear();
            entry.Value.ValidationState = ModelValidationState.Unvalidated;
        }
    }
 
    private ModelStateNode? GetNode(string key)
    {
        var current = _root;
        if (key.Length > 0)
        {
            var match = default(MatchResult);
            do
            {
                var subKey = FindNext(key, ref match);
                current = current.GetNode(subKey);
 
                // Path not found, exit early
                if (current == null)
                {
                    break;
                }
 
            } while (match.Type != Delimiter.None);
        }
 
        return current;
    }
 
    private ModelStateNode GetOrAddNode(string key)
    {
        Debug.Assert(key != null);
        // For a key of the format, foo.bar[0].baz[qux] we'll create the following nodes:
        // foo
        //  -> bar
        //   -> [0]
        //    -> baz
        //     -> [qux]
 
        var current = _root;
        if (key.Length > 0)
        {
            var currentDepth = 0;
            var match = default(MatchResult);
            do
            {
                if (MaxStateDepth != null && currentDepth >= MaxStateDepth)
                {
                    throw new InvalidOperationException(Resources.FormatModelStateDictionary_MaxModelStateDepth(MaxStateDepth));
                }
 
                var subKey = FindNext(key, ref match);
                current = current.GetOrAddNode(subKey);
                currentDepth++;
 
            } while (match.Type != Delimiter.None);
 
            if (current.Key == null)
            {
                // New Node - Set key
                current.Key = key;
            }
        }
 
        return current;
    }
 
    // Shared function factored out for clarity, force inlining to put back in
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static StringSegment FindNext(string key, ref MatchResult currentMatch)
    {
        var index = currentMatch.Index;
        var matchType = Delimiter.None;
 
        for (; index < key.Length; index++)
        {
            var ch = key[index];
            if (ch == DelimiterDot)
            {
                matchType = Delimiter.Dot;
                break;
            }
            else if (ch == DelimiterOpen)
            {
                matchType = Delimiter.OpenBracket;
                break;
            }
        }
 
        var keyStart = currentMatch.Type == Delimiter.OpenBracket
            ? currentMatch.Index - 1
            : currentMatch.Index;
 
        currentMatch.Type = matchType;
        currentMatch.Index = index + 1;
 
        return new StringSegment(key, keyStart, index - keyStart);
    }
 
    private ModelValidationState? GetValidity(ModelStateNode? node, int currentDepth)
    {
        if (node == null ||
            (MaxValidationDepth != null && currentDepth >= MaxValidationDepth))
        {
            return null;
        }
 
        ModelValidationState? validationState = null;
        if (!node.IsContainerNode)
        {
            validationState = ModelValidationState.Valid;
            if (node.ValidationState == ModelValidationState.Unvalidated)
            {
                // If any entries of a field is unvalidated, we'll treat the tree as unvalidated.
                return ModelValidationState.Unvalidated;
            }
 
            if (node.ValidationState == ModelValidationState.Invalid)
            {
                validationState = node.ValidationState;
            }
        }
 
        if (node.ChildNodes != null)
        {
            currentDepth++;
 
            for (var i = 0; i < node.ChildNodes.Count; i++)
            {
                var entryState = GetValidity(node.ChildNodes[i], currentDepth);
 
                if (entryState == ModelValidationState.Unvalidated)
                {
                    return entryState;
                }
 
                if (validationState == null || entryState == ModelValidationState.Invalid)
                {
                    validationState = entryState;
                }
            }
        }
 
        return validationState;
    }
 
    private void EnsureMaxErrorsReachedRecorded()
    {
        if (!HasRecordedMaxModelError)
        {
            var exception = new TooManyModelErrorsException(Resources.ModelStateDictionary_MaxModelStateErrors);
            AddModelErrorCore(string.Empty, exception);
            HasRecordedMaxModelError = true;
        }
    }
 
    private void AddModelErrorCore(string key, Exception exception)
    {
        var modelState = GetOrAddNode(key);
        Count += !modelState.IsContainerNode ? 0 : 1;
        modelState.ValidationState = ModelValidationState.Invalid;
        modelState.MarkNonContainerNode();
        modelState.Errors.Add(exception);
 
        ErrorCount++;
    }
 
    /// <summary>
    /// Removes all keys and values from this instance of <see cref="ModelStateDictionary"/>.
    /// </summary>
    public void Clear()
    {
        Count = 0;
        HasRecordedMaxModelError = false;
        ErrorCount = 0;
        _root.Reset();
        _root.ChildNodes?.Clear();
    }
 
    /// <inheritdoc />
    public bool ContainsKey(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        return !GetNode(key)?.IsContainerNode ?? false;
    }
 
    /// <summary>
    /// Removes the <see cref="ModelStateEntry"/> with the specified <paramref name="key"/>.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <returns><c>true</c> if the element is successfully removed; otherwise <c>false</c>. This method also
    /// returns <c>false</c> if key was not found.</returns>
    public bool Remove(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var node = GetNode(key);
        if (node?.IsContainerNode == false)
        {
            Count--;
            ErrorCount -= node.Errors.Count;
            node.Reset();
            return true;
        }
 
        return false;
    }
 
    /// <inheritdoc />
    public bool TryGetValue(string key, [NotNullWhen(true)] out ModelStateEntry? value)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var result = GetNode(key);
        if (result?.IsContainerNode == false)
        {
            value = result;
            return true;
        }
 
        value = null;
        return false;
    }
 
    /// <summary>
    /// Returns an enumerator that iterates through this instance of <see cref="ModelStateDictionary"/>.
    /// </summary>
    /// <returns>An <see cref="Enumerator"/>.</returns>
    public Enumerator GetEnumerator() => new Enumerator(this, prefix: string.Empty);
 
    /// <inheritdoc />
    IEnumerator<KeyValuePair<string, ModelStateEntry?>>
        IEnumerable<KeyValuePair<string, ModelStateEntry?>>.GetEnumerator() => GetEnumerator();
 
    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
    /// <summary>
    /// <para>
    /// This API supports the MVC's infrastructure and is not intended to be used
    /// directly from your code. This API may change or be removed in future releases.
    /// </para>
    /// </summary>
    public static bool StartsWithPrefix(string prefix, string key)
    {
        ArgumentNullException.ThrowIfNull(prefix);
        ArgumentNullException.ThrowIfNull(key);
 
        if (prefix.Length == 0)
        {
            // Everything is prefixed by the empty string.
            return true;
        }
 
        if (prefix.Length > key.Length)
        {
            return false; // Not long enough.
        }
 
        if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }
 
        if (key.Length == prefix.Length)
        {
            // Exact match
            return true;
        }
 
        var charAfterPrefix = key[prefix.Length];
        if (charAfterPrefix == '.' || charAfterPrefix == '[')
        {
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    /// Gets a <see cref="PrefixEnumerable"/> that iterates over this instance of <see cref="ModelStateDictionary"/>
    /// using the specified <paramref name="prefix"/>.
    /// </summary>
    /// <param name="prefix">The prefix.</param>
    /// <returns>The <see cref="PrefixEnumerable"/>.</returns>
    public PrefixEnumerable FindKeysWithPrefix(string prefix)
    {
        ArgumentNullException.ThrowIfNull(prefix);
 
        return new PrefixEnumerable(this, prefix);
    }
 
    private struct MatchResult
    {
        public Delimiter Type;
        public int Index;
    }
 
    private enum Delimiter
    {
        None = 0,
        Dot,
        OpenBracket
    }
 
    [DebuggerDisplay("SubKey = {SubKey}, Key = {Key}, ValidationState = {ValidationState}")]
    private sealed class ModelStateNode : ModelStateEntry
    {
        private bool _isContainerNode = true;
 
        public ModelStateNode(StringSegment subKey)
        {
            SubKey = subKey;
        }
 
        public List<ModelStateNode>? ChildNodes { get; set; }
 
        public override IReadOnlyList<ModelStateEntry>? Children => ChildNodes;
 
        public string Key { get; set; } = default!;
 
        public StringSegment SubKey { get; }
 
        public override bool IsContainerNode => _isContainerNode;
 
        public void MarkNonContainerNode()
        {
            _isContainerNode = false;
        }
 
        public void Copy(ModelStateEntry entry)
        {
            RawValue = entry.RawValue;
            AttemptedValue = entry.AttemptedValue;
            Errors.Clear();
            for (var i = 0; i < entry.Errors.Count; i++)
            {
                Errors.Add(entry.Errors[i]);
            }
 
            ValidationState = entry.ValidationState;
        }
 
        public void Reset()
        {
            _isContainerNode = true;
            RawValue = null;
            AttemptedValue = null;
            ValidationState = ModelValidationState.Unvalidated;
            Errors.Clear();
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ModelStateNode? GetNode(StringSegment subKey)
        {
            ModelStateNode? modelStateNode = null;
            if (subKey.Length == 0)
            {
                modelStateNode = this;
            }
            else if (ChildNodes != null)
            {
                var index = BinarySearch(subKey);
                if (index >= 0)
                {
                    modelStateNode = ChildNodes[index];
                }
            }
 
            return modelStateNode;
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ModelStateNode GetOrAddNode(StringSegment subKey)
        {
            ModelStateNode modelStateNode;
            if (subKey.Length == 0)
            {
                modelStateNode = this;
            }
            else if (ChildNodes == null)
            {
                ChildNodes = new List<ModelStateNode>(1);
                modelStateNode = new ModelStateNode(subKey);
                ChildNodes.Add(modelStateNode);
            }
            else
            {
                var index = BinarySearch(subKey);
                if (index >= 0)
                {
                    modelStateNode = ChildNodes[index];
                }
                else
                {
                    modelStateNode = new ModelStateNode(subKey);
                    ChildNodes.Insert(~index, modelStateNode);
                }
            }
 
            return modelStateNode;
        }
 
        public override ModelStateEntry? GetModelStateForProperty(string propertyName)
            => GetNode(new StringSegment(propertyName));
 
        private int BinarySearch(StringSegment searchKey)
        {
            Debug.Assert(ChildNodes != null);
 
            var low = 0;
            var high = ChildNodes.Count - 1;
            while (low <= high)
            {
                var mid = low + ((high - low) / 2);
                var midKey = ChildNodes[mid].SubKey;
                var result = midKey.Length - searchKey.Length;
                if (result == 0)
                {
                    result = string.Compare(
                        midKey.Buffer,
                        midKey.Offset,
                        searchKey.Buffer,
                        searchKey.Offset,
                        searchKey.Length,
                        StringComparison.OrdinalIgnoreCase);
                }
 
                if (result == 0)
                {
                    return mid;
                }
                if (result < 0)
                {
                    low = mid + 1;
                }
                else
                {
                    high = mid - 1;
                }
            }
 
            return ~low;
        }
    }
 
    /// <summary>
    /// Enumerates over <see cref="ModelStateDictionary"/> to provide entries that start with the
    /// specified prefix.
    /// </summary>
    public readonly struct PrefixEnumerable : IEnumerable<KeyValuePair<string, ModelStateEntry>>
    {
        private readonly ModelStateDictionary _dictionary;
        private readonly string _prefix;
 
        /// <summary>
        /// Initializes a new instance of <see cref="PrefixEnumerable"/>.
        /// </summary>
        /// <param name="dictionary">The <see cref="ModelStateDictionary"/>.</param>
        /// <param name="prefix">The prefix.</param>
        public PrefixEnumerable(ModelStateDictionary dictionary, string prefix)
        {
            ArgumentNullException.ThrowIfNull(dictionary);
            ArgumentNullException.ThrowIfNull(prefix);
 
            _dictionary = dictionary;
            _prefix = prefix;
        }
 
        /// <inheritdoc />
        public Enumerator GetEnumerator() => new Enumerator(_dictionary, _prefix);
 
        IEnumerator<KeyValuePair<string, ModelStateEntry>>
            IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator() => GetEnumerator();
 
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
 
    /// <summary>
    /// An <see cref="IEnumerator{T}"/> for <see cref="PrefixEnumerable"/>.
    /// </summary>
    public struct Enumerator : IEnumerator<KeyValuePair<string, ModelStateEntry>>
    {
        private readonly ModelStateNode? _rootNode;
        private ModelStateNode _modelStateNode;
        private List<ModelStateNode>? _nodes;
        private int _index;
        private bool _visitedRoot;
 
        /// <summary>
        /// Intializes a new instance of <see cref="Enumerator"/>.
        /// </summary>
        /// <param name="dictionary">The <see cref="ModelStateDictionary"/>.</param>
        /// <param name="prefix">The prefix.</param>
        public Enumerator(ModelStateDictionary dictionary, string prefix)
        {
            ArgumentNullException.ThrowIfNull(dictionary);
            ArgumentNullException.ThrowIfNull(prefix);
 
            _index = -1;
            _rootNode = dictionary.GetNode(prefix);
            _modelStateNode = default!;
            _nodes = null;
            _visitedRoot = false;
        }
 
        /// <inheritdoc />
        public KeyValuePair<string, ModelStateEntry> Current =>
            new KeyValuePair<string, ModelStateEntry>(_modelStateNode.Key, _modelStateNode);
 
        object IEnumerator.Current => Current;
 
        /// <inheritdoc />
        public void Dispose()
        {
        }
 
        /// <inheritdoc />
        public bool MoveNext()
        {
            if (_rootNode == null)
            {
                return false;
            }
 
            if (!_visitedRoot)
            {
                // Visit the root node
                _visitedRoot = true;
                if (_rootNode.ChildNodes?.Count > 0)
                {
                    _nodes = new List<ModelStateNode> { _rootNode };
                }
 
                if (!_rootNode.IsContainerNode)
                {
                    _modelStateNode = _rootNode;
                    return true;
                }
            }
 
            if (_nodes == null)
            {
                return false;
            }
 
            while (_nodes.Count > 0)
            {
                var node = _nodes[0];
                if (_index == node.ChildNodes!.Count - 1)
                {
                    // We've exhausted the current sublist.
                    _nodes.RemoveAt(0);
                    _index = -1;
                    continue;
                }
                else
                {
                    _index++;
                }
 
                var currentChild = node.ChildNodes[_index];
                if (currentChild.ChildNodes?.Count > 0)
                {
                    _nodes.Add(currentChild);
                }
 
                if (!currentChild.IsContainerNode)
                {
                    _modelStateNode = currentChild;
                    return true;
                }
            }
 
            return false;
        }
 
        /// <inheritdoc />
        public void Reset()
        {
            _index = -1;
            _nodes?.Clear();
            _visitedRoot = false;
            _modelStateNode = default!;
        }
    }
 
    /// <summary>
    /// A <see cref="IEnumerable{T}"/> for keys in <see cref="ModelStateDictionary"/>.
    /// </summary>
    public readonly struct KeyEnumerable : IEnumerable<string>
    {
        private readonly ModelStateDictionary _dictionary;
 
        /// <summary>
        /// Initializes a new instance of <see cref="KeyEnumerable"/>.
        /// </summary>
        /// <param name="dictionary">The <see cref="ModelStateDictionary"/>.</param>
        public KeyEnumerable(ModelStateDictionary dictionary)
        {
            _dictionary = dictionary;
        }
 
        /// <inheritdoc />
        public KeyEnumerator GetEnumerator() => new KeyEnumerator(_dictionary, prefix: string.Empty);
 
        IEnumerator<string> IEnumerable<string>.GetEnumerator() => GetEnumerator();
 
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
 
    /// <summary>
    /// An <see cref="IEnumerator{T}"/> for keys in <see cref="ModelStateDictionary"/>.
    /// </summary>
    public struct KeyEnumerator : IEnumerator<string>
    {
        private Enumerator _prefixEnumerator;
 
        /// <summary>
        /// Initializes a new instance of <see cref="KeyEnumerable"/>.
        /// </summary>
        /// <param name="dictionary">The <see cref="ModelStateDictionary"/>.</param>
        /// <param name="prefix">The prefix.</param>
        public KeyEnumerator(ModelStateDictionary dictionary, string prefix)
        {
            _prefixEnumerator = new Enumerator(dictionary, prefix);
            Current = default!;
        }
 
        /// <inheritdoc />
        public string Current { get; private set; }
 
        object IEnumerator.Current => Current;
 
        /// <inheritdoc />
        public void Dispose() => _prefixEnumerator.Dispose();
 
        /// <inheritdoc />
        public bool MoveNext()
        {
            var result = _prefixEnumerator.MoveNext();
            if (result)
            {
                var current = _prefixEnumerator.Current;
                Current = current.Key;
            }
            else
            {
                Current = default!;
            }
 
            return result;
        }
 
        /// <inheritdoc />
        public void Reset()
        {
            _prefixEnumerator.Reset();
            Current = default!;
        }
    }
 
    /// <summary>
    /// An <see cref="IEnumerable"/> for <see cref="ModelStateEntry"/>.
    /// </summary>
    public readonly struct ValueEnumerable : IEnumerable<ModelStateEntry>
    {
        private readonly ModelStateDictionary _dictionary;
 
        /// <summary>
        /// Initializes a new instance of <see cref="ValueEnumerable"/>.
        /// </summary>
        /// <param name="dictionary">The <see cref="ModelStateDictionary"/>.</param>
        public ValueEnumerable(ModelStateDictionary dictionary)
        {
            _dictionary = dictionary;
        }
 
        /// <inheritdoc />
        public ValueEnumerator GetEnumerator() => new ValueEnumerator(_dictionary, prefix: string.Empty);
 
        IEnumerator<ModelStateEntry> IEnumerable<ModelStateEntry>.GetEnumerator() => GetEnumerator();
 
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
 
    /// <summary>
    /// An enumerator for <see cref="ModelStateEntry"/>.
    /// </summary>
    public struct ValueEnumerator : IEnumerator<ModelStateEntry>
    {
        private Enumerator _prefixEnumerator;
 
        /// <summary>
        /// Initializes a new instance of <see cref="ValueEnumerator"/>.
        /// </summary>
        /// <param name="dictionary">The <see cref="ModelStateDictionary"/>.</param>
        /// <param name="prefix">The prefix to enumerate.</param>
        public ValueEnumerator(ModelStateDictionary dictionary, string prefix)
        {
            _prefixEnumerator = new Enumerator(dictionary, prefix);
            Current = default!;
        }
 
        /// <inheritdoc />
        public ModelStateEntry Current { get; private set; }
 
        object IEnumerator.Current => Current;
 
        /// <inheritdoc />
        public void Dispose() => _prefixEnumerator.Dispose();
 
        /// <inheritdoc />
        public bool MoveNext()
        {
            var result = _prefixEnumerator.MoveNext();
            if (result)
            {
                var current = _prefixEnumerator.Current;
                Current = current.Value;
            }
            else
            {
                Current = default!;
            }
 
            return result;
        }
 
        /// <inheritdoc />
        public void Reset()
        {
            _prefixEnumerator.Reset();
            Current = default!;
        }
    }
 
    private sealed class ModelStateDictionaryDebugView(ModelStateDictionary dictionary)
    {
        private readonly ModelStateDictionary _dictionary = dictionary;
 
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public DictionaryItemDebugView<string, ModelStateEntry?>[] Items => _dictionary.Select(pair => new DictionaryItemDebugView<string, ModelStateEntry?>(pair)).ToArray();
    }
}