From f651f18d3a0ecee0cb62560bb8f19578a9de1986 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 16 Feb 2016 18:05:40 -0800 Subject: [PATCH] Use a prefix tree as a backing store for ModelStateDictionary --- .../ModelBinding/ModelState.cs | 31 - .../ModelBinding/ModelStateDictionary.cs | 688 ++++++++++++------ .../ModelBinding/ModelStateEntry.cs | 54 ++ .../Internal/ValidationHelpers.cs | 67 +- .../ModelBinding/ModelStateDictionaryTest.cs | 633 +++++++++++----- .../Internal/DefaultObjectValidatorTests.cs | 17 +- .../ModelBinding/ModelBindingHelperTest.cs | 32 +- .../SerializableErrorTests.cs | 9 +- ...elpersWebSite.Employee.Create.Invalid.html | 2 +- .../ActionParametersIntegrationTest.cs | 17 +- ...nderTypeBasedModelBinderIntegrationTest.cs | 9 +- .../BodyValidationIntegrationTests.cs | 7 +- ...ellationTokenModelBinderIntegrationTest.cs | 2 +- .../SimpleTypeModelBinderIntegrationTest.cs | 13 +- .../ModelStateDictionaryExtensionsTest.cs | 65 +- .../Rendering/HtmlHelperDisplayTextTest.cs | 16 +- .../Rendering/HtmlHelperHiddenTest.cs | 49 +- .../Rendering/HtmlHelperPasswordTest.cs | 17 +- .../Rendering/HtmlHelperSelectTest.cs | 90 +-- .../HtmlHelperValidationSummaryTest.cs | 191 ++++- .../Rendering/HtmlHelperValueTest.cs | 24 +- .../HttpErrorTest.cs | 14 +- .../Controllers/BasicApiController.cs | 5 +- 23 files changed, 1376 insertions(+), 676 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelState.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelState.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelState.cs deleted file mode 100644 index 7433210d09..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelState.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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. - -namespace Microsoft.AspNetCore.Mvc.ModelBinding -{ - /// - /// An entry in a . - /// - public class ModelStateEntry - { - /// - /// Gets the raw value from the request associated with this entry. - /// - public object RawValue { get; set; } - - /// - /// Gets the set of values contained in , joined into a comma-separated string. - /// - public string AttemptedValue { get; set; } - - /// - /// Gets the for this entry. - /// - public ModelErrorCollection Errors { get; } = new ModelErrorCollection(); - - /// - /// Gets or sets the for this entry. - /// - public ModelValidationState ValidationState { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs index f6bdaf83dd..0f7ceec371 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs @@ -4,7 +4,9 @@ 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 { @@ -12,15 +14,16 @@ 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 : IDictionary + 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 Dictionary _innerDictionary; + private readonly ModelStateNode _root; private int _maxAllowedErrors; /// @@ -37,8 +40,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public ModelStateDictionary(int maxAllowedErrors) { MaxAllowedErrors = maxAllowedErrors; - - _innerDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + var emptySegment = new StringSegment(buffer: string.Empty); + _root = new ModelStateNode(subKey: emptySegment) + { + Key = string.Empty + }; } /// @@ -47,21 +53,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// The to copy values from. public ModelStateDictionary(ModelStateDictionary dictionary) + : this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors) { if (dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } - _innerDictionary = new Dictionary( - dictionary, - StringComparer.OrdinalIgnoreCase); - - MaxAllowedErrors = dictionary.MaxAllowedErrors; - ErrorCount = dictionary.ErrorCount; - HasRecordedMaxModelError = dictionary.HasRecordedMaxModelError; + 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. @@ -115,28 +121,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public int ErrorCount { get; private set; } /// - public int Count - { - get { return _innerDictionary.Count; } - } + public int Count { get; private set; } + + /// + /// Gets the key sequence. + /// + public KeyEnumerable Keys => new KeyEnumerable(this); /// - public bool IsReadOnly - { - get { return ((ICollection>)_innerDictionary).IsReadOnly; } - } + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// + /// Gets the value sequence. + /// + public ValueEnumerable Values => new ValueEnumerable(this); /// - public ICollection Keys - { - get { return _innerDictionary.Keys; } - } - - /// - public ICollection Values - { - get { return _innerDictionary.Values; } - } + IEnumerable IReadOnlyDictionary.Values => Values; /// /// Gets a value that indicates whether any model state values in this model state dictionary is invalid or not validated. @@ -150,14 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } /// - public ModelValidationState ValidationState - { - get - { - var entries = FindKeysWithPrefix(string.Empty); - return GetValidity(entries, defaultState: ModelValidationState.Valid); - } - } + public ModelValidationState ValidationState => GetValidity(_root) ?? ModelValidationState.Valid; /// public ModelStateEntry this[string key] @@ -169,29 +163,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - ModelStateEntry value; - _innerDictionary.TryGetValue(key, out value); - return value; + ModelStateEntry entry; + TryGetValue(key, out entry); + return entry; } - 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. @@ -337,8 +312,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } ErrorCount++; - var modelState = GetModelStateForKey(key); + var modelState = GetOrAddNode(key); + Count += !modelState.IsContainerNode ? 0 : 1; modelState.ValidationState = ModelValidationState.Invalid; + modelState.MarkNonContainerNode(); modelState.Errors.Add(errorMessage); return true; @@ -359,8 +336,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - var entries = FindKeysWithPrefix(key); - return GetValidity(entries, defaultState: ModelValidationState.Unvalidated); + var item = GetNode(key); + return GetValidity(item) ?? ModelValidationState.Unvalidated; } /// @@ -398,12 +375,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - var modelState = GetModelStateForKey(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; } @@ -419,12 +398,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - var modelState = GetModelStateForKey(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; } @@ -440,9 +421,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return; } - foreach (var entry in dictionary) + foreach (var source in dictionary) { - this[entry.Key] = entry.Value; + var target = GetOrAddNode(source.Key); + Count += !target.IsContainerNode ? 0 : 1; + ErrorCount += source.Value.Errors.Count - target.Errors.Count; + target.Copy(source.Value); + target.MarkNonContainerNode(); } } @@ -462,9 +447,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - var modelState = GetModelStateForKey(key); + var modelState = GetOrAddNode(key); + Count += !modelState.IsContainerNode ? 0 : 1; modelState.RawValue = rawValue; modelState.AttemptedValue = attemptedValue; + modelState.MarkNonContainerNode(); } /// @@ -515,46 +502,106 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } } - private ModelStateEntry GetModelStateForKey(string key) + 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) { - if (key == null) + Debug.Assert(key != null); + if (key.Length == 0) { - throw new ArgumentNullException(nameof(key)); + return _root; } - ModelStateEntry entry; - if (!TryGetValue(key, out entry)) + // 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) { - entry = new ModelStateEntry(); - this[key] = entry; + 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; } - return entry; + 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(PrefixEnumerable entries, ModelValidationState defaultState) + private static ModelValidationState? GetValidity(ModelStateNode node) { - - var hasEntries = false; - var validationState = ModelValidationState.Valid; - - foreach (var entry in entries) + if (node == null) { - hasEntries = true; + return null; + } - var entryState = entry.Value.ValidationState; - if (entryState == ModelValidationState.Unvalidated) + 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 entryState; + return ModelValidationState.Unvalidated; } - else if (entryState == ModelValidationState.Invalid) + + if (node.ValidationState == ModelValidationState.Invalid) { - validationState = entryState; + validationState = node.ValidationState; } } - return hasEntries ? validationState : defaultState; + 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() @@ -570,43 +617,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding private void AddModelErrorCore(string key, Exception exception) { - var modelState = GetModelStateForKey(key); + var modelState = GetOrAddNode(key); + Count += !modelState.IsContainerNode ? 0 : 1; modelState.ValidationState = ModelValidationState.Invalid; + modelState.MarkNonContainerNode(); 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); - } - - /// + /// + /// Removes all keys and values from ths instance of . + /// public void Clear() { - _innerDictionary.Clear(); - } - - /// - public bool Contains(KeyValuePair item) - { - return ((ICollection>)_innerDictionary).Contains(item); + Count = 0; + HasRecordedMaxModelError = false; + ErrorCount = 0; + _root.Reset(); + _root.ChildNodes.Clear(); } /// @@ -617,27 +644,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - return _innerDictionary.ContainsKey(key); + return !GetNode(key)?.IsContainerNode ?? false; } - /// - 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); - } - - /// + /// + /// 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) @@ -645,7 +660,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - return _innerDictionary.Remove(key); + var node = GetNode(key); + if (node?.IsContainerNode == false) + { + Count--; + ErrorCount -= node.Errors.Count; + node.Reset(); + return true; + } + + return false; } /// @@ -656,20 +680,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(key)); } - return _innerDictionary.TryGetValue(key, out value); + var result = GetNode(key); + if (result?.IsContainerNode == false) + { + value = result; + return true; + } + + value = null; + return false; } - /// - public IEnumerator> GetEnumerator() - { - return _innerDictionary.GetEnumerator(); - } + /// + /// Returns an enumerator that iterates through this instance of . + /// + /// An . + public Enumerator GetEnumerator() => new Enumerator(this, prefix: string.Empty); /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator> + IEnumerable>.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public static bool StartsWithPrefix(string prefix, string key) { @@ -698,7 +731,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { return false; } - + if (key.Length == prefix.Length) { // Exact match @@ -724,6 +757,126 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding 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; @@ -745,32 +898,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding _prefix = prefix; } - public PrefixEnumerator GetEnumerator() - { - return _dictionary == null ? new PrefixEnumerator() : new PrefixEnumerator(_dictionary, _prefix); - } + public Enumerator GetEnumerator() => new Enumerator(_dictionary, _prefix); IEnumerator> - IEnumerable>.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerable>.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - public struct PrefixEnumerator : IEnumerator> + public struct Enumerator : IEnumerator> { - private readonly ModelStateDictionary _dictionary; - private string _prefix; + private readonly ModelStateNode _rootNode; + private ModelStateNode _modelStateNode; + private List _nodes; + private int _index; + private bool _visitedRoot; - private ExactMatchState _exactMatch; - private Dictionary.Enumerator _enumerator; - - public PrefixEnumerator(ModelStateDictionary dictionary, string prefix) + public Enumerator(ModelStateDictionary dictionary, string prefix) { if (dictionary == null) { @@ -782,23 +926,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(prefix)); } - _dictionary = dictionary; - _prefix = prefix; - - _exactMatch = ExactMatchState.NotChecked; - _enumerator = default(Dictionary.Enumerator); - Current = default(KeyValuePair); + _index = -1; + _rootNode = dictionary.GetNode(prefix); + _modelStateNode = null; + _nodes = null; + _visitedRoot = false; } - public KeyValuePair Current { get; private set; } + public KeyValuePair Current => + new KeyValuePair(_modelStateNode.Key, _modelStateNode); - object IEnumerator.Current - { - get - { - return Current; - } - } + object IEnumerator.Current => Current; public void Dispose() { @@ -806,61 +944,56 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public bool MoveNext() { - if (_dictionary == null) + if (_rootNode == 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 (_exactMatch == ExactMatchState.NotChecked) + if (!_visitedRoot) { - _enumerator = _dictionary._innerDictionary.GetEnumerator(); - - ModelStateEntry entry; - if (_dictionary.TryGetValue(_prefix, out entry)) + // Visit the root node + _visitedRoot = true; + if (_rootNode.ChildNodes?.Count > 0) { - // Mark exact match as found - _exactMatch = ExactMatchState.Found; - Current = new KeyValuePair(_prefix, entry); - return true; + _nodes = new List { _rootNode }; } - else + + if (!_rootNode.IsContainerNode) { - // Mark exact match tested for - _exactMatch = ExactMatchState.NotFound; + _modelStateNode = _rootNode; + return true; } } - while (_enumerator.MoveNext()) + if (_nodes == null) { - var key = _enumerator.Current.Key; - if (_exactMatch == ExactMatchState.NotFound) + return false; + } + + while (_nodes.Count > 0) + { + var node = _nodes[0]; + if (_index == node.ChildNodes.Count - 1) { - if (StartsWithPrefix(_prefix, key)) - { - Current = _enumerator.Current; - return true; - } + // We've exhausted the current sublist. + _nodes.RemoveAt(0); + _index = -1; continue; } - else if (_exactMatch == ExactMatchState.ReferenceSet && Object.ReferenceEquals(_prefix, key)) + else { - // Fast path skip this one. Is the exact string reference set below. + _index++; } - else if (_exactMatch == ExactMatchState.Found && - string.Equals(_prefix, key, StringComparison.OrdinalIgnoreCase)) + + var currentChild = node.ChildNodes[_index]; + if (currentChild.ChildNodes?.Count > 0) { - // Update _prefix to be this exact string reference to enable fast path - _prefix = key; - // Mark exact reference set - _exactMatch = ExactMatchState.ReferenceSet; - // Skip this one. We've already handled the 'exact match' case. + _nodes.Add(currentChild); } - else if (StartsWithPrefix(_prefix, key)) + + if (!currentChild.IsContainerNode) { - Current = _enumerator.Current; + _modelStateNode = currentChild; return true; } } @@ -870,17 +1003,120 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void Reset() { - _exactMatch = ExactMatchState.NotChecked; - _enumerator = default(Dictionary.Enumerator); - Current = default(KeyValuePair); + _index = -1; + _nodes.Clear(); + _visitedRoot = false; + _modelStateNode = null; + } + } + + public struct KeyEnumerable : IEnumerable + { + private readonly ModelStateDictionary _dictionary; + + public KeyEnumerable(ModelStateDictionary dictionary) + { + _dictionary = dictionary; } - private enum ExactMatchState + 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) { - NotChecked, - NotFound, - Found, - ReferenceSet + _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; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs new file mode 100644 index 0000000000..0b8bd32d4b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs @@ -0,0 +1,54 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// An entry in a . + /// + public abstract class ModelStateEntry + { + /// + /// Gets the raw value from the request associated with this entry. + /// + public object RawValue { get; set; } + + /// + /// Gets the set of values contained in , joined into a comma-separated string. + /// + public string AttemptedValue { get; set; } + + /// + /// Gets the for this entry. + /// + public ModelErrorCollection Errors { get; } = new ModelErrorCollection(); + + /// + /// Gets or sets the for this entry. + /// + public ModelValidationState ValidationState { get; set; } + + /// + /// Gets a value that determines if the current instance of is a container node. + /// Container nodes represent prefix nodes that aren't explicitly added to the + /// . + /// + public abstract bool IsContainerNode { get; } + + /// + /// Gets the for a sub-property with the specified . + /// + /// The property name to lookup. + /// The if a sub-property was found; otherwise null. + /// This method returns any existing entry, even those with with value true.. + public abstract ModelStateEntry GetModelStateForProperty(string propertyName); + + /// + /// Gets the values for sub-properties. + /// + /// This method returns all existing entries, even those with with value true. + public abstract IReadOnlyList Children { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs index 5aaf64205d..b606ec2693 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs @@ -1,7 +1,6 @@ // 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.Generic; using System.Diagnostics; using System.Linq; @@ -11,6 +10,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public static class ValidationHelpers { + private static readonly ModelStateEntry[] EmptyModelStateEntries = new ModelStateEntry[0]; + public static string GetModelErrorMessageOrDefault(ModelError modelError) { Debug.Assert(modelError != null); @@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } // Returns non-null list of model states, which caller will render in order provided. - public static IEnumerable GetModelStateList( + public static IList GetModelStateList( ViewDataDictionary viewData, bool excludePropertyErrors) { @@ -57,49 +58,59 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { return new[] { ms }; } - - return Enumerable.Empty(); } - else + else if (viewData.ModelState.Count > 0) { var metadata = viewData.ModelMetadata; - var orderer = new ErrorsOrderer(metadata); + var modelStateDictionary = viewData.ModelState; + var entries = new List(); + Visit(modelStateDictionary, modelStateDictionary.Root, metadata, entries); - return viewData.ModelState - .OrderBy(data => orderer.GetOrder(data.Key)) - .Select(ms => ms.Value); + if (entries.Count < modelStateDictionary.Count) + { + // Account for entries in the ModelStateDictionary that do not have corresponding ModelMetadata values. + foreach (var entry in modelStateDictionary) + { + if (!entries.Contains(entry.Value)) + { + entries.Add(entry.Value); + } + } + } + + return entries; } + + return EmptyModelStateEntries; } - // Helper for sorting modelStates to respect the ordering in the metadata. - // ModelState doesn't refer to ModelMetadata, but we can correlate via the property name. - private class ErrorsOrderer + private static void Visit( + ModelStateDictionary dictionary, + ModelStateEntry modelStateEntry, + ModelMetadata metadata, + List orderedModelStateEntries) { - private readonly Dictionary _ordering = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - public ErrorsOrderer(ModelMetadata metadata) + if (metadata.ElementMetadata != null) { - if (metadata == null) + foreach (var indexEntry in modelStateEntry.Children) { - throw new ArgumentNullException(nameof(metadata)); - } - - foreach (var data in metadata.Properties) - { - _ordering[data.PropertyName] = data.Order; + Visit(dictionary, indexEntry, metadata.ElementMetadata, orderedModelStateEntries); } } - public int GetOrder(string key) + for (var i = 0; i < metadata.Properties.Count; i++) { - int value; - if (_ordering.TryGetValue(key, out value)) + var propertyMetadata = metadata.Properties[i]; + var propertyModelStateEntry = modelStateEntry.GetModelStateForProperty(propertyMetadata.PropertyName); + if (propertyModelStateEntry != null) { - return value; + Visit(dictionary, propertyModelStateEntry, propertyMetadata, orderedModelStateEntries); } + } - return ModelMetadata.DefaultOrder; + if (!modelStateEntry.IsContainerNode) + { + orderedModelStateEntries.Add(modelStateEntry); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs index 3d6c01778b..26256a07ae 100644 --- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; +using System.Diagnostics; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Xunit; @@ -12,20 +12,139 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public class ModelStateDictionaryTest { [Theory] - [InlineData(ModelValidationState.Valid)] - [InlineData(ModelValidationState.Unvalidated)] - public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsNotInValid(ModelValidationState validationState) + [InlineData("")] + [InlineData("foo")] + public void ContainsKey_ReturnsFalse_IfNodeHasNotBeenMutated(string key) { // Arrange - var entry = new ModelStateEntry - { - ValidationState = validationState - }; + var dictionary = new ModelStateDictionary(); + dictionary.AddModelError("foo.bar", "some error"); - var source = new ModelStateDictionary - { - { "key", entry } - }; + // Act + var result = dictionary.ContainsKey(key); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("")] + [InlineData("foo")] + public void ContainsKey_ReturnsFalse_IfNodeHasBeenRemoved(string key) + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.AddModelError(key, "some error"); + + // Act + var remove = dictionary.Remove(key); + var containsKey = dictionary.ContainsKey(key); + + // Assert + Assert.True(remove); + Assert.False(containsKey); + } + + [Theory] + [InlineData("")] + [InlineData("foo")] + [InlineData("foo.bar")] + [InlineData("foo[bar]")] + public void ContainsKey_ReturnsTrue_IfNodeHasBeenMutated(string key) + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldSkipped(key); + + // Act + var result = dictionary.ContainsKey(key); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("foo")] + [InlineData("foo.bar")] + [InlineData("foo.bar[10]")] + public void IndexerDoesNotReturnIntermediaryNodes(string key) + { + // Arrange + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.AddModelError("foo.bar[10].baz", "error-message"); + + // Act + var result = modelStateDictionary[key]; + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("prop")] + [InlineData("first.second")] + [InlineData("[0].prop")] + [InlineData("[qux]")] + [InlineData("[test].prop")] + [InlineData("first[0].second")] + [InlineData("first.second[0].third")] + [InlineData("first[second][0]")] + [InlineData("first.second.third[0]")] + [InlineData("first.second.third[0].fourth")] + [InlineData("first[0][second]")] + public void Indexer_ReturnsValuesAddedUsingSetModelValue(string key) + { + // Arrange + var value = "Hello world"; + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.SetModelValue(key, value, value); + + // Act + var result = modelStateDictionary[key]; + + // Assert + Assert.Equal(value, result.RawValue); + } + + [Fact] + public void Clear_RemovesAllEntries() + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.AddModelError("a", "a-error"); + dictionary.AddModelError("b", "b-error"); + dictionary.AddModelError("c", "c-error"); + + // Act + dictionary.Clear(); + + // Assert + Assert.Equal(0, dictionary.Count); + Assert.Equal(0, dictionary.ErrorCount); + Assert.Empty(dictionary); + Assert.Equal(ModelValidationState.Valid, dictionary.ValidationState); + } + + [Fact] + public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsUnvalidated() + { + // Arrange + var source = new ModelStateDictionary(); + source.SetModelValue("key", "value", "value"); + + // Act + source.MarkFieldSkipped("key"); + + // Assert + Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState); + } + + [Fact] + public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsValid() + { + // Arrange + var source = new ModelStateDictionary(); + source.MarkFieldValid("key"); // Act source.MarkFieldSkipped("key"); @@ -38,11 +157,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void MarkFieldSkipped_MarksFieldAsSkipped_IfKeyIsNotPresent() { // Arrange - var entry = new ModelStateEntry - { - ValidationState = ModelValidationState.Valid - }; - var source = new ModelStateDictionary(); // Act @@ -58,15 +172,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void MarkFieldSkipped_Throws_IfStateIsInvalid() { // Arrange - var entry = new ModelStateEntry - { - ValidationState = ModelValidationState.Invalid - }; - - var source = new ModelStateDictionary - { - { "key", entry } - }; + var source = new ModelStateDictionary(); + source.AddModelError("key", "some error"); // Act var exception = Assert.Throws(() => source.MarkFieldSkipped("key")); @@ -77,21 +184,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding exception.Message); } - [Theory] - [InlineData(ModelValidationState.Skipped)] - [InlineData(ModelValidationState.Unvalidated)] - public void MarkFieldValid_MarksFieldAsValid_IfStateIsNotInvalid(ModelValidationState validationState) + [Fact] + public void MarkFieldValid_MarksFieldAsValid_IfStateIsUnvalidated() { // Arrange - var entry = new ModelStateEntry - { - ValidationState = validationState - }; + var source = new ModelStateDictionary(); + source.SetModelValue("key", "value", "value"); - var source = new ModelStateDictionary - { - { "key", entry } - }; + // Act + source.MarkFieldValid("key"); + + // Assert + Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState); + } + + [Fact] + public void MarkFieldValid_MarksFieldAsValid_IfStateIsSkipped() + { + // Arrange + var source = new ModelStateDictionary(); + source.MarkFieldSkipped("key"); // Act source.MarkFieldValid("key"); @@ -119,15 +231,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void MarkFieldValid_Throws_IfStateIsInvalid() { // Arrange - var entry = new ModelStateEntry - { - ValidationState = ModelValidationState.Invalid - }; - - var source = new ModelStateDictionary - { - { "key", entry } - }; + var source = new ModelStateDictionary(); + source.AddModelError("key", "some-error"); // Act var exception = Assert.Throws(() => source.MarkFieldValid("key")); @@ -142,20 +247,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void CopyConstructor_CopiesModelStateData() { // Arrange - var entry = new ModelStateEntry(); - var source = new ModelStateDictionary - { - { "key", entry } - }; - + var source = new ModelStateDictionary(); + source.SetModelValue("key", "attempted-value", "raw-value"); + var entry = source["key"]; + entry.AttemptedValue = "attempted-value"; + entry.RawValue = "raw-value"; + entry.Errors.Add(new ModelError(new InvalidOperationException())); + entry.Errors.Add(new ModelError("error-message")); + entry.ValidationState = ModelValidationState.Skipped; // Act var target = new ModelStateDictionary(source); // Assert - Assert.Equal(0, target.ErrorCount); + Assert.Equal(2, target.ErrorCount); Assert.Equal(1, target.Count); - Assert.Same(entry, target["key"]); - Assert.IsType>(target.InnerDictionary); + var actual = target["key"]; + Assert.Equal(entry.RawValue, actual.RawValue); + Assert.Equal(entry.AttemptedValue, actual.AttemptedValue); + Assert.Equal(entry.Errors, actual.Errors); + Assert.Equal(entry.ValidationState, actual.ValidationState); } [Fact] @@ -202,10 +312,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void ConstructorWithDictionaryParameter() { // Arrange - var oldDictionary = new ModelStateDictionary() - { - { "foo", new ModelStateEntry() { RawValue = "bar" } } - }; + var oldDictionary = new ModelStateDictionary(); + oldDictionary.SetModelValue("foo", "bar", "bar"); // Act var newDictionary = new ModelStateDictionary(oldDictionary); @@ -266,14 +374,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key) { // Arrange - var validState = new ModelStateEntry - { - ValidationState = ModelValidationState.Valid - }; - var dictionary = new ModelStateDictionary - { - { key, validState } - }; + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid(key); // Act var validationState = dictionary.GetFieldValidationState("foo"); @@ -304,14 +406,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void GetFieldValidationState_IndexedPrefix_ReturnsValidIfModelStateDoesNotContainErrors(string key) { // Arrange - var validState = new ModelStateEntry - { - ValidationState = ModelValidationState.Valid - }; - var dictionary = new ModelStateDictionary - { - { key, validState } - }; + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid(key); // Act var validationState = dictionary.GetFieldValidationState("[0].foo"); @@ -324,20 +420,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void IsValidPropertyReturnsFalseIfErrors() { // Arrange - var errorState = new ModelStateEntry - { - ValidationState = ModelValidationState.Invalid - }; - var validState = new ModelStateEntry - { - ValidationState = ModelValidationState.Valid - }; - errorState.Errors.Add("some error"); - var dictionary = new ModelStateDictionary() - { - { "foo", validState }, - { "baz", errorState } - }; + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid("foo"); + dictionary.AddModelError("bar", "some error"); // Act var isValid = dictionary.IsValid; @@ -352,19 +437,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void IsValidPropertyReturnsTrueIfNoErrors() { // Arrange - var dictionary = new ModelStateDictionary() - { - { "foo", new ModelStateEntry - { - ValidationState = ModelValidationState.Valid, - } - }, - { "baz", new ModelStateEntry - { - ValidationState = ModelValidationState.Skipped, - } - } - }; + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid("foo"); + dictionary.MarkFieldSkipped("bar"); // Act var isValid = dictionary.IsValid; @@ -379,21 +454,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void IsValidPropertyReturnsFalse_IfSomeFieldsAreNotValidated() { // Arrange - var errorState = new ModelStateEntry - { - ValidationState = ModelValidationState.Invalid - }; - var validState = new ModelStateEntry - { - ValidationState = ModelValidationState.Valid - }; - errorState.Errors.Add("some error"); - var dictionary = new ModelStateDictionary() - { - { "foo", validState }, - { "baz", errorState }, - { "qux", new ModelStateEntry() } - }; + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid("foo"); + dictionary.SetModelValue("qux", "value", "value"); + dictionary.AddModelError("baz", "some error"); // Act var isValid = dictionary.IsValid; @@ -408,22 +472,70 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void MergeCopiesDictionaryEntries() { // Arrange - var dictionary1 = new ModelStateDictionary { { "foo", new ModelStateEntry() } }; - var dictionary2 = new ModelStateDictionary { { "bar", new ModelStateEntry() } }; + var dictionary1 = new ModelStateDictionary(); + dictionary1.SetModelValue("foo", "RawValue1", "AttemptedValue1"); + dictionary1.AddModelError("foo", "value1-Error1"); + dictionary1.AddModelError("foo", "value1-Error2"); + + var dictionary2 = new ModelStateDictionary(); + dictionary2.SetModelValue("bar", "RawValue2", "AttemptedValue2"); + dictionary2.AddModelError("bar", "value2-Error1"); // Act dictionary1.Merge(dictionary2); // Assert Assert.Equal(2, dictionary1.Count); - Assert.Equal(dictionary2["bar"], dictionary1["bar"]); + var item = dictionary1["foo"]; + Assert.Equal("AttemptedValue1", item.AttemptedValue); + Assert.Equal("RawValue1", item.RawValue); + + item = dictionary1["bar"]; + Assert.Equal("AttemptedValue2", item.AttemptedValue); + Assert.Equal("RawValue2", item.RawValue); + Assert.Collection(item.Errors, + error => Assert.Equal("value2-Error1", error.ErrorMessage)); + } + + [Theory] + [InlineData("")] + [InlineData("key1")] + public void MergeCopiesDictionaryOverwritesExistingValues(string key) + { + // Arrange + var dictionary1 = new ModelStateDictionary(); + dictionary1.SetModelValue(key, "RawValue1", "AttemptedValue1"); + dictionary1.AddModelError(key, "value1-Error1"); + dictionary1.AddModelError(key, "value1-Error2"); + dictionary1.SetModelValue("other-key", null, null); + + var dictionary2 = new ModelStateDictionary(); + dictionary2.SetModelValue(key, "RawValue2", "AttemptedValue2"); + dictionary2.AddModelError(key, "value2-Error1"); + + // Act + dictionary1.Merge(dictionary2); + + // Assert + Assert.Equal(2, dictionary1.Count); + var item = dictionary1["other-key"]; + Assert.Null(item.AttemptedValue); + Assert.Null(item.RawValue); + Assert.Empty(item.Errors); + + item = dictionary1[key]; + Assert.Equal("AttemptedValue2", item.AttemptedValue); + Assert.Equal("RawValue2", item.RawValue); + Assert.Collection(item.Errors, + error => Assert.Equal("value2-Error1", error.ErrorMessage)); } [Fact] public void MergeDoesNothingIfParameterIsNull() { // Arrange - var dictionary = new ModelStateDictionary() { { "foo", new ModelStateEntry() } }; + var dictionary = new ModelStateDictionary(); + dictionary.SetModelValue("foo", "value", "value"); // Act dictionary.Merge(null); @@ -491,7 +603,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; + dictionary.MarkFieldValid("user.Address"); dictionary.SetModelValue("user.Name", new string[] { "some value" }, "some value"); dictionary.AddModelError("user.Age", "Age is not a valid int"); @@ -510,8 +622,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; + dictionary.MarkFieldValid("user.Address"); + dictionary.MarkFieldValid("user.Name"); dictionary.AddModelError("user.Age", "Age is not a valid int"); // Act @@ -529,9 +641,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - - dictionary["[0].product.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["[0].product.Age[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; + dictionary.MarkFieldValid("[0].product.Name"); + dictionary.MarkFieldValid("[0].product.Age[0]"); dictionary.AddModelError("[0].product.Name", "Name is invalid"); // Act @@ -546,8 +657,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; + dictionary.MarkFieldValid("user.Address"); + dictionary.MarkFieldValid("user.Name"); // Act var validationState = dictionary.GetFieldValidationState("user"); @@ -860,16 +971,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - - dictionary["Property1"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - - dictionary["Property2"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("Property1"); dictionary.AddModelError("Property2", "Property2 invalid."); - - dictionary["Property3"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Property3", "Property invalid."); - - dictionary["Property4"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped }; + dictionary.MarkFieldSkipped("Property4"); // Act dictionary.ClearValidationState("Property1"); @@ -892,23 +997,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - - dictionary["Product"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - - dictionary["Product.Detail1"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("Product"); dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid."); - - dictionary["Product.Detail2[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Product.Detail2[0]", "Product Detail2[0] invalid."); - - dictionary["Product.Detail2[1]"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Product.Detail2[1]", "Product Detail2[1] invalid."); - - dictionary["Product.Detail2[2]"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped }; - - dictionary["Product.Detail3"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped }; - - dictionary["ProductName"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldSkipped("Product.Detail2[2]"); + dictionary.MarkFieldSkipped("Product.Detail3"); dictionary.AddModelError("ProductName", "ProductName invalid."); // Act @@ -936,16 +1030,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - - dictionary["Product"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - - dictionary["Product.Detail1"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("Product"); dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid."); - - dictionary["Product.Detail1.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Product.Detail1.Name", "Product Detail1 Name invalid."); - - dictionary["Product.Detail1Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped }; + dictionary.MarkFieldSkipped("Product.Detail1Name"); // Act dictionary.ClearValidationState("Product.Detail1"); @@ -966,16 +1054,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - - dictionary["Property1"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - - dictionary["Property2"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("Property1"); dictionary.AddModelError("Property2", "Property2 invalid."); - - dictionary["Property3"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Property3", "Property invalid."); - - dictionary["Property4"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped }; + dictionary.MarkFieldSkipped("Property4"); // Act dictionary.ClearValidationState(modelKey); @@ -990,5 +1072,220 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Equal(0, dictionary["Property4"].Errors.Count); Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState); } + + [Fact] + public void GetEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + // Act & Assert + Assert.Empty(dictionary); + } + + [Fact] + public void GetEnumerable_ReturnsAllNonContainerNodes() + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid("Property1"); + dictionary.SetModelValue("Property1.Property2", "value", "value"); + dictionary.AddModelError("Property2", "Property invalid."); + dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid."); + dictionary.MarkFieldSkipped("Property4"); + dictionary.Remove("Property2"); + + // Act & Assert + Assert.Collection( + dictionary, + entry => + { + Assert.Equal("Property1", entry.Key); + Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState); + Assert.Null(entry.Value.RawValue); + Assert.Null(entry.Value.AttemptedValue); + Assert.Empty(entry.Value.Errors); + }, + entry => + { + Assert.Equal("Property4", entry.Key); + Assert.Equal(ModelValidationState.Skipped, entry.Value.ValidationState); + Assert.Null(entry.Value.RawValue); + Assert.Null(entry.Value.AttemptedValue); + Assert.Empty(entry.Value.Errors); + }, + entry => + { + Assert.Equal("Property1.Property2", entry.Key); + Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState); + Assert.Equal("value", entry.Value.RawValue); + Assert.Equal("value", entry.Value.AttemptedValue); + Assert.Empty(entry.Value.Errors); + }, + entry => + { + Assert.Equal("Property2[Property3]", entry.Key); + Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState); + Assert.Null(entry.Value.RawValue); + Assert.Null(entry.Value.AttemptedValue); + Assert.Collection(entry.Value.Errors, + error => Assert.Equal("Property2[Property3] invalid.", error.ErrorMessage)); + }); + } + + [Fact] + public void GetEnumerable_WorksCorrectlyWhenSiblingIsAPrefix() + { + // Arrange + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.SetModelValue("prop", "value1", "value1"); + modelStateDictionary.SetModelValue("property_name", "value3", "value3"); + modelStateDictionary.SetModelValue("property", "value2", "value2"); + + // Act & Assert + Assert.Collection(modelStateDictionary, + entry => + { + Assert.Equal("prop", entry.Key); + Assert.Equal("value1", entry.Value.RawValue); + }, + entry => + { + Assert.Equal("property", entry.Key); + Assert.Equal("value2", entry.Value.RawValue); + }, + entry => + { + Assert.Equal("property_name", entry.Key); + Assert.Equal("value3", entry.Value.RawValue); + }); + } + + [Fact] + public void KeysEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + // Act + var keys = dictionary.Keys; + + // Assert + Assert.Empty(keys); + } + + [Fact] + public void KeysEnumerable_ReturnsAllKeys() + { + // Arrange + var expected = new[] { "Property1", "Property4", "Property1.Property2", "Property2[Property3]" }; + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid("Property1"); + dictionary.AddModelError("Property1.Property2", "Property2 invalid."); + dictionary.AddModelError("Property2", "Property invalid."); + dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid."); + dictionary.MarkFieldSkipped("Property4"); + dictionary.Remove("Property2"); + + // Act + var keys = dictionary.Keys; + + // Assert + Assert.Equal(expected, keys); + } + + [Fact] + public void ValuesEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + // Act + var values = dictionary.Values; + + // Assert + Assert.Empty(values); + } + + [Fact] + public void ValuesEnumerable_ReturnsAllEntries() + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.MarkFieldValid("Property1"); + dictionary.SetModelValue("Property1.Property2", "value", "value"); + dictionary.AddModelError("Property2", "Property invalid."); + dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid."); + dictionary.MarkFieldSkipped("Property4"); + dictionary.Remove("Property2"); + + // Act & Assert + Assert.Collection(dictionary.Values, + value => + { + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + Assert.Null(value.RawValue); + Assert.Null(value.AttemptedValue); + Assert.Empty(value.Errors); + }, + value => + { + Assert.Equal(ModelValidationState.Skipped, value.ValidationState); + Assert.Null(value.RawValue); + Assert.Null(value.AttemptedValue); + Assert.Empty(value.Errors); + }, + value => + { + Assert.Equal(ModelValidationState.Unvalidated, value.ValidationState); + Assert.Equal("value", value.RawValue); + Assert.Equal("value", value.AttemptedValue); + Assert.Empty(value.Errors); + }, + value => + { + Assert.Equal(ModelValidationState.Invalid, value.ValidationState); + Assert.Null(value.RawValue); + Assert.Null(value.AttemptedValue); + Assert.Collection( + value.Errors, + error => Assert.Equal("Property2[Property3] invalid.", error.ErrorMessage)); + }); + } + + [Fact] + public void GetModelStateForProperty_ReturnsModelStateForImmediateChildren() + { + // Arrange + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.SetModelValue("property1", "value1", "value1"); + modelStateDictionary.SetModelValue("property1.property2", "value2", "value2"); + + // Act 1 + var property1 = modelStateDictionary.Root.GetModelStateForProperty("property1"); + var property2 = modelStateDictionary.Root.GetModelStateForProperty("property1.property2"); + + // Assert 1 + Assert.Equal("value1", property1.RawValue); + Assert.Null(property2); + + // Act 2 + property2 = property1.GetModelStateForProperty("property2"); + Assert.Equal("value2", property2.RawValue); + } + + [Fact] + public void GetModelStateForProperty_ReturnsModelStateForIndexedChildren() + { + // Arrange + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.SetModelValue("[property]", "value1", "value1"); + + // Act + var property = modelStateDictionary.Root.GetModelStateForProperty("[property]"); + + // Assert + Assert.Equal("value1", property.RawValue); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs index d585845d04..faa985afe3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs @@ -5,9 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -#if NETSTANDARDAPP1_5 -using System.Reflection; -#endif using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; @@ -749,9 +746,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal var validator = CreateValidator(); - modelState.Add("items[0]", new ModelStateEntry()); - modelState.Add("items[1]", new ModelStateEntry()); - modelState.Add("items[2]", new ModelStateEntry()); + modelState.SetModelValue("items[0]", "value1", "value1"); + modelState.SetModelValue("items[1]", "value2", "value2"); + modelState.SetModelValue("items[2]", "value3", "value3"); validationState.Add(model, new ValidationStateEntry() { Key = "items", @@ -797,10 +794,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal { "BarKey", "BarValue" } }; - modelState.Add("items[0].Key", new ModelStateEntry()); - modelState.Add("items[0].Value", new ModelStateEntry()); - modelState.Add("items[1].Key", new ModelStateEntry()); - modelState.Add("items[1].Value", new ModelStateEntry()); + modelState.SetModelValue("items[0].Key", "key0", "key0"); + modelState.SetModelValue("items[0].Value", "value0", "value0"); + modelState.SetModelValue("items[1].Key", "key1", "key1"); + modelState.SetModelValue("items[1].Value", "value1", "value1"); validationState.Add(model, new ValidationStateEntry() { Key = "items" }); // Act diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs index 84d93522c6..87e71e1a13 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs @@ -691,14 +691,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelMetadata = metadataProvider.GetMetadataForType(typeof(Product)); var dictionary = new ModelStateDictionary(); - dictionary["Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Name", "MyProperty invalid."); - dictionary["Id"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Id", "Id invalid."); dictionary.AddModelError("Id", "Id is required."); - dictionary["Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - - dictionary["Unrelated"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("Category"); dictionary.AddModelError("Unrelated", "Unrelated is required."); // Act @@ -727,10 +723,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelMetadata = metadataProvider.GetMetadataForType(typeof(string)); var dictionary = new ModelStateDictionary(); - dictionary[string.Empty] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError(string.Empty, "MyProperty invalid."); - - dictionary["Unrelated"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Unrelated", "Unrelated is required."); // Act @@ -754,19 +747,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelMetadata = metadataProvider.GetMetadataForType(typeof(List)); var dictionary = new ModelStateDictionary(); - dictionary["[0].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("[0].Name", "Name invalid."); - dictionary["[0].Id"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("[0].Id", "Id invalid."); dictionary.AddModelError("[0].Id", "Id required."); - dictionary["[0].Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; + dictionary.MarkFieldValid("[0].Category"); - dictionary["[1].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["[1].Id"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["[1].Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("[1].Name"); + dictionary.MarkFieldValid("[1].Id"); dictionary.AddModelError("[1].Category", "Category invalid."); - - dictionary["Unrelated"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("Unrelated", "Unrelated is required."); // Act @@ -804,20 +792,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelMetadata = metadataProvider.GetMetadataForType(typeof(Product)); var dictionary = new ModelStateDictionary(); - dictionary["product.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("product.Name", "Name invalid."); - dictionary["product.Id"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("product.Id", "Id invalid."); dictionary.AddModelError("product.Id", "Id required."); - dictionary["product.Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["product.Category.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["product.Order[0].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("product.Category"); + dictionary.MarkFieldValid("product.Category.Name"); dictionary.AddModelError("product.Order[0].Name", "Order name invalid."); - dictionary["product.Order[0].Address.Street"] = - new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; dictionary.AddModelError("product.Order[0].Address.Street", "Street invalid."); - dictionary["product.Order[1].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; - dictionary["product.Order[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid }; + dictionary.MarkFieldValid("product.Order[1].Name"); dictionary.AddModelError("product.Order[0]", "Order invalid."); // Act diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/SerializableErrorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/SerializableErrorTests.cs index e19c66f3cc..8f8e272304 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/SerializableErrorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/SerializableErrorTests.cs @@ -1,7 +1,6 @@ // 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.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding; using Xunit; @@ -70,12 +69,8 @@ namespace Microsoft.AspNetCore.Mvc { // Arrange var modelState = new ModelStateDictionary(); - modelState.Add( - "key1", - new ModelStateEntry()); - modelState.Add( - "key2", - new ModelStateEntry()); + modelState.SetModelValue("key1", "value1", "value1"); + modelState.SetModelValue("key2", "value2", "value2"); // Act var serializableError = new SerializableError(modelState); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html index 59d0edef5b..77ad4a0012 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/TagHelpersWebSite.Employee.Create.Invalid.html @@ -17,8 +17,8 @@
  • The field Age must be between 10 and 100.
  • -
  • The value 'z' is not valid for Salary.
  • The JoinDate field is required.
  • +
  • The value 'z' is not valid for Salary.
diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs index 3249bd5fdc..af355f1eb6 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs @@ -63,8 +63,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "Address[0].Street"); + var key = Assert.Single(modelState.Keys); + Assert.Equal("Address[0].Street", key); Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Empty(modelState[key].Errors); @@ -141,6 +141,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var modelState = operationContext.ActionContext.ModelState; var model = new Person4(); + // Act var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult); @@ -157,8 +158,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "Address[0].Street"); + var key = Assert.Single(modelState.Keys); + Assert.Equal("Address[0].Street", key); Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Empty(modelState[key].Errors); @@ -243,8 +244,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "prefix.Address[0].Street"); + var key = Assert.Single(modelState.Keys); + Assert.Equal("prefix.Address[0].Street", key); Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Empty(modelState[key].Errors); @@ -334,8 +335,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "prefix.Address[0].Street"); + var key = Assert.Single(modelState.Keys); + Assert.Equal("prefix.Address[0].Street", key); Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Empty(modelState[key].Errors); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs index d68f211697..f24111d874 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs @@ -234,9 +234,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "Address.Street"); + var key = Assert.Single(modelState.Keys); + Assert.Equal("Address.Street", key); Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it. } @@ -274,8 +273,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Street"); + var key = Assert.Single(modelState.Keys); + Assert.Equal("CustomParameter.Address.Street", key); Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it. } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs index 4a5f65df79..b6ad7b7428 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -590,9 +591,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.NotNull(boundPerson); Assert.False(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - - var street = Assert.Single(modelState, kvp => kvp.Key == "CustomParameter.Address.Street").Value; + var entry = Assert.Single(modelState); + Assert.Equal("CustomParameter.Address.Street", entry.Key); + var street = entry.Value; Assert.Equal(ModelValidationState.Invalid, street.ValidationState); var error = Assert.Single(street.Errors); // Mono issue - https://github.com/aspnet/External/issues/19 diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs index aee2d1b944..a8cbde3141 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(0, modelState.Keys.Count); + Assert.Empty(modelState.Keys); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index 2d350d3537..5e83e1869a 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); + Assert.Equal(1, modelState.Keys.Count()); var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Zip"); Assert.Equal("1", modelState[key].AttemptedValue); Assert.Equal("1", modelState[key].RawValue); @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); + Assert.Equal(1, modelState.Keys.Count()); var key = Assert.Single(modelState.Keys, k => k == "Address.Zip"); Assert.Equal("1", modelState[key].AttemptedValue); Assert.Equal("1", modelState[key].RawValue); @@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); + Assert.Equal(1, modelState.Keys.Count()); var key = Assert.Single(modelState.Keys); Assert.Equal("Parameter1", key); Assert.Equal("someValue", modelState[key].AttemptedValue); @@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); + Assert.Equal(1, modelState.Keys.Count()); var key = Assert.Single(modelState.Keys); Assert.Equal("Parameter1", key); Assert.Equal("someValue,otherValue", modelState[key].AttemptedValue); @@ -512,7 +512,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests var modelState = operationContext.ActionContext.ModelState; // Act - var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult); + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? + default(ModelBindingResult); // Assert // ModelBindingResult @@ -529,7 +530,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // ModelState Assert.True(modelState.IsValid); - Assert.Equal(new[] { "Address.Lines", "Address.Zip", "Name" }, modelState.Keys.ToArray()); + Assert.Equal(new[] { "Address.Lines", "Address.Zip", "Name" }, modelState.Keys.OrderBy(p => p).ToArray()); var entry = modelState["Address.Lines"]; Assert.NotNull(entry); Assert.Empty(entry.Errors); diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs index 60eee79b76..aca11c043c 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs @@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - dictionary.Add("Text", new ModelStateEntry()); + dictionary.SetModelValue("Text", "value", "value"); // Act dictionary.Remove(model => model.Text); @@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - dictionary.Add("Child.Text", new ModelStateEntry()); + dictionary.SetModelValue("Child.Text", "value", "value"); // Act dictionary.Remove(model => model.Child.Text); @@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var dictionary = new ModelStateDictionary(); - dictionary.Add("Child.Value", new ModelStateEntry()); + dictionary.SetModelValue("Child.Value", "value", "value"); // Act dictionary.Remove(model => model.Child.Value); @@ -206,7 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding // Arrange var variable = "Test"; var dictionary = new ModelStateDictionary(); - dictionary.Add("variable", new ModelStateEntry()); + dictionary.SetModelValue("variable", "value", "value"); // Act dictionary.Remove(model => variable); @@ -219,12 +219,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void RemoveAll_ForSingleExpression_RemovesModelStateKeys() { // Arrange - var state = new ModelStateEntry(); var dictionary = new ModelStateDictionary(); - dictionary.Add("Key", state); - dictionary.Add("Text", new ModelStateEntry()); - dictionary.Add("Text.Length", new ModelStateEntry()); + dictionary.SetModelValue("Key", "value1", "value1"); + dictionary.SetModelValue("Text", "value2", "value2"); + dictionary.SetModelValue("Text.Length", "value3", "value3"); + var expected = dictionary["Key"]; // Act dictionary.RemoveAll(model => model.Text); @@ -233,19 +233,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelState = Assert.Single(dictionary); Assert.Equal("Key", modelState.Key); - Assert.Same(state, modelState.Value); + Assert.Same(expected, modelState.Value); } [Fact] public void RemoveAll_ForRelationExpression_RemovesModelStateKeys() { // Arrange - var state = new ModelStateEntry(); var dictionary = new ModelStateDictionary(); - - dictionary.Add("Key", state); - dictionary.Add("Child", new ModelStateEntry()); - dictionary.Add("Child.Text", new ModelStateEntry()); + dictionary.SetModelValue("Key", "value1", "value1"); + dictionary.SetModelValue("Child", "value2", "value2"); + dictionary.SetModelValue("Child.Text", "value3", "value3"); + var expected = dictionary["Key"]; // Act dictionary.RemoveAll(model => model.Child); @@ -254,18 +253,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelState = Assert.Single(dictionary); Assert.Equal("Key", modelState.Key); - Assert.Same(state, modelState.Value); + Assert.Same(expected, modelState.Value); } [Fact] public void RemoveAll_ForImplicitlyCastedToObjectExpression_RemovesModelStateKeys() { // Arrange - var state = new ModelStateEntry(); var dictionary = new ModelStateDictionary(); - - dictionary.Add("Child", state); - dictionary.Add("Child.Value", new ModelStateEntry()); + dictionary.SetModelValue("Child", "value1", "value1"); + dictionary.SetModelValue("Child.Value", "value2", "value2"); + var expected = dictionary["child"]; // Act dictionary.RemoveAll(model => model.Child.Value); @@ -274,7 +272,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelState = Assert.Single(dictionary); Assert.Equal("Child", modelState.Key); - Assert.Same(state, modelState.Value); + Assert.Same(expected, modelState.Value); } [Fact] @@ -282,13 +280,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var variable = "Test"; - var state = new ModelStateEntry(); var dictionary = new ModelStateDictionary(); + dictionary.SetModelValue("Key", "value1", "value1"); + dictionary.SetModelValue("variable", "value2", "value2"); + dictionary.SetModelValue("variable.Text", "value3", "value3"); + dictionary.SetModelValue("variable.Value", "value4", "value4"); - dictionary.Add("Key", state); - dictionary.Add("variable", new ModelStateEntry()); - dictionary.Add("variable.Text", new ModelStateEntry()); - dictionary.Add("variable.Value", new ModelStateEntry()); + var expected = dictionary["Key"]; // Act dictionary.RemoveAll(model => variable); @@ -297,21 +295,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelState = Assert.Single(dictionary); Assert.Equal("Key", modelState.Key); - Assert.Same(state, modelState.Value); + Assert.Same(expected, modelState.Value); } [Fact] public void RemoveAll_ForModelExpression_RemovesModelPropertyKeys() { // Arrange - var state = new ModelStateEntry(); var dictionary = new ModelStateDictionary(); - - dictionary.Add("Key", state); - dictionary.Add("Text", new ModelStateEntry()); - dictionary.Add("Child", new ModelStateEntry()); - dictionary.Add("Child.Text", new ModelStateEntry()); - dictionary.Add("Child.NoValue", new ModelStateEntry()); + dictionary.SetModelValue("Key", "value1", "value1"); + dictionary.SetModelValue("Text", "value2", "value2"); + dictionary.SetModelValue("Child", "value3", "value3"); + dictionary.SetModelValue("Child.Text", "value4", "value4"); + dictionary.SetModelValue("Child.NoValue", "value5", "value5"); + var expected = dictionary["Key"]; // Act dictionary.RemoveAll(model => model); @@ -320,7 +317,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding var modelState = Assert.Single(dictionary); Assert.Equal("Key", modelState.Key); - Assert.Same(state, modelState.Value); + Assert.Same(expected, modelState.Value); } private class TestModel diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayTextTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayTextTest.cs index 1adc9e2dc2..d0bbe3118f 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayTextTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperDisplayTextTest.cs @@ -259,10 +259,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering viewData["FieldPrefix.Name"] = "View data dictionary value"; viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; - var modelState = new ModelStateEntry(); - modelState.RawValue = new string[] { "Attempted name value" }; - modelState.AttemptedValue = "Attempted name value"; - viewData.ModelState["FieldPrefix.Name"] = modelState; + viewData.ModelState.SetModelValue( + "FieldPrefix.Name", + "Attempted name value", + "Attempted name value"); // Act var result = helper.DisplayText("Name"); @@ -284,10 +284,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering viewData["Name"] = "View data dictionary value"; viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; - var modelState = new ModelStateEntry(); - modelState.RawValue = new string[] { "Attempted name value" }; - modelState.AttemptedValue = "Attempted name value"; - viewData.ModelState["FieldPrefix.Name"] = modelState; + viewData.ModelState.SetModelValue( + "FieldPrefix.Name", + "Attempted name value", + "Attempted name value"); // Act var result = helper.DisplayTextFor(m => m.Name); diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs index 51312a93ef..8c2946c47e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs @@ -361,9 +361,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering idAttributeDotReplacement: "$"); helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix"; helper.ViewData.ModelState.Clear(); - helper.ViewData.ModelState.Add("Property1", GetModelStateEntry("modelstate-without-prefix")); - helper.ViewData.ModelState.Add("MyPrefix.Property1", GetModelStateEntry("modelstate-with-prefix")); - helper.ViewData.ModelState.Add("MyPrefix$Property1", GetModelStateEntry("modelstate-with-iddotreplacement")); + helper.ViewData.ModelState.SetModelValue( + "Property1", + "modelstate-without-prefix", + "modelstate-without-prefix"); + helper.ViewData.ModelState.SetModelValue( + "MyPrefix.Property1", + "modelstate-with-prefix", + "modelstate-with-prefix"); + helper.ViewData.ModelState.SetModelValue( + "MyPrefix$Property1", + "modelstate-with-iddotreplacement", + "modelstate-with-iddotreplacement"); // Act var result = helper.Hidden("Property1", "explicit-value", htmlAttributes: null); @@ -411,7 +420,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering // Act and Assert ExceptionAssert.ThrowsArgument( - () => helper.Hidden(string.Empty, string.Empty, attributes), + () => helper.Hidden(string.Empty, string.Empty, attributes), "expression", expected); } @@ -660,9 +669,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering helper.ViewData.Model.Property1 = "propValue"; helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix"; helper.ViewData.ModelState.Clear(); - helper.ViewData.ModelState.Add("Property1", GetModelStateEntry("modelstate-without-prefix")); - helper.ViewData.ModelState.Add("MyPrefix.Property1", GetModelStateEntry("modelstate-with-prefix")); - helper.ViewData.ModelState.Add("MyPrefix$Property1", GetModelStateEntry("modelstate-with-iddotreplacement")); + helper.ViewData.ModelState.SetModelValue( + "Property1", + "modelstate-without-prefix", + "modelstate-without-prefix"); + helper.ViewData.ModelState.SetModelValue( + "MyPrefix.Property1", + "modelstate-with-prefix", + "modelstate-with-prefix"); + helper.ViewData.ModelState.SetModelValue( + "MyPrefix$Property1", + "modelstate-with-iddotreplacement", + "modelstate-with-iddotreplacement"); // Act var result = helper.HiddenFor(m => m.Property1, htmlAttributes: null); @@ -802,9 +820,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { // Arrange var viewData = GetViewDataWithNullModelAndNonNullViewData(); - viewData.ModelState.Add("pre.Property3[key]", GetModelStateEntry("Prop3Val")); - viewData.ModelState.Add("pre.Property4.Property5", GetModelStateEntry("Prop5Val")); - viewData.ModelState.Add("pre.Property4.Property6[0]", GetModelStateEntry("Prop6Val")); + viewData.ModelState.SetModelValue("pre.Property3[key]", "Prop3Val", "Prop3Val"); + viewData.ModelState.SetModelValue("pre.Property4.Property5", "Prop5Val", "Prop5Val"); + viewData.ModelState.SetModelValue("pre.Property4.Property6[0]", "Prop6Val", "Prop6Val"); var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData); viewData.TemplateInfo.HtmlFieldPrefix = "pre"; @@ -911,7 +929,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { var viewData = GetViewDataWithNonNullModel(); viewData["Property1"] = "view-data-val"; - viewData.ModelState.Add("Property1", GetModelStateEntry("ModelStateValue")); + viewData.ModelState.SetModelValue("Property1", "ModelStateValue", "ModelStateValue"); return viewData; } @@ -924,15 +942,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering return viewData; } - private static ModelStateEntry GetModelStateEntry(string value) - { - return new ModelStateEntry - { - RawValue = new string[] { value }, - AttemptedValue = value, - }; - } - public class HiddenModel { public string Property1 { get; set; } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs index bcbeadd133..41a3a9654f 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs @@ -324,9 +324,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { // Arrange var viewData = GetViewDataWithModelStateAndModelAndViewDataValues(); - viewData.ModelState.Add("pre.Property3[key]", GetModelStateEntry("Property3Val")); - viewData.ModelState.Add("pre.Property4.Property5", GetModelStateEntry("Property5Val")); - viewData.ModelState.Add("pre.Property4.Property6[0]", GetModelStateEntry("Property6Val")); + viewData.ModelState.SetModelValue("pre.Property3[key]", "Property3Val", "Property3Val"); + viewData.ModelState.SetModelValue("pre.Property4.Property5", "Property5Val", "Property5Val"); + viewData.ModelState.SetModelValue("pre.Property4.Property6[0]", "Property6Val", "Property6Val"); viewData["pre.Property3[key]"] = "vdd-value1"; viewData["pre.Property4.Property5"] = "vdd-value2"; viewData["pre.Property4.Property6[0]"] = "vdd-value3"; @@ -426,7 +426,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { var viewData = GetViewDataWithNullModelAndNonEmptyViewData(); viewData.Model = new PasswordModel(); - viewData.ModelState.Add("Property1", GetModelStateEntry("ModelStateValue")); + viewData.ModelState.SetModelValue("Property1", "ModelStateValue", "ModelStateValue"); return viewData; } @@ -439,15 +439,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering return viewData; } - private static ModelStateEntry GetModelStateEntry(string value) - { - return new ModelStateEntry - { - RawValue = new string[] { value }, - AttemptedValue = value, - }; - } - public class PasswordModel { public string Property1 { get; set; } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs index f9cf3b33f1..bb9b443164 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperSelectTest.cs @@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering // Act & Assert var ex = Assert.Throws( - "expression", + "expression", () => helper.DropDownList(null, selectList: null, optionLabel: null, htmlAttributes: null)); Assert.Equal(expected, ex.Message); } @@ -412,19 +412,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering // Arrange var expectedHtml = GetExpectedSelectElement(SelectSources.ModelStateEntry, allowMultiple: false); - var modelState = new ModelStateDictionary - { - ["Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, - AttemptedValue = SelectSources.ModelStateEntry.ToString() - }, - ["Prefix.Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() }, - AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString() - }, - }; + var modelState = new ModelStateDictionary(); + modelState.SetModelValue( + "Property1", + SelectSources.ModelStateEntry, + SelectSources.ModelStateEntry.ToString()); + modelState.SetModelValue( + "Prefix.Property1", + SelectSources.ModelStateEntryWithPrefix, + SelectSources.ModelStateEntryWithPrefix.ToString()); var provider = TestModelMetadataProvider.CreateDefaultProvider(); var viewData = new ViewDataDictionary(provider, modelState) @@ -453,19 +449,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering SelectSources.ModelStateEntryWithPrefix, allowMultiple: false); - var modelState = new ModelStateDictionary - { - ["Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, - AttemptedValue = SelectSources.ModelStateEntry.ToString() - }, - ["Prefix.Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() }, - AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString() - }, - }; + var modelState = new ModelStateDictionary(); + modelState.SetModelValue( + "Property1", + SelectSources.ModelStateEntry, + SelectSources.ModelStateEntry.ToString()); + modelState.SetModelValue( + "Prefix.Property1", + SelectSources.ModelStateEntryWithPrefix, + SelectSources.ModelStateEntryWithPrefix.ToString()); var provider = TestModelMetadataProvider.CreateDefaultProvider(); var viewData = new ViewDataDictionary(provider, modelState) @@ -823,19 +815,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering // Arrange var expectedHtml = GetExpectedSelectElement(SelectSources.ModelStateEntry, allowMultiple: true); - var modelState = new ModelStateDictionary - { - ["Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, - AttemptedValue = SelectSources.ModelStateEntry.ToString() - }, - ["Prefix.Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() }, - AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString() - }, - }; + var modelState = new ModelStateDictionary(); + modelState.SetModelValue( + "Property1", + SelectSources.ModelStateEntry, + SelectSources.ModelStateEntry.ToString()); + modelState.SetModelValue( + "Prefix.Property1", + SelectSources.ModelStateEntryWithPrefix, + SelectSources.ModelStateEntryWithPrefix.ToString()); var provider = TestModelMetadataProvider.CreateDefaultProvider(); var viewData = new ViewDataDictionary(provider, modelState) @@ -864,19 +852,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering SelectSources.ModelStateEntryWithPrefix, allowMultiple: true); - var modelState = new ModelStateDictionary - { - ["Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, - AttemptedValue = SelectSources.ModelStateEntry.ToString() - }, - ["Prefix.Property1"] = new ModelStateEntry - { - RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() }, - AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString() - }, - }; + var modelState = new ModelStateDictionary(); + modelState.SetModelValue( + "Property1", + SelectSources.ModelStateEntry, + SelectSources.ModelStateEntry.ToString()); + modelState.SetModelValue( + "Prefix.Property1", + SelectSources.ModelStateEntryWithPrefix, + SelectSources.ModelStateEntryWithPrefix.ToString()); var provider = TestModelMetadataProvider.CreateDefaultProvider(); var viewData = new ViewDataDictionary(provider, modelState) diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValidationSummaryTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValidationSummaryTest.cs index 36257f9ba2..9f4b84c9bd 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValidationSummaryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValidationSummaryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.TestCommon; @@ -109,13 +110,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering "
  • HtmlEncode[[This is an error for Property3.]]
  • " + Environment.NewLine + "
    "; var divWithAllErrors = "
      " + - "
    • HtmlEncode[[This is an error for Property3.Property2.]]
    • " + Environment.NewLine + - "
    • HtmlEncode[[This is an error for Property3.OrderedProperty3.]]
    • " + Environment.NewLine + - "
    • HtmlEncode[[This is an error for Property3.OrderedProperty2.]]
    • " + Environment.NewLine + - "
    • HtmlEncode[[This is an error for Property3.]]
    • " + Environment.NewLine + "
    • HtmlEncode[[This is an error for Property2.]]
    • " + Environment.NewLine + "
    • HtmlEncode[[This is another error for Property2.]]
    • " + Environment.NewLine + "
    • HtmlEncode[[The value '' is not valid for Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.OrderedProperty3.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.OrderedProperty2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.]]
    • " + Environment.NewLine + "
    • HtmlEncode[[This is an error for the model root.]]
    • " + Environment.NewLine + "
    • HtmlEncode[[This is another error for the model root.]]
    • " + Environment.NewLine + "
    "; @@ -302,6 +303,166 @@ namespace Microsoft.AspNetCore.Mvc.Rendering Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); } + [Fact] + public void ValidationSummary_OrdersCorrectlyWhenElementsAreRemovedFromDictionary() + { + // Arrange + var expected = "
      " + + "
    • HtmlEncode[[New error for Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.OrderedProperty3.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for the model root.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is another error for the model root.]]
    • " + Environment.NewLine + + "
    "; + var model = new ValidationModel(); + var html = DefaultTemplatesUtilities.GetHtmlHelper(model); + AddMultipleErrors(html.ViewData.ModelState); + html.ViewData.ModelState.RemoveAll(m => m.Property2); + html.ViewData.ModelState.Remove(m => m.Property3); + html.ViewData.ModelState.Remove(m => m.Property3.OrderedProperty2); + html.ViewData.ModelState.AddModelError("Property2", "New error for Property2."); + + // Act + var result = html.ValidationSummary( + excludePropertyErrors: false, + message: null, + htmlAttributes: null, + tag: null); + + // Assert + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); + } + + [Fact] + public void ValidationSummary_IncludesErrorsThatAreNotPartOfMetadata() + { + // Arrange + var expected = "
      " + + "
    • HtmlEncode[[This is an error for Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is another error for Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[The value '' is not valid for Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.OrderedProperty3.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.OrderedProperty2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.Property2.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for Property3.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is an error for the model root.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[This is another error for the model root.]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[non-existent-error1]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[non-existent-error2]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[non-existent-error3]]
    • " + Environment.NewLine + + "
    "; + var model = new ValidationModel(); + var html = DefaultTemplatesUtilities.GetHtmlHelper(model); + AddMultipleErrors(html.ViewData.ModelState); + html.ViewData.ModelState.AddModelError("non-existent-property1", "non-existent-error1"); + html.ViewData.ModelState.AddModelError("non.existent.property2", "non-existent-error2"); + html.ViewData.ModelState.AddModelError("non.existent[0].property3", "non-existent-error3"); + + // Act + var result = html.ValidationSummary( + excludePropertyErrors: false, + message: null, + htmlAttributes: null, + tag: null); + + // Assert + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); + } + + [Fact] + public void ValidationSummary_IncludesErrorsForCollectionProperties() + { + // Arrange + var expected = "
      " + + "
    • HtmlEncode[[Property1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[Property2[0].OrderedProperty1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[Property2[0].Property1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[Property2[2].Property3 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[Property2[10].Property2 error]]
    • " + Environment.NewLine + + "
    "; + var model = new ModelWithCollection(); + var html = DefaultTemplatesUtilities.GetHtmlHelper(model); + html.ViewData.ModelState.AddModelError("Property1", "Property1 error"); + html.ViewData.ModelState.AddModelError("Property2[0].OrderedProperty1", "Property2[0].OrderedProperty1 error"); + html.ViewData.ModelState.AddModelError("Property2[10].Property2", "Property2[10].Property2 error"); + html.ViewData.ModelState.AddModelError("Property2[2].Property3", "Property2[2].Property3 error"); + html.ViewData.ModelState.AddModelError("Property2[0].Property1", "Property2[0].Property1 error"); + + // Act + var result = html.ValidationSummary( + excludePropertyErrors: false, + message: null, + htmlAttributes: null, + tag: null); + + // Assert + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); + } + + [Fact] + public void ValidationSummary_IncludesErrorsForTopLevelCollectionProperties() + { + // Arrange + var expected = "
      " + + "
    • HtmlEncode[[[0].OrderedProperty2 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[0].OrderedProperty1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[0].Property1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[2].OrderedProperty3 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[2].Property3 error]]
    • " + Environment.NewLine + + "
    "; + var model = new OrderedModel[5]; + var html = DefaultTemplatesUtilities.GetHtmlHelper(model); + html.ViewData.ModelState.AddModelError("[0].OrderedProperty2", "[0].OrderedProperty2 error"); + html.ViewData.ModelState.AddModelError("[0].Property1", "[0].Property1 error"); + html.ViewData.ModelState.AddModelError("[0].OrderedProperty1", "[0].OrderedProperty1 error"); + html.ViewData.ModelState.AddModelError("[2].Property3", "[2].Property3 error"); + html.ViewData.ModelState.AddModelError("[2].OrderedProperty3", "[2].OrderedProperty3 error"); + + // Act + var result = html.ValidationSummary( + excludePropertyErrors: false, + message: null, + htmlAttributes: null, + tag: null); + + // Assert + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); + } + + [Fact] + public void ValidationSummary_IncludesErrorsForPropertiesOnCollectionTypes() + { + // Arrange + var expected = "
      " + + "
    • HtmlEncode[[[0].OrderedProperty2 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[0].OrderedProperty1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[0].Property1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[2].OrderedProperty3 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[[2].Property3 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[OrderedProperty1 error]]
    • " + Environment.NewLine + + "
    • HtmlEncode[[OrderedProperty2 error]]
    • " + Environment.NewLine + + "
    "; + var model = new OrderedModel[5]; + var html = DefaultTemplatesUtilities.GetHtmlHelper(model); + html.ViewData.ModelState.AddModelError("[0].OrderedProperty2", "[0].OrderedProperty2 error"); + html.ViewData.ModelState.AddModelError("[0].Property1", "[0].Property1 error"); + html.ViewData.ModelState.AddModelError("[0].OrderedProperty1", "[0].OrderedProperty1 error"); + html.ViewData.ModelState.AddModelError("[2].Property3", "[2].Property3 error"); + html.ViewData.ModelState.AddModelError("[2].OrderedProperty3", "[2].OrderedProperty3 error"); + html.ViewData.ModelState.AddModelError("OrderedProperty1", "OrderedProperty1 error"); + html.ViewData.ModelState.AddModelError("OrderedProperty2", "OrderedProperty2 error"); + + // Act + var result = html.ValidationSummary( + excludePropertyErrors: false, + message: null, + htmlAttributes: null, + tag: null); + + // Assert + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); + } + [Fact] public void ValidationSummary_ErrorsInModelUsingOrder_SortsErrorsAsExpected() { @@ -313,10 +474,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering "
  • HtmlEncode[[This is yet-another error for OrderedProperty2.]]
  • " + Environment.NewLine + "
  • HtmlEncode[[This is an error for OrderedProperty1.]]
  • " + Environment.NewLine + "
  • HtmlEncode[[This is an error for Property3.]]
  • " + Environment.NewLine + - "
  • HtmlEncode[[This is an error for Property2.]]
  • " + Environment.NewLine + - "
  • HtmlEncode[[This is another error for Property2.]]
  • " + Environment.NewLine + "
  • HtmlEncode[[This is an error for Property1.]]
  • " + Environment.NewLine + "
  • HtmlEncode[[This is another error for Property1.]]
  • " + Environment.NewLine + + "
  • HtmlEncode[[This is an error for Property2.]]
  • " + Environment.NewLine + + "
  • HtmlEncode[[This is another error for Property2.]]
  • " + Environment.NewLine + "
  • HtmlEncode[[This is an error for LastProperty.]]
  • " + Environment.NewLine + ""; @@ -436,7 +597,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering helper.ViewData.ModelState.AddModelError("Property1", "Error for Property1"); // Act - var validationSummaryResult = helper.ValidationSummary(message: "Custom Message", htmlAttributes: new { attr="value" }); + var validationSummaryResult = helper.ValidationSummary(message: "Custom Message", htmlAttributes: new { attr = "value" }); // Assert Assert.Equal( @@ -577,5 +738,21 @@ namespace Microsoft.AspNetCore.Mvc.Rendering [Display(Order = 23)] public string OrderedProperty1 { get; set; } } + + private class ModelWithCollection + { + public string Property1 { get; set; } + + public List Property2 { get; set; } + } + + private class CollectionType : Collection + { + [Display(Order = 1)] + public string OrderedProperty2 { get; set; } + + [Display(Order = 2)] + public string OrderedProperty1 { get; set; } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValueTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValueTest.cs index 6af76f7b17..629a380865 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValueTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperValueTest.cs @@ -162,15 +162,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures viewData["StringProperty"] = "ViewDataValue"; viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; - var modelState = new ModelStateEntry(); - modelState.AttemptedValue = "StringPropertyAttemptedValue"; - modelState.RawValue = new string[] { "StringPropertyRawValue" }; - viewData.ModelState["FieldPrefix.StringProperty"] = modelState; + viewData.ModelState.SetModelValue( + "FieldPrefix.StringProperty", + "StringPropertyRawValue", + "StringPropertyAttemptedValue"); - modelState = new ModelStateEntry(); - modelState.AttemptedValue = "ModelAttemptedValue"; - modelState.RawValue = new string[] { "ModelRawValue" }; - viewData.ModelState["FieldPrefix"] = modelState; + viewData.ModelState.SetModelValue( + "FieldPrefix", + "ModelRawValue", + "ModelAttemptedValue"); // Act & Assert Assert.Equal("StringPropertyRawValue", helper.Value("StringProperty", format: null)); @@ -219,10 +219,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures var viewData = helper.ViewData; viewData["StringProperty"] = "ViewDataValue <\"\">"; - var modelState = new ModelStateEntry(); - modelState.AttemptedValue = "ObjectPropertyAttemptedValue <\"\">"; - modelState.RawValue = new string[] { "ObjectPropertyRawValue <\"\">" }; - viewData.ModelState["ObjectProperty"] = modelState; + viewData.ModelState.SetModelValue( + "ObjectProperty", + "ObjectPropertyRawValue <\"\">", + "ObjectPropertyAttemptedValue <\"\">"); // Act & Assert Assert.Equal( diff --git a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpErrorTest.cs b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpErrorTest.cs index 04ad65aa00..d509511d4d 100644 --- a/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpErrorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.WebApiCompatShimTest/HttpErrorTest.cs @@ -33,12 +33,10 @@ namespace System.Web.Http.Dispatcher yield return new[] { new HttpError() }; yield return new[] { new HttpError("error") }; yield return new[] { new HttpError(new NotImplementedException(), true) }; - yield return new[] { new HttpError( - new ModelStateDictionary() - { - { "key", new ModelStateEntry { Errors = { new ModelError("error") } } } - }, - true) }; + + var modelState = new ModelStateDictionary(); + modelState.AddModelError("key", "error"); + yield return new[] { new HttpError(modelState, true) }; } } @@ -247,7 +245,7 @@ namespace System.Web.Http.Dispatcher } [Theory] - [MemberData("ErrorKeyValue")] + [MemberData(nameof(ErrorKeyValue))] public void HttpErrorStringProperties_UseCorrectHttpErrorKey(HttpError httpError, Func productUnderTest, string key, string actualValue) { // Arrange @@ -289,7 +287,7 @@ namespace System.Web.Http.Dispatcher } [Theory] - [MemberData("HttpErrors")] + [MemberData(nameof(HttpErrors))] public void HttpErrors_UseCaseInsensitiveComparer(HttpError httpError) { // Arrange diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs index 90bbc4440b..ea8c0ee813 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/BasicApiController.cs @@ -83,8 +83,9 @@ namespace WebApiCompatShimWebSite var error = item.Value.Errors.SingleOrDefault(); if (error != null) { - var value = error.Exception != null ? error.Exception.Message : - error.ErrorMessage; + var value = error.Exception != null ? + error.Exception.Message : + error.ErrorMessage; result.Add(item.Key, value); } }