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.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
@ -12,15 +14,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// Represents the state of an attempt to bind values from an HTTP Request to an action method, which includes
/// validation information.
/// </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.
/// <summary>
/// The default value for <see cref="MaxAllowedErrors"/> of <c>200</c>.
/// </summary>
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;
/// <summary>
@ -37,8 +40,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public ModelStateDictionary(int maxAllowedErrors)
{
MaxAllowedErrors = maxAllowedErrors;
_innerDictionary = new Dictionary<string, ModelStateEntry>(StringComparer.OrdinalIgnoreCase);
var emptySegment = new StringSegment(buffer: string.Empty);
_root = new ModelStateNode(subKey: emptySegment)
{
Key = string.Empty
};
}
/// <summary>
@ -47,21 +53,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
/// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
public ModelStateDictionary(ModelStateDictionary dictionary)
: this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors)
{
if (dictionary == null)
{
throw new ArgumentNullException(nameof(dictionary));
}
_innerDictionary = new Dictionary<string, ModelStateEntry>(
dictionary,
StringComparer.OrdinalIgnoreCase);
MaxAllowedErrors = dictionary.MaxAllowedErrors;
ErrorCount = dictionary.ErrorCount;
HasRecordedMaxModelError = dictionary.HasRecordedMaxModelError;
Merge(dictionary);
}
/// <summary>
/// Root entry for the <see cref="ModelStateDictionary"/>.
/// </summary>
public ModelStateEntry Root => _root;
/// <summary>
/// Gets or sets the maximum allowed model state errors in this instance of <see cref="ModelStateDictionary"/>.
/// Defaults to <c>200</c>.
@ -115,28 +121,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public int ErrorCount { get; private set; }
/// <inheritdoc />
public int Count
{
get { return _innerDictionary.Count; }
}
public int Count { get; private set; }
/// <summary>
/// Gets the key sequence.
/// </summary>
public KeyEnumerable Keys => new KeyEnumerable(this);
/// <inheritdoc />
public bool IsReadOnly
{
get { return ((ICollection<KeyValuePair<string, ModelStateEntry>>)_innerDictionary).IsReadOnly; }
}
IEnumerable<string> IReadOnlyDictionary<string, ModelStateEntry>.Keys => Keys;
/// <summary>
/// Gets the value sequence.
/// </summary>
public ValueEnumerable Values => new ValueEnumerable(this);
/// <inheritdoc />
public ICollection<string> Keys
{
get { return _innerDictionary.Keys; }
}
/// <inheritdoc />
public ICollection<ModelStateEntry> Values
{
get { return _innerDictionary.Values; }
}
IEnumerable<ModelStateEntry> IReadOnlyDictionary<string, ModelStateEntry>.Values => Values;
/// <summary>
/// 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 />
public ModelValidationState ValidationState
{
get
{
var entries = FindKeysWithPrefix(string.Empty);
return GetValidity(entries, defaultState: ModelValidationState.Valid);
}
}
public ModelValidationState ValidationState => GetValidity(_root) ?? ModelValidationState.Valid;
/// <inheritdoc />
public ModelStateEntry this[string key]
@ -169,29 +163,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
ModelStateEntry value;
_innerDictionary.TryGetValue(key, out value);
return value;
ModelStateEntry entry;
TryGetValue(key, out entry);
return entry;
}
set
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_innerDictionary[key] = value;
}
}
// For unit testing
internal IDictionary<string, ModelStateEntry> InnerDictionary
{
get { return _innerDictionary; }
}
// Flag that indiciates if TooManyModelErrorException has already been added to this dictionary.
@ -337,8 +312,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
ErrorCount++;
var modelState = GetModelStateForKey(key);
var modelState = GetOrAddNode(key);
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.ValidationState = ModelValidationState.Invalid;
modelState.MarkNonContainerNode();
modelState.Errors.Add(errorMessage);
return true;
@ -359,8 +336,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
var entries = FindKeysWithPrefix(key);
return GetValidity(entries, defaultState: ModelValidationState.Unvalidated);
var item = GetNode(key);
return GetValidity(item) ?? ModelValidationState.Unvalidated;
}
/// <summary>
@ -398,12 +375,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
var modelState = GetModelStateForKey(key);
var modelState = GetOrAddNode(key);
if (modelState.ValidationState == ModelValidationState.Invalid)
{
throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset);
}
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.MarkNonContainerNode();
modelState.ValidationState = ModelValidationState.Valid;
}
@ -419,12 +398,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
var modelState = GetModelStateForKey(key);
var modelState = GetOrAddNode(key);
if (modelState.ValidationState == ModelValidationState.Invalid)
{
throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset_ToSkipped);
}
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.MarkNonContainerNode();
modelState.ValidationState = ModelValidationState.Skipped;
}
@ -440,9 +421,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return;
}
foreach (var entry in dictionary)
foreach (var source in dictionary)
{
this[entry.Key] = entry.Value;
var target = GetOrAddNode(source.Key);
Count += !target.IsContainerNode ? 0 : 1;
ErrorCount += source.Value.Errors.Count - target.Errors.Count;
target.Copy(source.Value);
target.MarkNonContainerNode();
}
}
@ -462,9 +447,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
var modelState = GetModelStateForKey(key);
var modelState = GetOrAddNode(key);
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.RawValue = rawValue;
modelState.AttemptedValue = attemptedValue;
modelState.MarkNonContainerNode();
}
/// <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;
if (!TryGetValue(key, out entry))
// For a key of the format, foo.bar[0].baz[qux] we'll create the following nodes:
// foo
// -> bar
// -> [0]
// -> baz
// -> [qux]
var current = _root;
var previousIndex = 0;
int index;
while ((index = key.IndexOfAny(Delimiters, previousIndex)) != -1)
{
entry = new ModelStateEntry();
this[key] = entry;
var keyStart = previousIndex == 0 || key[previousIndex - 1] == '.'
? previousIndex
: previousIndex - 1;
var subKey = new StringSegment(key, keyStart, index - keyStart);
current = current.GetNode(subKey, createIfNotExists);
if (current == null)
{
// createIfNotExists is set to false and a node wasn't found. Exit early.
return null;
}
previousIndex = index + 1;
}
return entry;
if (previousIndex < key.Length)
{
var keyStart = previousIndex == 0 || key[previousIndex - 1] == '.'
? previousIndex
: previousIndex - 1;
var subKey = new StringSegment(key, keyStart, key.Length - keyStart);
current = current.GetNode(subKey, createIfNotExists);
}
if (current != null && current.Key == null)
{
// Don't update the key if it's been previously assigned. This is to prevent change in key casing
// e.g. modelState.SetModelValue("foo", .., ..);
// var value = modelState["FOO"];
current.Key = key;
}
return current;
}
private static ModelValidationState GetValidity(PrefixEnumerable entries, ModelValidationState defaultState)
private static ModelValidationState? GetValidity(ModelStateNode node)
{
var hasEntries = false;
var validationState = ModelValidationState.Valid;
foreach (var entry in entries)
if (node == null)
{
hasEntries = true;
return null;
}
var entryState = entry.Value.ValidationState;
if (entryState == ModelValidationState.Unvalidated)
ModelValidationState? validationState = null;
if (!node.IsContainerNode)
{
validationState = ModelValidationState.Valid;
if (node.ValidationState == ModelValidationState.Unvalidated)
{
// If any entries of a field is unvalidated, we'll treat the tree as unvalidated.
return entryState;
return ModelValidationState.Unvalidated;
}
else if (entryState == ModelValidationState.Invalid)
if (node.ValidationState == ModelValidationState.Invalid)
{
validationState = entryState;
validationState = node.ValidationState;
}
}
return hasEntries ? validationState : defaultState;
if (node.ChildNodes != null)
{
for (var i = 0; i < node.ChildNodes.Count; i++)
{
var entryState = GetValidity(node.ChildNodes[i]);
if (entryState == ModelValidationState.Unvalidated)
{
return entryState;
}
if (validationState == null || entryState == ModelValidationState.Invalid)
{
validationState = entryState;
}
}
}
return validationState;
}
private void EnsureMaxErrorsReachedRecorded()
@ -570,43 +617,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private void AddModelErrorCore(string key, Exception exception)
{
var modelState = GetModelStateForKey(key);
var modelState = GetOrAddNode(key);
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.ValidationState = ModelValidationState.Invalid;
modelState.MarkNonContainerNode();
modelState.Errors.Add(exception);
}
/// <inheritdoc />
public void Add(KeyValuePair<string, ModelStateEntry> item)
{
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 />
/// <summary>
/// Removes all keys and values from ths instance of <see cref="ModelStateDictionary"/>.
/// </summary>
public void Clear()
{
_innerDictionary.Clear();
}
/// <inheritdoc />
public bool Contains(KeyValuePair<string, ModelStateEntry> item)
{
return ((ICollection<KeyValuePair<string, ModelStateEntry>>)_innerDictionary).Contains(item);
Count = 0;
HasRecordedMaxModelError = false;
ErrorCount = 0;
_root.Reset();
_root.ChildNodes.Clear();
}
/// <inheritdoc />
@ -617,27 +644,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
return _innerDictionary.ContainsKey(key);
return !GetNode(key)?.IsContainerNode ?? false;
}
/// <inheritdoc />
public void CopyTo(KeyValuePair<string, ModelStateEntry>[] array, int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
((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 />
/// <summary>
/// Removes the <see cref="ModelStateEntry"/> with the specified <paramref name="key"/>.
/// </summary>
/// <param name="key">The key.</param>
/// <returns><c>true</c> if the element is successfully removed; otherwise <c>false</c>. This method also
/// returns <c>false</c> if key was not found.</returns>
public bool Remove(string key)
{
if (key == null)
@ -645,7 +660,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
return _innerDictionary.Remove(key);
var node = GetNode(key);
if (node?.IsContainerNode == false)
{
Count--;
ErrorCount -= node.Errors.Count;
node.Reset();
return true;
}
return false;
}
/// <inheritdoc />
@ -656,20 +680,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key));
}
return _innerDictionary.TryGetValue(key, out value);
var result = GetNode(key);
if (result?.IsContainerNode == false)
{
value = result;
return true;
}
value = null;
return false;
}
/// <inheritdoc />
public IEnumerator<KeyValuePair<string, ModelStateEntry>> GetEnumerator()
{
return _innerDictionary.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through this instance of <see cref="ModelStateDictionary"/>.
/// </summary>
/// <returns>An <see cref="Enumerator"/>.</returns>
public Enumerator GetEnumerator() => new Enumerator(this, prefix: string.Empty);
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator<KeyValuePair<string, ModelStateEntry>>
IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator() => GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static bool StartsWithPrefix(string prefix, string key)
{
@ -698,7 +731,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
return false;
}
if (key.Length == prefix.Length)
{
// Exact match
@ -724,6 +757,126 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new PrefixEnumerable(this, prefix);
}
[DebuggerDisplay("SubKey={SubKey}, Key={Key}, ValidationState={ValidationState}")]
private class ModelStateNode : ModelStateEntry
{
private bool _isContainerNode = true;
public ModelStateNode(StringSegment subKey)
{
SubKey = subKey;
}
public List<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>>
{
private readonly ModelStateDictionary _dictionary;
@ -745,32 +898,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
_prefix = prefix;
}
public PrefixEnumerator GetEnumerator()
{
return _dictionary == null ? new PrefixEnumerator() : new PrefixEnumerator(_dictionary, _prefix);
}
public Enumerator GetEnumerator() => new Enumerator(_dictionary, _prefix);
IEnumerator<KeyValuePair<string, ModelStateEntry>>
IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerable<KeyValuePair<string, ModelStateEntry>>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public struct PrefixEnumerator : IEnumerator<KeyValuePair<string, ModelStateEntry>>
public struct Enumerator : IEnumerator<KeyValuePair<string, ModelStateEntry>>
{
private readonly ModelStateDictionary _dictionary;
private string _prefix;
private readonly ModelStateNode _rootNode;
private ModelStateNode _modelStateNode;
private List<ModelStateNode> _nodes;
private int _index;
private bool _visitedRoot;
private ExactMatchState _exactMatch;
private Dictionary<string, ModelStateEntry>.Enumerator _enumerator;
public PrefixEnumerator(ModelStateDictionary dictionary, string prefix)
public Enumerator(ModelStateDictionary dictionary, string prefix)
{
if (dictionary == null)
{
@ -782,23 +926,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(prefix));
}
_dictionary = dictionary;
_prefix = prefix;
_exactMatch = ExactMatchState.NotChecked;
_enumerator = default(Dictionary<string, ModelStateEntry>.Enumerator);
Current = default(KeyValuePair<string, ModelStateEntry>);
_index = -1;
_rootNode = dictionary.GetNode(prefix);
_modelStateNode = null;
_nodes = null;
_visitedRoot = false;
}
public KeyValuePair<string, ModelStateEntry> Current { get; private set; }
public KeyValuePair<string, ModelStateEntry> Current =>
new KeyValuePair<string, ModelStateEntry>(_modelStateNode.Key, _modelStateNode);
object IEnumerator.Current
{
get
{
return Current;
}
}
object IEnumerator.Current => Current;
public void Dispose()
{
@ -806,61 +944,56 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public bool MoveNext()
{
if (_dictionary == null)
if (_rootNode == null)
{
return false;
}
// ModelStateDictionary has a behavior where the first 'match' returned from iterating
// prefixes is the exact match for the prefix (if present). Only after looking for an
// exact match do we fall back to iteration to find 'starts-with' matches.
if (_exactMatch == ExactMatchState.NotChecked)
if (!_visitedRoot)
{
_enumerator = _dictionary._innerDictionary.GetEnumerator();
ModelStateEntry entry;
if (_dictionary.TryGetValue(_prefix, out entry))
// Visit the root node
_visitedRoot = true;
if (_rootNode.ChildNodes?.Count > 0)
{
// Mark exact match as found
_exactMatch = ExactMatchState.Found;
Current = new KeyValuePair<string, ModelStateEntry>(_prefix, entry);
return true;
_nodes = new List<ModelStateNode> { _rootNode };
}
else
if (!_rootNode.IsContainerNode)
{
// Mark exact match tested for
_exactMatch = ExactMatchState.NotFound;
_modelStateNode = _rootNode;
return true;
}
}
while (_enumerator.MoveNext())
if (_nodes == null)
{
var key = _enumerator.Current.Key;
if (_exactMatch == ExactMatchState.NotFound)
return false;
}
while (_nodes.Count > 0)
{
var node = _nodes[0];
if (_index == node.ChildNodes.Count - 1)
{
if (StartsWithPrefix(_prefix, key))
{
Current = _enumerator.Current;
return true;
}
// We've exhausted the current sublist.
_nodes.RemoveAt(0);
_index = -1;
continue;
}
else if (_exactMatch == ExactMatchState.ReferenceSet && Object.ReferenceEquals(_prefix, key))
else
{
// Fast path skip this one. Is the exact string reference set below.
_index++;
}
else if (_exactMatch == ExactMatchState.Found &&
string.Equals(_prefix, key, StringComparison.OrdinalIgnoreCase))
var currentChild = node.ChildNodes[_index];
if (currentChild.ChildNodes?.Count > 0)
{
// Update _prefix to be this exact string reference to enable fast path
_prefix = key;
// Mark exact reference set
_exactMatch = ExactMatchState.ReferenceSet;
// Skip this one. We've already handled the 'exact match' case.
_nodes.Add(currentChild);
}
else if (StartsWithPrefix(_prefix, key))
if (!currentChild.IsContainerNode)
{
Current = _enumerator.Current;
_modelStateNode = currentChild;
return true;
}
}
@ -870,17 +1003,120 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void Reset()
{
_exactMatch = ExactMatchState.NotChecked;
_enumerator = default(Dictionary<string, ModelStateEntry>.Enumerator);
Current = default(KeyValuePair<string, ModelStateEntry>);
_index = -1;
_nodes.Clear();
_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,
NotFound,
Found,
ReferenceSet
_prefixEnumerator = new Enumerator(dictionary, prefix);
Current = null;
}
public string Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose() => _prefixEnumerator.Dispose();
public bool MoveNext()
{
var result = _prefixEnumerator.MoveNext();
if (result)
{
var current = _prefixEnumerator.Current;
Current = current.Key;
}
else
{
Current = null;
}
return result;
}
public void Reset()
{
_prefixEnumerator.Reset();
Current = null;
}
}
public struct ValueEnumerable : IEnumerable<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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -11,6 +10,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public static class ValidationHelpers
{
private static readonly ModelStateEntry[] EmptyModelStateEntries = new ModelStateEntry[0];
public static string GetModelErrorMessageOrDefault(ModelError modelError)
{
Debug.Assert(modelError != null);
@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
// Returns non-null list of model states, which caller will render in order provided.
public static IEnumerable<ModelStateEntry> GetModelStateList(
public static IList<ModelStateEntry> GetModelStateList(
ViewDataDictionary viewData,
bool excludePropertyErrors)
{
@ -57,49 +58,59 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
return new[] { ms };
}
return Enumerable.Empty<ModelStateEntry>();
}
else
else if (viewData.ModelState.Count > 0)
{
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
.OrderBy(data => orderer.GetOrder(data.Key))
.Select(ms => ms.Value);
if (entries.Count < modelStateDictionary.Count)
{
// Account for entries in the ModelStateDictionary that do not have corresponding ModelMetadata values.
foreach (var entry in modelStateDictionary)
{
if (!entries.Contains(entry.Value))
{
entries.Add(entry.Value);
}
}
}
return entries;
}
return EmptyModelStateEntries;
}
// Helper for sorting modelStates to respect the ordering in the metadata.
// ModelState doesn't refer to ModelMetadata, but we can correlate via the property name.
private class ErrorsOrderer
private static void Visit(
ModelStateDictionary dictionary,
ModelStateEntry modelStateEntry,
ModelMetadata metadata,
List<ModelStateEntry> orderedModelStateEntries)
{
private readonly Dictionary<string, int> _ordering =
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
public ErrorsOrderer(ModelMetadata metadata)
if (metadata.ElementMetadata != null)
{
if (metadata == null)
foreach (var indexEntry in modelStateEntry.Children)
{
throw new ArgumentNullException(nameof(metadata));
}
foreach (var data in metadata.Properties)
{
_ordering[data.PropertyName] = data.Order;
Visit(dictionary, indexEntry, metadata.ElementMetadata, orderedModelStateEntries);
}
}
public int GetOrder(string key)
for (var i = 0; i < metadata.Properties.Count; i++)
{
int value;
if (_ordering.TryGetValue(key, out value))
var propertyMetadata = metadata.Properties[i];
var propertyModelStateEntry = modelStateEntry.GetModelStateForProperty(propertyMetadata.PropertyName);
if (propertyModelStateEntry != null)
{
return value;
Visit(dictionary, propertyModelStateEntry, propertyMetadata, orderedModelStateEntries);
}
}
return ModelMetadata.DefaultOrder;
if (!modelStateEntry.IsContainerNode)
{
orderedModelStateEntries.Add(modelStateEntry);
}
}
}

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Xunit;
@ -12,20 +12,139 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public class ModelStateDictionaryTest
{
[Theory]
[InlineData(ModelValidationState.Valid)]
[InlineData(ModelValidationState.Unvalidated)]
public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsNotInValid(ModelValidationState validationState)
[InlineData("")]
[InlineData("foo")]
public void ContainsKey_ReturnsFalse_IfNodeHasNotBeenMutated(string key)
{
// Arrange
var entry = new ModelStateEntry
{
ValidationState = validationState
};
var dictionary = new ModelStateDictionary();
dictionary.AddModelError("foo.bar", "some error");
var source = new ModelStateDictionary
{
{ "key", entry }
};
// Act
var result = dictionary.ContainsKey(key);
// Assert
Assert.False(result);
}
[Theory]
[InlineData("")]
[InlineData("foo")]
public void ContainsKey_ReturnsFalse_IfNodeHasBeenRemoved(string key)
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary.AddModelError(key, "some error");
// Act
var remove = dictionary.Remove(key);
var containsKey = dictionary.ContainsKey(key);
// Assert
Assert.True(remove);
Assert.False(containsKey);
}
[Theory]
[InlineData("")]
[InlineData("foo")]
[InlineData("foo.bar")]
[InlineData("foo[bar]")]
public void ContainsKey_ReturnsTrue_IfNodeHasBeenMutated(string key)
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldSkipped(key);
// Act
var result = dictionary.ContainsKey(key);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("foo")]
[InlineData("foo.bar")]
[InlineData("foo.bar[10]")]
public void IndexerDoesNotReturnIntermediaryNodes(string key)
{
// Arrange
var modelStateDictionary = new ModelStateDictionary();
modelStateDictionary.AddModelError("foo.bar[10].baz", "error-message");
// Act
var result = modelStateDictionary[key];
// Assert
Assert.Null(result);
}
[Theory]
[InlineData("prop")]
[InlineData("first.second")]
[InlineData("[0].prop")]
[InlineData("[qux]")]
[InlineData("[test].prop")]
[InlineData("first[0].second")]
[InlineData("first.second[0].third")]
[InlineData("first[second][0]")]
[InlineData("first.second.third[0]")]
[InlineData("first.second.third[0].fourth")]
[InlineData("first[0][second]")]
public void Indexer_ReturnsValuesAddedUsingSetModelValue(string key)
{
// Arrange
var value = "Hello world";
var modelStateDictionary = new ModelStateDictionary();
modelStateDictionary.SetModelValue(key, value, value);
// Act
var result = modelStateDictionary[key];
// Assert
Assert.Equal(value, result.RawValue);
}
[Fact]
public void Clear_RemovesAllEntries()
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary.AddModelError("a", "a-error");
dictionary.AddModelError("b", "b-error");
dictionary.AddModelError("c", "c-error");
// Act
dictionary.Clear();
// Assert
Assert.Equal(0, dictionary.Count);
Assert.Equal(0, dictionary.ErrorCount);
Assert.Empty(dictionary);
Assert.Equal(ModelValidationState.Valid, dictionary.ValidationState);
}
[Fact]
public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsUnvalidated()
{
// Arrange
var source = new ModelStateDictionary();
source.SetModelValue("key", "value", "value");
// Act
source.MarkFieldSkipped("key");
// Assert
Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState);
}
[Fact]
public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsValid()
{
// Arrange
var source = new ModelStateDictionary();
source.MarkFieldValid("key");
// Act
source.MarkFieldSkipped("key");
@ -38,11 +157,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MarkFieldSkipped_MarksFieldAsSkipped_IfKeyIsNotPresent()
{
// Arrange
var entry = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
var source = new ModelStateDictionary();
// Act
@ -58,15 +172,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MarkFieldSkipped_Throws_IfStateIsInvalid()
{
// Arrange
var entry = new ModelStateEntry
{
ValidationState = ModelValidationState.Invalid
};
var source = new ModelStateDictionary
{
{ "key", entry }
};
var source = new ModelStateDictionary();
source.AddModelError("key", "some error");
// Act
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped("key"));
@ -77,21 +184,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
exception.Message);
}
[Theory]
[InlineData(ModelValidationState.Skipped)]
[InlineData(ModelValidationState.Unvalidated)]
public void MarkFieldValid_MarksFieldAsValid_IfStateIsNotInvalid(ModelValidationState validationState)
[Fact]
public void MarkFieldValid_MarksFieldAsValid_IfStateIsUnvalidated()
{
// Arrange
var entry = new ModelStateEntry
{
ValidationState = validationState
};
var source = new ModelStateDictionary();
source.SetModelValue("key", "value", "value");
var source = new ModelStateDictionary
{
{ "key", entry }
};
// Act
source.MarkFieldValid("key");
// Assert
Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
}
[Fact]
public void MarkFieldValid_MarksFieldAsValid_IfStateIsSkipped()
{
// Arrange
var source = new ModelStateDictionary();
source.MarkFieldSkipped("key");
// Act
source.MarkFieldValid("key");
@ -119,15 +231,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MarkFieldValid_Throws_IfStateIsInvalid()
{
// Arrange
var entry = new ModelStateEntry
{
ValidationState = ModelValidationState.Invalid
};
var source = new ModelStateDictionary
{
{ "key", entry }
};
var source = new ModelStateDictionary();
source.AddModelError("key", "some-error");
// Act
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid("key"));
@ -142,20 +247,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void CopyConstructor_CopiesModelStateData()
{
// Arrange
var entry = new ModelStateEntry();
var source = new ModelStateDictionary
{
{ "key", entry }
};
var source = new ModelStateDictionary();
source.SetModelValue("key", "attempted-value", "raw-value");
var entry = source["key"];
entry.AttemptedValue = "attempted-value";
entry.RawValue = "raw-value";
entry.Errors.Add(new ModelError(new InvalidOperationException()));
entry.Errors.Add(new ModelError("error-message"));
entry.ValidationState = ModelValidationState.Skipped;
// Act
var target = new ModelStateDictionary(source);
// Assert
Assert.Equal(0, target.ErrorCount);
Assert.Equal(2, target.ErrorCount);
Assert.Equal(1, target.Count);
Assert.Same(entry, target["key"]);
Assert.IsType<Dictionary<string, ModelStateEntry>>(target.InnerDictionary);
var actual = target["key"];
Assert.Equal(entry.RawValue, actual.RawValue);
Assert.Equal(entry.AttemptedValue, actual.AttemptedValue);
Assert.Equal(entry.Errors, actual.Errors);
Assert.Equal(entry.ValidationState, actual.ValidationState);
}
[Fact]
@ -202,10 +312,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void ConstructorWithDictionaryParameter()
{
// Arrange
var oldDictionary = new ModelStateDictionary()
{
{ "foo", new ModelStateEntry() { RawValue = "bar" } }
};
var oldDictionary = new ModelStateDictionary();
oldDictionary.SetModelValue("foo", "bar", "bar");
// Act
var newDictionary = new ModelStateDictionary(oldDictionary);
@ -266,14 +374,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key)
{
// Arrange
var validState = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
var dictionary = new ModelStateDictionary
{
{ key, validState }
};
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid(key);
// Act
var validationState = dictionary.GetFieldValidationState("foo");
@ -304,14 +406,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void GetFieldValidationState_IndexedPrefix_ReturnsValidIfModelStateDoesNotContainErrors(string key)
{
// Arrange
var validState = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
var dictionary = new ModelStateDictionary
{
{ key, validState }
};
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid(key);
// Act
var validationState = dictionary.GetFieldValidationState("[0].foo");
@ -324,20 +420,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void IsValidPropertyReturnsFalseIfErrors()
{
// Arrange
var errorState = new ModelStateEntry
{
ValidationState = ModelValidationState.Invalid
};
var validState = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
errorState.Errors.Add("some error");
var dictionary = new ModelStateDictionary()
{
{ "foo", validState },
{ "baz", errorState }
};
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("foo");
dictionary.AddModelError("bar", "some error");
// Act
var isValid = dictionary.IsValid;
@ -352,19 +437,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void IsValidPropertyReturnsTrueIfNoErrors()
{
// Arrange
var dictionary = new ModelStateDictionary()
{
{ "foo", new ModelStateEntry
{
ValidationState = ModelValidationState.Valid,
}
},
{ "baz", new ModelStateEntry
{
ValidationState = ModelValidationState.Skipped,
}
}
};
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("foo");
dictionary.MarkFieldSkipped("bar");
// Act
var isValid = dictionary.IsValid;
@ -379,21 +454,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void IsValidPropertyReturnsFalse_IfSomeFieldsAreNotValidated()
{
// Arrange
var errorState = new ModelStateEntry
{
ValidationState = ModelValidationState.Invalid
};
var validState = new ModelStateEntry
{
ValidationState = ModelValidationState.Valid
};
errorState.Errors.Add("some error");
var dictionary = new ModelStateDictionary()
{
{ "foo", validState },
{ "baz", errorState },
{ "qux", new ModelStateEntry() }
};
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("foo");
dictionary.SetModelValue("qux", "value", "value");
dictionary.AddModelError("baz", "some error");
// Act
var isValid = dictionary.IsValid;
@ -408,22 +472,70 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void MergeCopiesDictionaryEntries()
{
// Arrange
var dictionary1 = new ModelStateDictionary { { "foo", new ModelStateEntry() } };
var dictionary2 = new ModelStateDictionary { { "bar", new ModelStateEntry() } };
var dictionary1 = new ModelStateDictionary();
dictionary1.SetModelValue("foo", "RawValue1", "AttemptedValue1");
dictionary1.AddModelError("foo", "value1-Error1");
dictionary1.AddModelError("foo", "value1-Error2");
var dictionary2 = new ModelStateDictionary();
dictionary2.SetModelValue("bar", "RawValue2", "AttemptedValue2");
dictionary2.AddModelError("bar", "value2-Error1");
// Act
dictionary1.Merge(dictionary2);
// Assert
Assert.Equal(2, dictionary1.Count);
Assert.Equal(dictionary2["bar"], dictionary1["bar"]);
var item = dictionary1["foo"];
Assert.Equal("AttemptedValue1", item.AttemptedValue);
Assert.Equal("RawValue1", item.RawValue);
item = dictionary1["bar"];
Assert.Equal("AttemptedValue2", item.AttemptedValue);
Assert.Equal("RawValue2", item.RawValue);
Assert.Collection(item.Errors,
error => Assert.Equal("value2-Error1", error.ErrorMessage));
}
[Theory]
[InlineData("")]
[InlineData("key1")]
public void MergeCopiesDictionaryOverwritesExistingValues(string key)
{
// Arrange
var dictionary1 = new ModelStateDictionary();
dictionary1.SetModelValue(key, "RawValue1", "AttemptedValue1");
dictionary1.AddModelError(key, "value1-Error1");
dictionary1.AddModelError(key, "value1-Error2");
dictionary1.SetModelValue("other-key", null, null);
var dictionary2 = new ModelStateDictionary();
dictionary2.SetModelValue(key, "RawValue2", "AttemptedValue2");
dictionary2.AddModelError(key, "value2-Error1");
// Act
dictionary1.Merge(dictionary2);
// Assert
Assert.Equal(2, dictionary1.Count);
var item = dictionary1["other-key"];
Assert.Null(item.AttemptedValue);
Assert.Null(item.RawValue);
Assert.Empty(item.Errors);
item = dictionary1[key];
Assert.Equal("AttemptedValue2", item.AttemptedValue);
Assert.Equal("RawValue2", item.RawValue);
Assert.Collection(item.Errors,
error => Assert.Equal("value2-Error1", error.ErrorMessage));
}
[Fact]
public void MergeDoesNothingIfParameterIsNull()
{
// Arrange
var dictionary = new ModelStateDictionary() { { "foo", new ModelStateEntry() } };
var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("foo", "value", "value");
// Act
dictionary.Merge(null);
@ -491,7 +603,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.MarkFieldValid("user.Address");
dictionary.SetModelValue("user.Name", new string[] { "some value" }, "some value");
dictionary.AddModelError("user.Age", "Age is not a valid int");
@ -510,8 +622,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.MarkFieldValid("user.Address");
dictionary.MarkFieldValid("user.Name");
dictionary.AddModelError("user.Age", "Age is not a valid int");
// Act
@ -529,9 +641,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["[0].product.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["[0].product.Age[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.MarkFieldValid("[0].product.Name");
dictionary.MarkFieldValid("[0].product.Age[0]");
dictionary.AddModelError("[0].product.Name", "Name is invalid");
// Act
@ -546,8 +657,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.MarkFieldValid("user.Address");
dictionary.MarkFieldValid("user.Name");
// Act
var validationState = dictionary.GetFieldValidationState("user");
@ -860,16 +971,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["Property1"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Property2"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.MarkFieldValid("Property1");
dictionary.AddModelError("Property2", "Property2 invalid.");
dictionary["Property3"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Property3", "Property invalid.");
dictionary["Property4"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
dictionary.MarkFieldSkipped("Property4");
// Act
dictionary.ClearValidationState("Property1");
@ -892,23 +997,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["Product"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Product.Detail1"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.MarkFieldValid("Product");
dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid.");
dictionary["Product.Detail2[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Product.Detail2[0]", "Product Detail2[0] invalid.");
dictionary["Product.Detail2[1]"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Product.Detail2[1]", "Product Detail2[1] invalid.");
dictionary["Product.Detail2[2]"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
dictionary["Product.Detail3"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
dictionary["ProductName"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.MarkFieldSkipped("Product.Detail2[2]");
dictionary.MarkFieldSkipped("Product.Detail3");
dictionary.AddModelError("ProductName", "ProductName invalid.");
// Act
@ -936,16 +1030,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["Product"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Product.Detail1"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.MarkFieldValid("Product");
dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid.");
dictionary["Product.Detail1.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Product.Detail1.Name", "Product Detail1 Name invalid.");
dictionary["Product.Detail1Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
dictionary.MarkFieldSkipped("Product.Detail1Name");
// Act
dictionary.ClearValidationState("Product.Detail1");
@ -966,16 +1054,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["Property1"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["Property2"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.MarkFieldValid("Property1");
dictionary.AddModelError("Property2", "Property2 invalid.");
dictionary["Property3"] = new ModelStateEntry { ValidationState = ModelValidationState.Invalid };
dictionary.AddModelError("Property3", "Property invalid.");
dictionary["Property4"] = new ModelStateEntry { ValidationState = ModelValidationState.Skipped };
dictionary.MarkFieldSkipped("Property4");
// Act
dictionary.ClearValidationState(modelKey);
@ -990,5 +1072,220 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(0, dictionary["Property4"].Errors.Count);
Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState);
}
[Fact]
public void GetEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty()
{
// Arrange
var dictionary = new ModelStateDictionary();
// Act & Assert
Assert.Empty(dictionary);
}
[Fact]
public void GetEnumerable_ReturnsAllNonContainerNodes()
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Property1");
dictionary.SetModelValue("Property1.Property2", "value", "value");
dictionary.AddModelError("Property2", "Property invalid.");
dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid.");
dictionary.MarkFieldSkipped("Property4");
dictionary.Remove("Property2");
// Act & Assert
Assert.Collection(
dictionary,
entry =>
{
Assert.Equal("Property1", entry.Key);
Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState);
Assert.Null(entry.Value.RawValue);
Assert.Null(entry.Value.AttemptedValue);
Assert.Empty(entry.Value.Errors);
},
entry =>
{
Assert.Equal("Property4", entry.Key);
Assert.Equal(ModelValidationState.Skipped, entry.Value.ValidationState);
Assert.Null(entry.Value.RawValue);
Assert.Null(entry.Value.AttemptedValue);
Assert.Empty(entry.Value.Errors);
},
entry =>
{
Assert.Equal("Property1.Property2", entry.Key);
Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
Assert.Equal("value", entry.Value.RawValue);
Assert.Equal("value", entry.Value.AttemptedValue);
Assert.Empty(entry.Value.Errors);
},
entry =>
{
Assert.Equal("Property2[Property3]", entry.Key);
Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
Assert.Null(entry.Value.RawValue);
Assert.Null(entry.Value.AttemptedValue);
Assert.Collection(entry.Value.Errors,
error => Assert.Equal("Property2[Property3] invalid.", error.ErrorMessage));
});
}
[Fact]
public void GetEnumerable_WorksCorrectlyWhenSiblingIsAPrefix()
{
// Arrange
var modelStateDictionary = new ModelStateDictionary();
modelStateDictionary.SetModelValue("prop", "value1", "value1");
modelStateDictionary.SetModelValue("property_name", "value3", "value3");
modelStateDictionary.SetModelValue("property", "value2", "value2");
// Act & Assert
Assert.Collection(modelStateDictionary,
entry =>
{
Assert.Equal("prop", entry.Key);
Assert.Equal("value1", entry.Value.RawValue);
},
entry =>
{
Assert.Equal("property", entry.Key);
Assert.Equal("value2", entry.Value.RawValue);
},
entry =>
{
Assert.Equal("property_name", entry.Key);
Assert.Equal("value3", entry.Value.RawValue);
});
}
[Fact]
public void KeysEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty()
{
// Arrange
var dictionary = new ModelStateDictionary();
// Act
var keys = dictionary.Keys;
// Assert
Assert.Empty(keys);
}
[Fact]
public void KeysEnumerable_ReturnsAllKeys()
{
// Arrange
var expected = new[] { "Property1", "Property4", "Property1.Property2", "Property2[Property3]" };
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Property1");
dictionary.AddModelError("Property1.Property2", "Property2 invalid.");
dictionary.AddModelError("Property2", "Property invalid.");
dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid.");
dictionary.MarkFieldSkipped("Property4");
dictionary.Remove("Property2");
// Act
var keys = dictionary.Keys;
// Assert
Assert.Equal(expected, keys);
}
[Fact]
public void ValuesEnumerable_ReturnsEmptySequenceWhenDictionaryIsEmpty()
{
// Arrange
var dictionary = new ModelStateDictionary();
// Act
var values = dictionary.Values;
// Assert
Assert.Empty(values);
}
[Fact]
public void ValuesEnumerable_ReturnsAllEntries()
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary.MarkFieldValid("Property1");
dictionary.SetModelValue("Property1.Property2", "value", "value");
dictionary.AddModelError("Property2", "Property invalid.");
dictionary.AddModelError("Property2[Property3]", "Property2[Property3] invalid.");
dictionary.MarkFieldSkipped("Property4");
dictionary.Remove("Property2");
// Act & Assert
Assert.Collection(dictionary.Values,
value =>
{
Assert.Equal(ModelValidationState.Valid, value.ValidationState);
Assert.Null(value.RawValue);
Assert.Null(value.AttemptedValue);
Assert.Empty(value.Errors);
},
value =>
{
Assert.Equal(ModelValidationState.Skipped, value.ValidationState);
Assert.Null(value.RawValue);
Assert.Null(value.AttemptedValue);
Assert.Empty(value.Errors);
},
value =>
{
Assert.Equal(ModelValidationState.Unvalidated, value.ValidationState);
Assert.Equal("value", value.RawValue);
Assert.Equal("value", value.AttemptedValue);
Assert.Empty(value.Errors);
},
value =>
{
Assert.Equal(ModelValidationState.Invalid, value.ValidationState);
Assert.Null(value.RawValue);
Assert.Null(value.AttemptedValue);
Assert.Collection(
value.Errors,
error => Assert.Equal("Property2[Property3] invalid.", error.ErrorMessage));
});
}
[Fact]
public void GetModelStateForProperty_ReturnsModelStateForImmediateChildren()
{
// Arrange
var modelStateDictionary = new ModelStateDictionary();
modelStateDictionary.SetModelValue("property1", "value1", "value1");
modelStateDictionary.SetModelValue("property1.property2", "value2", "value2");
// Act 1
var property1 = modelStateDictionary.Root.GetModelStateForProperty("property1");
var property2 = modelStateDictionary.Root.GetModelStateForProperty("property1.property2");
// Assert 1
Assert.Equal("value1", property1.RawValue);
Assert.Null(property2);
// Act 2
property2 = property1.GetModelStateForProperty("property2");
Assert.Equal("value2", property2.RawValue);
}
[Fact]
public void GetModelStateForProperty_ReturnsModelStateForIndexedChildren()
{
// Arrange
var modelStateDictionary = new ModelStateDictionary();
modelStateDictionary.SetModelValue("[property]", "value1", "value1");
// Act
var property = modelStateDictionary.Root.GetModelStateForProperty("[property]");
// Assert
Assert.Equal("value1", property.RawValue);
}
}
}

View File

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

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Xunit;
@ -70,12 +69,8 @@ namespace Microsoft.AspNetCore.Mvc
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.Add(
"key1",
new ModelStateEntry());
modelState.Add(
"key2",
new ModelStateEntry());
modelState.SetModelValue("key1", "value1", "value1");
modelState.SetModelValue("key2", "value2", "value2");
// Act
var serializableError = new SerializableError(modelState);

View File

@ -17,8 +17,8 @@
<hr />
<div class="text-danger validation-summary-errors" data-valmsg-summary="true">
<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 value &#x27;z&#x27; is not valid for Salary.</li>
</ul></div>
<div class="form-group">
<label class="control-label col-md-2" for="Age">Age</label>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.TestCommon;
@ -109,13 +110,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
"<li>HtmlEncode[[This is an error for Property3.]]</li>" + Environment.NewLine +
"</ul></div>";
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 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 +
"</ul></div>";
@ -302,6 +303,166 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
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]
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 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 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 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 +
"</ul></div>";
@ -436,7 +597,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
helper.ViewData.ModelState.AddModelError("Property1", "Error for Property1");
// Act
var validationSummaryResult = helper.ValidationSummary(message: "Custom Message", htmlAttributes: new { attr="value" });
var validationSummaryResult = helper.ValidationSummary(message: "Custom Message", htmlAttributes: new { attr = "value" });
// Assert
Assert.Equal(
@ -577,5 +738,21 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[Display(Order = 23)]
public string OrderedProperty1 { get; set; }
}
private class ModelWithCollection
{
public string Property1 { get; set; }
public List<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.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
var modelState = new ModelStateEntry();
modelState.AttemptedValue = "StringPropertyAttemptedValue";
modelState.RawValue = new string[] { "StringPropertyRawValue" };
viewData.ModelState["FieldPrefix.StringProperty"] = modelState;
viewData.ModelState.SetModelValue(
"FieldPrefix.StringProperty",
"StringPropertyRawValue",
"StringPropertyAttemptedValue");
modelState = new ModelStateEntry();
modelState.AttemptedValue = "ModelAttemptedValue";
modelState.RawValue = new string[] { "ModelRawValue" };
viewData.ModelState["FieldPrefix"] = modelState;
viewData.ModelState.SetModelValue(
"FieldPrefix",
"ModelRawValue",
"ModelAttemptedValue");
// Act & Assert
Assert.Equal("StringPropertyRawValue", helper.Value("StringProperty", format: null));
@ -219,10 +219,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var viewData = helper.ViewData;
viewData["StringProperty"] = "ViewDataValue <\"\">";
var modelState = new ModelStateEntry();
modelState.AttemptedValue = "ObjectPropertyAttemptedValue <\"\">";
modelState.RawValue = new string[] { "ObjectPropertyRawValue <\"\">" };
viewData.ModelState["ObjectProperty"] = modelState;
viewData.ModelState.SetModelValue(
"ObjectProperty",
"ObjectPropertyRawValue <\"\">",
"ObjectPropertyAttemptedValue <\"\">");
// Act & Assert
Assert.Equal(

View File

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

View File

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