diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs index 9dc3ead100..f2af63e1fd 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingContext.cs @@ -3,6 +3,7 @@ using System; using Microsoft.Framework.Internal; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -59,6 +60,8 @@ public static ModelBindingContext CreateBindingContext( ModelState = modelState, OperationBindingContext = operationBindingContext, ValueProvider = operationBindingContext.ValueProvider, + + ValidationState = new ValidationStateDictionary(), }; } @@ -74,6 +77,7 @@ public static ModelBindingContext CreateChildBindingContext( ModelState = parent.ModelState, OperationBindingContext = parent.OperationBindingContext, ValueProvider = parent.ValueProvider, + ValidationState = parent.ValidationState, Model = model, ModelMetadata = modelMetadata, @@ -174,5 +178,11 @@ public static ModelBindingContext CreateChildBindingContext( /// is eligible for model binding. /// public Func PropertyFilter { get; set; } + + /// + /// Gets or sets the . Used for tracking validation state to + /// customize validation behavior for a model object. + /// + public ValidationStateDictionary ValidationState { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingResult.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingResult.cs index 1246946199..94807b10d6 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingResult.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelBindingResult.cs @@ -31,7 +31,7 @@ public struct ModelBindingResult : IEquatable /// A representing a failed model binding operation. public static ModelBindingResult Failed([NotNull] string key) { - return new ModelBindingResult(key, model: null, isModelSet: false, validationNode: null); + return new ModelBindingResult(key, model: null, isModelSet: false); } /// @@ -49,14 +49,12 @@ public static Task FailedAsync([NotNull] string key) /// /// The key of the current model binding operation. /// The model value. May be null. - /// The . May be null. /// A representing a successful model bind. public static ModelBindingResult Success( [NotNull] string key, - object model, - ModelValidationNode validationNode) + object model) { - return new ModelBindingResult(key, model, isModelSet: true, validationNode: validationNode); + return new ModelBindingResult(key, model, isModelSet: true); } /// @@ -65,22 +63,19 @@ public static ModelBindingResult Success( /// /// The key of the current model binding operation. /// The model value. May be null. - /// The . May be null. /// A completed representing a successful model bind. public static Task SuccessAsync( [NotNull] string key, - object model, - ModelValidationNode validationNode) + object model) { - return Task.FromResult(Success(key, model, validationNode)); + return Task.FromResult(Success(key, model)); } - private ModelBindingResult(string key, object model, bool isModelSet, ModelValidationNode validationNode) + private ModelBindingResult(string key, object model, bool isModelSet) { Key = key; Model = model; IsModelSet = isModelSet; - ValidationNode = validationNode; } /// @@ -109,11 +104,6 @@ private ModelBindingResult(string key, object model, bool isModelSet, ModelValid /// public bool IsModelSet { get; } - /// - /// A associated with the current . - /// - public ModelValidationNode ValidationNode { get; } - /// public override bool Equals(object obj) { diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs index 6ab109e863..9cff64881f 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Reflection; using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Microsoft.Framework.Internal; @@ -14,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// A metadata representation of a model type, property or parameter. /// + [DebuggerDisplay("{DebuggerToString(),nq}")] public abstract class ModelMetadata { private bool? _isComplexType; @@ -407,5 +409,17 @@ public string GetDisplayName() { return DisplayName ?? PropertyName ?? ModelType.Name; } + + private string DebuggerToString() + { + if (Identity.MetadataKind == ModelMetadataKind.Type) + { + return $"ModelMetadata (Type: '{ModelType.Name}')"; + } + else + { + return $"ModelMetadata (Property: '{ContainerType.Name}.{PropertyName}' Type: '{ModelType.Name}')"; + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelValidationNode.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelValidationNode.cs deleted file mode 100644 index 1109f1352a..0000000000 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelValidationNode.cs +++ /dev/null @@ -1,76 +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. - -using System.Collections.Generic; -using Microsoft.Framework.Internal; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - /// - /// Captures the validation information for a particular model. - /// - public class ModelValidationNode - { - /// - /// Creates a new instance of . - /// - /// The key that will be used by the validation system to find - /// entries. - /// The for the . - /// The model object which is to be validated. - public ModelValidationNode([NotNull] string key, [NotNull] ModelMetadata modelMetadata, object model) - : this (key, modelMetadata, model, new List()) - { - } - - /// - /// Creates a new instance of . - /// - /// The key that will be used by the validation system to add - /// entries. - /// The for the . - /// The model object which will be validated. - /// A collection of child nodes. - public ModelValidationNode( - [NotNull] string key, - [NotNull] ModelMetadata modelMetadata, - object model, - [NotNull] IList childNodes) - { - Key = key; - ModelMetadata = modelMetadata; - ChildNodes = childNodes; - Model = model; - } - - /// - /// Gets the key used for adding entries. - /// - public string Key { get; } - - /// - /// Gets the . - /// - public ModelMetadata ModelMetadata { get; } - - /// - /// Gets the model instance which is to be validated. - /// - public object Model { get; } - - /// - /// Gets the child nodes. - /// - public IList ChildNodes { get; } - - /// - /// Gets or sets a value that indicates whether all properties of the model should be validated. - /// - public bool ValidateAllProperties { get; set; } - - /// - /// Gets or sets a value that indicates whether validation should be suppressed. - /// - public bool SuppressValidation { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/IValidationStrategy.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/IValidationStrategy.cs new file mode 100644 index 0000000000..19c36b1fab --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/IValidationStrategy.cs @@ -0,0 +1,23 @@ +// 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.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// Defines a strategy for enumerating the child entries of a model object which should be validated. + /// + public interface IValidationStrategy + { + /// + /// Gets an containing a for + /// each child entry of the model object to be validated. + /// + /// The associated with . + /// The model prefix associated with . + /// The model object. + /// An . + IEnumerator GetChildren(ModelMetadata metadata, string key, object model); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs index f5ed4860e3..d600ce7e94 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs @@ -1,64 +1,26 @@ // 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 Microsoft.Framework.Internal; - namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { + /// + /// A context object for . + /// public class ModelValidationContext { - public ModelValidationContext( - [NotNull] ModelBindingContext bindingContext, - [NotNull] ModelExplorer modelExplorer) - : this( - bindingContext.BindingSource, - bindingContext.OperationBindingContext.ValidatorProvider, - bindingContext.ModelState, - modelExplorer) - { - } - - public ModelValidationContext( - BindingSource bindingSource, - [NotNull] IModelValidatorProvider validatorProvider, - [NotNull] ModelStateDictionary modelState, - [NotNull] ModelExplorer modelExplorer) - { - ModelState = modelState; - ValidatorProvider = validatorProvider; - ModelExplorer = modelExplorer; - BindingSource = bindingSource; - } - /// - /// Constructs a new instance of the class using the - /// and . + /// Gets or sets the model object. /// - /// Existing . - /// - /// associated with the new . - /// - /// - /// A new instance of the class using the - /// and . - /// - public static ModelValidationContext GetChildValidationContext( - [NotNull] ModelValidationContext parentContext, - [NotNull] ModelExplorer modelExplorer) - { - return new ModelValidationContext( - modelExplorer.Metadata.BindingSource, - parentContext.ValidatorProvider, - parentContext.ModelState, - modelExplorer); - } - - public ModelExplorer ModelExplorer { get; } - - public ModelStateDictionary ModelState { get; } + public object Model { get; set; } - public BindingSource BindingSource { get; set; } + /// + /// Gets or sets the model container object. + /// + public object Container { get; set; } - public IModelValidatorProvider ValidatorProvider { get; } + /// + /// Gets or sets the associated with . + /// + public ModelMetadata Metadata { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationEntry.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationEntry.cs new file mode 100644 index 0000000000..33856c39a2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationEntry.cs @@ -0,0 +1,51 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// Contains data needed for validating a child entry of a model object. See . + /// + public struct ValidationEntry + { + /// + /// Creates a new . + /// + /// The associated with . + /// The model prefix associated with . + /// The model object. + public ValidationEntry(ModelMetadata metadata, string key, object model) + { + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + Metadata = metadata; + Key = key; + Model = model; + } + + /// + /// The model prefix associated with . + /// + public string Key { get; } + + /// + /// The associated with . + /// + public ModelMetadata Metadata { get; } + + /// + /// The model object. + /// + public object Model { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationStateDictionary.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationStateDictionary.cs new file mode 100644 index 0000000000..1a227692d7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationStateDictionary.cs @@ -0,0 +1,179 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// Used for tracking validation state to customize validation behavior for a model object. + /// + public class ValidationStateDictionary : + IDictionary, + IReadOnlyDictionary + { + private readonly Dictionary _inner; + + /// + /// Creates a new . + /// + public ValidationStateDictionary() + { + _inner = new Dictionary(ReferenceEqualityComparer.Instance); + } + + /// + public ValidationStateEntry this[object key] + { + get + { + ValidationStateEntry entry; + TryGetValue(key, out entry); + return entry; + } + + set + { + _inner[key] = value; + } + } + + /// + public int Count + { + get + { + return _inner.Count; + } + } + + /// + public bool IsReadOnly + { + get + { + return ((IDictionary)_inner).IsReadOnly; + } + } + + /// + public ICollection Keys + { + get + { + return ((IDictionary)_inner).Keys; + } + } + + /// + public ICollection Values + { + get + { + return ((IDictionary)_inner).Values; + } + } + + /// + IEnumerable IReadOnlyDictionary.Keys + { + get + { + return ((IReadOnlyDictionary)_inner).Keys; + } + } + + /// + IEnumerable IReadOnlyDictionary.Values + { + get + { + return ((IReadOnlyDictionary)_inner).Values; + } + } + + /// + public void Add(KeyValuePair item) + { + ((IDictionary)_inner).Add(item); + } + + /// + public void Add(object key, ValidationStateEntry value) + { + _inner.Add(key, value); + } + + /// + public void Clear() + { + _inner.Clear(); + } + + /// + public bool Contains(KeyValuePair item) + { + return ((IDictionary)_inner).Contains(item); + } + + /// + public bool ContainsKey(object key) + { + return _inner.ContainsKey(key); + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)_inner).CopyTo(array, arrayIndex); + } + + /// + public IEnumerator> GetEnumerator() + { + return ((IDictionary)_inner).GetEnumerator(); + } + + /// + public bool Remove(KeyValuePair item) + { + return _inner.Remove(item); + } + + /// + public bool Remove(object key) + { + return _inner.Remove(key); + } + + /// + public bool TryGetValue(object key, out ValidationStateEntry value) + { + return _inner.TryGetValue(key, out value); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IDictionary)_inner).GetEnumerator(); + } + + private class ReferenceEqualityComparer : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + + public new bool Equals(object x, object y) + { + return Object.ReferenceEquals(x, y); + } + + public int GetHashCode(object obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationStateEntry.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationStateEntry.cs new file mode 100644 index 0000000000..208d5062fa --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ValidationStateEntry.cs @@ -0,0 +1,33 @@ +// 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.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// An entry in a . Records state information to override the default + /// behavior of validation for an object. + /// + public class ValidationStateEntry + { + /// + /// Gets or sets the model prefix associated with the entry. + /// + public string Key { get; set; } + + /// + /// Gets or sets the associated with the entry. + /// + public ModelMetadata Metadata { get; set; } + + /// + /// Gets or sets a value indicating whether the associated model object should be validated. + /// + public bool SuppressValidation { get; set; } + + /// + /// Gets or sets an for enumerating child entries of the associated + /// model object. + /// + public IValidationStrategy Strategy { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs index 729f1a1780..25ea0482f0 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs @@ -82,20 +82,14 @@ public async Task BindModelAsync( parameter.Name); var modelBindingResult = await operationContext.ModelBinder.BindModelAsync(modelBindingContext); - if (modelBindingResult.IsModelSet && - modelBindingResult.ValidationNode != null) + if (modelBindingResult.IsModelSet) { - var modelExplorer = new ModelExplorer( - _modelMetadataProvider, - metadata, - modelBindingResult.Model); - var validationContext = new ModelValidationContext( - modelBindingContext.BindingSource, + _validator.Validate( operationContext.ValidatorProvider, modelState, - modelExplorer); - - _validator.Validate(validationContext, modelBindingResult.ValidationNode); + modelBindingContext.ValidationState, + modelBindingResult.Key, + modelBindingResult.Model); } return modelBindingResult; diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs index cfac161936..3cf40ec7f1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/BodyModelBinder.cs @@ -87,12 +87,7 @@ private async Task BindModelCoreAsync([NotNull] ModelBinding return ModelBindingResult.Failed(modelBindingKey); } - var validationNode = new ModelValidationNode(modelBindingKey, bindingContext.ModelMetadata, model) - { - ValidateAllProperties = true - }; - - return ModelBindingResult.Success(modelBindingKey, model, validationNode); + return ModelBindingResult.Success(modelBindingKey, model); } catch (Exception ex) { diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs index fea4920b5f..a32c2a1835 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ByteArrayModelBinder.cs @@ -44,12 +44,7 @@ public Task BindModelAsync([NotNull] ModelBindingContext bin try { var model = Convert.FromBase64String(value); - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } catch (Exception ex) { diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs index 0ebdb0ca7c..0ded18aa48 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CancellationTokenModelBinder.cs @@ -17,13 +17,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex if (bindingContext.ModelType == typeof(CancellationToken)) { var model = bindingContext.OperationBindingContext.HttpContext.RequestAborted; - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model) - { - SuppressValidation = true, - }; - - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } return ModelBindingResult.NoResultAsync; diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CollectionModelBinder.cs index 2582f954d1..d797b80fab 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CollectionModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CollectionModelBinder.cs @@ -11,6 +11,7 @@ using System.Reflection; #endif using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -38,12 +39,7 @@ public virtual async Task BindModelAsync([NotNull] ModelBind model = CreateEmptyCollection(bindingContext.ModelType); } - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - - return ModelBindingResult.Success(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.Success(bindingContext.ModelName, model); } return ModelBindingResult.NoResult; @@ -72,6 +68,15 @@ public virtual async Task BindModelAsync([NotNull] ModelBind CopyToModel(model, boundCollection); } + Debug.Assert(model != null); + if (result.ValidationStrategy != null) + { + bindingContext.ValidationState.Add(model, new ValidationStateEntry() + { + Strategy = result.ValidationStrategy, + }); + } + if (valueProviderResult != ValueProviderResult.None) { // If we did simple binding, then modelstate should be updated to reflect what we bound for ModelName. @@ -81,7 +86,7 @@ public virtual async Task BindModelAsync([NotNull] ModelBind valueProviderResult); } - return ModelBindingResult.Success(bindingContext.ModelName, model, result.ValidationNode); + return ModelBindingResult.Success(bindingContext.ModelName, model); } /// @@ -137,11 +142,6 @@ internal async Task BindSimpleCollection( var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - boundCollection); - var innerBindingContext = ModelBindingContext.CreateChildBindingContext( bindingContext, elementMetadata, @@ -164,18 +164,12 @@ internal async Task BindSimpleCollection( if (result != null && result.IsModelSet) { boundValue = result.Model; - if (result.ValidationNode != null) - { - validationNode.ChildNodes.Add(result.ValidationNode); - } - boundCollection.Add(ModelBindingHelper.CastOrDefault(boundValue)); } } return new CollectionResult { - ValidationNode = validationNode, Model = boundCollection }; } @@ -211,10 +205,7 @@ internal async Task BindComplexCollectionFromIndexes( var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement)); var boundCollection = new List(); - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - boundCollection); + foreach (var indexName in indexNames) { var fullChildName = ModelNames.CreateIndexModelName(bindingContext.ModelName, indexName); @@ -235,10 +226,6 @@ internal async Task BindComplexCollectionFromIndexes( { didBind = true; boundValue = result.Model; - if (result.ValidationNode != null) - { - validationNode.ChildNodes.Add(result.ValidationNode); - } } // infinite size collection stops on first bind failure @@ -252,17 +239,26 @@ internal async Task BindComplexCollectionFromIndexes( return new CollectionResult { - ValidationNode = validationNode, - Model = boundCollection + Model = boundCollection, + + // If we're working with a fixed set of indexes then this is the format like: + // + // ?parameter.index=zero,one,two¶meter[zero]=0&¶meter[one]=1¶meter[two]=2... + // + // We need to provide this data to the validation system so it can 'replay' the keys. + // But we can't just set ValidationState here, because it needs the 'real' model. + ValidationStrategy = indexNamesIsFinite ? + new ExplicitIndexCollectionValidationStrategy(indexNames) : + null, }; } // Internal for testing. internal class CollectionResult { - public ModelValidationNode ValidationNode { get; set; } - public IEnumerable Model { get; set; } + + public IValidationStrategy ValidationStrategy { get; set; } } /// diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeModelBinder.cs index 40ed1ee1bd..3bd49f42b9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeModelBinder.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -60,6 +61,19 @@ private async Task RunModelBinders(ModelBindingContext bindi if (result.IsModelSet || bindingContext.ModelState.ContainsKey(bindingContext.ModelName)) { + if (bindingContext.IsTopLevelObject && result.Model != null) + { + ValidationStateEntry entry; + if (!bindingContext.ValidationState.TryGetValue(result.Model, out entry)) + { + entry = new ValidationStateEntry(); + bindingContext.ValidationState.Add(result.Model, entry); + } + + entry.Key = entry.Key ?? result.Key; + entry.Metadata = entry.Metadata ?? bindingContext.ModelMetadata; + } + return result; } @@ -123,6 +137,7 @@ private static ModelBindingContext CreateNewBindingContext(ModelBindingContext o BindingSource = oldBindingContext.BindingSource, BinderType = oldBindingContext.BinderType, IsTopLevelObject = oldBindingContext.IsTopLevelObject, + ValidationState = oldBindingContext.ValidationState, }; if (bindingSource != null && bindingSource.IsGreedy) diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs index 28043fdcf6..68898d0ee4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs @@ -9,6 +9,7 @@ using System.Reflection; #endif using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -24,7 +25,7 @@ public class DictionaryModelBinder : CollectionModelBinder BindModelAsync([NotNull] ModelBindingContext bindingContext) { var result = await base.BindModelAsync(bindingContext); - if (result == null || !result.IsModelSet) + if (!result.IsModelSet) { // No match for the prefix at all. return result; @@ -65,8 +66,8 @@ public override async Task BindModelAsync([NotNull] ModelBin model: null); var modelBinder = bindingContext.OperationBindingContext.ModelBinder; - var validationNode = result.ValidationNode; + var keyMappings = new Dictionary(StringComparer.Ordinal); foreach (var kvp in keys) { // Use InvariantCulture to convert the key since ExpressionHelper.GetExpressionText() would use @@ -79,12 +80,14 @@ public override async Task BindModelAsync([NotNull] ModelBin // Always add an entry to the dictionary but validate only if binding was successful. model[convertedKey] = ModelBindingHelper.CastOrDefault(valueResult.Model); - if (valueResult.IsModelSet) - { - validationNode.ChildNodes.Add(valueResult.ValidationNode); - } + keyMappings.Add(kvp.Key, convertedKey); } + bindingContext.ValidationState.Add(model, new ValidationStateEntry() + { + Strategy = new ShortFormDictionaryValidationStrategy(keyMappings, valueMetadata), + }); + return result; } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs index ac073085d4..0c11c55000 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormCollectionModelBinder.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; using Microsoft.Framework.Primitives; @@ -46,13 +47,8 @@ private async Task BindModelCoreAsync(ModelBindingContext bi model = new EmptyFormCollection(); } - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model) - { - SuppressValidation = true, - }; - - return ModelBindingResult.Success(bindingContext.ModelName, model, validationNode); + bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true }); + return ModelBindingResult.Success(bindingContext.ModelName, model); } private class EmptyFormCollection : IFormCollection diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormFileModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormFileModelBinder.cs index cb320f918f..bbe3a46eb2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormFileModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/FormFileModelBinder.cs @@ -10,6 +10,7 @@ #endif using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; @@ -62,18 +63,13 @@ private async Task BindModelCoreAsync(ModelBindingContext bi } else { - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, value) - { - SuppressValidation = true, - }; - + bindingContext.ValidationState.Add(value, new ValidationStateEntry() { SuppressValidation = true }); bindingContext.ModelState.SetModelValue( bindingContext.ModelName, rawValue: null, attemptedValue: null); - return ModelBindingResult.Success(bindingContext.ModelName, value, validationNode); + return ModelBindingResult.Success(bindingContext.ModelName, value); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/HeaderModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/HeaderModelBinder.cs index 5397332f27..fbd4fa8770 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/HeaderModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/HeaderModelBinder.cs @@ -63,17 +63,12 @@ public Task BindModelAsync(ModelBindingContext bindingContex } else { - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - bindingContext.ModelState.SetModelValue( bindingContext.ModelName, request.Headers.GetCommaSeparatedValues(headerName), request.Headers[headerName]); - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs index 7d11b6eb28..f40f8d69a0 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/KeyValuePairModelBinder.cs @@ -15,9 +15,8 @@ public async Task BindModelAsync(ModelBindingContext binding typeof(KeyValuePair), allowNullModel: true); - var childNodes = new List(); - var keyResult = await TryBindStrongModel(bindingContext, "Key", childNodes); - var valueResult = await TryBindStrongModel(bindingContext, "Value", childNodes); + var keyResult = await TryBindStrongModel(bindingContext, "Key"); + var valueResult = await TryBindStrongModel(bindingContext, "Value"); if (keyResult.IsModelSet && valueResult.IsModelSet) { @@ -25,15 +24,7 @@ public async Task BindModelAsync(ModelBindingContext binding ModelBindingHelper.CastOrDefault(keyResult.Model), ModelBindingHelper.CastOrDefault(valueResult.Model)); - // Update the model for the top level validation node. - var modelValidationNode = - new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model, - childNodes); - - return ModelBindingResult.Success(bindingContext.ModelName, model, modelValidationNode); + return ModelBindingResult.Success(bindingContext.ModelName, model); } else if (!keyResult.IsModelSet && valueResult.IsModelSet) { @@ -62,13 +53,7 @@ public async Task BindModelAsync(ModelBindingContext binding if (bindingContext.IsTopLevelObject) { var model = new KeyValuePair(); - - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - - return ModelBindingResult.Success(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.Success(bindingContext.ModelName, model); } return ModelBindingResult.NoResult; @@ -77,8 +62,7 @@ public async Task BindModelAsync(ModelBindingContext binding internal async Task TryBindStrongModel( ModelBindingContext parentBindingContext, - string propertyName, - List childNodes) + string propertyName) { var propertyModelMetadata = parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(typeof(TModel)); @@ -91,19 +75,16 @@ internal async Task TryBindStrongModel( propertyModelName, model: null); - var modelBindingResult = await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync( + var result = await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync( propertyBindingContext); - if (modelBindingResult != ModelBindingResult.NoResult) + if (result.IsModelSet) { - if (modelBindingResult.ValidationNode != null) - { - childNodes.Add(modelBindingResult.ValidationNode); - } - - return modelBindingResult; + return result; + } + else + { + return ModelBindingResult.Failed(propertyModelName); } - - return ModelBindingResult.Failed(propertyModelName); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ModelBindingHelper.cs index 374047fe0a..9134c68af1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ModelBindingHelper.cs @@ -309,22 +309,12 @@ public static async Task TryUpdateModelAsync( var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext); if (modelBindingResult.IsModelSet) { - var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, modelBindingResult.Model); - var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer); - - var validationNode = modelBindingResult.ValidationNode; - if (validationNode == null) - { - validationNode = new ModelValidationNode( - modelBindingResult.Key, - modelMetadata, - modelBindingResult.Model) - { - ValidateAllProperties = true, - }; - } - - objectModelValidator.Validate(modelValidationContext, validationNode); + objectModelValidator.Validate( + operationBindingContext.ValidatorProvider, + modelState, + modelBindingContext.ValidationState, + modelBindingResult.Key, + modelBindingResult.Model); return modelState.IsValid; } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs index 4491225b5c..77bac2c741 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/MutableObjectModelBinder.cs @@ -52,16 +52,11 @@ private async Task BindModelCoreAsync( var results = await BindPropertiesAsync(bindingContext, mutableObjectBinderContext.PropertyMetadata); - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - // Post-processing e.g. property setters and hooking up validation. bindingContext.Model = model; - ProcessResults(bindingContext, results, validationNode); + ProcessResults(bindingContext, results); - return ModelBindingResult.Success(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.Success(bindingContext.ModelName, model); } /// @@ -398,10 +393,9 @@ internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingCon } // Internal for testing. - internal ModelValidationNode ProcessResults( + internal void ProcessResults( ModelBindingContext bindingContext, - IDictionary results, - ModelValidationNode validationNode) + IDictionary results) { var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = @@ -432,20 +426,8 @@ internal ModelValidationNode ProcessResults( var result = entry.Value; var propertyMetadata = entry.Key; SetProperty(bindingContext, modelExplorer, propertyMetadata, result); - - var propertyValidationNode = result.ValidationNode; - if (propertyValidationNode == null) - { - // Make sure that irrespective of whether the properties of the model were bound with a value, - // create a validation node so that these get validated. - propertyValidationNode = new ModelValidationNode(result.Key, entry.Key, result.Model); - } - - validationNode.ChildNodes.Add(propertyValidationNode); } } - - return validationNode; } /// @@ -576,30 +558,6 @@ private static void AddModelError( } } - // Returns true if validator execution adds a model error. - private static bool RunValidator( - IModelValidator validator, - ModelBindingContext bindingContext, - ModelExplorer propertyExplorer, - string modelStateKey) - { - var validationContext = new ModelValidationContext(bindingContext, propertyExplorer); - - var addedError = false; - foreach (var validationResult in validator.Validate(validationContext)) - { - bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message); - addedError = true; - } - - if (!addedError) - { - bindingContext.ModelState.MarkFieldValid(modelStateKey); - } - - return addedError; - } - internal sealed class PropertyValidationInfo { public PropertyValidationInfo() diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ServicesModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ServicesModelBinder.cs index c28cab3442..eab4244eb3 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ServicesModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/ServicesModelBinder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -30,13 +31,10 @@ public Task BindModelAsync(ModelBindingContext bindingContex var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices; var model = requestServices.GetRequiredService(bindingContext.ModelType); - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model) - { - SuppressValidation = true - }; - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode); + bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true }); + + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs index ae4756ebd5..3368a48971 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/SimpleTypeModelBinder.cs @@ -57,12 +57,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex } else { - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } } catch (Exception ex) diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultCollectionValidationStrategy.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultCollectionValidationStrategy.cs new file mode 100644 index 0000000000..d29ba1e1b2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultCollectionValidationStrategy.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// The default implementation of for a collection. + /// + /// + /// This implementation handles cases like: + /// + /// Model: IList<Student> + /// Query String: ?students[0].Age=8&students[1].Age=9 + /// + /// In this case the elements of the collection are identified in the input data set by an incrementing + /// integer index. + /// + /// + /// or: + /// + /// + /// Model: IDictionary<string, int> + /// Query String: ?students[0].Key=Joey&students[0].Value=8 + /// + /// In this case the dictionary is treated as a collection of key-value pairs, and the elements of the + /// collection are identified in the input data set by an incrementing integer index. + /// + /// + /// Using this key format, the enumerator enumerates model objects of type matching + /// . The indices of the elements in the collection are used to + /// compute the model prefix keys. + /// + public class DefaultCollectionValidationStrategy : IValidationStrategy + { + /// + /// Gets an instance of . + /// + public static readonly IValidationStrategy Instance = new DefaultCollectionValidationStrategy(); + + private DefaultCollectionValidationStrategy() + { + } + + /// + public IEnumerator GetChildren( + ModelMetadata metadata, + string key, + object model) + { + return new Enumerator(metadata.ElementMetadata, key, (IEnumerable)model); + } + + private class Enumerator : IEnumerator + { + private readonly string _key; + private readonly ModelMetadata _metadata; + private readonly IEnumerable _model; + private readonly IEnumerator _enumerator; + + private ValidationEntry _entry; + private int _index; + + public Enumerator( + ModelMetadata metadata, + string key, + IEnumerable model) + { + _metadata = metadata; + _key = key; + _model = model; + + _enumerator = _model.GetEnumerator(); + + _index = -1; + } + + public ValidationEntry Current + { + get + { + return _entry; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public bool MoveNext() + { + _index++; + if (!_enumerator.MoveNext()) + { + return false; + } + + var key = ModelNames.CreateIndexModelName(_key, _index); + var model = _enumerator.Current; + + _entry = new ValidationEntry(_metadata, key, model); + + return true; + } + + public void Dispose() + { + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs new file mode 100644 index 0000000000..1e7205deed --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// The default implementation of for a complex object. + /// + public class DefaultComplexObjectValidationStrategy : IValidationStrategy + { + /// + /// Gets an instance of . + /// + public static readonly IValidationStrategy Instance = new DefaultComplexObjectValidationStrategy(); + + private DefaultComplexObjectValidationStrategy() + { + } + + /// + public IEnumerator GetChildren( + ModelMetadata metadata, + string key, + object model) + { + return new Enumerator(metadata.Properties, key, model); + } + + private class Enumerator : IEnumerator + { + private readonly string _key; + private readonly object _model; + private readonly ModelPropertyCollection _properties; + + private ValidationEntry _entry; + private int _index; + + public Enumerator( + ModelPropertyCollection properties, + string key, + object model) + { + _properties = properties; + _key = key; + _model = model; + + _index = -1; + } + + public ValidationEntry Current + { + get + { + return _entry; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public bool MoveNext() + { + _index++; + if (_index >= _properties.Count) + { + return false; + } + + var property = _properties[_index]; + var propertyName = property.BinderModelName ?? property.PropertyName; + var key = ModelNames.CreatePropertyModelName(_key, propertyName); + var model = property.PropertyGetter(_model); + + _entry = new ValidationEntry(property, key, model); + + return true; + } + + public void Dispose() + { + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs index c426c1f27c..6e090cea58 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs @@ -2,18 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { /// - /// Recursively validate an object. + /// The default implementation of . /// public class DefaultObjectValidator : IObjectModelValidator { @@ -27,324 +21,49 @@ public class DefaultObjectValidator : IObjectModelValidator /// types to exclude from validation. /// The . public DefaultObjectValidator( - [NotNull] IList excludeFilters, - [NotNull] IModelMetadataProvider modelMetadataProvider) + IList excludeFilters, + IModelMetadataProvider modelMetadataProvider) { - _modelMetadataProvider = modelMetadataProvider; - _excludeFilters = excludeFilters; - } - - /// - public void Validate( - [NotNull] ModelValidationContext modelValidationContext, - [NotNull] ModelValidationNode validationNode) - { - var validationContext = new ValidationContext() - { - ModelValidationContext = modelValidationContext, - Visited = new HashSet(ReferenceEqualityComparer.Instance), - ValidationNode = validationNode - }; - - ValidateNonVisitedNodeAndChildren( - validationNode.Key, - validationContext, - validators: null); - } - - private bool ValidateNonVisitedNodeAndChildren( - string modelKey, - ValidationContext validationContext, - IList validators) - { - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); - - var modelValidationContext = validationContext.ModelValidationContext; - var modelExplorer = modelValidationContext.ModelExplorer; - var modelState = modelValidationContext.ModelState; - var currentValidationNode = validationContext.ValidationNode; - if (currentValidationNode.SuppressValidation) - { - // Short circuit if the node is marked to be suppressed. - // If there are any sub entries which were model bound, they need to be marked as skipped, - // Otherwise they will remain as unvalidated and the model state would be Invalid. - MarkChildNodesAsSkipped(modelKey, modelExplorer.Metadata, validationContext); - - // For validation purposes this model is valid. - return true; - } - - if (modelState.HasReachedMaxErrors) - { - // Short circuit if max errors have been recorded. In which case we treat this as invalid. - return false; - } - - var isValid = true; - if (validators == null) - { - // The validators are not null in the case of validating an array. Since the validators are - // the same for all the elements of the array, we do not do GetValidators for each element, - // instead we just pass them over. - validators = GetValidators(modelValidationContext.ValidatorProvider, modelExplorer.Metadata); - } - - // We don't need to recursively traverse the graph if there are no child nodes. - if (currentValidationNode.ChildNodes.Count == 0 && !currentValidationNode.ValidateAllProperties) - { - return ShallowValidate(modelKey, modelExplorer, validationContext, validators); - } - - // We don't need to recursively traverse the graph for types that shouldn't be validated - var modelType = modelExplorer.ModelType; - if (IsTypeExcludedFromValidation(_excludeFilters, modelType)) - { - var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators); - - // If there are any sub entries which were model bound, they need to be marked as skipped, - // Otherwise they will remain as unvalidated and the model state would be Invalid. - MarkChildNodesAsSkipped(modelKey, modelExplorer.Metadata, validationContext); - return result; - } - - // Check to avoid infinite recursion. This can happen with cycles in an object graph. - // Note that this is only applicable in case the model is pre-existing (like in case of TryUpdateModel). - if (validationContext.Visited.Contains(modelExplorer.Model)) - { - return true; - } - - validationContext.Visited.Add(modelExplorer.Model); - isValid = ValidateChildNodes(modelKey, modelExplorer, validationContext); - if (isValid) - { - // Don't bother to validate this node if children failed. - isValid = ShallowValidate(modelKey, modelExplorer, validationContext, validators); - } - - // Pop the object so that it can be validated again in a different path - validationContext.Visited.Remove(modelExplorer.Model); - return isValid; - } - - private void MarkChildNodesAsSkipped(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext) - { - var modelState = validationContext.ModelValidationContext.ModelState; - var fieldValidationState = modelState.GetFieldValidationState(currentModelKey); - - // Since shallow validation is done, if the modelvalidation state is still marked as unvalidated, - // it is because some properties in the subtree are marked as unvalidated. Mark all such properties - // as skipped. Models which have their subtrees as Valid or Invalid do not need to be marked as skipped. - if (fieldValidationState != ModelValidationState.Unvalidated) - { - return; - } - - // At this point we just want to mark all sub-entries present in the model state as skipped. - var entries = modelState.FindKeysWithPrefix(currentModelKey); - foreach (var entry in entries) - { - entry.Value.ValidationState = ModelValidationState.Skipped; - } - } - - private IList GetValidators(IModelValidatorProvider provider, ModelMetadata metadata) - { - var validatorProviderContext = new ModelValidatorProviderContext(metadata); - provider.GetValidators(validatorProviderContext); - - return validatorProviderContext - .Validators - .OrderBy(v => v, ValidatorOrderComparer.Instance) - .ToList(); - } - - private bool ValidateChildNodes( - string currentModelKey, - ModelExplorer modelExplorer, - ValidationContext validationContext) - { - var isValid = true; - var childNodes = GetChildNodes(validationContext, modelExplorer); - - IList validators = null; - var elementMetadata = modelExplorer.Metadata.ElementMetadata; - if (elementMetadata != null) - { - validators = GetValidators(validationContext.ModelValidationContext.ValidatorProvider, elementMetadata); - } - - foreach (var childNode in childNodes) - { - var childModelExplorer = childNode.ModelMetadata.MetadataKind == Metadata.ModelMetadataKind.Type ? - _modelMetadataProvider.GetModelExplorerForType(childNode.ModelMetadata.ModelType, childNode.Model) : - modelExplorer.GetExplorerForProperty(childNode.ModelMetadata.PropertyName); - - var propertyValidationContext = new ValidationContext() - { - ModelValidationContext = ModelValidationContext.GetChildValidationContext( - validationContext.ModelValidationContext, - childModelExplorer), - Visited = validationContext.Visited, - ValidationNode = childNode - }; - - if (!ValidateNonVisitedNodeAndChildren( - childNode.Key, - propertyValidationContext, - validators)) - { - isValid = false; - } - } - - return isValid; - } - - // Validates a single node (not including children) - // Returns true if validation passes successfully - private static bool ShallowValidate( - string modelKey, - ModelExplorer modelExplorer, - ValidationContext validationContext, - IList validators) - { - var isValid = true; - - var modelState = validationContext.ModelValidationContext.ModelState; - var fieldValidationState = modelState.GetFieldValidationState(modelKey); - if (fieldValidationState == ModelValidationState.Invalid) + if (excludeFilters == null) { - // Even if we have no validators it's possible that model binding may have added a - // validation error (conversion error, missing data). We want to still run - // validators even if that's the case. - isValid = false; + throw new ArgumentNullException(nameof(excludeFilters)); } - // When the are no validators we bail quickly. This saves a GetEnumerator allocation. - // In a large array (tens of thousands or more) scenario it's very significant. - if (validators == null || validators.Count > 0) + if (modelMetadataProvider == null) { - var modelValidationContext = ModelValidationContext.GetChildValidationContext( - validationContext.ModelValidationContext, - modelExplorer); - - var modelValidationState = modelState.GetValidationState(modelKey); - - // If either the model or its properties are unvalidated, validate them now. - if (modelValidationState == ModelValidationState.Unvalidated || - fieldValidationState == ModelValidationState.Unvalidated) - { - foreach (var validator in validators) - { - foreach (var error in validator.Validate(modelValidationContext)) - { - var errorKey = ModelNames.CreatePropertyModelName(modelKey, error.MemberName); - if (!modelState.TryAddModelError(errorKey, error.Message) && - modelState.GetFieldValidationState(errorKey) == ModelValidationState.Unvalidated) - { - - // If we are not able to add a model error - // for instance when the max error count is reached, mark the model as skipped. - modelState.MarkFieldSkipped(errorKey); - } - - isValid = false; - } - } - } + throw new ArgumentNullException(nameof(modelMetadataProvider)); } - // Add an entry only if there was an entry which was added by a model binder. - // This prevents adding spurious entries. - if (modelState.ContainsKey(modelKey) && isValid) - { - validationContext.ModelValidationContext.ModelState.MarkFieldValid(modelKey); - } - - return isValid; - } - - private bool IsTypeExcludedFromValidation(IList filters, Type type) - { - return filters.Any(filter => filter.IsTypeExcluded(type)); + _modelMetadataProvider = modelMetadataProvider; + _excludeFilters = excludeFilters; } - private IList GetChildNodes(ValidationContext context, ModelExplorer modelExplorer) + /// + public void Validate( + IModelValidatorProvider validatorProvider, + ModelStateDictionary modelState, + ValidationStateDictionary validationState, + string prefix, + object model) { - var validationNode = context.ValidationNode; - - // This is the trivial case where the node-tree that was built-up during binding already has - // all of the nodes we need. - if (validationNode.ChildNodes.Count != 0 || - !validationNode.ValidateAllProperties || - validationNode.Model == null) + if (validatorProvider == null) { - return validationNode.ChildNodes; + throw new ArgumentNullException(nameof(validatorProvider)); } - var childNodes = new List(validationNode.ChildNodes); - var elementMetadata = modelExplorer.Metadata.ElementMetadata; - if (elementMetadata == null) + if (modelState == null) { - foreach (var property in validationNode.ModelMetadata.Properties) - { - var propertyExplorer = modelExplorer.GetExplorerForProperty(property.PropertyName); - var propertyBindingName = property.BinderModelName ?? property.PropertyName; - var childKey = ModelNames.CreatePropertyModelName(validationNode.Key, propertyBindingName); - var childNode = new ModelValidationNode(childKey, property, propertyExplorer.Model) - { - ValidateAllProperties = true - }; - childNodes.Add(childNode); - } + throw new ArgumentNullException(nameof(modelState)); } - else - { - var enumerableModel = (IEnumerable)modelExplorer.Model; - - // An integer index is incorrect in scenarios where there is a custom index provided by the user. - // However those scenarios are supported by createing a ModelValidationNode with the right keys. - var index = 0; - foreach (var element in enumerableModel) - { - var elementExplorer = new ModelExplorer(_modelMetadataProvider, elementMetadata, element); - var elementKey = ModelNames.CreateIndexModelName(validationNode.Key, index); - var childNode = new ModelValidationNode(elementKey, elementMetadata, elementExplorer.Model) - { - ValidateAllProperties = true - }; - childNodes.Add(childNode); - index++; - } - } + var visitor = new ValidationVisitor( + validatorProvider, + _excludeFilters, + modelState, + validationState); - return childNodes; - } - - private class ValidationContext - { - public ModelValidationContext ModelValidationContext { get; set; } - - public HashSet Visited { get; set; } - - public ModelValidationNode ValidationNode { get; set; } - } - - // Sorts validators based on whether or not they are 'required'. We want to run - // 'required' validators first so that we get the best possible error message. - private class ValidatorOrderComparer : IComparer - { - public static readonly ValidatorOrderComparer Instance = new ValidatorOrderComparer(); - - public int Compare(IModelValidator x, IModelValidator y) - { - var xScore = x.IsRequired ? 0 : 1; - var yScore = y.IsRequired ? 0 : 1; - return xScore.CompareTo(yScore); - } + var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType()); + visitor.Validate(metadata, prefix, model); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ExplicitIndexCollectionValidationStrategy.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ExplicitIndexCollectionValidationStrategy.cs new file mode 100644 index 0000000000..0222674e74 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ExplicitIndexCollectionValidationStrategy.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// An implementation of for a collection bound using 'explict indexing' + /// style keys. + /// + /// + /// This implemenation handles cases like: + /// + /// Model: IList<Student> + /// Query String: ?students.index=Joey,Katherine&students[Joey].Age=8&students[Katherine].Age=9 + /// + /// In this case, 'Joey' and 'Katherine' need to be used in the model prefix keys, but cannot be inferred + /// form inspecting the collection. These prefixes are captured during model binding, and mapped to + /// the corresponding ordinal index of a model object in the collection. The enumerator returned from this + /// class will yield two 'Student' objects with corresponding keys 'students[Joey]' and 'students[Katherine]'. + /// + /// + /// Using this key format, the enumerator enumerates model objects of type matching + /// . The keys captured during model binding are mapped to the elements + /// in the collection to compute the model prefix keys. + /// + public class ExplicitIndexCollectionValidationStrategy : IValidationStrategy + { + /// + /// Creates a new . + /// + /// The keys of collection elements that were used during model binding. + public ExplicitIndexCollectionValidationStrategy(IEnumerable elementKeys) + { + if (elementKeys == null) + { + throw new ArgumentNullException(nameof(elementKeys)); + } + + ElementKeys = elementKeys; + } + + /// + /// Gets the keys of collection elements that were used during model binding. + /// + public IEnumerable ElementKeys { get; } + + /// + public IEnumerator GetChildren( + ModelMetadata metadata, + string key, + object model) + { + return new Enumerator(metadata.ElementMetadata, key, ElementKeys, (IEnumerable)model); + } + + private class Enumerator : IEnumerator + { + private readonly string _key; + private readonly ModelMetadata _metadata; + private readonly IEnumerator _enumerator; + private readonly IEnumerator _keyEnumerator; + + private ValidationEntry _entry; + + public Enumerator( + ModelMetadata metadata, + string key, + IEnumerable elementKeys, + IEnumerable model) + { + _metadata = metadata; + _key = key; + + _keyEnumerator = elementKeys.GetEnumerator(); + _enumerator = model.GetEnumerator(); + } + + public ValidationEntry Current + { + get + { + return _entry; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public bool MoveNext() + { + if (!_keyEnumerator.MoveNext()) + { + return false; + } + + if (!_enumerator.MoveNext()) + { + return false; + } + + var model = _enumerator.Current; + var key = ModelNames.CreateIndexModelName(_key, _keyEnumerator.Current); + + _entry = new ValidationEntry(_metadata, key, model); + + return true; + } + + public void Dispose() + { + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IObjectModelValidator.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IObjectModelValidator.cs index 54c01631a8..c815bc9ab3 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IObjectModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IObjectModelValidator.cs @@ -1,6 +1,8 @@ // 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 Microsoft.Framework.Internal; + namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { /// @@ -9,12 +11,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation public interface IObjectModelValidator { /// - /// Validates the given model in . + /// Validates the provided object. /// - /// The associated with the current call. - /// - /// The for the model which gets validated. + /// The . + /// The . + /// The . May be null. + /// + /// The model prefix. Used to map the model object to entries in . /// - void Validate(ModelValidationContext validationContext, ModelValidationNode validationNode); + /// The model object. + void Validate( + IModelValidatorProvider validatorProvider, + ModelStateDictionary modelState, + ValidationStateDictionary validationState, + string prefix, + object model); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ShortFormDictionaryValidationStrategy.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ShortFormDictionaryValidationStrategy.cs new file mode 100644 index 0000000000..673fbe235d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ShortFormDictionaryValidationStrategy.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// An implementation of for a dictionary bound with 'short form' style keys. + /// + /// The of the keys of the model dictionary. + /// The of the values of the model dictionary. + /// + /// This implemenation handles cases like: + /// + /// Model: IDictionary<string, Student> + /// Query String: ?students[Joey].Age=8&students[Katherine].Age=9 + /// + /// In this case, 'Joey' and 'Katherine' are the keys of the dictionary, used to bind two 'Student' + /// objects. The enumerator returned from this class will yield two 'Student' objects with corresponding + /// keys 'students[Joey]' and 'students[Katherine]' + /// + /// + /// Using this key format, the enumerator enumerates model objects of type . The + /// keys of the dictionary are not validated as they must be simple types. + /// + public class ShortFormDictionaryValidationStrategy : IValidationStrategy + { + private readonly ModelMetadata _valueMetadata; + + /// + /// Creates a new . + /// + /// The mapping from model prefix key to dictionary key. + /// + /// The associated with . + /// + public ShortFormDictionaryValidationStrategy( + IEnumerable> keyMappings, + ModelMetadata valueMetadata) + { + KeyMappings = keyMappings; + _valueMetadata = valueMetadata; + } + + /// + /// Gets the mapping from model prefix key to dictionary key. + /// + public IEnumerable> KeyMappings { get; } + + /// + public IEnumerator GetChildren( + ModelMetadata metadata, + string key, + object model) + { + return new Enumerator(_valueMetadata, key, KeyMappings, (IDictionary)model); + } + + private class Enumerator : IEnumerator + { + private readonly string _key; + private readonly ModelMetadata _metadata; + private readonly IDictionary _model; + private readonly IEnumerator> _keyMappingEnumerator; + + private ValidationEntry _entry; + + public Enumerator( + ModelMetadata metadata, + string key, + IEnumerable> keyMappings, + IDictionary model) + { + _metadata = metadata; + _key = key; + _model = model; + + _keyMappingEnumerator = keyMappings.GetEnumerator(); + } + + public ValidationEntry Current + { + get + { + return _entry; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public bool MoveNext() + { + TValue value; + while (true) + { + if (!_keyMappingEnumerator.MoveNext()) + { + return false; + } + + if (_model.TryGetValue(_keyMappingEnumerator.Current.Value, out value)) + { + // Skip over entries that we can't find in the dictionary, they will show up as unvalidated. + break; + } + } + + var key = ModelNames.CreateIndexModelName(_key, _keyMappingEnumerator.Current.Key); + var model = value; + + _entry = new ValidationEntry(_metadata, key, model); + + return true; + } + + public void Dispose() + { + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs new file mode 100644 index 0000000000..1f88660da3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -0,0 +1,366 @@ +// 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.Linq; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// A visitor implementation that interprets to traverse + /// a model object graph and perform validation. + /// + public class ValidationVisitor + { + private readonly IModelValidatorProvider _validatorProvider; + private readonly IList _excludeFilters; + private readonly ModelStateDictionary _modelState; + private readonly ValidationStateDictionary _validationState; + + private object _container; + private string _key; + private object _model; + private ModelMetadata _metadata; + private IValidationStrategy _strategy; + + private HashSet _currentPath; + + /// + /// Creates a new . + /// + /// The . + /// The list of . + /// The . + /// The . + public ValidationVisitor( + IModelValidatorProvider validatorProvider, + IList excludeFilters, + ModelStateDictionary modelState, + ValidationStateDictionary validationState) + { + if (validatorProvider == null) + { + throw new ArgumentNullException(nameof(validatorProvider)); + } + + if (excludeFilters == null) + { + throw new ArgumentNullException(nameof(excludeFilters)); + } + + if (modelState == null) + { + throw new ArgumentNullException(nameof(modelState)); + } + + _validatorProvider = validatorProvider; + _excludeFilters = excludeFilters; + _modelState = modelState; + _validationState = validationState; + + _currentPath = new HashSet(ReferenceEqualityComparer.Instance); + } + + /// + /// Validates a object. + /// + /// The associated with the model. + /// The model prefix key. + /// The model object. + /// true if the object is valid, otherwise false. + public bool Validate(ModelMetadata metadata, string key, object model) + { + if (model == null) + { + if (_modelState.GetValidationState(key) != ModelValidationState.Valid) + { + _modelState.MarkFieldValid(key); + } + + return true; + } + + return Visit(metadata, key, model); + } + + /// + /// Validates a single node in a model object graph. + /// + /// true if the node is valid, otherwise false. + protected virtual bool ValidateNode() + { + var state = _modelState.GetValidationState(_key); + if (state == ModelValidationState.Unvalidated) + { + var validators = GetValidators(_metadata); + + var count = validators.Count; + if (count > 0) + { + var context = new ModelValidationContext() + { + Container = _container, + Model = _model, + Metadata = _metadata, + }; + + var results = new List(); + for (var i = 0; i < count; i++) + { + results.AddRange(validators[i].Validate(context)); + } + + var resultsCount = results.Count; + for (var i = 0; i < resultsCount; i++) + { + var result = results[i]; + var key = ModelNames.CreatePropertyModelName(_key, result.MemberName); + _modelState.TryAddModelError(key, result.Message); + } + } + } + + state = _modelState.GetFieldValidationState(_key); + if (state == ModelValidationState.Invalid) + { + return false; + } + else + { + // If the field has an entry in ModelState, then record it as valid. Don't create + // extra entries if they don't exist already. + var entry = _modelState[_key]; + if (entry != null) + { + entry.ValidationState = ModelValidationState.Valid; + } + + return true; + } + } + + private bool Visit(ModelMetadata metadata, string key, object model) + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + + if (model != null && !_currentPath.Add(model)) + { + // This is a cycle, bail. + return true; + } + + var entry = GetValidationEntry(model); + key = entry?.Key ?? key ?? string.Empty; + metadata = entry?.Metadata ?? metadata; + var strategy = entry?.Strategy; + + if (_modelState.HasReachedMaxErrors) + { + SuppressValidation(key); + return false; + } + else if ((entry != null && entry.SuppressValidation)) + { + SuppressValidation(key); + return true; + } + + using (StateManager.Recurse(this, key, metadata, model, strategy)) + { + if (_metadata.IsEnumerableType) + { + return VisitEnumerableType(); + } + else if (_metadata.IsComplexType) + { + return VisitComplexType(); + } + else + { + return VisitSimpleType(); + } + } + } + + private bool VisitEnumerableType() + { + var isValid = true; + + if (_model != null) + { + var strategy = _strategy ?? DefaultCollectionValidationStrategy.Instance; + isValid = VisitChildren(strategy); + } + + // Double-checking HasReachedMaxErrors just in case this model has no elements. + if (isValid && !_modelState.HasReachedMaxErrors) + { + isValid &= ValidateNode(); + } + + return isValid; + } + + private bool VisitComplexType() + { + var isValid = true; + + if (_model != null && ShouldValidateProperties(_metadata)) + { + var strategy = _strategy ?? DefaultComplexObjectValidationStrategy.Instance; + isValid = VisitChildren(strategy); + } + else if (_model != null) + { + // Suppress validation for the prefix, but we still want to validate the object. + SuppressValidation(_key); + } + + // Double-checking HasReachedMaxErrors just in case this model has no properties. + if (isValid && !_modelState.HasReachedMaxErrors) + { + isValid &= ValidateNode(); + } + + return isValid; + } + + private bool VisitSimpleType() + { + if (_modelState.HasReachedMaxErrors) + { + SuppressValidation(_key); + return false; + } + + return ValidateNode(); + } + + private bool VisitChildren(IValidationStrategy strategy) + { + var isValid = true; + var enumerator = strategy.GetChildren(_metadata, _key, _model); + + while (enumerator.MoveNext()) + { + var metadata = enumerator.Current.Metadata; + var model = enumerator.Current.Model; + var key = enumerator.Current.Key; + + isValid &= Visit(metadata, key, model); + } + + return isValid; + } + + private IList GetValidators(ModelMetadata metadata) + { + var context = new ModelValidatorProviderContext(metadata); + _validatorProvider.GetValidators(context); + return context.Validators.OrderBy(v => v, ValidatorOrderComparer.Instance).ToList(); + } + + private void SuppressValidation(string key) + { + var entries = _modelState.FindKeysWithPrefix(key); + foreach (var entry in entries) + { + entry.Value.ValidationState = ModelValidationState.Skipped; + } + } + + private bool ShouldValidateProperties(ModelMetadata metadata) + { + var count = _excludeFilters.Count; + for (var i = 0; i < _excludeFilters.Count; i++) + { + if (_excludeFilters[i].IsTypeExcluded(metadata.UnderlyingOrModelType)) + { + return false; + } + } + + return true; + } + + private ValidationStateEntry GetValidationEntry(object model) + { + if (model == null || _validationState == null) + { + return null; + } + + ValidationStateEntry entry; + _validationState.TryGetValue(model, out entry); + return entry; + } + + private struct StateManager : IDisposable + { + private readonly ValidationVisitor _visitor; + private readonly object _container; + private readonly string _key; + private readonly ModelMetadata _metadata; + private readonly object _model; + private readonly object _newModel; + private readonly IValidationStrategy _strategy; + + public static StateManager Recurse( + ValidationVisitor visitor, + string key, + ModelMetadata metadata, + object model, + IValidationStrategy strategy) + { + var recursifier = new StateManager(visitor, model); + + visitor._container = visitor._model; + visitor._key = key; + visitor._metadata = metadata; + visitor._model = model; + visitor._strategy = strategy; + + return recursifier; + } + + public StateManager(ValidationVisitor visitor, object newModel) + { + _visitor = visitor; + _newModel = newModel; + + _container = _visitor._container; + _key = _visitor._key; + _metadata = _visitor._metadata; + _model = _visitor._model; + _strategy = _visitor._strategy; + } + + public void Dispose() + { + _visitor._container = _container; + _visitor._key = _key; + _visitor._metadata = _metadata; + _visitor._model = _model; + _visitor._strategy = _strategy; + + _visitor._currentPath.Remove(_newModel); + } + } + + // Sorts validators based on whether or not they are 'required'. We want to run + // 'required' validators first so that we get the best possible error message. + private class ValidatorOrderComparer : IComparer + { + public static readonly ValidatorOrderComparer Instance = new ValidatorOrderComparer(); + + public int Compare(IModelValidator x, IModelValidator y) + { + var xScore = x.IsRequired ? 0 : 1; + var yScore = y.IsRequired ? 0 : 1; + return xScore.CompareTo(yScore); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs index 6991dab020..086b3ecda2 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs @@ -33,20 +33,17 @@ public bool IsRequired public IEnumerable Validate(ModelValidationContext validationContext) { - var modelExplorer = validationContext.ModelExplorer; - var metadata = modelExplorer.Metadata; - + var metadata = validationContext.Metadata; var memberName = metadata.PropertyName ?? metadata.ModelType.Name; - var containerExplorer = modelExplorer.Container; + var container = validationContext.Container; - var container = containerExplorer?.Model; - var context = new ValidationContext(container ?? modelExplorer.Model) + var context = new ValidationContext(container ?? validationContext.Model) { DisplayName = metadata.GetDisplayName(), MemberName = memberName }; - var result = Attribute.GetValidationResult(modelExplorer.Model, context); + var result = Attribute.GetValidationResult(validationContext.Model, context); if (result != ValidationResult.Success) { // ModelValidationResult.MemberName is used by invoking validators (such as ModelValidator) to diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidatableObjectAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidatableObjectAdapter.cs index 5be1a1b959..c43cd8704e 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidatableObjectAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidatableObjectAdapter.cs @@ -18,7 +18,7 @@ public bool IsRequired public IEnumerable Validate(ModelValidationContext context) { - var model = context.ModelExplorer.Model; + var model = context.Model; if (model == null) { return Enumerable.Empty(); diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs index c8d2157b15..f2b9497f85 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs @@ -1514,18 +1514,12 @@ public virtual bool TryValidateModel( MetadataProvider, modelName); - var validationContext = new ModelValidationContext( - bindingSource: null, - validatorProvider: BindingContext.ValidatorProvider, - modelState: ModelState, - modelExplorer: modelExplorer); - ObjectValidator.Validate( - validationContext, - new ModelValidationNode(modelName, modelExplorer.Metadata, model) - { - ValidateAllProperties = true - }); + BindingContext.ValidatorProvider, + ModelState, + validationState: null, + prefix: prefix, + model: model); return ModelState.IsValid; } diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index 892203129e..ff283cda7d 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -417,18 +417,14 @@ public void Validate(TEntity entity, string keyPrefix) { var modelExplorer = MetadataProvider.GetModelExplorerForType(typeof(TEntity), entity); - var modelValidationContext = new ModelValidationContext( - bindingSource: null, - validatorProvider: BindingContext.ValidatorProvider, - modelState: ModelState, - modelExplorer: modelExplorer); + var validatidationState = new ValidationStateDictionary(); ObjectValidator.Validate( - modelValidationContext, - new ModelValidationNode(keyPrefix, modelExplorer.Metadata, entity) - { - ValidateAllProperties = true - }); + BindingContext.ValidatorProvider, + ModelState, + validatidationState, + keyPrefix, + entity); } protected virtual void Dispose(bool disposing) diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs index 0c638352a2..c932141e44 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/HttpRequestMessage/HttpRequestMessageModelBinder.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; namespace Microsoft.AspNet.Mvc.WebApiCompatShim { @@ -18,13 +19,8 @@ public Task BindModelAsync(ModelBindingContext bindingContex if (bindingContext.ModelType == typeof(HttpRequestMessage)) { var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage(); - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model) - { - SuppressValidation = true, - }; - - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode); + bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true }); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } return ModelBindingResult.NoResultAsync; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionArgumentBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionArgumentBinderTests.cs index 23cd52db65..20cde61c0e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionArgumentBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionArgumentBinderTests.cs @@ -115,7 +115,7 @@ public async Task BindActionArgumentsAsync_AddsActionArguments_IfBinderReturnsNo { context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string)); }) - .Returns(ModelBindingResult.SuccessAsync(string.Empty, value, validationNode: null)); + .Returns(ModelBindingResult.SuccessAsync(string.Empty, value)); var actionContext = new ActionContext( new DefaultHttpContext(), @@ -153,19 +153,30 @@ public async Task BindActionArgumentsAsync_CallsValidator_IfModelBinderSucceeds( var actionContext = GetActionContext(actionDescriptor); var actionBindingContext = GetActionBindingContext(); - var mockValidatorProvider = new Mock(MockBehavior.Strict); - mockValidatorProvider - .Setup(o => o.Validate(It.IsAny(), It.IsAny())) - .Verifiable(); - var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object); + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator + .Setup(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + + var argumentBinder = GetArgumentBinder(mockValidator.Object); // Act var result = await argumentBinder .BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController()); // Assert - mockValidatorProvider.Verify( - o => o.Validate(It.IsAny(), It.IsAny()), Times.Once()); + mockValidator + .Verify(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once()); } [Fact] @@ -197,19 +208,29 @@ public async Task BindActionArgumentsAsync_DoesNotCallValidator_IfModelBinderFai ModelBinder = binder.Object, }; - var mockValidatorProvider = new Mock(MockBehavior.Strict); - mockValidatorProvider - .Setup(o => o.Validate(It.IsAny(), It.IsAny())) - .Verifiable(); - var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object); + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator + .Setup(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + + var argumentBinder = GetArgumentBinder(mockValidator.Object); // Act var result = await argumentBinder .BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController()); // Assert - mockValidatorProvider.Verify( - o => o.Validate(It.IsAny(), It.IsAny()), + mockValidator + .Verify(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never()); } @@ -228,19 +249,30 @@ public async Task BindActionArgumentsAsync_CallsValidator_ForControllerPropertie var actionContext = GetActionContext(actionDescriptor); var actionBindingContext = GetActionBindingContext(); - var mockValidatorProvider = new Mock(MockBehavior.Strict); - mockValidatorProvider - .Setup(o => o.Validate(It.IsAny(), It.IsAny())) - .Verifiable(); - var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object); + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator + .Setup(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + + var argumentBinder = GetArgumentBinder(mockValidator.Object); // Act var result = await argumentBinder .BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController()); // Assert - mockValidatorProvider.Verify( - o => o.Validate(It.IsAny(), It.IsAny()), Times.Once()); + mockValidator + .Verify(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once()); } [Fact] @@ -271,20 +303,29 @@ public async Task BindActionArgumentsAsync_DoesNotCallValidator_ForControllerPro ModelBinder = binder.Object, }; - var mockValidatorProvider = new Mock(MockBehavior.Strict); - mockValidatorProvider - .Setup(o => o.Validate(It.IsAny(), It.IsAny())) - .Verifiable(); + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator + .Setup(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); - var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object); + var argumentBinder = GetArgumentBinder(mockValidator.Object); // Act var result = await argumentBinder .BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController()); // Assert - mockValidatorProvider.Verify( - o => o.Validate(It.IsAny(), It.IsAny()), + mockValidator + .Verify(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never()); } @@ -363,7 +404,7 @@ public async Task BindActionArgumentsAsync_DoesNotSetNullValues_ForNonNullablePr var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) - .Returns(ModelBindingResult.SuccessAsync(string.Empty, model: null, validationNode: null)); + .Returns(ModelBindingResult.SuccessAsync(string.Empty, model: null)); var actionBindingContext = new ActionBindingContext() { @@ -401,7 +442,7 @@ public async Task BindActionArgumentsAsync_SetsNullValues_ForNullableProperties( var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) - .Returns(ModelBindingResult.SuccessAsync(key: string.Empty, model: null, validationNode: null)); + .Returns(ModelBindingResult.SuccessAsync(key: string.Empty, model: null)); var actionBindingContext = new ActionBindingContext() { @@ -545,7 +586,7 @@ public async Task BindActionArgumentsAsync_SetsMultipleControllerProperties() object model; if (inputPropertyValues.TryGetValue(bindingContext.ModelName, out model)) { - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode: null); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } else { @@ -600,8 +641,7 @@ private static ActionBindingContext GetActionBindingContext(object model) binder.Setup(b => b.BindModelAsync(It.IsAny())) .Returns(mbc => { - var validationNode = new ModelValidationNode(string.Empty, mbc.ModelMetadata, model); - return ModelBindingResult.SuccessAsync(string.Empty, model, validationNode: validationNode); + return ModelBindingResult.SuccessAsync(string.Empty, model); }); return new ActionBindingContext() @@ -614,10 +654,7 @@ private static DefaultControllerActionArgumentBinder GetArgumentBinder(IObjectMo { if (validator == null) { - var mockValidator = new Mock(MockBehavior.Strict); - mockValidator.Setup( - o => o.Validate(It.IsAny(), It.IsAny())); - validator = mockValidator.Object; + validator = CreateMockValidator(); } return new DefaultControllerActionArgumentBinder( @@ -625,6 +662,19 @@ private static DefaultControllerActionArgumentBinder GetArgumentBinder(IObjectMo validator); } + private static IObjectModelValidator CreateMockValidator() + { + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator + .Setup(o => o.Validate( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + return mockValidator.Object; + } + // No need for bind-related attributes on properties in this controller class. Properties are added directly // to the BoundProperties collection, bypassing usual requirements. private class TestController diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs index b21cd78ec9..c69637d80a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ArrayModelBinderTest.cs @@ -62,10 +62,6 @@ public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject() Assert.Empty(Assert.IsType(result.Model)); Assert.Equal("modelName", result.Key); Assert.True(result.IsModelSet); - - Assert.Same(result.ValidationNode.Model, result.Model); - Assert.Same(result.ValidationNode.Key, result.Key); - Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata); } [Theory] @@ -173,7 +169,7 @@ private static IModelBinder CreateIntBinder() if (value != ValueProviderResult.None) { var model = value.ConvertTo(mbc.ModelType); - return ModelBindingResult.SuccessAsync(mbc.ModelName, model, validationNode: null); + return ModelBindingResult.SuccessAsync(mbc.ModelName, model); } return ModelBindingResult.NoResultAsync; }); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs index d6bec3962e..1a4c760da1 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BinderTypeBasedModelBinderModelBinderTest.cs @@ -67,9 +67,6 @@ public async Task BindModel_CallsBindAsync_OnProvidedModelBinder() var p = (Person)binderResult.Model; Assert.Equal(model.Age, p.Age); Assert.Equal(model.Name, p.Name); - Assert.NotNull(binderResult.ValidationNode); - Assert.Equal(bindingContext.ModelName, binderResult.ValidationNode.Key); - Assert.Same(binderResult.Model, binderResult.ValidationNode.Model); } [Fact] @@ -141,9 +138,7 @@ public NotNullModelBinder() public Task BindModelAsync(ModelBindingContext bindingContext) { - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, _model); - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, _model, validationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, _model); } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs index 1553f383c4..6bed04e909 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs @@ -50,13 +50,6 @@ public async Task BindModel_CallsSelectedInputFormatterOnce() mockInputFormatter.Verify(v => v.ReadAsync(It.IsAny()), Times.Once); Assert.NotNull(binderResult); Assert.True(binderResult.IsModelSet); - Assert.NotNull(binderResult.ValidationNode); - Assert.True(binderResult.ValidationNode.ValidateAllProperties); - Assert.False(binderResult.ValidationNode.SuppressValidation); - Assert.Empty(binderResult.ValidationNode.ChildNodes); - Assert.Equal(binderResult.Key, binderResult.ValidationNode.Key); - Assert.Equal(bindingContext.ModelMetadata, binderResult.ValidationNode.ModelMetadata); - Assert.Same(binderResult.Model, binderResult.ValidationNode.Model); } [Fact] @@ -78,7 +71,6 @@ public async Task BindModel_NoInputFormatterFound_SetsModelStateError() // Returns non-null because it understands the metadata type. Assert.NotNull(binderResult); Assert.False(binderResult.IsModelSet); - Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.Model); // Key is empty because this was a top-level binding. @@ -104,7 +96,6 @@ public async Task BindModel_IsGreedy() // Assert Assert.NotNull(binderResult); Assert.False(binderResult.IsModelSet); - Assert.Null(binderResult.ValidationNode); } [Fact] @@ -172,7 +163,6 @@ public async Task CustomFormatterDeserializationException_AddedToModelState() // Returns non-null because it understands the metadata type. Assert.NotNull(binderResult); Assert.False(binderResult.IsModelSet); - Assert.Null(binderResult.ValidationNode); Assert.Null(binderResult.Model); // Key is empty because this was a top-level binding. @@ -209,7 +199,6 @@ public async Task NullFormatterError_AddedToModelState() Assert.NotNull(binderResult); Assert.False(binderResult.IsModelSet); Assert.Null(binderResult.Model); - Assert.Null(binderResult.ValidationNode); // Key is empty because this was a top-level binding. var entry = Assert.Single(bindingContext.ModelState); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs index 5b0db783d0..ee4cda1211 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CancellationTokenModelBinderTests.cs @@ -25,8 +25,6 @@ public async Task CancellationTokenModelBinder_ReturnsNonEmptyResult_ForCancella Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, result.Model); - Assert.NotNull(result.ValidationNode); - Assert.True(result.ValidationNode.SuppressValidation); } [Theory] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs index ccc2bc775e..9604a9e64c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CollectionModelBinderTest.cs @@ -9,6 +9,7 @@ #endif using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; #if DNX451 using Moq; #endif @@ -32,13 +33,16 @@ public async Task BindComplexCollectionFromIndexes_FiniteIndexes() var binder = new CollectionModelBinder(); // Act - var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" }); + var collectionResult = await binder.BindComplexCollectionFromIndexes( + bindingContext, + new[] { "foo", "bar", "baz" }); // Assert - Assert.Equal(new[] { 42, 0, 200 }, boundCollection.Model.ToArray()); - Assert.Equal( - new[] { "someName[foo]", "someName[baz]" }, - boundCollection.ValidationNode.ChildNodes.Select(o => o.Key).ToArray()); + Assert.Equal(new[] { 42, 0, 200 }, collectionResult.Model.ToArray()); + + // This requires a non-default IValidationStrategy + var strategy = Assert.IsType(collectionResult.ValidationStrategy); + Assert.Equal(new[] { "foo", "bar", "baz" }, strategy.ElementKeys); } [Fact] @@ -59,9 +63,9 @@ public async Task BindComplexCollectionFromIndexes_InfiniteIndexes() // Assert Assert.Equal(new[] { 42, 100 }, boundCollection.Model.ToArray()); - Assert.Equal( - new[] { "someName[0]", "someName[1]" }, - boundCollection.ValidationNode.ChildNodes.Select(o => o.Key).ToArray()); + + // This uses the default IValidationStrategy + Assert.DoesNotContain(boundCollection, bindingContext.ValidationState.Keys); } [Theory] @@ -197,7 +201,6 @@ public async Task BindModelAsync_SimpleCollectionWithNullValue_Succeeds() Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.NotNull(result.Model); - Assert.NotNull(result.ValidationNode); var model = Assert.IsType>(result.Model); Assert.Empty(model); @@ -252,10 +255,6 @@ public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObjec Assert.Empty(Assert.IsType>(result.Model)); Assert.Equal("modelName", result.Key); Assert.True(result.IsModelSet); - - Assert.Same(result.ValidationNode.Model, result.Model); - Assert.Same(result.ValidationNode.Key, result.Key); - Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata); } // Setup like CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject except @@ -290,10 +289,6 @@ public async Task CollectionModelBinder_DoesNotCreateEmptyCollection_IfModelNonN Assert.Empty(list); Assert.Equal("modelName", result.Key); Assert.True(result.IsModelSet); - - Assert.Same(result.ValidationNode.Model, result.Model); - Assert.Same(result.ValidationNode.Key, result.Key); - Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata); } [Theory] @@ -361,14 +356,13 @@ public async Task BindSimpleCollection_SubBindingSucceeds() // Arrange var culture = new CultureInfo("fr-FR"); var bindingContext = GetModelBindingContext(new SimpleValueProvider()); - ModelValidationNode childValidationNode = null; + Mock.Get(bindingContext.OperationBindingContext.ModelBinder) .Setup(o => o.BindModelAsync(It.IsAny())) .Returns((ModelBindingContext mbc) => { Assert.Equal("someName", mbc.ModelName); - childValidationNode = new ModelValidationNode("someName", mbc.ModelMetadata, mbc.Model); - return ModelBindingResult.SuccessAsync(mbc.ModelName, 42, childValidationNode); + return ModelBindingResult.SuccessAsync(mbc.ModelName, 42); }); var modelBinder = new CollectionModelBinder(); @@ -379,7 +373,6 @@ public async Task BindSimpleCollection_SubBindingSucceeds() // Assert Assert.Equal(new[] { 42 }, boundCollection.Model.ToArray()); - Assert.Equal(new[] { childValidationNode }, boundCollection.ValidationNode.ChildNodes.ToArray()); } private static ModelBindingContext GetModelBindingContext( @@ -399,7 +392,8 @@ private static ModelBindingContext GetModelBindingContext( { ModelBinder = CreateIntBinder(), MetadataProvider = metadataProvider - } + }, + ValidationState = new ValidationStateDictionary(), }; return bindingContext; @@ -425,8 +419,7 @@ private static IModelBinder CreateIntBinder() } else { - var validationNode = new ModelValidationNode(mbc.ModelName, mbc.ModelMetadata, model); - return ModelBindingResult.SuccessAsync(mbc.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(mbc.ModelName, model); } }); return mockIntBinder.Object; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs index b8492d5cf7..311d01c83e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeModelBinderTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Moq; using Xunit; @@ -28,6 +29,7 @@ public async Task BindModel_SuccessfulBind_ReturnsModel() { { "someName", "dummyValue" } }, + ValidationState = new ValidationStateDictionary(), }; var mockIntBinder = new Mock(); @@ -40,7 +42,7 @@ public async Task BindModel_SuccessfulBind_ReturnsModel() Assert.Equal("someName", context.ModelName); Assert.Same(bindingContext.ValueProvider, context.ValueProvider); - return ModelBindingResult.SuccessAsync("someName", 42, validationNode: null); + return ModelBindingResult.SuccessAsync("someName", 42); }); var shimBinder = CreateCompositeBinder(mockIntBinder.Object); @@ -53,6 +55,96 @@ public async Task BindModel_SuccessfulBind_ReturnsModel() Assert.Equal(42, result.Model); } + [Fact] + public async Task BindModel_SuccessfulBind_SetsValidationStateAtTopLevel() + { + // Arrange + var bindingContext = new ModelBindingContext + { + FallbackToEmptyPrefix = true, + IsTopLevelObject = true, + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), + ModelName = "someName", + ModelState = new ModelStateDictionary(), + OperationBindingContext = new OperationBindingContext(), + ValueProvider = new SimpleValueProvider + { + { "someName", "dummyValue" } + }, + ValidationState = new ValidationStateDictionary(), + }; + + var mockIntBinder = new Mock(); + mockIntBinder + .Setup(o => o.BindModelAsync(It.IsAny())) + .Returns( + delegate (ModelBindingContext context) + { + Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata); + Assert.Equal("someName", context.ModelName); + Assert.Same(bindingContext.ValueProvider, context.ValueProvider); + + return ModelBindingResult.SuccessAsync("someName", 42); + }); + var shimBinder = CreateCompositeBinder(mockIntBinder.Object); + + // Act + var result = await shimBinder.BindModelAsync(bindingContext); + + // Assert + Assert.NotEqual(ModelBindingResult.NoResult, result); + Assert.True(result.IsModelSet); + Assert.Equal(42, result.Model); + + Assert.Contains(result.Model, bindingContext.ValidationState.Keys); + var entry = bindingContext.ValidationState[result.Model]; + Assert.Equal("someName", entry.Key); + Assert.Same(bindingContext.ModelMetadata, entry.Metadata); + } + + [Fact] + public async Task BindModel_SuccessfulBind_DoesNotSetValidationState_WhenNotTopLevel() + { + // Arrange + var bindingContext = new ModelBindingContext + { + FallbackToEmptyPrefix = true, + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), + ModelName = "someName", + ModelState = new ModelStateDictionary(), + OperationBindingContext = new OperationBindingContext(), + ValueProvider = new SimpleValueProvider + { + { "someName", "dummyValue" } + }, + ValidationState = new ValidationStateDictionary(), + }; + + var mockIntBinder = new Mock(); + mockIntBinder + .Setup(o => o.BindModelAsync(It.IsAny())) + .Returns( + delegate (ModelBindingContext context) + { + Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata); + Assert.Equal("someName", context.ModelName); + Assert.Same(bindingContext.ValueProvider, context.ValueProvider); + + return ModelBindingResult.SuccessAsync("someName", 42); + }); + var shimBinder = CreateCompositeBinder(mockIntBinder.Object); + + // Act + var result = await shimBinder.BindModelAsync(bindingContext); + + // Assert + Assert.NotEqual(ModelBindingResult.NoResult, result); + Assert.True(result.IsModelSet); + Assert.Equal(42, result.Model); + + Assert.Empty(bindingContext.ValidationState); + } + [Fact] public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel() { @@ -71,6 +163,7 @@ public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel() { { "someOtherName", "dummyValue" } }, + ValidationState = new ValidationStateDictionary(), }; var mockIntBinder = new Mock(); @@ -88,7 +181,7 @@ public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel() Assert.Equal("", mbc.ModelName); Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); - return ModelBindingResult.SuccessAsync(string.Empty, expectedModel, validationNode: null); + return ModelBindingResult.SuccessAsync(string.Empty, expectedModel); }); var shimBinder = CreateCompositeBinder(mockIntBinder.Object); @@ -223,7 +316,7 @@ public async Task ModelBinder_ReturnsNonEmptyResult_SetsNullValue_SetsModelState var modelBinder = new Mock(); modelBinder .Setup(mb => mb.BindModelAsync(It.IsAny())) - .Returns(ModelBindingResult.SuccessAsync("someName", model: null, validationNode: null)); + .Returns(ModelBindingResult.SuccessAsync("someName", model: null)); var composite = CreateCompositeBinder(modelBinder.Object); @@ -308,26 +401,6 @@ public async Task BindModel_WithDefaultBinders_BindsSimpleType() var model = Assert.IsType(result.Model); Assert.Equal("firstName-value", model.FirstName); Assert.Equal("lastName-value", model.LastName); - - Assert.NotNull(result.ValidationNode); - Assert.Equal(2, result.ValidationNode.ChildNodes.Count); - Assert.Equal("", result.ValidationNode.Key); - Assert.Equal(bindingContext.ModelMetadata, result.ValidationNode.ModelMetadata); - model = Assert.IsType(result.ValidationNode.Model); - Assert.Equal("firstName-value", model.FirstName); - Assert.Equal("lastName-value", model.LastName); - - Assert.Equal(2, result.ValidationNode.ChildNodes.Count); - - var validationNode = result.ValidationNode.ChildNodes[0]; - Assert.Equal("FirstName", validationNode.Key); - Assert.Equal("firstName-value", validationNode.Model); - Assert.Empty(validationNode.ChildNodes); - - validationNode = result.ValidationNode.ChildNodes[1]; - Assert.Equal("LastName", validationNode.Key); - Assert.Equal("lastName-value", validationNode.Model); - Assert.Empty(validationNode.ChildNodes); } [Fact] @@ -415,15 +488,13 @@ public async Task BindModel_UsesTheValidationNodeOnModelBindingResult_IfPresent( { // Arrange var valueProvider = new SimpleValueProvider(); - ModelValidationNode validationNode = null; var mockBinder = new Mock(); mockBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns((ModelBindingContext context) => { - validationNode = new ModelValidationNode("someName", context.ModelMetadata, 42); - return ModelBindingResult.SuccessAsync("someName", 42, validationNode); + return ModelBindingResult.SuccessAsync("someName", 42); }); var binder = CreateCompositeBinder(mockBinder.Object); @@ -435,7 +506,6 @@ public async Task BindModel_UsesTheValidationNodeOnModelBindingResult_IfPresent( // Assert Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); - Assert.Same(validationNode, result.ValidationNode); } private static ModelBindingContext CreateBindingContext( @@ -456,7 +526,8 @@ private static ModelBindingContext CreateBindingContext( { MetadataProvider = metadataProvider, ModelBinder = binder, - } + }, + ValidationState = new ValidationStateDictionary(), }; return bindingContext; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs index 6714ae95bc..775341bf65 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Primitives; using Moq; using Xunit; @@ -45,6 +46,9 @@ public async Task BindModel_Succeeds(bool isReadOnly) Assert.Equal(2, dictionary.Count); Assert.Equal("forty-two", dictionary[42]); Assert.Equal("eighty-four", dictionary[84]); + + // This uses the default IValidationStrategy + Assert.DoesNotContain(result.Model, bindingContext.ValidationState.Keys); } [Theory] @@ -78,6 +82,9 @@ public async Task BindModel_WithExistingModel_Succeeds(bool isReadOnly) Assert.Equal(2, dictionary.Count); Assert.Equal("forty-two", dictionary[42]); Assert.Equal("eighty-four", dictionary[84]); + + // This uses the default IValidationStrategy + Assert.DoesNotContain(result.Model, bindingContext.ValidationState.Keys); } // modelName, keyFormat, dictionary @@ -135,7 +142,6 @@ public async Task BindModel_FallsBackToBindingValues( Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Equal(modelName, result.Key); - Assert.NotNull(result.ValidationNode); var resultDictionary = Assert.IsAssignableFrom>(result.Model); Assert.Equal(dictionary, resultDictionary); @@ -172,7 +178,6 @@ public async Task BindModel_DoesNotFallBack_WithoutEnumerableValueProvider() Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Equal("prefix", result.Key); - Assert.NotNull(result.ValidationNode); var resultDictionary = Assert.IsAssignableFrom>(result.Model); Assert.Empty(resultDictionary); @@ -223,7 +228,6 @@ public async Task BindModel_FallsBackToBindingValues_WithValueTypes(IDictionary< Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Equal("prefix", result.Key); - Assert.NotNull(result.ValidationNode); var resultDictionary = Assert.IsAssignableFrom>(result.Model); Assert.Equal(dictionary, resultDictionary); @@ -264,10 +268,21 @@ public async Task BindModel_FallsBackToBindingValues_WithComplexValues() Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Equal("prefix", result.Key); - Assert.NotNull(result.ValidationNode); var resultDictionary = Assert.IsAssignableFrom>(result.Model); Assert.Equal(dictionary, resultDictionary); + + // This requires a non-default IValidationStrategy + Assert.Contains(result.Model, context.ValidationState.Keys); + var entry = context.ValidationState[result.Model]; + var strategy = Assert.IsType>(entry.Strategy); + Assert.Equal( + new KeyValuePair[] + { + new KeyValuePair("23", 23), + new KeyValuePair("27", 27), + }.OrderBy(kvp => kvp.Key), + strategy.KeyMappings.OrderBy(kvp => kvp.Key)); } [Theory] @@ -298,7 +313,6 @@ public async Task BindModel_FallsBackToBindingValues_WithCustomDictionary( Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Equal(modelName, result.Key); - Assert.NotNull(result.ValidationNode); var resultDictionary = Assert.IsAssignableFrom>(result.Model); Assert.Equal(expectedDictionary, resultDictionary); @@ -330,10 +344,6 @@ public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObjec Assert.Empty(Assert.IsType>(result.Model)); Assert.Equal("modelName", result.Key); Assert.True(result.IsModelSet); - - Assert.Same(result.ValidationNode.Model, result.Model); - Assert.Same(result.ValidationNode.Key, result.Key); - Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata); } [Theory] @@ -403,7 +413,8 @@ private static ModelBindingContext CreateContext() { HttpContext = new DefaultHttpContext(), MetadataProvider = new TestModelMetadataProvider(), - } + }, + ValidationState = new ValidationStateDictionary(), }; return modelBindingContext; @@ -462,7 +473,7 @@ private static ModelBindingContext GetModelBindingContext( KeyValuePair value; if (values.TryGetValue(mbc.ModelName, out value)) { - return ModelBindingResult.SuccessAsync(mbc.ModelName, value, validationNode: null); + return ModelBindingResult.SuccessAsync(mbc.ModelName, value); } else { @@ -488,6 +499,7 @@ private static ModelBindingContext GetModelBindingContext( ValueProvider = valueProvider, }, ValueProvider = valueProvider, + ValidationState = new ValidationStateDictionary(), }; return bindingContext; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs index 28dcc100e4..418858f30a 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormCollectionModelBinderTest.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Primitives; using Moq; using Xunit; @@ -36,8 +37,11 @@ public async Task FormCollectionModelBinder_ValidType_BindSuccessful() // Assert Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); - Assert.NotNull(result.ValidationNode); - Assert.True(result.ValidationNode.SuppressValidation); + + var entry = bindingContext.ValidationState[result.Model]; + Assert.True(entry.SuppressValidation); + Assert.Null(entry.Key); + Assert.Null(entry.Metadata); var form = Assert.IsAssignableFrom(result.Model); Assert.Equal(2, form.Count); @@ -124,7 +128,8 @@ private static ModelBindingContext GetBindingContext(Type modelType, HttpContext ModelBinder = new FormCollectionModelBinder(), MetadataProvider = metadataProvider, HttpContext = httpContext, - } + }, + ValidationState = new ValidationStateDictionary(), }; return bindingContext; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs index 023a1681ba..a6c3fa6a7d 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormFileModelBinderTest.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -17,6 +18,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public class FormFileModelBinderTest { + [Fact] + public async Task FormFileModelBinder_SuppressesValidation() + { + // Arrange + var formFiles = new FormFileCollection(); + formFiles.Add(GetMockFormFile("file", "file1.txt")); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IEnumerable), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.NotEqual(ModelBindingResult.NoResult, result); + Assert.True(result.IsModelSet); + + var entry = bindingContext.ValidationState[result.Model]; + Assert.True(entry.SuppressValidation); + Assert.Null(entry.Key); + Assert.Null(entry.Metadata); + } + [Fact] public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful() { @@ -34,8 +58,11 @@ public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful() // Assert Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); - Assert.NotNull(result.ValidationNode); - Assert.True(result.ValidationNode.SuppressValidation); + + var entry = bindingContext.ValidationState[result.Model]; + Assert.True(entry.SuppressValidation); + Assert.Null(entry.Key); + Assert.Null(entry.Metadata); var files = Assert.IsAssignableFrom>(result.Model); Assert.Equal(2, files.Count); @@ -197,7 +224,8 @@ private static ModelBindingContext GetBindingContext(Type modelType, HttpContext ModelBinder = new FormFileModelBinder(), MetadataProvider = metadataProvider, HttpContext = httpContext, - } + }, + ValidationState = new ValidationStateDictionary(), }; return bindingContext; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs index 20640d2503..8c61635250 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs @@ -32,7 +32,6 @@ public async Task BindModel_MissingKey_ReturnsResult_AndAddsModelValidationError Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.Null(result.Model); Assert.False(bindingContext.ModelState.IsValid); - Assert.Null(result.ValidationNode); Assert.Equal("someName", bindingContext.ModelName); var error = Assert.Single(bindingContext.ModelState["someName.Key"].Errors); Assert.Equal("A value is required.", error.ErrorMessage); @@ -54,7 +53,6 @@ public async Task BindModel_MissingValue_ReturnsResult_AndAddsModelValidationErr // Assert Assert.Null(result.Model); Assert.False(bindingContext.ModelState.IsValid); - Assert.Null(result.ValidationNode); Assert.Equal("someName", bindingContext.ModelName); Assert.Equal(bindingContext.ModelState["someName.Value"].Errors.First().ErrorMessage, "A value is required."); } @@ -99,19 +97,6 @@ public async Task BindModel_SubBindingSucceeds() // Assert Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.Equal(new KeyValuePair(42, "some-value"), result.Model); - Assert.NotNull(result.ValidationNode); - Assert.Equal(new KeyValuePair(42, "some-value"), result.ValidationNode.Model); - Assert.Equal("someName", result.ValidationNode.Key); - - var validationNode = result.ValidationNode.ChildNodes[0]; - Assert.Equal("someName.Key", validationNode.Key); - Assert.Equal(42, validationNode.Model); - Assert.Empty(validationNode.ChildNodes); - - validationNode = result.ValidationNode.ChildNodes[1]; - Assert.Equal("someName.Value", validationNode.Key); - Assert.Equal("some-value", validationNode.Model); - Assert.Empty(validationNode.ChildNodes); } [Theory] @@ -126,11 +111,11 @@ public async Task TryBindStrongModel_InnerBinderReturnsAResult_ReturnsInnerBinde ModelBindingResult innerResult; if (isSuccess) { - innerResult = ModelBindingResult.Success(string.Empty, model, validationNode: null); + innerResult = ModelBindingResult.Success("somename.key", model); } else { - innerResult = ModelBindingResult.Failed(string.Empty); + innerResult = ModelBindingResult.Failed("somename.key"); } var innerBinder = new Mock(); @@ -144,10 +129,9 @@ public async Task TryBindStrongModel_InnerBinderReturnsAResult_ReturnsInnerBinde var bindingContext = GetBindingContext(new SimpleValueProvider(), innerBinder.Object); var binder = new KeyValuePairModelBinder(); - var modelValidationNodeList = new List(); // Act - var result = await binder.TryBindStrongModel(bindingContext, "key", modelValidationNodeList); + var result = await binder.TryBindStrongModel(bindingContext, "key"); // Assert Assert.Equal(innerResult, result); @@ -180,10 +164,6 @@ public async Task KeyValuePairModelBinder_CreatesEmptyCollection_IfIsTopLevelObj Assert.Equal(default(KeyValuePair), Assert.IsType>(result.Model)); Assert.Equal("modelName", result.Key); Assert.True(result.IsModelSet); - - Assert.Equal(result.ValidationNode.Model, result.Model); - Assert.Same(result.ValidationNode.Key, result.Key); - Assert.Same(result.ValidationNode.ModelMetadata, context.ModelMetadata); } [Theory] @@ -260,8 +240,7 @@ private static IModelBinder CreateIntBinder() if (mbc.ModelType == typeof(int)) { var model = 42; - var validationNode = new ModelValidationNode(mbc.ModelName, mbc.ModelMetadata, model); - return ModelBindingResult.SuccessAsync(mbc.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(mbc.ModelName, model); } return ModelBindingResult.NoResultAsync; }); @@ -278,8 +257,7 @@ private static IModelBinder CreateStringBinder() if (mbc.ModelType == typeof(string)) { var model = "some-value"; - var validationNode = new ModelValidationNode(mbc.ModelName, mbc.ModelMetadata, model); - return ModelBindingResult.SuccessAsync(mbc.ModelName, model, validationNode); + return ModelBindingResult.SuccessAsync(mbc.ModelName, model); } return ModelBindingResult.NoResultAsync; }); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingResultTest.cs index 6ea79193d8..0f7085e54c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingResultTest.cs @@ -15,16 +15,14 @@ public void Success_SetsProperties() // Arrange var key = "someName"; var model = "some model"; - var validationNode = new ModelValidationNode(key, null, model); // Act - var result = ModelBindingResult.Success(key, model, validationNode); + var result = ModelBindingResult.Success(key, model); // Assert Assert.Same(key, result.Key); Assert.True(result.IsModelSet); Assert.Same(model, result.Model); - Assert.Same(validationNode, result.ValidationNode); } [Fact] @@ -33,16 +31,14 @@ public async Task SuccessAsync_SetsProperties() // Arrange var key = "someName"; var model = "some model"; - var validationNode = new ModelValidationNode(key, null, model); // Act - var result = await ModelBindingResult.SuccessAsync(key, model, validationNode); + var result = await ModelBindingResult.SuccessAsync(key, model); // Assert Assert.Same(key, result.Key); Assert.True(result.IsModelSet); Assert.Same(model, result.Model); - Assert.Same(validationNode, result.ValidationNode); } [Fact] @@ -58,7 +54,6 @@ public void Failed_SetsProperties() Assert.Same(key, result.Key); Assert.False(result.IsModelSet); Assert.Null(result.Model); - Assert.Null(result.ValidationNode); } [Fact] @@ -74,7 +69,6 @@ public async Task FailedAsync_SetsProperties() Assert.Same(key, result.Key); Assert.False(result.IsModelSet); Assert.Null(result.Model); - Assert.Null(result.ValidationNode); } [Fact] @@ -87,7 +81,6 @@ public void NoResult_SetsProperties() Assert.Null(result.Key); Assert.False(result.IsModelSet); Assert.Null(result.Model); - Assert.Null(result.ValidationNode); } [Fact] @@ -100,7 +93,6 @@ public async Task NoResultAsync_SetsProperties() Assert.Null(result.Key); Assert.False(result.IsModelSet); Assert.Null(result.Model); - Assert.Null(result.ValidationNode); } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs index d80f29afb1..412e2e5034 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/MutableObjectModelBinderTest.cs @@ -786,13 +786,12 @@ public void ProcessResults_BindRequiredFieldMissing_RaisesModelError() property => property, property => ModelBindingResult.Failed(property.PropertyName)); var nameProperty = containerMetadata.Properties[nameof(model.Name)]; - results[nameProperty] = ModelBindingResult.Success(string.Empty, "John Doe", validationNode: null); - - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); + results[nameProperty] = ModelBindingResult.Success(string.Empty, "John Doe"); + var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert var modelStateDictionary = bindingContext.ModelState; @@ -837,13 +836,12 @@ public void ProcessResults_DataMemberIsRequiredFieldMissing_RaisesModelError() property => property, property => ModelBindingResult.Failed(property.PropertyName)); var nameProperty = containerMetadata.Properties[nameof(model.Name)]; - results[nameProperty] = ModelBindingResult.Success(string.Empty, "John Doe", validationNode: null); + results[nameProperty] = ModelBindingResult.Success(string.Empty, "John Doe"); - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert var modelStateDictionary = bindingContext.ModelState; @@ -888,18 +886,17 @@ public void ProcessResults_ValueTypePropertyWithBindRequired_SetToNull_CapturesE property => property, property => ModelBindingResult.Failed(property.PropertyName)); var propertyMetadata = containerMetadata.Properties[nameof(model.Name)]; - results[propertyMetadata] = ModelBindingResult.Success("theModel.Name", "John Doe", validationNode: null); + results[propertyMetadata] = ModelBindingResult.Success("theModel.Name", "John Doe"); // Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this // case because the binding exists. propertyMetadata = containerMetadata.Properties[nameof(model.Age)]; - results[propertyMetadata] = ModelBindingResult.Success("theModel.Age", model: null, validationNode: null); + results[propertyMetadata] = ModelBindingResult.Success("theModel.Age", model: null); var testableBinder = new TestableMutableObjectModelBinder(); - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert var modelStateDictionary = bindingContext.ModelState; @@ -929,10 +926,9 @@ public void ProcessResults_ValueTypeProperty_WithBindingOptional_NoValueSet_NoEr property => ModelBindingResult.Failed(property.PropertyName)); var testableBinder = new TestableMutableObjectModelBinder(); - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert var modelStateDictionary = bindingContext.ModelState; @@ -952,10 +948,9 @@ public void ProcessResults_NullableValueTypeProperty_NoValueSet_NoError() property => ModelBindingResult.Failed(property.PropertyName)); var testableBinder = new TestableMutableObjectModelBinder(); - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert var modelStateDictionary = bindingContext.ModelState; @@ -984,20 +979,16 @@ public void ProcessResults_ValueTypeProperty_TriesToSetNullModel_CapturesExcepti var propertyMetadata = containerMetadata.Properties[nameof(Person.ValueTypeRequired)]; results[propertyMetadata] = ModelBindingResult.Success( key: "theModel." + nameof(Person.ValueTypeRequired), - model: null, - validationNode: null); + model: null); // Make ValueTypeRequiredWithDefaultValue invalid propertyMetadata = containerMetadata.Properties[nameof(Person.ValueTypeRequiredWithDefaultValue)]; results[propertyMetadata] = ModelBindingResult.Success( key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue), - model: null, - validationNode: null); - - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); + model: null); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert Assert.False(modelStateDictionary.IsValid); @@ -1056,10 +1047,8 @@ public void ProcessResults_ValueTypeProperty_NoValue_NoError() results[propertyMetadata] = ModelBindingResult.Failed( key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); - // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert Assert.True(modelState.IsValid); @@ -1084,24 +1073,21 @@ public void ProcessResults_ProvideRequiredFields_Success() var propertyMetadata = containerMetadata.Properties[nameof(Person.ValueTypeRequired)]; results[propertyMetadata] = ModelBindingResult.Success( key: "theModel." + nameof(Person.ValueTypeRequired), - model: 41, - validationNode: null); + model: 41); // Make ValueTypeRequiredWithDefaultValue valid. propertyMetadata = containerMetadata.Properties[nameof(Person.ValueTypeRequiredWithDefaultValue)]; results[propertyMetadata] = ModelBindingResult.Success( key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue), - model: 57, - validationNode: null); + model: 57); // Also remind ProcessResults about PropertyWithDefaultValue -- as BindPropertiesAsync() would. propertyMetadata = containerMetadata.Properties[nameof(Person.PropertyWithDefaultValue)]; results[propertyMetadata] = ModelBindingResult.Failed( key: "theModel." + nameof(Person.PropertyWithDefaultValue)); - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert Assert.True(modelStateDictionary.IsValid); @@ -1140,13 +1126,9 @@ public void ProcessResults_ValueTypePropertyWithBindRequired_RequiredValidatorIg .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)]; results[propertyMetadata] = ModelBindingResult.Success( key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty), - model: "value", - validationNode: null); - - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); - + model: "value"); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert Assert.False(modelStateDictionary.IsValid); @@ -1185,19 +1167,15 @@ public void ProcessResults_ReferenceTypePropertyWithBindRequired_RequiredValidat .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)]; results[propertyMetadata] = ModelBindingResult.Success( key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty), - model: 17, - validationNode: null); + model: 17); // Make ReferenceTypeProperty not have a value. propertyMetadata = containerMetadata .Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)]; results[propertyMetadata] = ModelBindingResult.Failed( key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)); - - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); - // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert Assert.False(modelStateDictionary.IsValid); @@ -1235,57 +1213,23 @@ public void ProcessResults_Success() var firstNameProperty = containerMetadata.Properties[nameof(model.FirstName)]; results[firstNameProperty] = ModelBindingResult.Success( nameof(model.FirstName), - "John", - validationNode: null); + "John"); var lastNameProperty = containerMetadata.Properties[nameof(model.LastName)]; results[lastNameProperty] = ModelBindingResult.Success( nameof(model.LastName), - "Doe", - validationNode: null); - - var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); + "Doe"); + var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.ProcessResults(bindingContext, results, modelValidationNode); + testableBinder.ProcessResults(bindingContext, results); // Assert Assert.Equal("John", model.FirstName); Assert.Equal("Doe", model.LastName); Assert.Equal(dob, model.DateOfBirth); Assert.True(bindingContext.ModelState.IsValid); - - // Ensure that we add child nodes for all the nodes which have a result (irrespective of if they - // are bound or not). - Assert.Equal(5, modelValidationNode.ChildNodes.Count); - - Assert.Collection(modelValidationNode.ChildNodes, - child => - { - Assert.Equal(nameof(model.DateOfBirth), child.Key); - Assert.Equal(null, child.Model); - }, - child => - { - Assert.Equal(nameof(model.DateOfDeath), child.Key); - Assert.Equal(null, child.Model); - }, - child => - { - Assert.Equal(nameof(model.FirstName), child.Key); - Assert.Equal("John", child.Model); - }, - child => - { - Assert.Equal(nameof(model.LastName), child.Key); - Assert.Equal("Doe", child.Model); - }, - child => - { - Assert.Equal(nameof(model.NonUpdateableProperty), child.Key); - Assert.Equal(null, child.Model); - }); } [Fact] @@ -1420,8 +1364,7 @@ public void SetProperty_ValueProvidedAndCanUpdatePropertyTrue_DoesNothing( var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName]; var result = ModelBindingResult.Success( propertyName, - new Simple { Name = "Hanna" }, - validationNode: null); + new Simple { Name = "Hanna" }); var testableBinder = new TestableMutableObjectModelBinder(); @@ -1496,7 +1439,7 @@ public void SetProperty_CollectionProperty_UpdatesModel( var modelExplorer = metadataProvider.GetModelExplorerForType(type, model); var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName]; - var result = ModelBindingResult.Success(propertyName, collection, validationNode: null); + var result = ModelBindingResult.Success(propertyName, collection); var testableBinder = new TestableMutableObjectModelBinder(); // Act @@ -1519,7 +1462,7 @@ public void SetProperty_PropertyIsSettable_CallsSetter() var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)]; - var result = ModelBindingResult.Success("foo", new DateTime(2001, 1, 1), validationNode: null); + var result = ModelBindingResult.Success("foo", new DateTime(2001, 1, 1)); var testableBinder = new TestableMutableObjectModelBinder(); // Act @@ -1546,7 +1489,7 @@ public void SetProperty_PropertyIsSettable_SetterThrows_RecordsError() var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfDeath)]; - var result = ModelBindingResult.Success("foo", new DateTime(1800, 1, 1), validationNode: null); + var result = ModelBindingResult.Success("foo", new DateTime(1800, 1, 1)); var testableBinder = new TestableMutableObjectModelBinder(); // Act @@ -1571,7 +1514,7 @@ public void SetProperty_SettingNonNullableValueTypeToNull_CapturesException() var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)]; - var result = ModelBindingResult.Success("foo.DateOfBirth", model: null, validationNode: null); + var result = ModelBindingResult.Success("foo.DateOfBirth", model: null); var testableBinder = new TestableMutableObjectModelBinder(); // Act @@ -1599,7 +1542,7 @@ public void SetProperty_PropertySetterThrows_CapturesException() var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(ModelWhosePropertySetterThrows), model); var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.NameNoAttribute)]; - var result = ModelBindingResult.Success("foo.NameNoAttribute", model: null, validationNode: null); + var result = ModelBindingResult.Success("foo.NameNoAttribute", model: null); var testableBinder = new TestableMutableObjectModelBinder(); // Act diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ServiceModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ServiceModelBinderTest.cs index 5ffb12904a..910063f3b3 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ServiceModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ServiceModelBinderTest.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.DependencyInjection; using Xunit; @@ -29,9 +30,10 @@ public async Task ServiceModelBinder_BindsService() Assert.NotNull(result.Model); Assert.Equal("modelName", result.Key); - Assert.NotNull(result.ValidationNode); - Assert.Equal("modelName", result.ValidationNode.Key); - Assert.True(result.ValidationNode.SuppressValidation); + var entry = modelBindingContext.ValidationState[result.Model]; + Assert.True(entry.SuppressValidation); + Assert.Null(entry.Key); + Assert.Null(entry.Metadata); } [Fact] @@ -95,6 +97,7 @@ private static ModelBindingContext GetBindingContext(Type modelType) }, BinderModelName = modelMetadata.BinderModelName, BindingSource = modelMetadata.BindingSource, + ValidationState = new ValidationStateDictionary(), }; return bindingContext; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs index f521323919..602019f75c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/SimpleTypeModelBinderTest.cs @@ -103,7 +103,6 @@ public async Task BindModel_CreatesError_WhenTypeConversionIsNull(Type destinati // Assert Assert.False(result.IsModelSet); Assert.Null(result.Model); - Assert.Null(result.ValidationNode); var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors); Assert.Equal(error.ErrorMessage, "The value '' is invalid.", StringComparer.Ordinal); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultCollectionValidationStrategyTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultCollectionValidationStrategyTest.cs new file mode 100644 index 0000000000..ff67a16152 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultCollectionValidationStrategyTest.cs @@ -0,0 +1,98 @@ +// 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; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + public class DefaultCollectionValidationStrategyTest + { + [Fact] + public void EnumerateElements() + { + // Arrange + var model = new List() { 2, 3, 5 }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(List)); + var strategy = DefaultCollectionValidationStrategy.Instance; + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[0]", e.Key); + Assert.Equal(2, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[1]", e.Key); + Assert.Equal(3, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[2]", e.Key); + Assert.Equal(5, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }); + } + + [Fact] + public void EnumerateElements_Dictionary() + { + // Arrange + var model = new Dictionary() + { + { 2, "two" }, + { 3, "three" }, + { 5, "five" }, + }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(List)); + var strategy = DefaultCollectionValidationStrategy.Instance; + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[0]", e.Key); + Assert.Equal(new KeyValuePair(2, "two"), e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[1]", e.Key); + Assert.Equal(new KeyValuePair(3, "three"), e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[2]", e.Key); + Assert.Equal(new KeyValuePair(5, "five"), e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }); + } + + private List BufferEntries(IEnumerator enumerator) + { + var entries = new List(); + while (enumerator.MoveNext()) + { + entries.Add(enumerator.Current); + } + + return entries; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultComplexObjectValidationStrategyTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultComplexObjectValidationStrategyTest.cs new file mode 100644 index 0000000000..3b6b8bdab2 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultComplexObjectValidationStrategyTest.cs @@ -0,0 +1,72 @@ +// 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; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + public class DefaultComplexObjectValidationStrategyTest + { + [Fact] + public void EnumerateElements() + { + // Arrange + var model = new Person() + { + Age = 23, + Id = 1, + Name = "Joey", + }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(Person)); + var strategy = DefaultComplexObjectValidationStrategy.Instance; + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix.Age", e.Key); + Assert.Equal(23, e.Model); + Assert.Same(metadata.Properties["Age"], e.Metadata); + }, + e => + { + Assert.Equal("prefix.Id", e.Key); + Assert.Equal(1, e.Model); + Assert.Same(metadata.Properties["Id"], e.Metadata); + }, + e => + { + Assert.Equal("prefix.Name", e.Key); + Assert.Equal("Joey", e.Model); + Assert.Same(metadata.Properties["Name"], e.Metadata); + }); + } + + private List BufferEntries(IEnumerator enumerator) + { + var entries = new List(); + while (enumerator.MoveNext()) + { + entries.Add(enumerator.Current); + } + + return entries; + } + + private class Person + { + public int Id { get; set; } + + public int Age { get; set; } + + public string Name { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs index 8a791b5672..0cb44b8ebb 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs @@ -14,560 +14,625 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { public class DefaultObjectValidatorTests { - private static Person LonelyPerson; + private IModelMetadataProvider MetadataProvider { get; } = TestModelMetadataProvider.CreateDefaultProvider(); - static DefaultObjectValidatorTests() + [Fact] + public void Validate_SimpleValueType_Valid_WithPrefix() { - LonelyPerson = new Person() { Name = "Reallllllllly Long Name" }; - LonelyPerson.Friend = LonelyPerson; + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)15; + + modelState.SetModelValue("parameter", "15", "15"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + AssertKeysEqual(modelState, "parameter"); + + var entry = modelState["parameter"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); } - public static IEnumerable ValidationErrors + [Fact] + public void Validate_SimpleReferenceType_Valid_WithPrefix() { - get - { - // returns an array of model, type of model and expected errors. - // Primitives - yield return new object[] { null, typeof(Person), new Dictionary() }; - yield return new object[] { 14, typeof(int), new Dictionary() }; - yield return new object[] { "foo", typeof(string), new Dictionary() }; - - // Object Traversal : make sure we can traverse the object graph without throwing - yield return new object[] - { - new ValueType() { Reference = "ref", Value = 256 }, - typeof(ValueType), - new Dictionary() - }; - yield return new object[] - { - new ReferenceType() { Reference = "ref", Value = 256 }, - typeof(ReferenceType), - new Dictionary() - }; + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - // Classes - yield return new object[] - { - new Person() { Name = "Rick", Profession = "Astronaut" }, - typeof(Person), - new Dictionary() - }; - yield return new object[] - { - new Person(), - typeof(Person), - new Dictionary() - { - { "Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") } - } - }; - - yield return new object[] - { - new Person() { Name = "Rick", Friend = new Person() }, - typeof(Person), - new Dictionary() - { - { "Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") }, - { "Friend.Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "Friend.Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") } - } - }; - - // Collections - yield return new object[] - { - new Person[] { new Person(), new Person() }, - typeof(Person[]), - new Dictionary() - { - { "[0].Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "[0].Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") }, - { "[1].Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "[1].Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") } - } - }; - - yield return new object[] - { - new List { new Person(), new Person() }, - typeof(Person[]), - new Dictionary() - { - { "[0].Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "[0].Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") }, - { "[1].Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "[1].Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") } - } - }; - - if (!TestPlatformHelper.IsMono) - { - // In Mono this throws a NullRef Exception. - // https://github.com/aspnet/External/issues/23 - yield return new object[] - { - new Dictionary { { "Joe", new Person() } , { "Mark", new Person() } }, - typeof(Dictionary), - new Dictionary() - { - { "[0].Value.Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "[0].Value.Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") }, - { "[1].Value.Name", ValidationAttributeUtil.GetRequiredErrorMessage("Name") }, - { "[1].Value.Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") } - } - }; - } + var validator = CreateValidator(); - // IValidatableObject's - yield return new object[] - { - new ValidatableModel(), - typeof(ValidatableModel), - new Dictionary() - { - { "", "Error1" }, - { "Property1", "Error2" }, - { "Property2", "Error3" }, - { "Property3", "Error3" } - } - }; - - yield return new object[] - { - new[] { new ValidatableModel() }, - typeof(ValidatableModel[]), - new Dictionary() - { - { "[0]", "Error1" }, - { "[0].Property1", "Error2" }, - { "[0].Property2", "Error3" }, - { "[0].Property3", "Error3" } - } - }; - - // Nested Objects - yield return new object[] - { - new Org() - { - Id = 1, - OrgName = "Org", - Dev = new Team - { - Id = 10, - TeamName = "HelloWorldTeam", - Lead = "SampleLeadDev", - TeamSize = 2 - }, - Test = new Team - { - Id = 11, - TeamName = "HWT", - Lead = "SampleTestLead", - TeamSize = 12 - } - }, - typeof(Org), - new Dictionary() - { - { "OrgName", ValidationAttributeUtil.GetStringLengthErrorMessage(4, 20, "OrgName") }, - { "Dev.Lead", ValidationAttributeUtil.GetMaxLengthErrorMessage(10, "Lead") }, - { "Dev.TeamSize", ValidationAttributeUtil.GetRangeErrorMessage(3, 100, "TeamSize") }, - { "Test.TeamName", ValidationAttributeUtil.GetStringLengthErrorMessage(4, 20, "TeamName") }, - { "Test.Lead", ValidationAttributeUtil.GetMaxLengthErrorMessage(10, "Lead") } - } - }; - - // Testing we don't validate fields - yield return new object[] - { - new VariableTest() { test = 5 }, - typeof(VariableTest), - new Dictionary() - }; + var model = (object)"test"; - // Testing we don't blow up on cycles - yield return new object[] - { - LonelyPerson, - typeof(Person), - new Dictionary() - { - { "Name", ValidationAttributeUtil.GetStringLengthErrorMessage(null, 10, "Name") }, - { "Profession", ValidationAttributeUtil.GetRequiredErrorMessage("Profession") } - } - }; - } + modelState.SetModelValue("parameter", "test", "test"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "parameter"); + + var entry = modelState["parameter"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); } - [Theory] - [ReplaceCulture] - [MemberData(nameof(ValidationErrors))] - public void ExpectedValidationErrorsRaised(object model, Type type, Dictionary expectedErrors) + [Fact] + public void Validate_SimpleType_MaxErrorsReached() { // Arrange - var context = GetModelValidationContext(model, type); + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - var validator = new DefaultObjectValidator(context.ExcludeFilters, context.ModelMetadataProvider); - var topLevelValidationNode = - new ModelValidationNode(string.Empty, context.ModelValidationContext.ModelExplorer.Metadata, model) - { - ValidateAllProperties = true - }; + var validator = CreateValidator(); + + var model = (object)"test"; + + modelState.MaxAllowedErrors = 1; + modelState.AddModelError("other.Model", "error"); + modelState.SetModelValue("parameter", "test", "test"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, string.Empty, "parameter"); + + var entry = modelState["parameter"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); + } + + [Fact] + public void Validate_SimpleType_SuppressValidation() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)"test"; + + modelState.SetModelValue("parameter", "test", "test"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter", SuppressValidation = true }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "parameter"); + + var entry = modelState["parameter"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); + } + + + [Fact] + public void Validate_ComplexValueType_Valid() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new ValueType() { Reference = "ref", Value = 256 }; + + modelState.SetModelValue("parameter.Reference", "ref", "ref"); + modelState.SetModelValue("parameter.Value", "256", "256"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "parameter.Reference", "parameter.Value"); + + var entry = modelState["parameter.Reference"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["parameter.Value"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + } + + [Fact] + public void Validate_ComplexReferenceType_Valid() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new ReferenceType() { Reference = "ref", Value = 256 }; + + modelState.SetModelValue("parameter.Reference", "ref", "ref"); + modelState.SetModelValue("parameter.Value", "256", "256"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "parameter.Reference", "parameter.Value"); + + var entry = modelState["parameter.Reference"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["parameter.Value"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + } + + [Fact] + public void Validate_ComplexReferenceType_Invalid() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new Person(); + + validationState.Add(model, new ValidationStateEntry() { Key = string.Empty }); // Act - validator.Validate(context.ModelValidationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); // Assert - var actualErrors = new Dictionary(); - foreach (var keyStatePair in context.ModelValidationContext.ModelState) + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "Name", "Profession"); + + var entry = modelState["Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage); + + entry = modelState["Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); + } + + [Fact] + public void Validate_ComplexType_SuppressValidation() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = new Person2() { - foreach (var error in keyStatePair.Value.Errors) - { - actualErrors.Add(keyStatePair.Key, error.ErrorMessage); - } - } + Name = "Billy", + Address = new Address { Street = "GreaterThan5Characters" } + }; - Assert.Equal(expectedErrors.Count, actualErrors.Count); - foreach (var keyErrorPair in expectedErrors) + modelState.SetModelValue("person.Name", "Billy", "Billy"); + modelState.SetModelValue("person.Address.Street", "GreaterThan5Characters", "GreaterThan5Characters"); + validationState.Add(model, new ValidationStateEntry() { Key = "person" }); + validationState.Add(model.Address, new ValidationStateEntry() { - Assert.Contains(keyErrorPair.Key, actualErrors.Keys); - Assert.Equal(keyErrorPair.Value, actualErrors[keyErrorPair.Key]); - } + Key = "person.Address", + SuppressValidation = true + }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "person", model); + + // Assert + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "person.Name", "person.Address.Street"); + + var entry = modelState["person.Name"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["person.Address.Street"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); } [Fact] [ReplaceCulture] - public void Validator_Throws_IfPropertyAccessorThrows() + public void Validate_ComplexReferenceType_Invalid_MultipleErrorsOnProperty() { // Arrange - var testValidationContext = GetModelValidationContext(new Uri("/api/values", UriKind.Relative), typeof(Uri)); - var topLevelValidationNode = - new ModelValidationNode( - string.Empty, - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - // Act & Assert - Assert.Throws( - typeof(InvalidOperationException), - () => - { - new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider) - .Validate(testValidationContext.ModelValidationContext, topLevelValidationNode); - }); + var validator = CreateValidator(); + + var model = (object)new Address() { Street = "Microsoft Way" }; + + modelState.SetModelValue("parameter.Street", "Microsoft Way", "Microsoft Way"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "parameter.Street"); + + var entry = modelState["parameter.Street"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + + Assert.Equal(2, entry.Errors.Count); + var errorMessages = entry.Errors.Select(e => e.ErrorMessage); + Assert.Contains(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 5, "Street"), errorMessages); + Assert.Contains(ValidationAttributeUtil.GetRegExErrorMessage("hehehe", "Street"), errorMessages); } - public static IEnumerable ObjectsWithPropertiesWhichThrowOnGet + [Fact] + [ReplaceCulture] + public void Validate_ComplexReferenceType_Invalid_MultipleErrorsOnProperty_EmptyPrefix() { - get - { - yield return new object[] { - new Uri("/api/values", UriKind.Relative), - typeof(Uri), - new List() { typeof(Uri) } - }; - - // https://github.com/aspnet/External/issues/23 - if (!TestPlatformHelper.IsMono) - { - yield return new object[] { new Dictionary { - { "values", new Uri("/api/values", UriKind.Relative) }, - { "hello", new Uri("/api/hello", UriKind.Relative) } - }, typeof(Dictionary), new List() { typeof(Uri) } }; - } - } + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new Address() { Street = "Microsoft Way" }; + + modelState.SetModelValue("Street", "Microsoft Way", "Microsoft Way"); + validationState.Add(model, new ValidationStateEntry() { Key = string.Empty }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); + + // Assert + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "Street"); + + var entry = modelState["Street"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + + Assert.Equal(2, entry.Errors.Count); + var errorMessages = entry.Errors.Select(e => e.ErrorMessage); + Assert.Contains(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 5, "Street"), errorMessages); + Assert.Contains(ValidationAttributeUtil.GetRegExErrorMessage("hehehe", "Street"), errorMessages); } - [Theory] - [MemberData(nameof(ObjectsWithPropertiesWhichThrowOnGet))] + [Fact] [ReplaceCulture] - public void Validator_DoesNotThrow_IfExcludedPropertyAccessorsThrow( - object input, Type type, List excludedTypes) + public void Validate_NestedComplexReferenceType_Invalid() { // Arrange - var testValidationContext = GetModelValidationContext(input, type, excludedTypes); - var topLevelValidationNode = - new ModelValidationNode( - string.Empty, - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - // Act & Assert (does not throw) - new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider) - .Validate(testValidationContext.ModelValidationContext, topLevelValidationNode); - Assert.True(testValidationContext.ModelValidationContext.ModelState.IsValid); + var validator = CreateValidator(); + + var model = (object)new Person() { Name = "Rick", Friend = new Person() }; + + modelState.SetModelValue("Name", "Rick", "Rick"); + validationState.Add(model, new ValidationStateEntry() { Key = string.Empty }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); + + // Assert + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "Name", "Profession", "Friend.Name", "Friend.Profession"); + + var entry = modelState["Name"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + + entry = modelState["Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); + + entry = modelState["Friend.Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage); + + entry = modelState["Friend.Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); } + // IValidatableObject is significant because the validators are on the object + // itself, not just the properties. [Fact] [ReplaceCulture] - public void Validator_Throws_IfPropertyGetterThrows() + public void Validate_ComplexType_IValidatableObject_Invalid() { // Arrange - var testValidationContext = GetModelValidationContext( - new Uri("/api/values", UriKind.Relative), typeof(Uri)); - var validationContext = testValidationContext.ModelValidationContext; - var topLevelValidationNode = - new ModelValidationNode( - string.Empty, - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - // Act & Assert - Assert.Throws( - () => - { - new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider) - .Validate(validationContext, topLevelValidationNode); - }); - Assert.True(validationContext.ModelState.IsValid); + var validator = CreateValidator(); + + var model = (object)new ValidatableModel(); + + modelState.SetModelValue("parameter", "model", "model"); + + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "parameter", "parameter.Property1", "parameter.Property2", "parameter.Property3"); + + var entry = modelState["parameter"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal("Error1", error.ErrorMessage); + + entry = modelState["parameter.Property1"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal("Error2", error.ErrorMessage); + + entry = modelState["parameter.Property2"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal("Error3", error.ErrorMessage); + + entry = modelState["parameter.Property3"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal("Error3", error.ErrorMessage); } [Fact] [ReplaceCulture] - public void MultipleValidationErrorsOnSameMemberReported() + public void Validate_ComplexType_FieldsAreIgnored_Valid() { // Arrange - var model = new Address() { Street = "Microsoft Way" }; - var testValidationContext = GetModelValidationContext(model, model.GetType()); - var validationContext = testValidationContext.ModelValidationContext; - var topLevelValidationNode = - new ModelValidationNode( - string.Empty, - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); - // Act (does not throw) - new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider) - .Validate(validationContext, topLevelValidationNode); + var model = (object)new VariableTest() { test = 5 }; + + modelState.SetModelValue("parameter", "5", "5"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); // Assert - Assert.Equal(1, validationContext.ModelState.Count); - Assert.Contains("Street", validationContext.ModelState.Keys); - var streetState = validationContext.ModelState["Street"]; - Assert.Equal(2, streetState.Errors.Count); - var errorCollection = streetState.Errors.Select(e => e.ErrorMessage); - Assert.Contains(ValidationAttributeUtil.GetStringLengthErrorMessage(null, 5, "Street"), errorCollection); - Assert.Contains(ValidationAttributeUtil.GetRegExErrorMessage("hehehe", "Street"), errorCollection); + Assert.True(modelState.IsValid); + Assert.Equal(1, modelState.Count); + + var entry = modelState["parameter"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); } [Fact] - public void Validate_DoesNotUseOverridden_GetHashCodeOrEquals() + [ReplaceCulture] + public void Validate_ComplexType_CyclesNotFollowed_Invalid() { // Arrange - var instance = new[] { - new TypeThatOverridesEquals { Funny = "hehe" }, - new TypeThatOverridesEquals { Funny = "hehe" } - }; - var testValidationContext = GetModelValidationContext(instance, typeof(TypeThatOverridesEquals[])); - var topLevelValidationNode = - new ModelValidationNode( - string.Empty, - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - // Act & Assert (does not throw) - new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider) - .Validate(testValidationContext.ModelValidationContext, topLevelValidationNode); + var validator = CreateValidator(); + + var person = new Person() { Name = "Billy" }; + person.Friend = person; + + var model = (object)person; + + modelState.SetModelValue("parameter.Name", "Billy", "Billy"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); + + // Act + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); + + // Assert + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "parameter.Name", "parameter.Profession"); + + var entry = modelState["parameter.Name"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["parameter.Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession")); } [Fact] - public void Validation_ShortCircuit_WhenMaxErrorCountIsSet() + public void Validate_ComplexType_ShortCircuit_WhenMaxErrorCountIsSet() { // Arrange - var user = new User() + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(typeof(string)); + + var model = new User() { Password = "password-val", ConfirmPassword = "not-password-val" }; - var testValidationContext = GetModelValidationContext( - user, - typeof(User), - new List { typeof(string) }); - - var validationContext = testValidationContext.ModelValidationContext; - validationContext.ModelState.MaxAllowedErrors = 2; - validationContext.ModelState.AddModelError("key1", "error1"); - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var topLevelValidationNode = - new ModelValidationNode( - "user", - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + modelState.MaxAllowedErrors = 2; + modelState.AddModelError("key1", "error1"); + modelState.SetModelValue("user.Password", "password-val", "password-val"); + modelState.SetModelValue("user.ConfirmPassword", "not-password-val", "not-password-val"); + + validationState.Add(model, new ValidationStateEntry() { Key = "user", }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, "user", model); // Assert - Assert.Equal(new[] { "key1", "", "user.ConfirmPassword" }, - validationContext.ModelState.Keys.ToArray()); - var modelState = validationContext.ModelState["user.ConfirmPassword"]; - Assert.Empty(modelState.Errors); - Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, string.Empty, "key1", "user.ConfirmPassword", "user.Password"); - var error = Assert.Single(validationContext.ModelState[""].Errors); + var entry = modelState[string.Empty]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); Assert.IsType(error.Exception); } [Fact] - public void ForExcludedNonModelBoundTypes_NoEntryInModelState() + [ReplaceCulture] + public void Validate_CollectionType_ArrayOfSimpleType_Valid_DefaultKeyPattern() { // Arrange - var user = new User() - { - Password = "password-val", - ConfirmPassword = "not-password-val" - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - var testValidationContext = GetModelValidationContext( - user, - typeof(User), - new List { typeof(User) }); - var validationContext = testValidationContext.ModelValidationContext; - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var topLevelValidationNode = - new ModelValidationNode( - "user", - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validator = CreateValidator(); + + var model = (object)new int[] { 5, 17 }; + + modelState.SetModelValue("parameter[0]", "5", "17"); + modelState.SetModelValue("parameter[1]", "17", "5"); + validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, "parameter", model); // Assert - Assert.True(validationContext.ModelState.IsValid); - Assert.Empty(validationContext.ModelState); + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "parameter[0]", "parameter[1]"); + + var entry = modelState["parameter[0]"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["parameter[0]"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); } [Fact] - public void ForExcludedModelBoundTypes_Properties_MarkedAsSkipped() + [ReplaceCulture] + public void Validate_CollectionType_ArrayOfComplexType_Invalid() { // Arrange - var user = new User() - { - Password = "password-val", - ConfirmPassword = "not-password-val" - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); - var testValidationContext = GetModelValidationContext( - user, - typeof(User), - new List { typeof(User) }); - var validationContext = testValidationContext.ModelValidationContext; - - // Set the value on model state as a model binder would. - validationContext.ModelState.SetModelValue("user.Password", new string[] { "password" }, "password"); - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var topLevelValidationNode = - new ModelValidationNode( - "user", - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validator = CreateValidator(); + + var model = (object)new Person[] { new Person(), new Person() }; + + validationState.Add(model, new ValidationStateEntry() { Key = string.Empty }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); // Assert - var entry = Assert.Single(validationContext.ModelState); - Assert.Equal("user.Password", entry.Key); - Assert.Empty(entry.Value.Errors); - Assert.Equal(entry.Value.ValidationState, ModelValidationState.Skipped); - Assert.Equal(new string[] { "password" }, entry.Value.RawValue); - Assert.Same("password", entry.Value.AttemptedValue); - } + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "[0].Name", "[0].Profession", "[1].Name", "[1].Profession"); - private class Person2 - { - public Address Address { get; set; } + var entry = modelState["[0].Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage); + + entry = modelState["[0].Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); + + entry = modelState["[1].Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage); + + entry = modelState["[1].Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); } [Fact] - public void Validate_IfSuppressIsSet_MarkedAsSkipped() + [ReplaceCulture] + public void Validate_CollectionType_ListOfComplexType_Invalid() { // Arrange - var testValidationContext = GetModelValidationContext( - new Person2() - { - Address = new Address { Street = "GreaterThan5Characters" } - }, - typeof(Person2)); - var validationContext = testValidationContext.ModelValidationContext; - - // Create an entry like a model binder would. - validationContext.ModelState.Add("person.Address", new ModelState()); - - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var modelExplorer = testValidationContext.ModelValidationContext.ModelExplorer; - var topLevelValidationNode = new ModelValidationNode( - "person", - modelExplorer.Metadata, - modelExplorer.Model); - - var propertyExplorer = modelExplorer.GetExplorerForProperty("Address"); - var childNode = new ModelValidationNode( - "person.Address", - propertyExplorer.Metadata, - propertyExplorer.Model) - { - SuppressValidation = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new List { new Person(), new Person() }; - topLevelValidationNode.ChildNodes.Add(childNode); + validationState.Add(model, new ValidationStateEntry() { Key = string.Empty }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); // Assert - Assert.True(validationContext.ModelState.IsValid); - Assert.Equal(1, validationContext.ModelState.Count); - var modelState = validationContext.ModelState["person.Address"]; - Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped); + Assert.False(modelState.IsValid); + AssertKeysEqual(modelState, "[0].Name", "[0].Profession", "[1].Name", "[1].Profession"); + + var entry = modelState["[0].Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage); + + entry = modelState["[0].Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); + + entry = modelState["[1].Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Name"), error.ErrorMessage); + + entry = modelState["[1].Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); } [Theory] @@ -577,63 +642,55 @@ public void Validate_IfSuppressIsSet_MarkedAsSkipped() [InlineData(new[] { "Foo", "Bar", "Baz" }, typeof(HashSet))] [InlineData(new[] { "1/1/14", "2/2/14", "3/3/14" }, typeof(ICollection))] [InlineData(new[] { "Foo", "Bar", "Baz" }, typeof(HashSet))] - public void EnumerableType_ValidationSuccessful(object model, Type type) + public void Validate_IndexedCollectionTypes_Valid(object model, Type type) { // Arrange - var modelStateDictionary = new ModelStateDictionary(); - modelStateDictionary.Add("items[0]", new ModelState()); - modelStateDictionary.Add("items[1]", new ModelState()); - modelStateDictionary.Add("items[2]", new ModelState()); - - var testValidationContext = GetModelValidationContext( - model, - type, - excludedTypes: null, - modelStateDictionary: modelStateDictionary); - - var excludeTypeFilters = new List(); - excludeTypeFilters.Add(new SimpleTypesExcludeFilter()); - testValidationContext.ExcludeFilters = excludeTypeFilters; - - var validationContext = testValidationContext.ModelValidationContext; - - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var topLevelValidationNode = - new ModelValidationNode( - "items", - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(new SimpleTypesExcludeFilter()); + + modelState.Add("items[0]", new ModelState()); + modelState.Add("items[1]", new ModelState()); + modelState.Add("items[2]", new ModelState()); + validationState.Add(model, new ValidationStateEntry() + { + Key = "items", + + // Force the validator to treat it as the specified type. + Metadata = MetadataProvider.GetMetadataForType(type), + }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, "items", model); // Assert - Assert.True(validationContext.ModelState.IsValid); - Assert.Equal(3, validationContext.ModelState.Count); - var modelState = validationContext.ModelState["items[0]"]; - Assert.Equal(modelState.ValidationState, ModelValidationState.Valid); + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "items[0]", "items[1]", "items[2]"); + + var entry = modelState["items[0]"]; + Assert.Equal(entry.ValidationState, ModelValidationState.Valid); + Assert.Empty(entry.Errors); - modelState = validationContext.ModelState["items[1]"]; - Assert.Equal(modelState.ValidationState, ModelValidationState.Valid); + entry = modelState["items[1]"]; + Assert.Equal(entry.ValidationState, ModelValidationState.Valid); + Assert.Empty(entry.Errors); - modelState = validationContext.ModelState["items[2]"]; - Assert.Equal(modelState.ValidationState, ModelValidationState.Valid); + entry = modelState["items[2]"]; + Assert.Equal(entry.ValidationState, ModelValidationState.Valid); + Assert.Empty(entry.Errors); } [Fact] - public void DictionaryType_ValidationSuccessful() + public void Validate_CollectionType_DictionaryOfSimpleType_Invalid() { // Arrange - var modelStateDictionary = new ModelStateDictionary(); - modelStateDictionary.Add("items[0].Key", new ModelState()); - modelStateDictionary.Add("items[0].Value", new ModelState()); - modelStateDictionary.Add("items[1].Key", new ModelState()); - modelStateDictionary.Add("items[1].Value", new ModelState()); + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(new SimpleTypesExcludeFilter()); var model = new Dictionary() { @@ -641,161 +698,215 @@ public void DictionaryType_ValidationSuccessful() { "BarKey", "BarValue" } }; - var testValidationContext = GetModelValidationContext( - model, - typeof(Dictionary), - excludedTypes: null, - modelStateDictionary: modelStateDictionary); + modelState.Add("items[0].Key", new ModelState()); + modelState.Add("items[0].Value", new ModelState()); + modelState.Add("items[1].Key", new ModelState()); + modelState.Add("items[1].Value", new ModelState()); + validationState.Add(model, new ValidationStateEntry() { Key = "items" }); - var excludeTypeFilters = new List(); - excludeTypeFilters.Add(new SimpleTypesExcludeFilter()); - testValidationContext.ExcludeFilters = excludeTypeFilters; + // Act + validator.Validate(validatorProvider, modelState, validationState, "items", model); - var validationContext = testValidationContext.ModelValidationContext; + // Assert + Assert.True(modelState.IsValid); + AssertKeysEqual(modelState, "items[0].Key", "items[0].Value", "items[1].Key", "items[1].Value"); - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); + var entry = modelState["items[0].Key"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); - var topLevelValidationNode = - new ModelValidationNode( - "items", - testValidationContext.ModelValidationContext.ModelExplorer.Metadata, - testValidationContext.ModelValidationContext.ModelExplorer.Model) - { - ValidateAllProperties = true - }; + entry = modelState["items[0].Value"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); - // Act - validator.Validate(validationContext, topLevelValidationNode); + entry = modelState["items[1].Key"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); - // Assert - Assert.True(validationContext.ModelState.IsValid); - Assert.Equal(4, validationContext.ModelState.Count); - var modelState = validationContext.ModelState["items[0].Key"]; - Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState); - modelState = validationContext.ModelState["items[0].Value"]; - Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState); - modelState = validationContext.ModelState["items[1].Key"]; - Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState); - modelState = validationContext.ModelState["items[1].Value"]; - Assert.Equal(ModelValidationState.Skipped, modelState.ValidationState); + entry = modelState["items[1].Value"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); } [Fact] - public void Validator_IfValidateAllPropertiesIsNotSet_DoesNotAutoExpand() + [ReplaceCulture] + public void Validate_CollectionType_DictionaryOfComplexType_Invalid() { // Arrange - var testValidationContext = GetModelValidationContext( - LonelyPerson, - typeof(Person)); - - var validationContext = testValidationContext.ModelValidationContext; - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var modelExplorer = testValidationContext.ModelValidationContext.ModelExplorer; - - // No ChildNode added - var topLevelValidationNode = new ModelValidationNode( - "person", - modelExplorer.Metadata, - modelExplorer.Model); + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = (object)new Dictionary { { "Joe", new Person() }, { "Mark", new Person() } }; + + modelState.SetModelValue("[0].Key", "Joe", "Joe"); + modelState.SetModelValue("[1].Key", "Mark", "Mark"); + validationState.Add(model, new ValidationStateEntry() { Key = string.Empty }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); // Assert - Assert.True(validationContext.ModelState.IsValid); + Assert.False(modelState.IsValid); + AssertKeysEqual( + modelState, + "[0].Key", + "[0].Value.Name", + "[0].Value.Profession", + "[1].Key", + "[1].Value.Name", + "[1].Value.Profession"); + + var entry = modelState["[0].Key"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["[1].Key"]; + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.Empty(entry.Errors); + + entry = modelState["[0].Value.Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + var error = Assert.Single(entry.Errors); + Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Name")); + + entry = modelState["[0].Value.Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession")); + + entry = modelState["[1].Value.Name"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Name")); + + entry = modelState["[1].Value.Profession"]; + Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); + error = Assert.Single(entry.Errors); + Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession")); + } + + [Fact] + [ReplaceCulture] + public void Validate_DoesntCatchExceptions_FromPropertyAccessors() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = new ThrowingProperty(); - // Since Person is not IValidatable and we do not look at its properties, the state is empty. - Assert.Empty(validationContext.ModelState.Keys); + // Act & Assert + Assert.Throws( + typeof(InvalidTimeZoneException), + () => + { + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); + }); } + // We use the reference equality comparer for breaking cycles [Fact] - public void Validator_IfValidateAllPropertiesSet_WithChildNodes_DoesNotAutoExpand() + public void Validate_DoesNotUseOverridden_GetHashCodeOrEquals() { // Arrange - var testValidationContext = GetModelValidationContext( - LonelyPerson, - typeof(Person)); - - var validationContext = testValidationContext.ModelValidationContext; - var validator = new DefaultObjectValidator( - testValidationContext.ExcludeFilters, - testValidationContext.ModelMetadataProvider); - var modelExplorer = testValidationContext.ModelValidationContext.ModelExplorer; - - var topLevelValidationNode = new ModelValidationNode( - "person", - modelExplorer.Metadata, - modelExplorer.Model) + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(); + + var model = new TypeThatOverridesEquals[] { - ValidateAllProperties = true + new TypeThatOverridesEquals { Funny = "hehe" }, + new TypeThatOverridesEquals { Funny = "hehe" } }; - var propertyExplorer = modelExplorer.GetExplorerForProperty("Profession"); - var childNode = new ModelValidationNode( - "person.Profession", - propertyExplorer.Metadata, - propertyExplorer.Model); + // Act & Assert (does not throw) + validator.Validate(validatorProvider, modelState, validationState, string.Empty, model); + } - topLevelValidationNode.ChildNodes.Add(childNode); + [Fact] + public void Validate_ForExcludedType_PropertiesMarkedAsSkipped() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var modelState = new ModelStateDictionary(); + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(typeof(User)); + + var model = new User() + { + Password = "password-val", + ConfirmPassword = "not-password-val" + }; + + // Note that user.ConfirmPassword has no entry in modelstate - we should not + // create one just to mark it as skipped. + modelState.SetModelValue("user.Password", "password-val", "password-val"); + validationState.Add(model, new ValidationStateEntry() { Key = "user", }); // Act - validator.Validate(validationContext, topLevelValidationNode); + validator.Validate(validatorProvider, modelState, validationState, "user", model); // Assert - var modelState = validationContext.ModelState; - Assert.False(modelState.IsValid); + Assert.Equal(ModelValidationState.Valid, modelState.ValidationState); + AssertKeysEqual(modelState, "user.Password"); - // Since the model is invalid at property level there is no entry in the model state for top level node. - Assert.Single(modelState.Keys, k => k == "person.Profession"); - Assert.Equal(1, modelState.Count); + var entry = modelState["user.Password"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); } - private TestModelValidationContext GetModelValidationContext( - object model, - Type type, - List excludedTypes = null) + private static IModelValidatorProvider CreateValidatorProvider() { - return GetModelValidationContext(model, type, excludedTypes, new ModelStateDictionary()); + return TestModelValidatorProvider.CreateDefaultProvider(); } - private TestModelValidationContext GetModelValidationContext( - object model, - Type type, - List excludedTypes, - ModelStateDictionary modelStateDictionary) + private static DefaultObjectValidator CreateValidator(Type excludedType) { - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - - var excludedValidationTypesPredicate = new List(); - if (excludedTypes != null) + var excludeFilters = new List(); + if (excludedType != null) { - var mockExcludeTypeFilter = new Mock(); - mockExcludeTypeFilter + var excludeFilter = new Mock(); + excludeFilter .Setup(o => o.IsTypeExcluded(It.IsAny())) - .Returns(excludedType => excludedTypes.Any(t => t.IsAssignableFrom(excludedType))); + .Returns(t => t.IsAssignableFrom(excludedType)); - excludedValidationTypesPredicate.Add(mockExcludeTypeFilter.Object); + excludeFilters.Add(excludeFilter.Object); } - var modelExplorer = modelMetadataProvider.GetModelExplorerForType(type, model); + return new DefaultObjectValidator(excludeFilters, TestModelMetadataProvider.CreateDefaultProvider()); + } + + private static DefaultObjectValidator CreateValidator(params IExcludeTypeValidationFilter[] excludeFilters) + { + return new DefaultObjectValidator(excludeFilters, TestModelMetadataProvider.CreateDefaultProvider()); + } + + private static void AssertKeysEqual(ModelStateDictionary modelState, params string[] keys) + { + Assert.Equal(keys.OrderBy(k => k).ToArray(), modelState.Keys.OrderBy(k => k).ToArray()); + } - return new TestModelValidationContext + private class ThrowingProperty + { + public string WatchOut { - ModelValidationContext = new ModelValidationContext( - null, - TestModelValidatorProvider.CreateDefaultProvider(), - modelStateDictionary, - modelExplorer), - ModelMetadataProvider = modelMetadataProvider, - ExcludeFilters = excludedValidationTypesPredicate - }; + get + { + throw new InvalidTimeZoneException(); + } + } } - public class Person + private class Person { [Required, StringLength(10)] public string Name { get; set; } @@ -806,33 +917,32 @@ public class Person public Person Friend { get; set; } } - public class Address + private class Person2 { - [StringLength(5)] - [RegularExpression("hehehe")] - public string Street { get; set; } + public string Name { get; set; } + public Address Address { get; set; } } - public struct ValueType + private class Address { - public int Value; - public string Reference; + [StringLength(5)] + [RegularExpression("hehehe")] + public string Street { get; set; } } - public class ReferenceType + private struct ValueType { - public static string StaticProperty { get { return "static"; } } - public int Value; - public string Reference; + public int Value { get; set; } + public string Reference { get; set; } } - public class Pet + private class ReferenceType { - [Required] - public Person Owner { get; set; } + public int Value { get; set; } + public string Reference { get; set; } } - public class ValidatableModel : IValidatableObject + private class ValidatableModel : IValidatableObject { public IEnumerable Validate(ValidationContext validationContext) { @@ -842,7 +952,7 @@ public IEnumerable Validate(ValidationContext validationContex } } - public class TypeThatOverridesEquals + private class TypeThatOverridesEquals { [StringLength(2)] public string Funny { get; set; } @@ -858,44 +968,12 @@ public override int GetHashCode() } } - public class VariableTest + private class VariableTest { [Range(15, 25)] public int test; } - public class Team - { - [Required] - public int Id { get; set; } - - [Required] - [StringLength(20, MinimumLength = 4)] - public string TeamName { get; set; } - - [MaxLength(10)] - public string Lead { get; set; } - - [Range(3, 100)] - public int TeamSize { get; set; } - - public string TeamDescription { get; set; } - } - - public class Org - { - [Required] - public int Id { get; set; } - - [StringLength(20, MinimumLength = 4)] - public string OrgName { get; set; } - - [Required] - public Team Dev { get; set; } - - public Team Test { get; set; } - } - private class User : IValidatableObject { public string Password { get; set; } @@ -911,26 +989,6 @@ public IEnumerable Validate(ValidationContext validationContex } } } - - public class TestServiceProvider - { - [FromServices] - [Required] - public ITestService TestService { get; set; } - } - - public interface ITestService - { - } - - private class TestModelValidationContext - { - public ModelValidationContext ModelValidationContext { get; set; } - - public IModelMetadataProvider ModelMetadataProvider { get; set; } - - public IList ExcludeFilters { get; set; } - } } } #endif diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ExplicitIndexCollectionValidationStrategyTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ExplicitIndexCollectionValidationStrategyTest.cs new file mode 100644 index 0000000000..6dd7654c7a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ExplicitIndexCollectionValidationStrategyTest.cs @@ -0,0 +1,159 @@ +// 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; +using System.Linq; +using Xunit; + + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + public class ExplicitIndexCollectionValidationStrategyTest + { + [Fact] + public void EnumerateElements_List() + { + // Arrange + var model = new List() { 2, 3, 5 }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(List)); + var strategy = new ExplicitIndexCollectionValidationStrategy(new string[] { "zero", "one", "two" }); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[one]", e.Key); + Assert.Equal(3, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[two]", e.Key); + Assert.Equal(5, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[zero]", e.Key); + Assert.Equal(2, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }); + } + + [Fact] + public void EnumerateElements_Dictionary() + { + // Arrange + var model = new Dictionary() + { + { 2, "two" }, + { 3, "three" }, + { 5, "five" }, + }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(List)); + var strategy = new ExplicitIndexCollectionValidationStrategy(new string[] { "zero", "one", "two" }); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[one]", e.Key); + Assert.Equal(new KeyValuePair(3, "three"), e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[two]", e.Key); + Assert.Equal(new KeyValuePair(5, "five"), e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[zero]", e.Key); + Assert.Equal(new KeyValuePair(2, "two"), e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }); + } + + [Fact] + public void EnumerateElements_RunOutOfIndices() + { + // Arrange + var model = new List() { 2, 3, 5 }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(List)); + + var strategy = new ExplicitIndexCollectionValidationStrategy(new string[] { "zero", "one", }); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[one]", e.Key); + Assert.Equal(3, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[zero]", e.Key); + Assert.Equal(2, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }); + } + + [Fact] + public void EnumerateElements_RunOutOfElements() + { + // Arrange + var model = new List() { 2, 3, }; + + var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(List)); + + var strategy = new ExplicitIndexCollectionValidationStrategy(new string[] { "zero", "one", "two" }); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[one]", e.Key); + Assert.Equal(3, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[zero]", e.Key); + Assert.Equal(2, e.Model); + Assert.Same(metadata.ElementMetadata, e.Metadata); + }); + } + + private List BufferEntries(IEnumerator enumerator) + { + var entries = new List(); + while (enumerator.MoveNext()) + { + entries.Add(enumerator.Current); + } + + return entries; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ShortFormDictionaryValidationStrategyTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ShortFormDictionaryValidationStrategyTest.cs new file mode 100644 index 0000000000..4dae37c485 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ShortFormDictionaryValidationStrategyTest.cs @@ -0,0 +1,154 @@ +// 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; +using System.Linq; +using Xunit; + + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + public class ShortFormDictionaryValidationStrategyTest + { + [Fact] + public void EnumerateElements() + { + // Arrange + var model = new Dictionary() + { + { 2, "two" }, + { 3, "three" }, + { 5, "five" }, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = metadataProvider.GetMetadataForType(typeof(List)); + var valueMetadata = metadataProvider.GetMetadataForType(typeof(string)); + var strategy = new ShortFormDictionaryValidationStrategy(new Dictionary() + { + { "2", 2 }, + { "3", 3 }, + { "5", 5 }, + }, + valueMetadata); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[2]", e.Key); + Assert.Equal("two", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[3]", e.Key); + Assert.Equal("three", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[5]", e.Key); + Assert.Equal("five", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }); + } + + [Fact] + public void EnumerateElements_RunOutOfIndices() + { + // Arrange + var model = new Dictionary() + { + { 2, "two" }, + { 3, "three" }, + { 5, "five" }, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = metadataProvider.GetMetadataForType(typeof(List)); + var valueMetadata = metadataProvider.GetMetadataForType(typeof(string)); + var strategy = new ShortFormDictionaryValidationStrategy(new Dictionary() + { + { "2", 2 }, + { "3", 3 }, + }, + valueMetadata); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[2]", e.Key); + Assert.Equal("two", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[3]", e.Key); + Assert.Equal("three", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }); + } + + [Fact] + public void EnumerateElements_RunOutOfElements() + { + // Arrange + var model = new Dictionary() + { + { 2, "two" }, + { 3, "three" }, + }; + + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = metadataProvider.GetMetadataForType(typeof(List)); + var valueMetadata = metadataProvider.GetMetadataForType(typeof(string)); + var strategy = new ShortFormDictionaryValidationStrategy(new Dictionary() + { + { "2", 2 }, + { "3", 3 }, + { "5", 5 }, + }, + valueMetadata); + + // Act + var enumerator = strategy.GetChildren(metadata, "prefix", model); + + // Assert + Assert.Collection( + BufferEntries(enumerator).OrderBy(e => e.Key), + e => + { + Assert.Equal("prefix[2]", e.Key); + Assert.Equal("two", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }, + e => + { + Assert.Equal("prefix[3]", e.Key); + Assert.Equal("three", e.Model); + Assert.Same(valueMetadata, e.Metadata); + }); + } + + private List BufferEntries(IEnumerator enumerator) + { + var entries = new List(); + while (enumerator.MoveNext()) + { + entries.Add(enumerator.Current); + } + + return entries; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs index b3aa9c04e1..c0dc751b97 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs @@ -262,11 +262,12 @@ public void IsRequiredTests() private static ModelValidationContext CreateValidationContext(ModelExplorer modelExplorer) { - return new ModelValidationContext( - bindingSource: null, - modelState: null, - validatorProvider: null, - modelExplorer: modelExplorer); + return new ModelValidationContext() + { + Container = modelExplorer.Container, + Metadata = modelExplorer.Metadata, + Model = modelExplorer.Model, + }; } private class DerivedRequiredAttribute : RequiredAttribute diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs index cd11d2844a..4dc293bfbe 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ActionParametersIntegrationTest.cs @@ -107,13 +107,13 @@ public async Task ActionParameter_ReadOnlyCollectionModel_EmptyPrefix_DoesNotGet // Read-only collection should not be updated. Assert.Empty(boundModel.Address); - // ModelState (data is valid but is not copied into Address). - Assert.True(modelState.IsValid); + // ModelState (data is can't be validated). + Assert.False(modelState.IsValid); var entry = Assert.Single(modelState); Assert.Equal("Address[0].Street", entry.Key); var state = entry.Value; Assert.NotNull(state); - Assert.Equal(ModelValidationState.Valid, state.ValidationState); + Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState); Assert.Equal("SomeStreet", state.RawValue); Assert.Equal("SomeStreet", state.AttemptedValue); } @@ -286,13 +286,13 @@ public async Task ActionParameter_ReadOnlyCollectionModel_WithPrefix_DoesNotGetB // Read-only collection should not be updated. Assert.Empty(boundModel.Address); - // ModelState (data is valid but is not copied into Address). - Assert.True(modelState.IsValid); + // ModelState (data cannot be validated). + Assert.False(modelState.IsValid); var entry = Assert.Single(modelState); Assert.Equal("prefix.Address[0].Street", entry.Key); var state = entry.Value; Assert.NotNull(state); - Assert.Equal(ModelValidationState.Valid, state.ValidationState); + Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState); Assert.Equal("SomeStreet", state.AttemptedValue); Assert.Equal("SomeStreet", state.RawValue); } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs index 67397c7dd2..717e5db2e1 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs @@ -44,7 +44,11 @@ public async Task BindParameter_WithModelBinderType_NullData_ReturnsNull() // ModelState (not set unless inner binder sets it) Assert.True(modelState.IsValid); - Assert.Empty(modelState); + var entry = modelState[string.Empty]; + Assert.Null(entry.AttemptedValue); + Assert.Null(entry.RawValue); + Assert.Empty(entry.Errors); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); } [Fact] @@ -294,15 +298,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex new string[] { address.Street }, address.Street); - var validationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - address) - { - ValidateAllProperties = true - }; - - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, address, validationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, address); } } @@ -316,11 +312,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex new string[] { model }, model); - var modelValidationNode = new ModelValidationNode( - bindingContext.ModelName, - bindingContext.ModelMetadata, - model); - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, modelValidationNode); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } } @@ -328,7 +320,7 @@ private class NullModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model: null, validationNode: null); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model: null); } } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs index e592e584ee..dd5c38aae5 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs @@ -630,12 +630,12 @@ void ICollection>.CopyTo(KeyValuePair> IEnumerable>.GetEnumerator() { - throw new NotImplementedException(); + return _data.GetEnumerator(); } bool ICollection>.Remove(KeyValuePair item) @@ -650,7 +650,7 @@ bool IDictionary.Remove(string key) bool IDictionary.TryGetValue(string key, out string value) { - throw new NotImplementedException(); + return _data.TryGetValue(key, out value); } } @@ -735,12 +735,12 @@ void ICollection>.CopyTo(KeyValuePair[] IEnumerator IEnumerable.GetEnumerator() { - throw new NotImplementedException(); + return _data.GetEnumerator(); } IEnumerator> IEnumerable>.GetEnumerator() { - throw new NotImplementedException(); + return _data.GetEnumerator(); } bool ICollection>.Remove(KeyValuePair item) @@ -755,7 +755,7 @@ bool IDictionary.Remove(TKey key) bool IDictionary.TryGetValue(TKey key, out TValue value) { - throw new NotImplementedException(); + return _data.TryGetValue(key, out value); } } } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs index 9233dba834..2209e4bd3a 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs @@ -161,7 +161,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex return ModelBindingResult.NoResultAsync; } - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, new Address(), validationNode: null); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, new Address()); } } diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs index 2890a57e13..74f4820941 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/HttpRequestMessage/HttpRequestMessageModelBinderTest.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Xunit; namespace Microsoft.AspNet.Mvc.WebApiCompatShim @@ -27,8 +28,11 @@ public async Task BindModelAsync_ReturnsNonEmptyResult_ForHttpRequestMessageType Assert.NotEqual(ModelBindingResult.NoResult, result); Assert.True(result.IsModelSet); Assert.Same(expectedModel, result.Model); - Assert.NotNull(result.ValidationNode); - Assert.True(result.ValidationNode.SuppressValidation); + + var entry = bindingContext.ValidationState[result.Model]; + Assert.True(entry.SuppressValidation); + Assert.Null(entry.Key); + Assert.Null(entry.Metadata); } [Theory] @@ -59,7 +63,8 @@ private static ModelBindingContext GetBindingContext(Type modelType) { HttpContext = new DefaultHttpContext(), MetadataProvider = metadataProvider, - } + }, + ValidationState = new ValidationStateDictionary(), }; bindingContext.OperationBindingContext.HttpContext.Request.Method = "GET"; diff --git a/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs index c17b407b47..365feb282c 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_ProductController.cs @@ -75,9 +75,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex OrderStatus model; if (Enum.TryParse("Status" + request.Query["status"], out model)) { - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model); - return ModelBindingResult.SuccessAsync("status", model, validationNode); + return ModelBindingResult.SuccessAsync("status", model); } return ModelBindingResult.FailedAsync("status"); @@ -105,9 +103,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex var value = bindingContext.ValueProvider.GetValue(key); model.ProductId = value.ConvertTo(); - var validationNode = - new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, value); - return ModelBindingResult.SuccessAsync(key, model, validationNode); + return ModelBindingResult.SuccessAsync(key, model); } return ModelBindingResult.NoResultAsync; diff --git a/test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs b/test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs index 10f9bb4cab..4f0931c5a8 100644 --- a/test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs +++ b/test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs @@ -27,7 +27,7 @@ public Task BindModelAsync(ModelBindingContext bindingContex if (!IsSimpleType(bindingContext.ModelType)) { model = Activator.CreateInstance(bindingContext.ModelType); - return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode: null); + return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model); } return ModelBindingResult.FailedAsync(bindingContext.ModelName);