// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ModelBinding { /// /// Represents the state of an attempt to bind values from an HTTP Request to an action method, which includes /// validation information. /// public class ModelStateDictionary : IReadOnlyDictionary { // Make sure to update the doc headers if this value is changed. /// /// The default value for of 200. /// public static readonly int DefaultMaxAllowedErrors = 200; private static readonly char[] Delimiters = new char[] { '.', '[' }; private readonly ModelStateNode _root; private int _maxAllowedErrors; /// /// Initializes a new instance of the class. /// public ModelStateDictionary() : this(DefaultMaxAllowedErrors) { } /// /// Initializes a new instance of the class. /// public ModelStateDictionary(int maxAllowedErrors) { MaxAllowedErrors = maxAllowedErrors; var emptySegment = new StringSegment(buffer: string.Empty); _root = new ModelStateNode(subKey: emptySegment) { Key = string.Empty }; } /// /// Initializes a new instance of the class by using values that are copied /// from the specified . /// /// The to copy values from. public ModelStateDictionary(ModelStateDictionary dictionary) : this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } Merge(dictionary); } /// /// Root entry for the . /// public ModelStateEntry Root => _root; /// /// Gets or sets the maximum allowed model state errors in this instance of . /// Defaults to 200. /// /// /// /// tracks the number of model errors added by calls to /// or /// . /// Once the value of MaxAllowedErrors - 1 is reached, if another attempt is made to add an error, /// the error message will be ignored and a will be added. /// /// /// Errors added via modifying directly do not count towards this limit. /// /// public int MaxAllowedErrors { get { return _maxAllowedErrors; } set { if (value < 0) { throw new ArgumentOutOfRangeException(nameof(value)); } _maxAllowedErrors = value; } } /// /// Gets a value indicating whether or not the maximum number of errors have been /// recorded. /// /// /// Returns true if a has been recorded; /// otherwise false. /// public bool HasReachedMaxErrors { get { return ErrorCount >= MaxAllowedErrors; } } /// /// Gets the number of errors added to this instance of via /// or . /// public int ErrorCount { get; private set; } /// public int Count { get; private set; } /// /// Gets the key sequence. /// public KeyEnumerable Keys => new KeyEnumerable(this); /// IEnumerable IReadOnlyDictionary.Keys => Keys; /// /// Gets the value sequence. /// public ValueEnumerable Values => new ValueEnumerable(this); /// IEnumerable IReadOnlyDictionary.Values => Values; /// /// Gets a value that indicates whether any model state values in this model state dictionary is invalid or not validated. /// public bool IsValid { get { return ValidationState == ModelValidationState.Valid || ValidationState == ModelValidationState.Skipped; } } /// public ModelValidationState ValidationState => GetValidity(_root) ?? ModelValidationState.Valid; /// public ModelStateEntry this[string key] { get { if (key == null) { throw new ArgumentNullException(nameof(key)); } ModelStateEntry entry; TryGetValue(key, out entry); return entry; } } // Flag that indiciates if TooManyModelErrorException has already been added to this dictionary. private bool HasRecordedMaxModelError { get; set; } /// /// Adds the specified to the instance /// that is associated with the specified . /// /// The key of the to add errors to. /// The to add. /// The associated with the model. public void AddModelError(string key, Exception exception, ModelMetadata metadata) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (exception == null) { throw new ArgumentNullException(nameof(exception)); } if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } TryAddModelError(key, exception, metadata); } /// /// Attempts to add the specified to the /// instance that is associated with the specified . If the maximum number of allowed /// errors has already been recorded, records a exception instead. /// /// The key of the to add errors to. /// The to add. /// The associated with the model. /// /// True if the given error was added, false if the error was ignored. /// See . /// public bool TryAddModelError(string key, Exception exception, ModelMetadata metadata) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (exception == null) { throw new ArgumentNullException(nameof(exception)); } if (metadata == null) { throw new ArgumentNullException(nameof(metadata)); } if (ErrorCount >= MaxAllowedErrors - 1) { EnsureMaxErrorsReachedRecorded(); return false; } if (exception is FormatException || exception is OverflowException) { // Convert FormatExceptions and OverflowExceptions to Invalid value messages. ModelStateEntry entry; TryGetValue(key, out entry); var name = metadata.GetDisplayName(); string errorMessage; if (entry == null) { errorMessage = metadata.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor(name); } else { errorMessage = metadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor( entry.AttemptedValue, name); } return TryAddModelError(key, errorMessage); } ErrorCount++; AddModelErrorCore(key, exception); return true; } /// /// Adds the specified to the instance /// that is associated with the specified . /// /// The key of the to add errors to. /// The error message to add. public void AddModelError(string key, string errorMessage) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (errorMessage == null) { throw new ArgumentNullException(nameof(errorMessage)); } TryAddModelError(key, errorMessage); } /// /// Attempts to add the specified to the /// instance that is associated with the specified . If the maximum number of allowed /// errors has already been recorded, records a exception instead. /// /// The key of the to add errors to. /// The error message to add. /// /// True if the given error was added, false if the error was ignored. /// See . /// public bool TryAddModelError(string key, string errorMessage) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (errorMessage == null) { throw new ArgumentNullException(nameof(errorMessage)); } if (ErrorCount >= MaxAllowedErrors - 1) { EnsureMaxErrorsReachedRecorded(); return false; } ErrorCount++; var modelState = GetOrAddNode(key); Count += !modelState.IsContainerNode ? 0 : 1; modelState.ValidationState = ModelValidationState.Invalid; modelState.MarkNonContainerNode(); modelState.Errors.Add(errorMessage); return true; } /// /// Returns the aggregate for items starting with the /// specified . /// /// The key to look up model state errors for. /// Returns if no entries are found for the specified /// key, if at least one instance is found with one or more model /// state errors; otherwise. public ModelValidationState GetFieldValidationState(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } var item = GetNode(key); return GetValidity(item) ?? ModelValidationState.Unvalidated; } /// /// Returns for the . /// /// The key to look up model state errors for. /// Returns if no entry is found for the specified /// key, if an instance is found with one or more model /// state errors; otherwise. public ModelValidationState GetValidationState(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } ModelStateEntry validationState; if (TryGetValue(key, out validationState)) { return validationState.ValidationState; } return ModelValidationState.Unvalidated; } /// /// Marks the for the entry with the specified /// as . /// /// The key of the to mark as valid. public void MarkFieldValid(string key) { if (key == null) { throw new ArgumentNullException(nameof(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; } /// /// Marks the for the entry with the specified /// as . /// /// The key of the to mark as skipped. public void MarkFieldSkipped(string key) { if (key == null) { throw new ArgumentNullException(nameof(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; } /// /// Copies the values from the specified into this instance, overwriting /// existing values if keys are the same. /// /// The to copy values from. 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(); } } /// /// Sets the of and for /// the with the specified . /// /// The key for the entry. /// The raw value for the entry. /// /// The values of in a comma-separated . /// public void SetModelValue(string key, object rawValue, string attemptedValue) { if (key == null) { throw new ArgumentNullException(nameof(key)); } var modelState = GetOrAddNode(key); Count += !modelState.IsContainerNode ? 0 : 1; modelState.RawValue = rawValue; modelState.AttemptedValue = attemptedValue; modelState.MarkNonContainerNode(); } /// /// Sets the value for the with the specified . /// /// The key for the entry /// /// A with data for the entry. /// public void SetModelValue(string key, ValueProviderResult valueProviderResult) { if (key == null) { throw new ArgumentNullException(nameof(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()); } /// /// Clears entries that match the key that is passed as parameter. /// /// The key of to clear. 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) => GetNode(key, createIfNotExists: false); private ModelStateNode GetOrAddNode(string key) => GetNode(key, createIfNotExists: true); private ModelStateNode GetNode(string key, bool createIfNotExists) { Debug.Assert(key != null); if (key.Length == 0) { return _root; } // 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; var previousIndex = 0; int index; while ((index = key.IndexOfAny(Delimiters, previousIndex)) != -1) { var keyStart = previousIndex == 0 || key[previousIndex - 1] == '.' ? previousIndex : previousIndex - 1; var subKey = new StringSegment(key, keyStart, index - keyStart); current = current.GetNode(subKey, createIfNotExists); if (current == null) { // createIfNotExists is set to false and a node wasn't found. Exit early. return null; } previousIndex = index + 1; } if (previousIndex < key.Length) { var keyStart = previousIndex == 0 || key[previousIndex - 1] == '.' ? previousIndex : previousIndex - 1; var subKey = new StringSegment(key, keyStart, key.Length - keyStart); current = current.GetNode(subKey, createIfNotExists); } if (current != null && current.Key == null) { // Don't update the key if it's been previously assigned. This is to prevent change in key casing // e.g. modelState.SetModelValue("foo", .., ..); // var value = modelState["FOO"]; current.Key = key; } return current; } private static ModelValidationState? GetValidity(ModelStateNode node) { if (node == null) { 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) { for (var i = 0; i < node.ChildNodes.Count; i++) { var entryState = GetValidity(node.ChildNodes[i]); 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; ErrorCount++; } } 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); } /// /// Removes all keys and values from ths instance of . /// public void Clear() { Count = 0; HasRecordedMaxModelError = false; ErrorCount = 0; _root.Reset(); _root.ChildNodes.Clear(); } /// public bool ContainsKey(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return !GetNode(key)?.IsContainerNode ?? false; } /// /// Removes the with the specified . /// /// The key. /// true if the element is successfully removed; otherwise false. This method also /// returns false if key was not found. public bool Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } var node = GetNode(key); if (node?.IsContainerNode == false) { Count--; ErrorCount -= node.Errors.Count; node.Reset(); return true; } return false; } /// public bool TryGetValue(string key, out ModelStateEntry value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } var result = GetNode(key); if (result?.IsContainerNode == false) { value = result; return true; } value = null; return false; } /// /// Returns an enumerator that iterates through this instance of . /// /// An . public Enumerator GetEnumerator() => new Enumerator(this, prefix: string.Empty); /// IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public static bool StartsWithPrefix(string prefix, string key) { if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } if (key == null) { throw new ArgumentNullException(nameof(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; } public PrefixEnumerable FindKeysWithPrefix(string prefix) { if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } return new PrefixEnumerable(this, prefix); } [DebuggerDisplay("SubKey={SubKey}, Key={Key}, ValidationState={ValidationState}")] private class ModelStateNode : ModelStateEntry { private bool _isContainerNode = true; public ModelStateNode(StringSegment subKey) { SubKey = subKey; } public List ChildNodes { get; set; } public override IReadOnlyList Children => ChildNodes; public string Key { get; set; } 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(); } public ModelStateNode GetNode(StringSegment subKey, bool createIfNotExists) { if (subKey.Length == 0) { return this; } var index = BinarySearch(subKey); ModelStateNode modelStateNode = null; if (index >= 0) { modelStateNode = ChildNodes[index]; } else if (createIfNotExists) { if (ChildNodes == null) { ChildNodes = new List(1); } modelStateNode = new ModelStateNode(subKey); ChildNodes.Insert(~index, modelStateNode); } return modelStateNode; } public override ModelStateEntry GetModelStateForProperty(string propertyName) => GetNode(new StringSegment(propertyName), createIfNotExists: false); private int BinarySearch(StringSegment searchKey) { if (ChildNodes == null) { return -1; } 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; } } public struct PrefixEnumerable : IEnumerable> { private readonly ModelStateDictionary _dictionary; private readonly string _prefix; public PrefixEnumerable(ModelStateDictionary dictionary, string prefix) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } _dictionary = dictionary; _prefix = prefix; } public Enumerator GetEnumerator() => new Enumerator(_dictionary, _prefix); IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public struct Enumerator : IEnumerator> { private readonly ModelStateNode _rootNode; private ModelStateNode _modelStateNode; private List _nodes; private int _index; private bool _visitedRoot; public Enumerator(ModelStateDictionary dictionary, string prefix) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } _index = -1; _rootNode = dictionary.GetNode(prefix); _modelStateNode = null; _nodes = null; _visitedRoot = false; } public KeyValuePair Current => new KeyValuePair(_modelStateNode.Key, _modelStateNode); object IEnumerator.Current => Current; public void Dispose() { } public bool MoveNext() { if (_rootNode == null) { return false; } if (!_visitedRoot) { // Visit the root node _visitedRoot = true; if (_rootNode.ChildNodes?.Count > 0) { _nodes = new List { _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; } public void Reset() { _index = -1; _nodes.Clear(); _visitedRoot = false; _modelStateNode = null; } } public struct KeyEnumerable : IEnumerable { private readonly ModelStateDictionary _dictionary; public KeyEnumerable(ModelStateDictionary dictionary) { _dictionary = dictionary; } public KeyEnumerator GetEnumerator() => new KeyEnumerator(_dictionary, prefix: string.Empty); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public struct KeyEnumerator : IEnumerator { private Enumerator _prefixEnumerator; public KeyEnumerator(ModelStateDictionary dictionary, string prefix) { _prefixEnumerator = new Enumerator(dictionary, prefix); Current = null; } public string Current { get; private set; } object IEnumerator.Current => Current; public void Dispose() => _prefixEnumerator.Dispose(); public bool MoveNext() { var result = _prefixEnumerator.MoveNext(); if (result) { var current = _prefixEnumerator.Current; Current = current.Key; } else { Current = null; } return result; } public void Reset() { _prefixEnumerator.Reset(); Current = null; } } public struct ValueEnumerable : IEnumerable { private readonly ModelStateDictionary _dictionary; public ValueEnumerable(ModelStateDictionary dictionary) { _dictionary = dictionary; } public ValueEnumerator GetEnumerator() => new ValueEnumerator(_dictionary, prefix: string.Empty); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public struct ValueEnumerator : IEnumerator { private Enumerator _prefixEnumerator; public ValueEnumerator(ModelStateDictionary dictionary, string prefix) { _prefixEnumerator = new Enumerator(dictionary, prefix); Current = null; } public ModelStateEntry Current { get; private set; } object IEnumerator.Current => Current; public void Dispose() => _prefixEnumerator.Dispose(); public bool MoveNext() { var result = _prefixEnumerator.MoveNext(); if (result) { var current = _prefixEnumerator.Current; Current = current.Value; } else { Current = null; } return result; } public void Reset() { _prefixEnumerator.Reset(); Current = null; } } } }