Use a prefix tree as a backing store for ModelStateDictionary

This commit is contained in:
Pranav K 2016-02-16 18:05:40 -08:00
parent 432cfa0035
commit f651f18d3a
23 changed files with 1376 additions and 676 deletions

View File

@ -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
{
/// <summary>
/// An entry in a <see cref="ModelStateDictionary"/>.
/// </summary>
public class ModelStateEntry
{
/// <summary>
/// Gets the raw value from the request associated with this entry.
/// </summary>
public object RawValue { get; set; }
/// <summary>
/// Gets the set of values contained in <see cref="RawValue"/>, joined into a comma-separated string.
/// </summary>
public string AttemptedValue { get; set; }
/// <summary>
/// Gets the <see cref="ModelErrorCollection"/> for this entry.
/// </summary>
public ModelErrorCollection Errors { get; } = new ModelErrorCollection();
/// <summary>
/// Gets or sets the <see cref="ModelValidationState"/> for this entry.
/// </summary>
public ModelValidationState ValidationState { get; set; }
}
}

View File

@ -4,7 +4,9 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.ModelBinding 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 /// Represents the state of an attempt to bind values from an HTTP Request to an action method, which includes
/// validation information. /// validation information.
/// </summary> /// </summary>
public class ModelStateDictionary : IDictionary<string, ModelStateEntry> public class ModelStateDictionary : IReadOnlyDictionary<string, ModelStateEntry>
{ {
// Make sure to update the doc headers if this value is changed. // Make sure to update the doc headers if this value is changed.
/// <summary> /// <summary>
/// The default value for <see cref="MaxAllowedErrors"/> of <c>200</c>. /// The default value for <see cref="MaxAllowedErrors"/> of <c>200</c>.
/// </summary> /// </summary>
public static readonly int DefaultMaxAllowedErrors = 200; public static readonly int DefaultMaxAllowedErrors = 200;
private static readonly char[] Delimiters = new char[] { '.', '[' };
private readonly Dictionary<string, ModelStateEntry> _innerDictionary; private readonly ModelStateNode _root;
private int _maxAllowedErrors; private int _maxAllowedErrors;
/// <summary> /// <summary>
@ -37,8 +40,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public ModelStateDictionary(int maxAllowedErrors) public ModelStateDictionary(int maxAllowedErrors)
{ {
MaxAllowedErrors = maxAllowedErrors; MaxAllowedErrors = maxAllowedErrors;
var emptySegment = new StringSegment(buffer: string.Empty);
_innerDictionary = new Dictionary<string, ModelStateEntry>(StringComparer.OrdinalIgnoreCase); _root = new ModelStateNode(subKey: emptySegment)
{
Key = string.Empty
};
} }
/// <summary> /// <summary>
@ -47,21 +53,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary> /// </summary>
/// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param> /// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
public ModelStateDictionary(ModelStateDictionary dictionary) public ModelStateDictionary(ModelStateDictionary dictionary)
: this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors)
{ {
if (dictionary == null) if (dictionary == null)
{ {
throw new ArgumentNullException(nameof(dictionary)); throw new ArgumentNullException(nameof(dictionary));
} }
_innerDictionary = new Dictionary<string, ModelStateEntry>( Merge(dictionary);
dictionary,
StringComparer.OrdinalIgnoreCase);
MaxAllowedErrors = dictionary.MaxAllowedErrors;
ErrorCount = dictionary.ErrorCount;
HasRecordedMaxModelError = dictionary.HasRecordedMaxModelError;
} }
/// <summary>
/// Root entry for the <see cref="ModelStateDictionary"/>.
/// </summary>
public ModelStateEntry Root => _root;
/// <summary> /// <summary>
/// Gets or sets the maximum allowed model state errors in this instance of <see cref="ModelStateDictionary"/>. /// Gets or sets the maximum allowed model state errors in this instance of <see cref="ModelStateDictionary"/>.
/// Defaults to <c>200</c>. /// Defaults to <c>200</c>.
@ -115,28 +121,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public int ErrorCount { get; private set; } public int ErrorCount { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public int Count public int Count { get; private set; }
{
get { return _innerDictionary.Count; } /// <summary>
} /// Gets the key sequence.
/// </summary>
public KeyEnumerable Keys => new KeyEnumerable(this);
/// <inheritdoc /> /// <inheritdoc />
public bool IsReadOnly IEnumerable<string> IReadOnlyDictionary<string, ModelStateEntry>.Keys => Keys;
{
get { return ((ICollection<KeyValuePair<string, ModelStateEntry>>)_innerDictionary).IsReadOnly; } /// <summary>
} /// Gets the value sequence.
/// </summary>
public ValueEnumerable Values => new ValueEnumerable(this);
/// <inheritdoc /> /// <inheritdoc />
public ICollection<string> Keys IEnumerable<ModelStateEntry> IReadOnlyDictionary<string, ModelStateEntry>.Values => Values;
{
get { return _innerDictionary.Keys; }
}
/// <inheritdoc />
public ICollection<ModelStateEntry> Values
{
get { return _innerDictionary.Values; }
}
/// <summary> /// <summary>
/// Gets a value that indicates whether any model state values in this model state dictionary is invalid or not validated. /// 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
} }
/// <inheritdoc /> /// <inheritdoc />
public ModelValidationState ValidationState public ModelValidationState ValidationState => GetValidity(_root) ?? ModelValidationState.Valid;
{
get
{
var entries = FindKeysWithPrefix(string.Empty);
return GetValidity(entries, defaultState: ModelValidationState.Valid);
}
}
/// <inheritdoc /> /// <inheritdoc />
public ModelStateEntry this[string key] public ModelStateEntry this[string key]
@ -169,29 +163,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
ModelStateEntry value; ModelStateEntry entry;
_innerDictionary.TryGetValue(key, out value); TryGetValue(key, out entry);
return value; 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<string, ModelStateEntry> InnerDictionary
{
get { return _innerDictionary; }
} }
// Flag that indiciates if TooManyModelErrorException has already been added to this dictionary. // Flag that indiciates if TooManyModelErrorException has already been added to this dictionary.
@ -337,8 +312,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
ErrorCount++; ErrorCount++;
var modelState = GetModelStateForKey(key); var modelState = GetOrAddNode(key);
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.ValidationState = ModelValidationState.Invalid; modelState.ValidationState = ModelValidationState.Invalid;
modelState.MarkNonContainerNode();
modelState.Errors.Add(errorMessage); modelState.Errors.Add(errorMessage);
return true; return true;
@ -359,8 +336,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
var entries = FindKeysWithPrefix(key); var item = GetNode(key);
return GetValidity(entries, defaultState: ModelValidationState.Unvalidated); return GetValidity(item) ?? ModelValidationState.Unvalidated;
} }
/// <summary> /// <summary>
@ -398,12 +375,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
var modelState = GetModelStateForKey(key); var modelState = GetOrAddNode(key);
if (modelState.ValidationState == ModelValidationState.Invalid) if (modelState.ValidationState == ModelValidationState.Invalid)
{ {
throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset); throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset);
} }
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.MarkNonContainerNode();
modelState.ValidationState = ModelValidationState.Valid; modelState.ValidationState = ModelValidationState.Valid;
} }
@ -419,12 +398,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
var modelState = GetModelStateForKey(key); var modelState = GetOrAddNode(key);
if (modelState.ValidationState == ModelValidationState.Invalid) if (modelState.ValidationState == ModelValidationState.Invalid)
{ {
throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset_ToSkipped); throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset_ToSkipped);
} }
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.MarkNonContainerNode();
modelState.ValidationState = ModelValidationState.Skipped; modelState.ValidationState = ModelValidationState.Skipped;
} }
@ -440,9 +421,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return; 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)); throw new ArgumentNullException(nameof(key));
} }
var modelState = GetModelStateForKey(key); var modelState = GetOrAddNode(key);
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.RawValue = rawValue; modelState.RawValue = rawValue;
modelState.AttemptedValue = attemptedValue; modelState.AttemptedValue = attemptedValue;
modelState.MarkNonContainerNode();
} }
/// <summary> /// <summary>
@ -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; // For a key of the format, foo.bar[0].baz[qux] we'll create the following nodes:
if (!TryGetValue(key, out entry)) // foo
// -> bar
// -> [0]
// -> baz
// -> [qux]
var current = _root;
var previousIndex = 0;
int index;
while ((index = key.IndexOfAny(Delimiters, previousIndex)) != -1)
{ {
entry = new ModelStateEntry(); var keyStart = previousIndex == 0 || key[previousIndex - 1] == '.'
this[key] = entry; ? 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)
{ {
if (node == null)
var hasEntries = false;
var validationState = ModelValidationState.Valid;
foreach (var entry in entries)
{ {
hasEntries = true; return null;
}
var entryState = entry.Value.ValidationState; ModelValidationState? validationState = null;
if (entryState == ModelValidationState.Unvalidated) 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. // 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() private void EnsureMaxErrorsReachedRecorded()
@ -570,43 +617,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private void AddModelErrorCore(string key, Exception exception) 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.ValidationState = ModelValidationState.Invalid;
modelState.MarkNonContainerNode();
modelState.Errors.Add(exception); modelState.Errors.Add(exception);
} }
/// <inheritdoc /> /// <summary>
public void Add(KeyValuePair<string, ModelStateEntry> item) /// Removes all keys and values from ths instance of <see cref="ModelStateDictionary"/>.
{ /// </summary>
Add(item.Key, item.Value);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public void Clear() public void Clear()
{ {
_innerDictionary.Clear(); Count = 0;
} HasRecordedMaxModelError = false;
ErrorCount = 0;
/// <inheritdoc /> _root.Reset();
public bool Contains(KeyValuePair<string, ModelStateEntry> item) _root.ChildNodes.Clear();
{
return ((ICollection<KeyValuePair<string, ModelStateEntry>>)_innerDictionary).Contains(item);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -617,27 +644,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
return _innerDictionary.ContainsKey(key); return !GetNode(key)?.IsContainerNode ?? false;
} }
/// <inheritdoc /> /// <summary>
public void CopyTo(KeyValuePair<string, ModelStateEntry>[] array, int arrayIndex) /// Removes the <see cref="ModelStateEntry"/> with the specified <paramref name="key"/>.
{ /// </summary>
if (array == null) /// <param name="key">The key.</param>
{ /// <returns><c>true</c> if the element is successfully removed; otherwise <c>false</c>. This method also
throw new ArgumentNullException(nameof(array)); /// returns <c>false</c> if key was not found.</returns>
}
((ICollection<KeyValuePair<string, ModelStateEntry>>)_innerDictionary).CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public bool Remove(KeyValuePair<string, ModelStateEntry> item)
{
return ((ICollection<KeyValuePair<string, ModelStateEntry>>)_innerDictionary).Remove(item);
}
/// <inheritdoc />
public bool Remove(string key) public bool Remove(string key)
{ {
if (key == null) if (key == null)
@ -645,7 +660,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); 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;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -656,20 +680,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); 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;
} }
/// <inheritdoc /> /// <summary>
public IEnumerator<KeyValuePair<string, ModelStateEntry>> GetEnumerator() /// Returns an enumerator that iterates through this instance of <see cref="ModelStateDictionary"/>.
{ /// </summary>
return _innerDictionary.GetEnumerator(); /// <returns>An <see cref="Enumerator"/>.</returns>
} public Enumerator GetEnumerator() => new Enumerator(this, prefix: string.Empty);
/// <inheritdoc /> /// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() IEnumerator<KeyValuePair<string, ModelStateEntry>>
{ IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator() => GetEnumerator();
return GetEnumerator();
} /// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static bool StartsWithPrefix(string prefix, string key) public static bool StartsWithPrefix(string prefix, string key)
{ {
@ -698,7 +731,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
return false; return false;
} }
if (key.Length == prefix.Length) if (key.Length == prefix.Length)
{ {
// Exact match // Exact match
@ -724,6 +757,126 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new PrefixEnumerable(this, prefix); return new PrefixEnumerable(this, prefix);
} }
[DebuggerDisplay("SubKey={SubKey}, Key={Key}, ValidationState={ValidationState}")]
private class ModelStateNode : ModelStateEntry
{
private bool _isContainerNode = true;
public ModelStateNode(StringSegment subKey)
{
SubKey = subKey;
}
public List<ModelStateNode> ChildNodes { get; set; }
public override IReadOnlyList<ModelStateEntry> 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<ModelStateNode>(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<KeyValuePair<string, ModelStateEntry>> public struct PrefixEnumerable : IEnumerable<KeyValuePair<string, ModelStateEntry>>
{ {
private readonly ModelStateDictionary _dictionary; private readonly ModelStateDictionary _dictionary;
@ -745,32 +898,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
_prefix = prefix; _prefix = prefix;
} }
public PrefixEnumerator GetEnumerator() public Enumerator GetEnumerator() => new Enumerator(_dictionary, _prefix);
{
return _dictionary == null ? new PrefixEnumerator() : new PrefixEnumerator(_dictionary, _prefix);
}
IEnumerator<KeyValuePair<string, ModelStateEntry>> IEnumerator<KeyValuePair<string, ModelStateEntry>>
IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator() IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator() => GetEnumerator();
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
{
return GetEnumerator();
}
} }
public struct PrefixEnumerator : IEnumerator<KeyValuePair<string, ModelStateEntry>> public struct Enumerator : IEnumerator<KeyValuePair<string, ModelStateEntry>>
{ {
private readonly ModelStateDictionary _dictionary; private readonly ModelStateNode _rootNode;
private string _prefix; private ModelStateNode _modelStateNode;
private List<ModelStateNode> _nodes;
private int _index;
private bool _visitedRoot;
private ExactMatchState _exactMatch; public Enumerator(ModelStateDictionary dictionary, string prefix)
private Dictionary<string, ModelStateEntry>.Enumerator _enumerator;
public PrefixEnumerator(ModelStateDictionary dictionary, string prefix)
{ {
if (dictionary == null) if (dictionary == null)
{ {
@ -782,23 +926,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(prefix)); throw new ArgumentNullException(nameof(prefix));
} }
_dictionary = dictionary; _index = -1;
_prefix = prefix; _rootNode = dictionary.GetNode(prefix);
_modelStateNode = null;
_exactMatch = ExactMatchState.NotChecked; _nodes = null;
_enumerator = default(Dictionary<string, ModelStateEntry>.Enumerator); _visitedRoot = false;
Current = default(KeyValuePair<string, ModelStateEntry>);
} }
public KeyValuePair<string, ModelStateEntry> Current { get; private set; } public KeyValuePair<string, ModelStateEntry> Current =>
new KeyValuePair<string, ModelStateEntry>(_modelStateNode.Key, _modelStateNode);
object IEnumerator.Current object IEnumerator.Current => Current;
{
get
{
return Current;
}
}
public void Dispose() public void Dispose()
{ {
@ -806,61 +944,56 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public bool MoveNext() public bool MoveNext()
{ {
if (_dictionary == null) if (_rootNode == null)
{ {
return false; return false;
} }
// ModelStateDictionary has a behavior where the first 'match' returned from iterating if (!_visitedRoot)
// 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)
{ {
_enumerator = _dictionary._innerDictionary.GetEnumerator(); // Visit the root node
_visitedRoot = true;
ModelStateEntry entry; if (_rootNode.ChildNodes?.Count > 0)
if (_dictionary.TryGetValue(_prefix, out entry))
{ {
// Mark exact match as found _nodes = new List<ModelStateNode> { _rootNode };
_exactMatch = ExactMatchState.Found;
Current = new KeyValuePair<string, ModelStateEntry>(_prefix, entry);
return true;
} }
else
if (!_rootNode.IsContainerNode)
{ {
// Mark exact match tested for _modelStateNode = _rootNode;
_exactMatch = ExactMatchState.NotFound; return true;
} }
} }
while (_enumerator.MoveNext()) if (_nodes == null)
{ {
var key = _enumerator.Current.Key; return false;
if (_exactMatch == ExactMatchState.NotFound) }
while (_nodes.Count > 0)
{
var node = _nodes[0];
if (_index == node.ChildNodes.Count - 1)
{ {
if (StartsWithPrefix(_prefix, key)) // We've exhausted the current sublist.
{ _nodes.RemoveAt(0);
Current = _enumerator.Current; _index = -1;
return true;
}
continue; 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 _nodes.Add(currentChild);
_prefix = key;
// Mark exact reference set
_exactMatch = ExactMatchState.ReferenceSet;
// Skip this one. We've already handled the 'exact match' case.
} }
else if (StartsWithPrefix(_prefix, key))
if (!currentChild.IsContainerNode)
{ {
Current = _enumerator.Current; _modelStateNode = currentChild;
return true; return true;
} }
} }
@ -870,17 +1003,120 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void Reset() public void Reset()
{ {
_exactMatch = ExactMatchState.NotChecked; _index = -1;
_enumerator = default(Dictionary<string, ModelStateEntry>.Enumerator); _nodes.Clear();
Current = default(KeyValuePair<string, ModelStateEntry>); _visitedRoot = false;
_modelStateNode = null;
}
}
public struct KeyEnumerable : IEnumerable<string>
{
private readonly ModelStateDictionary _dictionary;
public KeyEnumerable(ModelStateDictionary dictionary)
{
_dictionary = dictionary;
} }
private enum ExactMatchState public KeyEnumerator GetEnumerator() => new KeyEnumerator(_dictionary, prefix: string.Empty);
IEnumerator<string> IEnumerable<string>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public struct KeyEnumerator : IEnumerator<string>
{
private Enumerator _prefixEnumerator;
public KeyEnumerator(ModelStateDictionary dictionary, string prefix)
{ {
NotChecked, _prefixEnumerator = new Enumerator(dictionary, prefix);
NotFound, Current = null;
Found, }
ReferenceSet
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<ModelStateEntry>
{
private readonly ModelStateDictionary _dictionary;
public ValueEnumerable(ModelStateDictionary dictionary)
{
_dictionary = dictionary;
}
public ValueEnumerator GetEnumerator() => new ValueEnumerator(_dictionary, prefix: string.Empty);
IEnumerator<ModelStateEntry> IEnumerable<ModelStateEntry>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public struct ValueEnumerator : IEnumerator<ModelStateEntry>
{
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;
} }
} }
} }

View File

@ -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
{
/// <summary>
/// An entry in a <see cref="ModelStateDictionary"/>.
/// </summary>
public abstract class ModelStateEntry
{
/// <summary>
/// Gets the raw value from the request associated with this entry.
/// </summary>
public object RawValue { get; set; }
/// <summary>
/// Gets the set of values contained in <see cref="RawValue"/>, joined into a comma-separated string.
/// </summary>
public string AttemptedValue { get; set; }
/// <summary>
/// Gets the <see cref="ModelErrorCollection"/> for this entry.
/// </summary>
public ModelErrorCollection Errors { get; } = new ModelErrorCollection();
/// <summary>
/// Gets or sets the <see cref="ModelValidationState"/> for this entry.
/// </summary>
public ModelValidationState ValidationState { get; set; }
/// <summary>
/// Gets a value that determines if the current instance of <see cref="ModelStateEntry"/> is a container node.
/// Container nodes represent prefix nodes that aren't explicitly added to the
/// <see cref="ModelStateDictionary"/>.
/// </summary>
public abstract bool IsContainerNode { get; }
/// <summary>
/// Gets the <see cref="ModelStateEntry"/> for a sub-property with the specified <paramref name="propertyName"/>.
/// </summary>
/// <param name="propertyName">The property name to lookup.</param>
/// <returns>The <see cref="ModelStateEntry"/> if a sub-property was found; otherwise <c>null</c>.</returns>
/// <remarks>This method returns any existing entry, even those with <see cref="IsContainerNode"/> with value <c>true</c>..</remarks>
public abstract ModelStateEntry GetModelStateForProperty(string propertyName);
/// <summary>
/// Gets the <see cref="ModelStateEntry"/> values for sub-properties.
/// </summary>
/// <remarks>This method returns all existing entries, even those with <see cref="IsContainerNode"/> with value <c>true</c>.</remarks>
public abstract IReadOnlyList<ModelStateEntry> Children { get; }
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -11,6 +10,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
public static class ValidationHelpers public static class ValidationHelpers
{ {
private static readonly ModelStateEntry[] EmptyModelStateEntries = new ModelStateEntry[0];
public static string GetModelErrorMessageOrDefault(ModelError modelError) public static string GetModelErrorMessageOrDefault(ModelError modelError)
{ {
Debug.Assert(modelError != null); 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. // Returns non-null list of model states, which caller will render in order provided.
public static IEnumerable<ModelStateEntry> GetModelStateList( public static IList<ModelStateEntry> GetModelStateList(
ViewDataDictionary viewData, ViewDataDictionary viewData,
bool excludePropertyErrors) bool excludePropertyErrors)
{ {
@ -57,49 +58,59 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
return new[] { ms }; return new[] { ms };
} }
return Enumerable.Empty<ModelStateEntry>();
} }
else else if (viewData.ModelState.Count > 0)
{ {
var metadata = viewData.ModelMetadata; var metadata = viewData.ModelMetadata;
var orderer = new ErrorsOrderer(metadata); var modelStateDictionary = viewData.ModelState;
var entries = new List<ModelStateEntry>();
Visit(modelStateDictionary, modelStateDictionary.Root, metadata, entries);
return viewData.ModelState if (entries.Count < modelStateDictionary.Count)
.OrderBy(data => orderer.GetOrder(data.Key)) {
.Select(ms => ms.Value); // 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. private static void Visit(
// ModelState doesn't refer to ModelMetadata, but we can correlate via the property name. ModelStateDictionary dictionary,
private class ErrorsOrderer ModelStateEntry modelStateEntry,
ModelMetadata metadata,
List<ModelStateEntry> orderedModelStateEntries)
{ {
private readonly Dictionary<string, int> _ordering = if (metadata.ElementMetadata != null)
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
public ErrorsOrderer(ModelMetadata metadata)
{ {
if (metadata == null) foreach (var indexEntry in modelStateEntry.Children)
{ {
throw new ArgumentNullException(nameof(metadata)); Visit(dictionary, indexEntry, metadata.ElementMetadata, orderedModelStateEntries);
}
foreach (var data in metadata.Properties)
{
_ordering[data.PropertyName] = data.Order;
} }
} }
public int GetOrder(string key) for (var i = 0; i < metadata.Properties.Count; i++)
{ {
int value; var propertyMetadata = metadata.Properties[i];
if (_ordering.TryGetValue(key, out value)) 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);
} }
} }
} }

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic; using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Xunit; using Xunit;
@ -12,20 +12,139 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public class ModelStateDictionaryTest public class ModelStateDictionaryTest
{ {
[Theory] [Theory]
[InlineData(ModelValidationState.Valid)] [InlineData("")]
[InlineData(ModelValidationState.Unvalidated)] [InlineData("foo")]
public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsNotInValid(ModelValidationState validationState) public void ContainsKey_ReturnsFalse_IfNodeHasNotBeenMutated(string key)
{ {
// Arrange // Arrange
var entry = new ModelStateEntry var dictionary = new ModelStateDictionary();
{ dictionary.AddModelError("foo.bar", "some error");
ValidationState = validationState
};
var source = new ModelStateDictionary // Act
{ var result = dictionary.ContainsKey(key);
{ "key", entry }
}; // 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 // Act
source.MarkFieldSkipped("key"); source.MarkFieldSkipped("key");
@ -38,11 +157,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MarkFieldSkipped_MarksFieldAsSkipped_IfKeyIsNotPresent() public void MarkFieldSkipped_MarksFieldAsSkipped_IfKeyIsNotPresent()
{ {
// Arrange // Arrange
var entry = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
var source = new ModelStateDictionary(); var source = new ModelStateDictionary();
// Act // Act
@ -58,15 +172,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MarkFieldSkipped_Throws_IfStateIsInvalid() public void MarkFieldSkipped_Throws_IfStateIsInvalid()
{ {
// Arrange // Arrange
var entry = new ModelStateEntry var source = new ModelStateDictionary();
{ source.AddModelError("key", "some error");
ValidationState = ModelValidationState.Invalid
};
var source = new ModelStateDictionary
{
{ "key", entry }
};
// Act // Act
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped("key")); var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped("key"));
@ -77,21 +184,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
exception.Message); exception.Message);
} }
[Theory] [Fact]
[InlineData(ModelValidationState.Skipped)] public void MarkFieldValid_MarksFieldAsValid_IfStateIsUnvalidated()
[InlineData(ModelValidationState.Unvalidated)]
public void MarkFieldValid_MarksFieldAsValid_IfStateIsNotInvalid(ModelValidationState validationState)
{ {
// Arrange // Arrange
var entry = new ModelStateEntry var source = new ModelStateDictionary();
{ source.SetModelValue("key", "value", "value");
ValidationState = validationState
};
var source = new ModelStateDictionary // Act
{ source.MarkFieldValid("key");
{ "key", entry }
}; // Assert
Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
}
[Fact]
public void MarkFieldValid_MarksFieldAsValid_IfStateIsSkipped()
{
// Arrange
var source = new ModelStateDictionary();
source.MarkFieldSkipped("key");
// Act // Act
source.MarkFieldValid("key"); source.MarkFieldValid("key");
@ -119,15 +231,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MarkFieldValid_Throws_IfStateIsInvalid() public void MarkFieldValid_Throws_IfStateIsInvalid()
{ {
// Arrange // Arrange
var entry = new ModelStateEntry var source = new ModelStateDictionary();
{ source.AddModelError("key", "some-error");
ValidationState = ModelValidationState.Invalid
};
var source = new ModelStateDictionary
{
{ "key", entry }
};
// Act // Act
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid("key")); var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid("key"));
@ -142,20 +247,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CopyConstructor_CopiesModelStateData() public void CopyConstructor_CopiesModelStateData()
{ {
// Arrange // Arrange
var entry = new ModelStateEntry(); var source = new ModelStateDictionary();
var source = new ModelStateDictionary source.SetModelValue("key", "attempted-value", "raw-value");
{ var entry = source["key"];
{ "key", entry } 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 // Act
var target = new ModelStateDictionary(source); var target = new ModelStateDictionary(source);
// Assert // Assert
Assert.Equal(0, target.ErrorCount); Assert.Equal(2, target.ErrorCount);
Assert.Equal(1, target.Count); Assert.Equal(1, target.Count);
Assert.Same(entry, target["key"]); var actual = target["key"];
Assert.IsType<Dictionary<string, ModelStateEntry>>(target.InnerDictionary); 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] [Fact]
@ -202,10 +312,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void ConstructorWithDictionaryParameter() public void ConstructorWithDictionaryParameter()
{ {
// Arrange // Arrange
var oldDictionary = new ModelStateDictionary() var oldDictionary = new ModelStateDictionary();
{ oldDictionary.SetModelValue("foo", "bar", "bar");
{ "foo", new ModelStateEntry() { RawValue = "bar" } }
};
// Act // Act
var newDictionary = new ModelStateDictionary(oldDictionary); var newDictionary = new ModelStateDictionary(oldDictionary);
@ -266,14 +374,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key) public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key)
{ {
// Arrange // Arrange
var validState = new ModelStateEntry var dictionary = new ModelStateDictionary();
{ dictionary.MarkFieldValid(key);
ValidationState = ModelValidationState.Valid
};
var dictionary = new ModelStateDictionary
{
{ key, validState }
};
// Act // Act
var validationState = dictionary.GetFieldValidationState("foo"); var validationState = dictionary.GetFieldValidationState("foo");
@ -304,14 +406,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void GetFieldValidationState_IndexedPrefix_ReturnsValidIfModelStateDoesNotContainErrors(string key) public void GetFieldValidationState_IndexedPrefix_ReturnsValidIfModelStateDoesNotContainErrors(string key)
{ {
// Arrange // Arrange
var validState = new ModelStateEntry var dictionary = new ModelStateDictionary();
{ dictionary.MarkFieldValid(key);
ValidationState = ModelValidationState.Valid
};
var dictionary = new ModelStateDictionary
{
{ key, validState }
};
// Act // Act
var validationState = dictionary.GetFieldValidationState("[0].foo"); var validationState = dictionary.GetFieldValidationState("[0].foo");
@ -324,20 +420,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void IsValidPropertyReturnsFalseIfErrors() public void IsValidPropertyReturnsFalseIfErrors()
{ {
// Arrange // Arrange
var errorState = new ModelStateEntry var dictionary = new ModelStateDictionary();
{ dictionary.MarkFieldValid("foo");
ValidationState = ModelValidationState.Invalid dictionary.AddModelError("bar", "some error");
};
var validState = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
errorState.Errors.Add("some error");
var dictionary = new ModelStateDictionary()
{
{ "foo", validState },
{ "baz", errorState }
};
// Act // Act
var isValid = dictionary.IsValid; var isValid = dictionary.IsValid;
@ -352,19 +437,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void IsValidPropertyReturnsTrueIfNoErrors() public void IsValidPropertyReturnsTrueIfNoErrors()
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary() var dictionary = new ModelStateDictionary();
{ dictionary.MarkFieldValid("foo");
{ "foo", new ModelStateEntry dictionary.MarkFieldSkipped("bar");
{
ValidationState = ModelValidationState.Valid,
}
},
{ "baz", new ModelStateEntry
{
ValidationState = ModelValidationState.Skipped,
}
}
};
// Act // Act
var isValid = dictionary.IsValid; var isValid = dictionary.IsValid;
@ -379,21 +454,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void IsValidPropertyReturnsFalse_IfSomeFieldsAreNotValidated() public void IsValidPropertyReturnsFalse_IfSomeFieldsAreNotValidated()
{ {
// Arrange // Arrange
var errorState = new ModelStateEntry var dictionary = new ModelStateDictionary();
{ dictionary.MarkFieldValid("foo");
ValidationState = ModelValidationState.Invalid dictionary.SetModelValue("qux", "value", "value");
}; dictionary.AddModelError("baz", "some error");
var validState = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
errorState.Errors.Add("some error");
var dictionary = new ModelStateDictionary()
{
{ "foo", validState },
{ "baz", errorState },
{ "qux", new ModelStateEntry() }
};
// Act // Act
var isValid = dictionary.IsValid; var isValid = dictionary.IsValid;
@ -408,22 +472,70 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MergeCopiesDictionaryEntries() public void MergeCopiesDictionaryEntries()
{ {
// Arrange // Arrange
var dictionary1 = new ModelStateDictionary { { "foo", new ModelStateEntry() } }; var dictionary1 = new ModelStateDictionary();
var dictionary2 = new ModelStateDictionary { { "bar", new ModelStateEntry() } }; 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 // Act
dictionary1.Merge(dictionary2); dictionary1.Merge(dictionary2);
// Assert // Assert
Assert.Equal(2, dictionary1.Count); 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] [Fact]
public void MergeDoesNothingIfParameterIsNull() public void MergeDoesNothingIfParameterIsNull()
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary() { { "foo", new ModelStateEntry() } }; var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("foo", "value", "value");
// Act // Act
dictionary.Merge(null); dictionary.Merge(null);
@ -491,7 +603,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); 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.SetModelValue("user.Name", new string[] { "some value" }, "some value");
dictionary.AddModelError("user.Age", "Age is not a valid int"); dictionary.AddModelError("user.Age", "Age is not a valid int");
@ -510,8 +622,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("user.Address");
dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("user.Name");
dictionary.AddModelError("user.Age", "Age is not a valid int"); dictionary.AddModelError("user.Age", "Age is not a valid int");
// Act // Act
@ -529,9 +641,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("[0].product.Name");
dictionary["[0].product.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("[0].product.Age[0]");
dictionary["[0].product.Age[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.AddModelError("[0].product.Name", "Name is invalid"); dictionary.AddModelError("[0].product.Name", "Name is invalid");
// Act // Act
@ -546,8 +657,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("user.Address");
dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("user.Name");
// Act // Act
var validationState = dictionary.GetFieldValidationState("user"); var validationState = dictionary.GetFieldValidationState("user");
@ -860,16 +971,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Property1");
dictionary["Property1"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Property2"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Property2", "Property2 invalid."); dictionary.AddModelError("Property2", "Property2 invalid.");
dictionary["Property3"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Property3", "Property invalid."); dictionary.AddModelError("Property3", "Property invalid.");
dictionary.MarkFieldSkipped("Property4");
dictionary["Property4"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
// Act // Act
dictionary.ClearValidationState("Property1"); dictionary.ClearValidationState("Property1");
@ -892,23 +997,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Product");
dictionary["Product"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Product.Detail1"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid."); 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.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.AddModelError("Product.Detail2[1]", "Product Detail2[1] invalid.");
dictionary.MarkFieldSkipped("Product.Detail2[2]");
dictionary["Product.Detail2[2]"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped }; dictionary.MarkFieldSkipped("Product.Detail3");
dictionary["Product.Detail3"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
dictionary["ProductName"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("ProductName", "ProductName invalid."); dictionary.AddModelError("ProductName", "ProductName invalid.");
// Act // Act
@ -936,16 +1030,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Product");
dictionary["Product"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Product.Detail1"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid."); 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.AddModelError("Product.Detail1.Name", "Product Detail1 Name invalid.");
dictionary.MarkFieldSkipped("Product.Detail1Name");
dictionary["Product.Detail1Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
// Act // Act
dictionary.ClearValidationState("Product.Detail1"); dictionary.ClearValidationState("Product.Detail1");
@ -966,16 +1054,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Property1");
dictionary["Property1"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Property2"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Property2", "Property2 invalid."); dictionary.AddModelError("Property2", "Property2 invalid.");
dictionary["Property3"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Property3", "Property invalid."); dictionary.AddModelError("Property3", "Property invalid.");
dictionary.MarkFieldSkipped("Property4");
dictionary["Property4"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
// Act // Act
dictionary.ClearValidationState(modelKey); dictionary.ClearValidationState(modelKey);
@ -990,5 +1072,220 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(0, dictionary["Property4"].Errors.Count); Assert.Equal(0, dictionary["Property4"].Errors.Count);
Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState); 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);
}
} }
} }

View File

@ -5,9 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
#if NETSTANDARDAPP1_5
using System.Reflection;
#endif
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
@ -749,9 +746,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var validator = CreateValidator(); var validator = CreateValidator();
modelState.Add("items[0]", new ModelStateEntry()); modelState.SetModelValue("items[0]", "value1", "value1");
modelState.Add("items[1]", new ModelStateEntry()); modelState.SetModelValue("items[1]", "value2", "value2");
modelState.Add("items[2]", new ModelStateEntry()); modelState.SetModelValue("items[2]", "value3", "value3");
validationState.Add(model, new ValidationStateEntry() validationState.Add(model, new ValidationStateEntry()
{ {
Key = "items", Key = "items",
@ -797,10 +794,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ "BarKey", "BarValue" } { "BarKey", "BarValue" }
}; };
modelState.Add("items[0].Key", new ModelStateEntry()); modelState.SetModelValue("items[0].Key", "key0", "key0");
modelState.Add("items[0].Value", new ModelStateEntry()); modelState.SetModelValue("items[0].Value", "value0", "value0");
modelState.Add("items[1].Key", new ModelStateEntry()); modelState.SetModelValue("items[1].Key", "key1", "key1");
modelState.Add("items[1].Value", new ModelStateEntry()); modelState.SetModelValue("items[1].Value", "value1", "value1");
validationState.Add(model, new ValidationStateEntry() { Key = "items" }); validationState.Add(model, new ValidationStateEntry() { Key = "items" });
// Act // Act

View File

@ -691,14 +691,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelMetadata = metadataProvider.GetMetadataForType(typeof(Product)); var modelMetadata = metadataProvider.GetMetadataForType(typeof(Product));
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary["Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Name", "MyProperty invalid."); dictionary.AddModelError("Name", "MyProperty invalid.");
dictionary["Id"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Id", "Id invalid."); dictionary.AddModelError("Id", "Id invalid.");
dictionary.AddModelError("Id", "Id is required."); dictionary.AddModelError("Id", "Id is required.");
dictionary["Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("Category");
dictionary["Unrelated"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Unrelated", "Unrelated is required."); dictionary.AddModelError("Unrelated", "Unrelated is required.");
// Act // Act
@ -727,10 +723,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelMetadata = metadataProvider.GetMetadataForType(typeof(string)); var modelMetadata = metadataProvider.GetMetadataForType(typeof(string));
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary[string.Empty] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError(string.Empty, "MyProperty invalid."); dictionary.AddModelError(string.Empty, "MyProperty invalid.");
dictionary["Unrelated"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Unrelated", "Unrelated is required."); dictionary.AddModelError("Unrelated", "Unrelated is required.");
// Act // Act
@ -754,19 +747,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelMetadata = metadataProvider.GetMetadataForType(typeof(List<Product>)); var modelMetadata = metadataProvider.GetMetadataForType(typeof(List<Product>));
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary["[0].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("[0].Name", "Name 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 invalid.");
dictionary.AddModelError("[0].Id", "Id required."); 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.MarkFieldValid("[1].Name");
dictionary["[1].Id"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("[1].Id");
dictionary["[1].Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("[1].Category", "Category invalid."); dictionary.AddModelError("[1].Category", "Category invalid.");
dictionary["Unrelated"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Unrelated", "Unrelated is required."); dictionary.AddModelError("Unrelated", "Unrelated is required.");
// Act // Act
@ -804,20 +792,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelMetadata = metadataProvider.GetMetadataForType(typeof(Product)); var modelMetadata = metadataProvider.GetMetadataForType(typeof(Product));
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary["product.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("product.Name", "Name 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 invalid.");
dictionary.AddModelError("product.Id", "Id required."); dictionary.AddModelError("product.Id", "Id required.");
dictionary["product.Category"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("product.Category");
dictionary["product.Category.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("product.Category.Name");
dictionary["product.Order[0].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("product.Order[0].Name", "Order name invalid."); 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.AddModelError("product.Order[0].Address.Street", "Street invalid.");
dictionary["product.Order[1].Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid }; dictionary.MarkFieldValid("product.Order[1].Name");
dictionary["product.Order[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("product.Order[0]", "Order invalid."); dictionary.AddModelError("product.Order[0]", "Order invalid.");
// Act // Act

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 Microsoft.AspNetCore.Mvc.ModelBinding;
using Xunit; using Xunit;
@ -70,12 +69,8 @@ namespace Microsoft.AspNetCore.Mvc
{ {
// Arrange // Arrange
var modelState = new ModelStateDictionary(); var modelState = new ModelStateDictionary();
modelState.Add( modelState.SetModelValue("key1", "value1", "value1");
"key1", modelState.SetModelValue("key2", "value2", "value2");
new ModelStateEntry());
modelState.Add(
"key2",
new ModelStateEntry());
// Act // Act
var serializableError = new SerializableError(modelState); var serializableError = new SerializableError(modelState);

View File

@ -17,8 +17,8 @@
<hr /> <hr />
<div class="text-danger validation-summary-errors" data-valmsg-summary="true"> <div class="text-danger validation-summary-errors" data-valmsg-summary="true">
<ul><li>The field Age must be between 10 and 100.</li> <ul><li>The field Age must be between 10 and 100.</li>
<li>The value &#x27;z&#x27; is not valid for Salary.</li>
<li>The JoinDate field is required.</li> <li>The JoinDate field is required.</li>
<li>The value &#x27;z&#x27; is not valid for Salary.</li>
</ul></div> </ul></div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="Age">Age</label> <label class="control-label col-md-2" for="Age">Age</label>

View File

@ -63,8 +63,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); var key = Assert.Single(modelState.Keys);
var key = Assert.Single(modelState.Keys, k => k == "Address[0].Street"); Assert.Equal("Address[0].Street", key);
Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].AttemptedValue);
Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Equal("SomeStreet", modelState[key].RawValue);
Assert.Empty(modelState[key].Errors); Assert.Empty(modelState[key].Errors);
@ -141,6 +141,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var modelState = operationContext.ActionContext.ModelState; var modelState = operationContext.ActionContext.ModelState;
var model = new Person4(); var model = new Person4();
// Act // Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult); var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult);
@ -157,8 +158,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); var key = Assert.Single(modelState.Keys);
var key = Assert.Single(modelState.Keys, k => k == "Address[0].Street"); Assert.Equal("Address[0].Street", key);
Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].AttemptedValue);
Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Equal("SomeStreet", modelState[key].RawValue);
Assert.Empty(modelState[key].Errors); Assert.Empty(modelState[key].Errors);
@ -243,8 +244,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); var key = Assert.Single(modelState.Keys);
var key = Assert.Single(modelState.Keys, k => k == "prefix.Address[0].Street"); Assert.Equal("prefix.Address[0].Street", key);
Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].AttemptedValue);
Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Equal("SomeStreet", modelState[key].RawValue);
Assert.Empty(modelState[key].Errors); Assert.Empty(modelState[key].Errors);
@ -334,8 +335,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); var key = Assert.Single(modelState.Keys);
var key = Assert.Single(modelState.Keys, k => k == "prefix.Address[0].Street"); Assert.Equal("prefix.Address[0].Street", key);
Assert.Equal("SomeStreet", modelState[key].AttemptedValue); Assert.Equal("SomeStreet", modelState[key].AttemptedValue);
Assert.Equal("SomeStreet", modelState[key].RawValue); Assert.Equal("SomeStreet", modelState[key].RawValue);
Assert.Empty(modelState[key].Errors); Assert.Empty(modelState[key].Errors);

View File

@ -234,9 +234,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
var key = Assert.Single(modelState.Keys);
Assert.Equal(1, modelState.Keys.Count); Assert.Equal("Address.Street", key);
var key = Assert.Single(modelState.Keys, k => k == "Address.Street");
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it. 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 // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); var key = Assert.Single(modelState.Keys);
var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Street"); Assert.Equal("CustomParameter.Address.Street", key);
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it. Assert.NotNull(modelState[key].RawValue); // Value is set by test model binder, no need to validate it.
} }

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
@ -590,9 +591,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.NotNull(boundPerson); Assert.NotNull(boundPerson);
Assert.False(modelState.IsValid); Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); var entry = Assert.Single(modelState);
Assert.Equal("CustomParameter.Address.Street", entry.Key);
var street = Assert.Single(modelState, kvp => kvp.Key == "CustomParameter.Address.Street").Value; var street = entry.Value;
Assert.Equal(ModelValidationState.Invalid, street.ValidationState); Assert.Equal(ModelValidationState.Invalid, street.ValidationState);
var error = Assert.Single(street.Errors); var error = Assert.Single(street.Errors);
// Mono issue - https://github.com/aspnet/External/issues/19 // Mono issue - https://github.com/aspnet/External/issues/19

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(0, modelState.Keys.Count); Assert.Empty(modelState.Keys);
} }
[Fact] [Fact]

View File

@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); 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"); var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Zip");
Assert.Equal("1", modelState[key].AttemptedValue); Assert.Equal("1", modelState[key].AttemptedValue);
Assert.Equal("1", modelState[key].RawValue); Assert.Equal("1", modelState[key].RawValue);
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); 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"); var key = Assert.Single(modelState.Keys, k => k == "Address.Zip");
Assert.Equal("1", modelState[key].AttemptedValue); Assert.Equal("1", modelState[key].AttemptedValue);
Assert.Equal("1", modelState[key].RawValue); Assert.Equal("1", modelState[key].RawValue);
@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); Assert.Equal(1, modelState.Keys.Count());
var key = Assert.Single(modelState.Keys); var key = Assert.Single(modelState.Keys);
Assert.Equal("Parameter1", key); Assert.Equal("Parameter1", key);
Assert.Equal("someValue", modelState[key].AttemptedValue); Assert.Equal("someValue", modelState[key].AttemptedValue);
@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Equal(1, modelState.Keys.Count); Assert.Equal(1, modelState.Keys.Count());
var key = Assert.Single(modelState.Keys); var key = Assert.Single(modelState.Keys);
Assert.Equal("Parameter1", key); Assert.Equal("Parameter1", key);
Assert.Equal("someValue,otherValue", modelState[key].AttemptedValue); Assert.Equal("someValue,otherValue", modelState[key].AttemptedValue);
@ -512,7 +512,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var modelState = operationContext.ActionContext.ModelState; var modelState = operationContext.ActionContext.ModelState;
// Act // Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult); var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert // Assert
// ModelBindingResult // ModelBindingResult
@ -529,7 +530,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// ModelState // ModelState
Assert.True(modelState.IsValid); 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"]; var entry = modelState["Address.Lines"];
Assert.NotNull(entry); Assert.NotNull(entry);
Assert.Empty(entry.Errors); Assert.Empty(entry.Errors);

View File

@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.Add("Text", new ModelStateEntry()); dictionary.SetModelValue("Text", "value", "value");
// Act // Act
dictionary.Remove<TestModel>(model => model.Text); dictionary.Remove<TestModel>(model => model.Text);
@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.Add("Child.Text", new ModelStateEntry()); dictionary.SetModelValue("Child.Text", "value", "value");
// Act // Act
dictionary.Remove<TestModel>(model => model.Child.Text); dictionary.Remove<TestModel>(model => model.Child.Text);
@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.Add("Child.Value", new ModelStateEntry()); dictionary.SetModelValue("Child.Value", "value", "value");
// Act // Act
dictionary.Remove<TestModel>(model => model.Child.Value); dictionary.Remove<TestModel>(model => model.Child.Value);
@ -206,7 +206,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Arrange // Arrange
var variable = "Test"; var variable = "Test";
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.Add("variable", new ModelStateEntry()); dictionary.SetModelValue("variable", "value", "value");
// Act // Act
dictionary.Remove<TestModel>(model => variable); dictionary.Remove<TestModel>(model => variable);
@ -219,12 +219,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void RemoveAll_ForSingleExpression_RemovesModelStateKeys() public void RemoveAll_ForSingleExpression_RemovesModelStateKeys()
{ {
// Arrange // Arrange
var state = new ModelStateEntry();
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.Add("Key", state); dictionary.SetModelValue("Key", "value1", "value1");
dictionary.Add("Text", new ModelStateEntry()); dictionary.SetModelValue("Text", "value2", "value2");
dictionary.Add("Text.Length", new ModelStateEntry()); dictionary.SetModelValue("Text.Length", "value3", "value3");
var expected = dictionary["Key"];
// Act // Act
dictionary.RemoveAll<TestModel>(model => model.Text); dictionary.RemoveAll<TestModel>(model => model.Text);
@ -233,19 +233,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = Assert.Single(dictionary); var modelState = Assert.Single(dictionary);
Assert.Equal("Key", modelState.Key); Assert.Equal("Key", modelState.Key);
Assert.Same(state, modelState.Value); Assert.Same(expected, modelState.Value);
} }
[Fact] [Fact]
public void RemoveAll_ForRelationExpression_RemovesModelStateKeys() public void RemoveAll_ForRelationExpression_RemovesModelStateKeys()
{ {
// Arrange // Arrange
var state = new ModelStateEntry();
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("Key", "value1", "value1");
dictionary.Add("Key", state); dictionary.SetModelValue("Child", "value2", "value2");
dictionary.Add("Child", new ModelStateEntry()); dictionary.SetModelValue("Child.Text", "value3", "value3");
dictionary.Add("Child.Text", new ModelStateEntry()); var expected = dictionary["Key"];
// Act // Act
dictionary.RemoveAll<TestModel>(model => model.Child); dictionary.RemoveAll<TestModel>(model => model.Child);
@ -254,18 +253,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = Assert.Single(dictionary); var modelState = Assert.Single(dictionary);
Assert.Equal("Key", modelState.Key); Assert.Equal("Key", modelState.Key);
Assert.Same(state, modelState.Value); Assert.Same(expected, modelState.Value);
} }
[Fact] [Fact]
public void RemoveAll_ForImplicitlyCastedToObjectExpression_RemovesModelStateKeys() public void RemoveAll_ForImplicitlyCastedToObjectExpression_RemovesModelStateKeys()
{ {
// Arrange // Arrange
var state = new ModelStateEntry();
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("Child", "value1", "value1");
dictionary.Add("Child", state); dictionary.SetModelValue("Child.Value", "value2", "value2");
dictionary.Add("Child.Value", new ModelStateEntry()); var expected = dictionary["child"];
// Act // Act
dictionary.RemoveAll<TestModel>(model => model.Child.Value); dictionary.RemoveAll<TestModel>(model => model.Child.Value);
@ -274,7 +272,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = Assert.Single(dictionary); var modelState = Assert.Single(dictionary);
Assert.Equal("Child", modelState.Key); Assert.Equal("Child", modelState.Key);
Assert.Same(state, modelState.Value); Assert.Same(expected, modelState.Value);
} }
[Fact] [Fact]
@ -282,13 +280,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
// Arrange // Arrange
var variable = "Test"; var variable = "Test";
var state = new ModelStateEntry();
var dictionary = new ModelStateDictionary(); 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); var expected = dictionary["Key"];
dictionary.Add("variable", new ModelStateEntry());
dictionary.Add("variable.Text", new ModelStateEntry());
dictionary.Add("variable.Value", new ModelStateEntry());
// Act // Act
dictionary.RemoveAll<TestModel>(model => variable); dictionary.RemoveAll<TestModel>(model => variable);
@ -297,21 +295,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = Assert.Single(dictionary); var modelState = Assert.Single(dictionary);
Assert.Equal("Key", modelState.Key); Assert.Equal("Key", modelState.Key);
Assert.Same(state, modelState.Value); Assert.Same(expected, modelState.Value);
} }
[Fact] [Fact]
public void RemoveAll_ForModelExpression_RemovesModelPropertyKeys() public void RemoveAll_ForModelExpression_RemovesModelPropertyKeys()
{ {
// Arrange // Arrange
var state = new ModelStateEntry();
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("Key", "value1", "value1");
dictionary.Add("Key", state); dictionary.SetModelValue("Text", "value2", "value2");
dictionary.Add("Text", new ModelStateEntry()); dictionary.SetModelValue("Child", "value3", "value3");
dictionary.Add("Child", new ModelStateEntry()); dictionary.SetModelValue("Child.Text", "value4", "value4");
dictionary.Add("Child.Text", new ModelStateEntry()); dictionary.SetModelValue("Child.NoValue", "value5", "value5");
dictionary.Add("Child.NoValue", new ModelStateEntry()); var expected = dictionary["Key"];
// Act // Act
dictionary.RemoveAll<TestModel>(model => model); dictionary.RemoveAll<TestModel>(model => model);
@ -320,7 +317,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var modelState = Assert.Single(dictionary); var modelState = Assert.Single(dictionary);
Assert.Equal("Key", modelState.Key); Assert.Equal("Key", modelState.Key);
Assert.Same(state, modelState.Value); Assert.Same(expected, modelState.Value);
} }
private class TestModel private class TestModel

View File

@ -259,10 +259,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
viewData["FieldPrefix.Name"] = "View data dictionary value"; viewData["FieldPrefix.Name"] = "View data dictionary value";
viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
var modelState = new ModelStateEntry(); viewData.ModelState.SetModelValue(
modelState.RawValue = new string[] { "Attempted name value" }; "FieldPrefix.Name",
modelState.AttemptedValue = "Attempted name value"; "Attempted name value",
viewData.ModelState["FieldPrefix.Name"] = modelState; "Attempted name value");
// Act // Act
var result = helper.DisplayText("Name"); var result = helper.DisplayText("Name");
@ -284,10 +284,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
viewData["Name"] = "View data dictionary value"; viewData["Name"] = "View data dictionary value";
viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
var modelState = new ModelStateEntry(); viewData.ModelState.SetModelValue(
modelState.RawValue = new string[] { "Attempted name value" }; "FieldPrefix.Name",
modelState.AttemptedValue = "Attempted name value"; "Attempted name value",
viewData.ModelState["FieldPrefix.Name"] = modelState; "Attempted name value");
// Act // Act
var result = helper.DisplayTextFor(m => m.Name); var result = helper.DisplayTextFor(m => m.Name);

View File

@ -361,9 +361,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
idAttributeDotReplacement: "$"); idAttributeDotReplacement: "$");
helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix"; helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
helper.ViewData.ModelState.Clear(); helper.ViewData.ModelState.Clear();
helper.ViewData.ModelState.Add("Property1", GetModelStateEntry("modelstate-without-prefix")); helper.ViewData.ModelState.SetModelValue(
helper.ViewData.ModelState.Add("MyPrefix.Property1", GetModelStateEntry("modelstate-with-prefix")); "Property1",
helper.ViewData.ModelState.Add("MyPrefix$Property1", GetModelStateEntry("modelstate-with-iddotreplacement")); "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 // Act
var result = helper.Hidden("Property1", "explicit-value", htmlAttributes: null); var result = helper.Hidden("Property1", "explicit-value", htmlAttributes: null);
@ -411,7 +420,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Act and Assert // Act and Assert
ExceptionAssert.ThrowsArgument( ExceptionAssert.ThrowsArgument(
() => helper.Hidden(string.Empty, string.Empty, attributes), () => helper.Hidden(string.Empty, string.Empty, attributes),
"expression", "expression",
expected); expected);
} }
@ -660,9 +669,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
helper.ViewData.Model.Property1 = "propValue"; helper.ViewData.Model.Property1 = "propValue";
helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix"; helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
helper.ViewData.ModelState.Clear(); helper.ViewData.ModelState.Clear();
helper.ViewData.ModelState.Add("Property1", GetModelStateEntry("modelstate-without-prefix")); helper.ViewData.ModelState.SetModelValue(
helper.ViewData.ModelState.Add("MyPrefix.Property1", GetModelStateEntry("modelstate-with-prefix")); "Property1",
helper.ViewData.ModelState.Add("MyPrefix$Property1", GetModelStateEntry("modelstate-with-iddotreplacement")); "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 // Act
var result = helper.HiddenFor(m => m.Property1, htmlAttributes: null); var result = helper.HiddenFor(m => m.Property1, htmlAttributes: null);
@ -802,9 +820,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{ {
// Arrange // Arrange
var viewData = GetViewDataWithNullModelAndNonNullViewData(); var viewData = GetViewDataWithNullModelAndNonNullViewData();
viewData.ModelState.Add("pre.Property3[key]", GetModelStateEntry("Prop3Val")); viewData.ModelState.SetModelValue("pre.Property3[key]", "Prop3Val", "Prop3Val");
viewData.ModelState.Add("pre.Property4.Property5", GetModelStateEntry("Prop5Val")); viewData.ModelState.SetModelValue("pre.Property4.Property5", "Prop5Val", "Prop5Val");
viewData.ModelState.Add("pre.Property4.Property6[0]", GetModelStateEntry("Prop6Val")); viewData.ModelState.SetModelValue("pre.Property4.Property6[0]", "Prop6Val", "Prop6Val");
var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData); var helper = DefaultTemplatesUtilities.GetHtmlHelper(viewData);
viewData.TemplateInfo.HtmlFieldPrefix = "pre"; viewData.TemplateInfo.HtmlFieldPrefix = "pre";
@ -911,7 +929,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{ {
var viewData = GetViewDataWithNonNullModel(); var viewData = GetViewDataWithNonNullModel();
viewData["Property1"] = "view-data-val"; viewData["Property1"] = "view-data-val";
viewData.ModelState.Add("Property1", GetModelStateEntry("ModelStateValue")); viewData.ModelState.SetModelValue("Property1", "ModelStateValue", "ModelStateValue");
return viewData; return viewData;
} }
@ -924,15 +942,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
return viewData; return viewData;
} }
private static ModelStateEntry GetModelStateEntry(string value)
{
return new ModelStateEntry
{
RawValue = new string[] { value },
AttemptedValue = value,
};
}
public class HiddenModel public class HiddenModel
{ {
public string Property1 { get; set; } public string Property1 { get; set; }

View File

@ -324,9 +324,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{ {
// Arrange // Arrange
var viewData = GetViewDataWithModelStateAndModelAndViewDataValues(); var viewData = GetViewDataWithModelStateAndModelAndViewDataValues();
viewData.ModelState.Add("pre.Property3[key]", GetModelStateEntry("Property3Val")); viewData.ModelState.SetModelValue("pre.Property3[key]", "Property3Val", "Property3Val");
viewData.ModelState.Add("pre.Property4.Property5", GetModelStateEntry("Property5Val")); viewData.ModelState.SetModelValue("pre.Property4.Property5", "Property5Val", "Property5Val");
viewData.ModelState.Add("pre.Property4.Property6[0]", GetModelStateEntry("Property6Val")); viewData.ModelState.SetModelValue("pre.Property4.Property6[0]", "Property6Val", "Property6Val");
viewData["pre.Property3[key]"] = "vdd-value1"; viewData["pre.Property3[key]"] = "vdd-value1";
viewData["pre.Property4.Property5"] = "vdd-value2"; viewData["pre.Property4.Property5"] = "vdd-value2";
viewData["pre.Property4.Property6[0]"] = "vdd-value3"; viewData["pre.Property4.Property6[0]"] = "vdd-value3";
@ -426,7 +426,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{ {
var viewData = GetViewDataWithNullModelAndNonEmptyViewData(); var viewData = GetViewDataWithNullModelAndNonEmptyViewData();
viewData.Model = new PasswordModel(); viewData.Model = new PasswordModel();
viewData.ModelState.Add("Property1", GetModelStateEntry("ModelStateValue")); viewData.ModelState.SetModelValue("Property1", "ModelStateValue", "ModelStateValue");
return viewData; return viewData;
} }
@ -439,15 +439,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
return viewData; return viewData;
} }
private static ModelStateEntry GetModelStateEntry(string value)
{
return new ModelStateEntry
{
RawValue = new string[] { value },
AttemptedValue = value,
};
}
public class PasswordModel public class PasswordModel
{ {
public string Property1 { get; set; } public string Property1 { get; set; }

View File

@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Act & Assert // Act & Assert
var ex = Assert.Throws<ArgumentException>( var ex = Assert.Throws<ArgumentException>(
"expression", "expression",
() => helper.DropDownList(null, selectList: null, optionLabel: null, htmlAttributes: null)); () => helper.DropDownList(null, selectList: null, optionLabel: null, htmlAttributes: null));
Assert.Equal(expected, ex.Message); Assert.Equal(expected, ex.Message);
} }
@ -412,19 +412,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Arrange // Arrange
var expectedHtml = GetExpectedSelectElement(SelectSources.ModelStateEntry, allowMultiple: false); var expectedHtml = GetExpectedSelectElement(SelectSources.ModelStateEntry, allowMultiple: false);
var modelState = new ModelStateDictionary var modelState = new ModelStateDictionary();
{ modelState.SetModelValue(
["Property1"] = new ModelStateEntry "Property1",
{ SelectSources.ModelStateEntry,
RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, SelectSources.ModelStateEntry.ToString());
AttemptedValue = SelectSources.ModelStateEntry.ToString() modelState.SetModelValue(
}, "Prefix.Property1",
["Prefix.Property1"] = new ModelStateEntry SelectSources.ModelStateEntryWithPrefix,
{ SelectSources.ModelStateEntryWithPrefix.ToString());
RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() },
AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString()
},
};
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var viewData = new ViewDataDictionary<ModelContainingSources>(provider, modelState) var viewData = new ViewDataDictionary<ModelContainingSources>(provider, modelState)
@ -453,19 +449,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
SelectSources.ModelStateEntryWithPrefix, SelectSources.ModelStateEntryWithPrefix,
allowMultiple: false); allowMultiple: false);
var modelState = new ModelStateDictionary var modelState = new ModelStateDictionary();
{ modelState.SetModelValue(
["Property1"] = new ModelStateEntry "Property1",
{ SelectSources.ModelStateEntry,
RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, SelectSources.ModelStateEntry.ToString());
AttemptedValue = SelectSources.ModelStateEntry.ToString() modelState.SetModelValue(
}, "Prefix.Property1",
["Prefix.Property1"] = new ModelStateEntry SelectSources.ModelStateEntryWithPrefix,
{ SelectSources.ModelStateEntryWithPrefix.ToString());
RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() },
AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString()
},
};
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var viewData = new ViewDataDictionary<ModelContainingSources>(provider, modelState) var viewData = new ViewDataDictionary<ModelContainingSources>(provider, modelState)
@ -823,19 +815,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Arrange // Arrange
var expectedHtml = GetExpectedSelectElement(SelectSources.ModelStateEntry, allowMultiple: true); var expectedHtml = GetExpectedSelectElement(SelectSources.ModelStateEntry, allowMultiple: true);
var modelState = new ModelStateDictionary var modelState = new ModelStateDictionary();
{ modelState.SetModelValue(
["Property1"] = new ModelStateEntry "Property1",
{ SelectSources.ModelStateEntry,
RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, SelectSources.ModelStateEntry.ToString());
AttemptedValue = SelectSources.ModelStateEntry.ToString() modelState.SetModelValue(
}, "Prefix.Property1",
["Prefix.Property1"] = new ModelStateEntry SelectSources.ModelStateEntryWithPrefix,
{ SelectSources.ModelStateEntryWithPrefix.ToString());
RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() },
AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString()
},
};
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var viewData = new ViewDataDictionary<ModelContainingListOfSources>(provider, modelState) var viewData = new ViewDataDictionary<ModelContainingListOfSources>(provider, modelState)
@ -864,19 +852,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
SelectSources.ModelStateEntryWithPrefix, SelectSources.ModelStateEntryWithPrefix,
allowMultiple: true); allowMultiple: true);
var modelState = new ModelStateDictionary var modelState = new ModelStateDictionary();
{ modelState.SetModelValue(
["Property1"] = new ModelStateEntry "Property1",
{ SelectSources.ModelStateEntry,
RawValue = new string[] { SelectSources.ModelStateEntry.ToString() }, SelectSources.ModelStateEntry.ToString());
AttemptedValue = SelectSources.ModelStateEntry.ToString() modelState.SetModelValue(
}, "Prefix.Property1",
["Prefix.Property1"] = new ModelStateEntry SelectSources.ModelStateEntryWithPrefix,
{ SelectSources.ModelStateEntryWithPrefix.ToString());
RawValue = new string[] { SelectSources.ModelStateEntryWithPrefix.ToString() },
AttemptedValue = SelectSources.ModelStateEntryWithPrefix.ToString()
},
};
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var viewData = new ViewDataDictionary<ModelContainingListOfSources>(provider, modelState) var viewData = new ViewDataDictionary<ModelContainingListOfSources>(provider, modelState)

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Mvc.TestCommon;
@ -109,13 +110,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
"<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine +
"</ul></div>"; "</ul></div>";
var divWithAllErrors = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" + var divWithAllErrors = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" +
"<li>HtmlEncode[[This is an error for Property3.Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property2.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for Property2.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is another error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[The value '' is not valid for Property2.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[The value '' is not valid for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for the model root.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for the model root.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for the model root.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is another error for the model root.]]</li>" + Environment.NewLine +
"</ul></div>"; "</ul></div>";
@ -302,6 +303,166 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result)); Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
} }
[Fact]
public void ValidationSummary_OrdersCorrectlyWhenElementsAreRemovedFromDictionary()
{
// Arrange
var expected = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" +
"<li>HtmlEncode[[New error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for the model root.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for the model root.]]</li>" + Environment.NewLine +
"</ul></div>";
var model = new ValidationModel();
var html = DefaultTemplatesUtilities.GetHtmlHelper(model);
AddMultipleErrors(html.ViewData.ModelState);
html.ViewData.ModelState.RemoveAll<ValidationModel>(m => m.Property2);
html.ViewData.ModelState.Remove<ValidationModel>(m => m.Property3);
html.ViewData.ModelState.Remove<ValidationModel>(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 = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" +
"<li>HtmlEncode[[This is an error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[The value '' is not valid for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.OrderedProperty2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for the model root.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for the model root.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[non-existent-error1]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[non-existent-error2]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[non-existent-error3]]</li>" + Environment.NewLine +
"</ul></div>";
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 = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" +
"<li>HtmlEncode[[Property1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[Property2[0].OrderedProperty1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[Property2[0].Property1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[Property2[2].Property3 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[Property2[10].Property2 error]]</li>" + Environment.NewLine +
"</ul></div>";
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 = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" +
"<li>HtmlEncode[[[0].OrderedProperty2 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[0].OrderedProperty1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[0].Property1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[2].OrderedProperty3 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[2].Property3 error]]</li>" + Environment.NewLine +
"</ul></div>";
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 = "<div class=\"HtmlEncode[[validation-summary-errors]]\" data-valmsg-summary=\"HtmlEncode[[true]]\"><ul>" +
"<li>HtmlEncode[[[0].OrderedProperty2 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[0].OrderedProperty1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[0].Property1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[2].OrderedProperty3 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[[2].Property3 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[OrderedProperty1 error]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[OrderedProperty2 error]]</li>" + Environment.NewLine +
"</ul></div>";
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] [Fact]
public void ValidationSummary_ErrorsInModelUsingOrder_SortsErrorsAsExpected() public void ValidationSummary_ErrorsInModelUsingOrder_SortsErrorsAsExpected()
{ {
@ -313,10 +474,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
"<li>HtmlEncode[[This is yet-another error for OrderedProperty2.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is yet-another error for OrderedProperty2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for OrderedProperty1.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for OrderedProperty1.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property1.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for Property1.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for Property1.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is another error for Property1.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is another error for Property2.]]</li>" + Environment.NewLine +
"<li>HtmlEncode[[This is an error for LastProperty.]]</li>" + Environment.NewLine + "<li>HtmlEncode[[This is an error for LastProperty.]]</li>" + Environment.NewLine +
"</ul></div>"; "</ul></div>";
@ -436,7 +597,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
helper.ViewData.ModelState.AddModelError("Property1", "Error for Property1"); helper.ViewData.ModelState.AddModelError("Property1", "Error for Property1");
// Act // 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
Assert.Equal( Assert.Equal(
@ -577,5 +738,21 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[Display(Order = 23)] [Display(Order = 23)]
public string OrderedProperty1 { get; set; } public string OrderedProperty1 { get; set; }
} }
private class ModelWithCollection
{
public string Property1 { get; set; }
public List<OrderedModel> Property2 { get; set; }
}
private class CollectionType : Collection<OrderedModel>
{
[Display(Order = 1)]
public string OrderedProperty2 { get; set; }
[Display(Order = 2)]
public string OrderedProperty1 { get; set; }
}
} }
} }

View File

@ -162,15 +162,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
viewData["StringProperty"] = "ViewDataValue"; viewData["StringProperty"] = "ViewDataValue";
viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix"; viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
var modelState = new ModelStateEntry(); viewData.ModelState.SetModelValue(
modelState.AttemptedValue = "StringPropertyAttemptedValue"; "FieldPrefix.StringProperty",
modelState.RawValue = new string[] { "StringPropertyRawValue" }; "StringPropertyRawValue",
viewData.ModelState["FieldPrefix.StringProperty"] = modelState; "StringPropertyAttemptedValue");
modelState = new ModelStateEntry(); viewData.ModelState.SetModelValue(
modelState.AttemptedValue = "ModelAttemptedValue"; "FieldPrefix",
modelState.RawValue = new string[] { "ModelRawValue" }; "ModelRawValue",
viewData.ModelState["FieldPrefix"] = modelState; "ModelAttemptedValue");
// Act & Assert // Act & Assert
Assert.Equal("StringPropertyRawValue", helper.Value("StringProperty", format: null)); Assert.Equal("StringPropertyRawValue", helper.Value("StringProperty", format: null));
@ -219,10 +219,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var viewData = helper.ViewData; var viewData = helper.ViewData;
viewData["StringProperty"] = "ViewDataValue <\"\">"; viewData["StringProperty"] = "ViewDataValue <\"\">";
var modelState = new ModelStateEntry(); viewData.ModelState.SetModelValue(
modelState.AttemptedValue = "ObjectPropertyAttemptedValue <\"\">"; "ObjectProperty",
modelState.RawValue = new string[] { "ObjectPropertyRawValue <\"\">" }; "ObjectPropertyRawValue <\"\">",
viewData.ModelState["ObjectProperty"] = modelState; "ObjectPropertyAttemptedValue <\"\">");
// Act & Assert // Act & Assert
Assert.Equal( Assert.Equal(

View File

@ -33,12 +33,10 @@ namespace System.Web.Http.Dispatcher
yield return new[] { new HttpError() }; yield return new[] { new HttpError() };
yield return new[] { new HttpError("error") }; yield return new[] { new HttpError("error") };
yield return new[] { new HttpError(new NotImplementedException(), true) }; yield return new[] { new HttpError(new NotImplementedException(), true) };
yield return new[] { new HttpError(
new ModelStateDictionary() var modelState = new ModelStateDictionary();
{ modelState.AddModelError("key", "error");
{ "key", new ModelStateEntry { Errors = { new ModelError("error") } } } yield return new[] { new HttpError(modelState, true) };
},
true) };
} }
} }
@ -247,7 +245,7 @@ namespace System.Web.Http.Dispatcher
} }
[Theory] [Theory]
[MemberData("ErrorKeyValue")] [MemberData(nameof(ErrorKeyValue))]
public void HttpErrorStringProperties_UseCorrectHttpErrorKey(HttpError httpError, Func<string> productUnderTest, string key, string actualValue) public void HttpErrorStringProperties_UseCorrectHttpErrorKey(HttpError httpError, Func<string> productUnderTest, string key, string actualValue)
{ {
// Arrange // Arrange
@ -289,7 +287,7 @@ namespace System.Web.Http.Dispatcher
} }
[Theory] [Theory]
[MemberData("HttpErrors")] [MemberData(nameof(HttpErrors))]
public void HttpErrors_UseCaseInsensitiveComparer(HttpError httpError) public void HttpErrors_UseCaseInsensitiveComparer(HttpError httpError)
{ {
// Arrange // Arrange

View File

@ -83,8 +83,9 @@ namespace WebApiCompatShimWebSite
var error = item.Value.Errors.SingleOrDefault(); var error = item.Value.Errors.SingleOrDefault();
if (error != null) if (error != null)
{ {
var value = error.Exception != null ? error.Exception.Message : var value = error.Exception != null ?
error.ErrorMessage; error.Exception.Message :
error.ErrorMessage;
result.Add(item.Key, value); result.Add(item.Key, value);
} }
} }