// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; using Microsoft.AspNet.Mvc.ModelBinding.Internal; using Microsoft.Framework.Internal; 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 IDictionary _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([NotNull] ModelStateDictionary dictionary) { _innerDictionary = new CopyOnWriteDictionary(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 _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 { return GetValidity(_innerDictionary); } } /// public ModelState this[[NotNull] string key] { get { ModelState value; _innerDictionary.TryGetValue(key, out value); return value; } set { 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([NotNull] string key, [NotNull] Exception exception) { TryAddModelError(key, exception); } /// /// 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([NotNull] string key, [NotNull] Exception exception) { if (ErrorCount >= MaxAllowedErrors - 1) { EnsureMaxErrorsReachedRecorded(); return false; } if (exception is FormatException) { // Convert FormatExceptions to Invalid value messages. ModelState modelState; TryGetValue(key, out modelState); string errorMessage; if (modelState == null) { errorMessage = Resources.FormatModelBinderUtil_ValueInvalidGeneric(key); } else { errorMessage = Resources.FormatModelBinderUtil_ValueInvalid( modelState.Value.AttemptedValue, key); } 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([NotNull] string key, [NotNull] string 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([NotNull] string key, [NotNull] string 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([NotNull] string key) { var entries = DictionaryHelper.FindKeysWithPrefix(this, key); if (!entries.Any()) { return ModelValidationState.Unvalidated; } return GetValidity(entries); } /// /// 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([NotNull] string key) { ModelState 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([NotNull] string 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([NotNull] string 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 value for the with the specified to the /// specified . /// /// The key for the entry. /// The value to assign. public void SetModelValue([NotNull] string key, [NotNull] ValueProviderResult value) { GetModelStateForKey(key).Value = value; } /// /// 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 = (string.IsNullOrEmpty(key)) ? _innerDictionary : DictionaryHelper.FindKeysWithPrefix(this, key); foreach (var entry in entries) { entry.Value.Errors.Clear(); entry.Value.ValidationState = ModelValidationState.Unvalidated; } } private ModelState GetModelStateForKey([NotNull] string key) { ModelState modelState; if (!TryGetValue(key, out modelState)) { modelState = new ModelState(); this[key] = modelState; } return modelState; } private static ModelValidationState GetValidity(IEnumerable> entries) { var validationState = ModelValidationState.Valid; foreach (var entry in entries) { 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 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 = GetModelStateForKey(key); modelState.ValidationState = ModelValidationState.Invalid; modelState.Errors.Add(exception); } /// public void Add(KeyValuePair item) { Add(item.Key, item.Value); } /// public void Add([NotNull] string key, [NotNull] ModelState value) { _innerDictionary.Add(key, value); } /// public void Clear() { _innerDictionary.Clear(); } /// public bool Contains(KeyValuePair item) { return _innerDictionary.Contains(item); } /// public bool ContainsKey([NotNull] string key) { return _innerDictionary.ContainsKey(key); } /// public void CopyTo([NotNull] KeyValuePair[] array, int arrayIndex) { _innerDictionary.CopyTo(array, arrayIndex); } /// public bool Remove(KeyValuePair item) { return _innerDictionary.Remove(item); } /// public bool Remove([NotNull] string key) { return _innerDictionary.Remove(key); } /// public bool TryGetValue([NotNull] string key, out ModelState value) { return _innerDictionary.TryGetValue(key, out value); } /// public IEnumerator> GetEnumerator() { return _innerDictionary.GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } }