// 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;
}
}
}
}