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