// 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 Microsoft.AspNet.Mvc.Abstractions; namespace Microsoft.AspNet.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 : IDictionary { // 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 readonly Dictionary _innerDictionary; 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; _innerDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// 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) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } _innerDictionary = new Dictionary( dictionary, StringComparer.OrdinalIgnoreCase); MaxAllowedErrors = dictionary.MaxAllowedErrors; ErrorCount = dictionary.ErrorCount; HasRecordedMaxModelError = dictionary.HasRecordedMaxModelError; } /// /// 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 { return _innerDictionary.Count; } } /// public bool IsReadOnly { get { return ((ICollection>)_innerDictionary).IsReadOnly; } } /// public ICollection Keys { get { return _innerDictionary.Keys; } } /// public ICollection Values { get { return _innerDictionary.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 { get { var entries = FindKeysWithPrefix(string.Empty); return GetValidity(entries, defaultState: ModelValidationState.Valid); } } /// public ModelStateEntry this[string key] { get { if (key == null) { throw new ArgumentNullException(nameof(key)); } ModelStateEntry value; _innerDictionary.TryGetValue(key, out value); return value; } set { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } _innerDictionary[key] = value; } } // For unit testing internal IDictionary InnerDictionary { get { return _innerDictionary; } } // 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. 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. /// /// 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 = Resources.FormatModelError_InvalidValue_GenericMessage(name); } else { errorMessage = Resources.FormatModelError_InvalidValue_MessageWithModelValue( 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 = GetModelStateForKey(key); modelState.ValidationState = ModelValidationState.Invalid; 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 entries = FindKeysWithPrefix(key); return GetValidity(entries, defaultState: 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 = GetModelStateForKey(key); if (modelState.ValidationState == ModelValidationState.Invalid) { throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset); } 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 = GetModelStateForKey(key); if (modelState.ValidationState == ModelValidationState.Invalid) { throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset_ToSkipped); } 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 entry in dictionary) { this[entry.Key] = entry.Value; } } /// /// 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 = GetModelStateForKey(key); modelState.RawValue = rawValue; modelState.AttemptedValue = attemptedValue; } /// /// 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 ModelStateEntry GetModelStateForKey(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } ModelStateEntry entry; if (!TryGetValue(key, out entry)) { entry = new ModelStateEntry(); this[key] = entry; } return entry; } private static ModelValidationState GetValidity(PrefixEnumerable entries, ModelValidationState defaultState) { var hasEntries = false; var validationState = ModelValidationState.Valid; foreach (var entry in entries) { hasEntries = true; var entryState = entry.Value.ValidationState; if (entryState == ModelValidationState.Unvalidated) { // If any entries of a field is unvalidated, we'll treat the tree as unvalidated. return entryState; } else if (entryState == ModelValidationState.Invalid) { validationState = entryState; } } return hasEntries ? validationState : defaultState; } 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 = GetModelStateForKey(key); modelState.ValidationState = ModelValidationState.Invalid; modelState.Errors.Add(exception); } /// public void Add(KeyValuePair item) { Add(item.Key, item.Value); } /// public void Add(string key, ModelStateEntry value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } _innerDictionary.Add(key, value); } /// public void Clear() { _innerDictionary.Clear(); } /// public bool Contains(KeyValuePair item) { return ((ICollection>)_innerDictionary).Contains(item); } /// public bool ContainsKey(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _innerDictionary.ContainsKey(key); } /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { if (array == null) { throw new ArgumentNullException(nameof(array)); } ((ICollection>)_innerDictionary).CopyTo(array, arrayIndex); } /// public bool Remove(KeyValuePair item) { return ((ICollection>)_innerDictionary).Remove(item); } /// public bool Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _innerDictionary.Remove(key); } /// public bool TryGetValue(string key, out ModelStateEntry value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _innerDictionary.TryGetValue(key, out value); } /// public IEnumerator> GetEnumerator() { return _innerDictionary.GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return 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 (StringComparer.OrdinalIgnoreCase.Equals(key, prefix)) { return true; } if (key.Length <= prefix.Length) { return false; } if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { if (key.StartsWith("[", StringComparison.OrdinalIgnoreCase)) { var subKey = key.Substring(key.IndexOf('.') + 1); if (!subKey.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return false; } if (string.Equals(prefix, subKey, StringComparison.OrdinalIgnoreCase)) { return true; } key = subKey; } else { return false; } } // Everything is prefixed by the empty string if (prefix.Length == 0) { return true; } else { var charAfterPrefix = key[prefix.Length]; switch (charAfterPrefix) { case '[': case '.': return true; } } return false; } public PrefixEnumerable FindKeysWithPrefix(string prefix) { if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } return new PrefixEnumerable(this, prefix); } 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 PrefixEnumerator GetEnumerator() { return _dictionary == null ? new PrefixEnumerator() : new PrefixEnumerator(_dictionary, _prefix); } IEnumerator> IEnumerable>.GetEnumerator() { return GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public struct PrefixEnumerator : IEnumerator> { private readonly ModelStateDictionary _dictionary; private readonly string _prefix; private bool _exactMatchUsed; private Dictionary.Enumerator _enumerator; public PrefixEnumerator(ModelStateDictionary dictionary, string prefix) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } _dictionary = dictionary; _prefix = prefix; _exactMatchUsed = false; _enumerator = default(Dictionary.Enumerator); Current = default(KeyValuePair); } public KeyValuePair Current { get; private set; } object IEnumerator.Current { get { return Current; } } public void Dispose() { } public bool MoveNext() { if (_dictionary == null) { return false; } // ModelStateDictionary has a behavior where the first 'match' returned from iterating // prefixes is the exact match for the prefix (if present). Only after looking for an // exact match do we fall back to iteration to find 'starts-with' matches. if (!_exactMatchUsed) { _exactMatchUsed = true; _enumerator = _dictionary._innerDictionary.GetEnumerator(); ModelStateEntry entry; if (_dictionary.TryGetValue(_prefix, out entry)) { Current = new KeyValuePair(_prefix, entry); return true; } } while (_enumerator.MoveNext()) { if (string.Equals(_prefix, _enumerator.Current.Key, StringComparison.OrdinalIgnoreCase)) { // Skip this one. We've already handle the 'exact match' case. } else if (StartsWithPrefix(_prefix, _enumerator.Current.Key)) { Current = _enumerator.Current; return true; } } return false; } public void Reset() { _exactMatchUsed = false; _enumerator = default(Dictionary.Enumerator); Current = default(KeyValuePair); } } } }