Restoring modelvalidation node.

This commit is contained in:
Harsh Gupta 2015-05-06 14:58:40 -07:00
parent 8b5223518f
commit 22f1881cc6
33 changed files with 853 additions and 207 deletions

View File

@ -16,10 +16,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <param name="isModelSet">A value that represents if the model has been set by the
/// <see cref="IModelBinder"/>.</param>
public ModelBindingResult(object model, string key, bool isModelSet)
: this (model, key, isModelSet, validationNode: null)
{
}
/// <summary>
/// Creates a new <see cref="ModelBindingResult"/>.
/// </summary>
/// <param name="model">The model which was created by the <see cref="IModelBinder"/>.</param>
/// <param name="key">The key using which was used to attempt binding the model.</param>
/// <param name="isModelSet">A value that represents if the model has been set by the
/// <see cref="IModelBinder"/>.</param>
/// <param name="validationNode">A <see cref="ModelValidationNode"/> which captures the validation information.
/// </param>
public ModelBindingResult(object model, string key, bool isModelSet, ModelValidationNode validationNode)
{
Model = model;
Key = key;
IsModelSet = isModelSet;
ValidationNode = validationNode;
}
/// <summary>
@ -47,5 +62,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </para>
/// </summary>
public bool IsModelSet { get; }
/// <summary>
/// A <see cref="ModelValidationNode"/> associated with the current <see cref="ModelBindingResult"/>.
/// </summary>
public ModelValidationNode ValidationNode { get; }
}
}

View File

@ -0,0 +1,76 @@
// 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
{
/// <summary>
/// Captures the validation information for a particular model.
/// </summary>
public class ModelValidationNode
{
/// <summary>
/// Creates a new instance of <see cref="ModelValidationNode"/>.
/// </summary>
/// <param name="key">The key that will be used by the validation system to find <see cref="ModelState"/>
/// entries.</param>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/> for the <paramref name="model"/>.</param>
/// <param name="model">The model object which is to be validated.</param>
public ModelValidationNode([NotNull] string key, [NotNull] ModelMetadata modelMetadata, object model)
: this (key, modelMetadata, model, new List<ModelValidationNode>())
{
}
/// <summary>
/// Creates a new instance of <see cref="ModelValidationNode"/>.
/// </summary>
/// <param name="key">The key that will be used by the validation system to add
/// <see cref="ModelStateDictionary"/> entries.</param>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/> for the <paramref name="model"/>.</param>
/// <param name="model">The model object which will be validated.</param>
/// <param name="childNodes">A collection of child nodes.</param>
public ModelValidationNode(
[NotNull] string key,
[NotNull] ModelMetadata modelMetadata,
object model,
[NotNull] IList<ModelValidationNode> childNodes)
{
Key = key;
ModelMetadata = modelMetadata;
ChildNodes = childNodes;
Model = model;
}
/// <summary>
/// Gets the key used for adding <see cref="ModelStateDictionary"/> entries.
/// </summary>
public string Key { get; }
/// <summary>
/// Gets the <see cref="ModelMetadata"/>.
/// </summary>
public ModelMetadata ModelMetadata { get; }
/// <summary>
/// Gets the model instance which is to be validated.
/// </summary>
public object Model { get; }
/// <summary>
/// Gets the child nodes.
/// </summary>
public IList<ModelValidationNode> ChildNodes { get; }
/// <summary>
/// Gets or sets a value that indicates whether all properties of the model should be validated.
/// </summary>
public bool ValidateAllProperties { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether validation should be suppressed.
/// </summary>
public bool SuppressValidation { get; set; }
}
}

View File

@ -10,23 +10,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelValidationContext(
[NotNull] ModelBindingContext bindingContext,
[NotNull] ModelExplorer modelExplorer)
: this(bindingContext.ModelName,
bindingContext.BindingSource,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
modelExplorer)
: this(
bindingContext.BindingSource,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
modelExplorer)
{
}
public ModelValidationContext(
string rootPrefix,
BindingSource bindingSource,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] ModelStateDictionary modelState,
[NotNull] ModelExplorer modelExplorer)
{
ModelState = modelState;
RootPrefix = rootPrefix;
ValidatorProvider = validatorProvider;
ModelExplorer = modelExplorer;
BindingSource = bindingSource;
@ -45,7 +43,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
[NotNull] ModelExplorer modelExplorer)
{
return new ModelValidationContext(
parentContext.RootPrefix,
modelExplorer.Metadata.BindingSource,
parentContext.ValidatorProvider,
parentContext.ModelState,
@ -56,8 +53,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelStateDictionary ModelState { get; }
public string RootPrefix { get; set; }
public BindingSource BindingSource { get; set; }
public IModelValidatorProvider ValidatorProvider { get; }

View File

@ -1336,13 +1336,17 @@ namespace Microsoft.AspNet.Mvc
modelName);
var validationContext = new ModelValidationContext(
modelName,
bindingSource: null,
validatorProvider: BindingContext.ValidatorProvider,
modelState: ModelState,
modelExplorer: modelExplorer);
ObjectValidator.Validate(validationContext);
ObjectValidator.Validate(
validationContext,
new ModelValidationNode(modelName, modelExplorer.Metadata, model)
{
ValidateAllProperties = true
});
return ModelState.IsValid;
}

View File

@ -90,12 +90,11 @@ namespace Microsoft.AspNet.Mvc
modelBindingResult.Model);
var validationContext = new ModelValidationContext(
key,
modelBindingContext.BindingSource,
operationContext.ValidatorProvider,
modelState,
modelExplorer);
_validator.Validate(validationContext);
_validator.Validate(validationContext, modelBindingResult.ValidationNode);
}
return modelBindingResult;

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await modelBinder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) :
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// A model binder was specified by metadata and this binder handles all such cases.

View File

@ -81,7 +81,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var modelBindingResult =
result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) :
new ModelBindingResult(model: null, key: context.ModelName, isModelSet: false);
// This model binder is the only handler for its binding source.

View File

@ -54,11 +54,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
// For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of
// For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of
// the parameter name/a custom name. In all other cases (like when binding body to a property) we
// consider the entire ModelName as a prefix.
var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName;
return new ModelBindingResult(model, key: modelBindingKey, isModelSet: true);
var validationNode = new ModelValidationNode(modelBindingKey, bindingContext.ModelMetadata, model)
{
ValidateAllProperties = true
};
return new ModelBindingResult(
model,
key: modelBindingKey,
isModelSet: true,
validationNode: validationNode);
}
catch (Exception ex)
{

View File

@ -39,7 +39,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
try
{
var model = Convert.FromBase64String(value);
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
// We do not need to set an explict ModelValidationNode since CompositeModelBinder does that automatically.
return new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true);
}
catch (Exception ex)
{

View File

@ -31,16 +31,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
IEnumerable<TElement> boundCollection;
CollectionResult result;
if (valueProviderResult == null)
{
boundCollection = await BindComplexCollection(bindingContext);
result = await BindComplexCollection(bindingContext);
boundCollection = result.Model;
}
else
{
boundCollection = await BindSimpleCollection(
result = await BindSimpleCollection(
bindingContext,
valueProviderResult.RawValue,
valueProviderResult.Culture);
boundCollection = result.Model;
}
var model = bindingContext.Model;
@ -54,12 +57,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
CopyToModel(model, boundCollection);
}
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
return new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true,
validationNode: result?.ValidationNode);
}
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
// is [ "1", "2" ] and needs to be converted to an int[].
internal async Task<IEnumerable<TElement>> BindSimpleCollection(
internal async Task<CollectionResult> BindSimpleCollection(
ModelBindingContext bindingContext,
object rawValue,
CultureInfo culture)
@ -74,6 +81,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement));
var validationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
boundCollection);
var rawValueArray = RawValueToObjectArray(rawValue);
foreach (var rawValueElement in rawValueArray)
{
@ -91,18 +102,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
object boundValue = null;
var result =
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext);
if (result != null)
if (result != null && result.IsModelSet)
{
boundValue = result.Model;
if (result.ValidationNode != null)
{
validationNode.ChildNodes.Add(result.ValidationNode);
}
}
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
}
return boundCollection;
return new CollectionResult
{
ValidationNode = validationNode,
Model = boundCollection
};
}
// Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1].
private async Task<IEnumerable<TElement>> BindComplexCollection(ModelBindingContext bindingContext)
private async Task<CollectionResult> BindComplexCollection(ModelBindingContext bindingContext)
{
var indexPropertyName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "index");
var valueProviderResultIndex = await bindingContext.ValueProvider.GetValueAsync(indexPropertyName);
@ -111,7 +130,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
}
internal async Task<IEnumerable<TElement>> BindComplexCollectionFromIndexes(
internal async Task<CollectionResult> BindComplexCollectionFromIndexes(
ModelBindingContext bindingContext,
IEnumerable<string> indexNames)
{
@ -131,6 +150,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var elementMetadata = metadataProvider.GetMetadataForType(typeof(TElement));
var boundCollection = new List<TElement>();
var validationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
boundCollection);
foreach (var indexName in indexNames)
{
var fullChildName = ModelNames.CreateIndexModelName(bindingContext.ModelName, indexName);
@ -146,10 +169,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result =
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext);
if (result != null)
if (result != null && result.IsModelSet)
{
didBind = true;
boundValue = result.Model;
if (result.ValidationNode != null)
{
validationNode.ChildNodes.Add(result.ValidationNode);
}
}
// infinite size collection stops on first bind failure
@ -161,7 +188,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
}
return boundCollection;
return new CollectionResult
{
ValidationNode = validationNode,
Model = boundCollection
};
}
internal class CollectionResult
{
public ModelValidationNode ValidationNode { get; set; }
public IEnumerable<TElement> Model { get; set; }
}
/// <summary>

View File

@ -55,6 +55,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.OperationBindingContext.BodyBindingState =
newBindingContext.OperationBindingContext.BodyBindingState;
var bindingKey = bindingContext.ModelName;
if (modelBindingResult.IsModelSet)
{
// Update the model state key if we are bound using an empty prefix and it is a complex type.
@ -78,15 +79,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// In this case, for the model parameter the key would be SimpleType instead of model.SimpleType.
// (i.e here the prefix for the model key is empty).
// For the id parameter the key would be id.
return modelBindingResult;
bindingKey = string.Empty;
}
}
// Fall through to update the ModelBindingResult's key.
// Update the model validation node if the model binding result was set but no validation node was provided.
// This would typically be the case where leaf level model binders, do not have to add a validation node
// for validation to take effect. The composite being the entry point for model binders, takes care or
// adding missing validation nodes.
var modelValidationNode = modelBindingResult.ValidationNode;
if (modelBindingResult.IsModelSet && modelValidationNode == null)
{
modelValidationNode = new ModelValidationNode(
bindingKey,
bindingContext.ModelMetadata,
modelBindingResult.Model);
}
return new ModelBindingResult(
modelBindingResult.Model,
bindingContext.ModelName,
modelBindingResult.IsModelSet);
bindingKey,
modelBindingResult.IsModelSet,
modelValidationNode);
}
private async Task<ModelBindingResult> TryBind(ModelBindingContext bindingContext)

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
new ModelBindingResult(result.Model, result.Key, result.IsModelSet, result.ValidationNode) :
new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// Were able to resolve a binder type.

View File

@ -51,7 +51,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, isModelSet: model != null));
ModelValidationNode validationNode = null;
if (model != null)
{
validationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
model);
}
return Task.FromResult(
new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: model != null,
validationNode: validationNode));
}
}
}

View File

@ -15,8 +15,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
typeof(KeyValuePair<TKey, TValue>),
allowNullModel: true);
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value");
var childNodes = new List<ModelValidationNode>();
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key", childNodes);
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value", childNodes);
if (keyResult.IsModelSet && valueResult.IsModelSet)
{
@ -24,8 +25,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelBindingHelper.CastOrDefault<TKey>(keyResult.Model),
ModelBindingHelper.CastOrDefault<TValue>(valueResult.Model));
// Update the model for the top level validation node.
var modelValidationNode =
new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
model,
childNodes);
// Success
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
return new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true,
validationNode: modelValidationNode);
}
else if (!keyResult.IsModelSet && valueResult.IsModelSet)
{
@ -55,8 +68,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(ModelBindingContext parentBindingContext,
string propertyName)
internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(
ModelBindingContext parentBindingContext,
string propertyName,
List<ModelValidationNode> childNodes)
{
var propertyModelMetadata =
parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(typeof(TModel));
@ -72,6 +87,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
propertyBindingContext);
if (modelBindingResult != null)
{
if (modelBindingResult.ValidationNode != null)
{
childNodes.Add(modelBindingResult.ValidationNode);
}
return modelBindingResult;
}

View File

@ -44,12 +44,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
EnsureModel(bindingContext);
var result = await CreateAndPopulateDto(bindingContext, mutableObjectBinderContext.PropertyMetadata);
var validationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
bindingContext.Model);
// post-processing, e.g. property setters and hooking up validation
ProcessDto(bindingContext, (ComplexModelDto)result.Model);
ProcessDto(bindingContext, (ComplexModelDto)result.Model, validationNode);
return new ModelBindingResult(
bindingContext.Model,
bindingContext.ModelName,
isModelSet: true);
isModelSet: true,
validationNode: validationNode);
}
/// <summary>
@ -359,11 +365,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return validationInfo;
}
internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto)
// Internal for testing.
internal ModelValidationNode ProcessDto(
ModelBindingContext bindingContext,
ComplexModelDto dto,
ModelValidationNode validationNode)
{
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var modelExplorer = metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model);
var validationInfo = GetPropertyValidationInfo(bindingContext);
// Eliminate provided properties from requiredProperties; leaving just *missing* required properties.
@ -415,8 +424,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
out requiredValidator);
SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult, requiredValidator);
var dtoValidationNode = dtoResult.ValidationNode;
if (dtoValidationNode == null)
{
// Make sure that irrespective of if the properties of the model were bound with a value,
// create a validation node so that these get validated.
dtoValidationNode = new ModelValidationNode(dtoResult.Key, entry.Key, dtoResult.Model);
}
validationNode.ChildNodes.Add(dtoValidationNode);
}
}
return validationNode;
}
/// <summary>

View File

@ -26,7 +26,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var model = requestServices.GetRequiredService(bindingContext.ModelType);
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true));
var validationNode =
new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model)
{
SuppressValidation = true
};
return Task.FromResult(new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true,
validationNode: validationNode));
}
}
}

View File

@ -30,7 +30,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
return new ModelBindingResult(newModel, bindingContext.ModelName, isModelSet: true);
// We do not need to set an explict ModelValidationNode since CompositeModelBinder does that automatically.
return new ModelBindingResult(
newModel,
bindingContext.ModelName,
isModelSet: true);
}
catch (Exception ex)
{

View File

@ -19,7 +19,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var model = valueProviderResult.RawValue;
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
// We do not need to set an explict ModelValidationNode since CompositeModelBinder does that automatically.
return new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true);
}
internal static async Task<ValueProviderResult> GetCompatibleValueProviderResult(ModelBindingContext context)

View File

@ -35,16 +35,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
/// <inheritdoc />
public void Validate([NotNull] ModelValidationContext modelValidationContext)
public void Validate(
[NotNull] ModelValidationContext modelValidationContext,
[NotNull] ModelValidationNode validationNode)
{
var validationContext = new ValidationContext()
{
ModelValidationContext = modelValidationContext,
Visited = new HashSet<object>(ReferenceEqualityComparer.Instance),
ValidationNode = validationNode
};
ValidateNonVisitedNodeAndChildren(
modelValidationContext.RootPrefix,
validationNode.Key,
validationContext,
validators: null);
}
@ -54,19 +57,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
ValidationContext validationContext,
IList<IModelValidator> validators)
{
var modelValidationContext = validationContext.ModelValidationContext;
var modelExplorer = modelValidationContext.ModelExplorer;
// Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack();
var modelValidationContext = validationContext.ModelValidationContext;
var modelExplorer = modelValidationContext.ModelExplorer;
var modelState = modelValidationContext.ModelState;
var bindingSource = modelValidationContext.BindingSource;
if (bindingSource != null && !bindingSource.IsFromRequest)
var currentValidationNode = validationContext.ValidationNode;
if (currentValidationNode.SuppressValidation)
{
// Short circuit if the metadata represents something that was not bound using request data.
// For example model bound using [FromServices]. Treat such objects as skipped.
// Short circuit if the node is marked to be suppressed
var validationState = modelState.GetFieldValidationState(modelKey);
if (validationState == ModelValidationState.Unvalidated)
{
@ -88,22 +88,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// 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. See ValidateElements function.
var validatorProvider = modelValidationContext.ValidatorProvider;
var validatorProviderContext = new ModelValidatorProviderContext(modelExplorer.Metadata);
validatorProvider.GetValidators(validatorProviderContext);
validators = validatorProviderContext.Validators;
// instead we just pass them over.
validators = GetValidators(modelValidationContext.ValidatorProvider, modelExplorer.Metadata);
}
// We don't need to recursively traverse the graph for null values
if (modelExplorer.Model == null)
// 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.Model.GetType();
var modelType = modelExplorer.ModelType;
if (IsTypeExcludedFromValidation(_excludeFilters, modelType))
{
var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators);
@ -112,24 +108,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
// 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);
// Validate the children first - depth-first traversal
var enumerableModel = modelExplorer.Model as IEnumerable;
if (enumerableModel == null)
{
isValid = ValidateProperties(modelKey, modelExplorer, validationContext);
}
else
{
isValid = ValidateElements(modelKey, enumerableModel, validationContext);
}
isValid = ValidateChildNodes(modelKey, modelExplorer, validationContext);
if (isValid)
{
// Don't bother to validate this node if children failed.
@ -166,32 +152,49 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
}
private bool ValidateProperties(
private IList<IModelValidator> GetValidators(IModelValidatorProvider provider, ModelMetadata metadata)
{
var validatorProviderContext = new ModelValidatorProviderContext(metadata);
provider.GetValidators(validatorProviderContext);
return validatorProviderContext.Validators;
}
private bool ValidateChildNodes(
string currentModelKey,
ModelExplorer modelExplorer,
ValidationContext validationContext)
{
var isValid = true;
ExpandValidationNode(validationContext, modelExplorer);
foreach (var property in modelExplorer.Metadata.Properties)
IList<IModelValidator> validators = null;
if (modelExplorer.Metadata.IsCollectionType && modelExplorer.Model != null)
{
var propertyExplorer = modelExplorer.GetExplorerForProperty(property.PropertyName);
var propertyMetadata = propertyExplorer.Metadata;
var enumerableModel = (IEnumerable)modelExplorer.Model;
var elementType = GetElementType(enumerableModel.GetType());
var elementMetadata = _modelMetadataProvider.GetMetadataForType(elementType);
validators = GetValidators(validationContext.ModelValidationContext.ValidatorProvider, elementMetadata);
}
foreach (var childNode in validationContext.ValidationNode.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,
propertyExplorer),
Visited = validationContext.Visited
childModelExplorer),
Visited = validationContext.Visited,
ValidationNode = childNode
};
var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
var childKey = ModelNames.CreatePropertyModelName(currentModelKey, propertyBindingName);
if (!ValidateNonVisitedNodeAndChildren(
childKey,
propertyValidationContext,
validators: null))
childNode.Key,
propertyValidationContext,
validators))
{
isValid = false;
}
@ -200,51 +203,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return isValid;
}
private bool ValidateElements(string currentKey, IEnumerable model, ValidationContext validationContext)
{
var elementType = GetElementType(model.GetType());
var elementMetadata = _modelMetadataProvider.GetMetadataForType(elementType);
var validatorProvider = validationContext.ModelValidationContext.ValidatorProvider;
var validatorProviderContext = new ModelValidatorProviderContext(elementMetadata);
validatorProvider.GetValidators(validatorProviderContext);
var validators = validatorProviderContext.Validators;
// If there are no validators or the object is null we bail out quickly
// when there are large arrays of null, this will save a significant amount of processing
// with minimal impact to other scenarios.
var anyValidatorsDefined = validators.Any();
var index = 0;
var isValid = true;
foreach (var element in model)
{
// If the element is non null, the recursive calls might find more validators.
// If it's null, then a shallow validation will be performed.
if (element != null || anyValidatorsDefined)
{
var elementExplorer = new ModelExplorer(_modelMetadataProvider, elementMetadata, element);
var elementKey = ModelNames.CreateIndexModelName(currentKey, index);
var elementValidationContext = new ValidationContext()
{
ModelValidationContext = ModelValidationContext.GetChildValidationContext(
validationContext.ModelValidationContext,
elementExplorer),
Visited = validationContext.Visited
};
if (!ValidateNonVisitedNodeAndChildren(elementKey, elementValidationContext, validators))
{
isValid = false;
}
}
index++;
}
return isValid;
}
// Validates a single node (not including children)
// Returns true if validation passes successfully
private static bool ShallowValidate(
@ -312,6 +270,54 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return filters.Any(filter => filter.IsTypeExcluded(type));
}
private void ExpandValidationNode(ValidationContext context, ModelExplorer modelExplorer)
{
var validationNode = context.ValidationNode;
if (validationNode.ChildNodes.Count != 0 ||
!validationNode.ValidateAllProperties ||
validationNode.Model == null)
{
return;
}
if (!modelExplorer.Metadata.IsCollectionType)
{
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
};
validationNode.ChildNodes.Add(childNode);
}
}
else
{
var enumerableModel = (IEnumerable)modelExplorer.Model;
var elementType = GetElementType(enumerableModel.GetType());
var elementMetadata = _modelMetadataProvider.GetMetadataForType(elementType);
// 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
};
validationNode.ChildNodes.Add(childNode);
index++;
}
}
}
private static Type GetElementType(Type type)
{
Debug.Assert(typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()));
@ -337,6 +343,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelValidationContext ModelValidationContext { get; set; }
public HashSet<object> Visited { get; set; }
public ModelValidationNode ValidationNode { get; set; }
}
}
}

View File

@ -13,6 +13,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// </summary>
/// <param name="validationContext">The <see cref="ModelValidationContext"/> associated with the current call.
/// </param>
void Validate(ModelValidationContext validationContext);
/// <param name="validationNode">The <see cref="ModelValidationNode"/> for the model which gets validated.
/// </param>
void Validate(ModelValidationContext validationContext, ModelValidationNode validationNode);
}
}

View File

@ -312,8 +312,12 @@ namespace Microsoft.AspNet.Mvc
{
var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, modelBindingResult.Model);
var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer);
modelValidationContext.RootPrefix = prefix;
objectModelValidator.Validate(modelValidationContext);
objectModelValidator.Validate(
modelValidationContext,
new ModelValidationNode(prefix, modelBindingContext.ModelMetadata, modelBindingResult.Model)
{
ValidateAllProperties = true
});
return modelState.IsValid;
}

View File

@ -417,13 +417,17 @@ namespace System.Web.Http
var modelExplorer = MetadataProvider.GetModelExplorerForType(typeof(TEntity), entity);
var modelValidationContext = new ModelValidationContext(
keyPrefix,
bindingSource: null,
validatorProvider: BindingContext.ValidatorProvider,
modelState: ModelState,
modelExplorer: modelExplorer);
ObjectValidator.Validate(modelValidationContext);
ObjectValidator.Validate(
modelValidationContext,
new ModelValidationNode(keyPrefix, modelExplorer.Metadata, entity)
{
ValidateAllProperties = true
});
}
protected virtual void Dispose(bool disposing)

View File

@ -51,7 +51,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = GetBindingContext(typeof(Person), binderType: typeof(TrueModelBinder));
var model = new Person();
var innerModelBinder = new TrueModelBinder();
var serviceProvider = new ServiceCollection()
.AddSingleton(typeof(IModelBinder))
.BuildServiceProvider();
@ -67,6 +66,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
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]
@ -138,7 +140,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
return Task.FromResult(new ModelBindingResult(_model, bindingContext.ModelName, true));
var validationNode =
new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, _model);
return Task.FromResult(new ModelBindingResult(_model, bindingContext.ModelName, true, validationNode));
}
}
}

View File

@ -50,6 +50,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
mockInputFormatter.Verify(v => v.ReadAsync(It.IsAny<InputFormatterContext>()), 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]
@ -71,6 +78,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type.
Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
}
@ -92,6 +100,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert
Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
}
[Fact]
@ -159,6 +168,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type.
Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
var errorMessage = bindingContext.ModelState["someName"].Errors[0].Exception.Message;
@ -192,6 +202,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.Model);
Assert.Null(binderResult.ValidationNode);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
var errorMessage = bindingContext.ModelState["someName"].Errors[0].ErrorMessage;
Assert.Equal("Unsupported content type 'text/xyz'.", errorMessage);

View File

@ -33,7 +33,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" });
// Assert
Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray());
Assert.Equal(new[] { 42, 0, 200 }, boundCollection.Model.ToArray());
Assert.Equal(
new[] { "someName[foo]", "someName[baz]" },
boundCollection.ValidationNode.ChildNodes.Select(o => o.Key).ToArray());
}
[Fact]
@ -53,7 +56,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
// Assert
Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
Assert.Equal(new[] { 42, 100 }, boundCollection.Model.ToArray());
Assert.Equal(
new[] { "someName[0]", "someName[1]" },
boundCollection.ValidationNode.ChildNodes.Select(o => o.Key).ToArray());
}
[Theory]
@ -193,8 +199,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var boundCollection = await binder.BindSimpleCollection(context, rawValue: new object[0], culture: null);
// Assert
Assert.NotNull(boundCollection);
Assert.Empty(boundCollection);
Assert.NotNull(boundCollection.Model);
Assert.Empty(boundCollection.Model);
}
[Fact]
@ -217,13 +223,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Arrange
var culture = new CultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
ModelValidationNode childValidationNode = null;
Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder)
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) =>
{
Assert.Equal("someName", mbc.ModelName);
return Task.FromResult(new ModelBindingResult(42, mbc.ModelName, true));
childValidationNode = new ModelValidationNode("someName", mbc.ModelMetadata, mbc.Model);
return Task.FromResult(new ModelBindingResult(42, mbc.ModelName, true, childValidationNode));
});
var modelBinder = new CollectionModelBinder<int>();
@ -231,7 +238,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var boundCollection = await modelBinder.BindSimpleCollection(bindingContext, new int[1], culture);
// Assert
Assert.Equal(new[] { 42 }, boundCollection.ToArray());
Assert.Equal(new[] { 42 }, boundCollection.Model.ToArray());
Assert.Equal(new[] { childValidationNode }, boundCollection.ValidationNode.ChildNodes.ToArray());
}
private static ModelBindingContext GetModelBindingContext(
@ -267,7 +275,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
if (value != null)
{
var model = value.ConvertTo(mbc.ModelType);
return new ModelBindingResult(model, mbc.ModelName, true);
var modelValidationNode = new ModelValidationNode(mbc.ModelName, mbc.ModelMetadata, model);
return new ModelBindingResult(model, mbc.ModelName, true, modelValidationNode);
}
return null;

View File

@ -374,6 +374,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var model = Assert.IsType<SimplePropertiesModel>(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<SimplePropertiesModel>(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]
@ -414,6 +434,79 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal(new byte[] { 227, 233, 133, 121, 58, 119, 180, 241 }, model.Resume);
}
[Fact]
public async Task BindModel_DoesNotAddAValidationNode_IfModelIsNotSet()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider();
var mockBinder = new Mock<IModelBinder>();
mockBinder
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(
delegate (ModelBindingContext context)
{
return Task.FromResult(
new ModelBindingResult(model: 42, key: "someName", isModelSet: false));
});
var binder = CreateCompositeBinder(mockBinder.Object);
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelAsync(bindingContext);
// Assert
// The result is null because of issue #2473
Assert.Null(result);
}
[Fact]
public async Task BindModel_DoesNotAddAValidationNode_IfModelBindingResultIsNull()
{
// Arrange
var mockBinder = new Mock<IModelBinder>();
mockBinder
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
var binder = CreateCompositeBinder(mockBinder.Object);
var valueProvider = new SimpleHttpValueProvider();
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.Null(result);
}
[Fact]
public async Task BindModel_UsesTheValidationNodeOnModelBindingResult_IfPresent()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider();
ModelValidationNode validationNode = null;
var mockBinder = new Mock<IModelBinder>();
mockBinder
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(
delegate (ModelBindingContext context)
{
validationNode = new ModelValidationNode("someName", context.ModelMetadata, 42);
return Task.FromResult(
new ModelBindingResult(42, "someName", isModelSet: true, validationNode: validationNode));
});
var binder = CreateCompositeBinder(mockBinder.Object);
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.Same(validationNode, result.ValidationNode);
}
private static ModelBindingContext CreateBindingContext(IModelBinder binder,
IValueProvider valueProvider,
Type type,

View File

@ -31,6 +31,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(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);
@ -53,6 +54,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result);
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.");
}
@ -97,17 +99,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert
Assert.NotNull(result);
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), result.Model);
Assert.NotNull(result.ValidationNode);
Assert.Equal(new KeyValuePair<int, string>(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);
}
[Fact]
public async Task TryBindStrongModel_BinderExists_BinderReturnsCorrectlyTypedObject_ReturnsTrue()
{
// Arrange
ModelBindingContext bindingContext = GetBindingContext(new SimpleHttpValueProvider());
var bindingContext = GetBindingContext(new SimpleHttpValueProvider());
var binder = new KeyValuePairModelBinder<int, string>();
var modelValidationNodeList = new List<ModelValidationNode>();
// Act
var result = await binder.TryBindStrongModel<int>(bindingContext, "key");
var result = await binder.TryBindStrongModel<int>(bindingContext, "key", modelValidationNodeList);
// Assert
Assert.True(result.IsModelSet);
@ -131,9 +147,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new KeyValuePairModelBinder<int, string>();
var modelValidationNodeList = new List<ModelValidationNode>();
// Act
var result = await binder.TryBindStrongModel<int>(bindingContext, "key");
var result = await binder.TryBindStrongModel<int>(bindingContext, "key", modelValidationNodeList);
// Assert
Assert.True(result.IsModelSet);

View File

@ -776,11 +776,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
"John Doe",
isModelSet: true,
key: "");
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
var testableBinder = new TestableMutableObjectModelBinder();
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
var modelStateDictionary = bindingContext.ModelState;
@ -827,10 +827,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
isModelSet: true,
key: "");
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
var testableBinder = new TestableMutableObjectModelBinder();
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
var modelStateDictionary = bindingContext.ModelState;
@ -888,8 +889,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
isModelSet: true,
key: "theModel.Age");
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
var modelStateDictionary = bindingContext.ModelState;
@ -920,9 +923,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Set no properties though Age (a non-Nullable struct) and City (a class) properties are required.
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder();
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
var modelStateDictionary = bindingContext.ModelState;
@ -973,9 +977,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
null,
isModelSet: true,
key: "theModel.City");
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
var modelStateDictionary = bindingContext.ModelState;
@ -1004,9 +1009,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Set no properties though ValueTypeRequired (a non-Nullable struct) property is required.
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder();
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
var modelStateDictionary = bindingContext.ModelState;
@ -1074,9 +1080,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
model: null,
isModelSet: isModelSet,
key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
Assert.False(modelStateDictionary.IsValid);
@ -1149,9 +1156,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
model: null,
isModelSet: false,
key: "theModel." + nameof(Person.PropertyWithDefaultValue));
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
Assert.True(modelStateDictionary.IsValid);
@ -1191,17 +1199,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth");
dto.Results[dobProperty] = null;
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
var testableBinder = new TestableMutableObjectModelBinder();
// Act
testableBinder.ProcessDto(bindingContext, dto);
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// 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(2, modelValidationNode.ChildNodes.Count());
var validationNode = modelValidationNode.ChildNodes[0];
Assert.Equal("", validationNode.Key);
Assert.Equal("John", validationNode.Model);
validationNode = modelValidationNode.ChildNodes[1];
Assert.Equal("", validationNode.Key);
Assert.Equal("Doe", validationNode.Model);
}
[Fact]

View File

@ -231,7 +231,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
private static ModelValidationContext CreateValidationContext(ModelExplorer modelExplorer)
{
return new ModelValidationContext(
rootPrefix: null,
bindingSource: null,
modelState: null,
validatorProvider: null,

View File

@ -212,9 +212,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var context = GetModelValidationContext(model, type);
var validator = new DefaultObjectValidator(context.ExcludeFilters, context.ModelMetadataProvider);
var topLevelValidationNode =
new ModelValidationNode(string.Empty, context.ModelValidationContext.ModelExplorer.Metadata, model)
{
ValidateAllProperties = true
};
// Act
validator.Validate(context.ModelValidationContext);
validator.Validate(context.ModelValidationContext, topLevelValidationNode);
// Assert
var actualErrors = new Dictionary<string, string>();
@ -240,6 +245,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// 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
};
// Act & Assert
Assert.Throws(
@ -249,7 +262,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
.Validate(testValidationContext.ModelValidationContext, topLevelValidationNode);
});
}
@ -265,7 +278,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
yield return new object[] { new Dictionary<string, Uri> {
{ "values", new Uri("/api/values", UriKind.Relative) },
{ "hello", new Uri("/api/hello", UriKind.Relative) }
}, typeof(Uri), new List<Type>() { typeof(Uri) } };
}, typeof(Dictionary<string, Uri>), new List<Type>() { typeof(Uri) } };
}
}
@ -276,13 +289,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
object input, Type type, List<Type> excludedTypes)
{
// Arrange
var testValidationContext = GetModelValidationContext(input, type, string.Empty, excludedTypes);
var testValidationContext = GetModelValidationContext(input, type, excludedTypes);
var topLevelValidationNode =
new ModelValidationNode(
string.Empty,
testValidationContext.ModelValidationContext.ModelExplorer.Metadata,
testValidationContext.ModelValidationContext.ModelExplorer.Model)
{
ValidateAllProperties = true
};
// Act & Assert (does not throw)
new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
.Validate(testValidationContext.ModelValidationContext, topLevelValidationNode);
Assert.True(testValidationContext.ModelValidationContext.ModelState.IsValid);
}
@ -294,6 +315,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
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
};
// Act & Assert
Assert.Throws<InvalidOperationException>(
@ -302,7 +331,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider)
.Validate(validationContext);
.Validate(validationContext, topLevelValidationNode);
});
Assert.True(validationContext.ModelState.IsValid);
}
@ -315,12 +344,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
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
};
// Act (does not throw)
new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider)
.Validate(validationContext);
.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.Contains("Street", validationContext.ModelState.Keys);
@ -340,12 +377,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
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
};
// Act & Assert (does not throw)
new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
.Validate(testValidationContext.ModelValidationContext, topLevelValidationNode);
}
[Fact]
@ -361,7 +406,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var testValidationContext = GetModelValidationContext(
user,
typeof(User),
"user",
new List<Type> { typeof(string) });
var validationContext = testValidationContext.ModelValidationContext;
@ -370,9 +414,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider);
var topLevelValidationNode =
new ModelValidationNode(
"user",
testValidationContext.ModelValidationContext.ModelExplorer.Metadata,
testValidationContext.ModelValidationContext.ModelExplorer.Model)
{
ValidateAllProperties = true
};
// Act
validator.Validate(validationContext);
validator.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.Equal(new[] { "key1", "user.Password", "", "user.ConfirmPassword" },
@ -380,7 +432,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var modelState = validationContext.ModelState["user.ConfirmPassword"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
var error = Assert.Single(validationContext.ModelState[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
}
@ -398,15 +450,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var testValidationContext = GetModelValidationContext(
user,
typeof(User),
"user",
new List<Type> { 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
};
// Act
validator.Validate(validationContext);
validator.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.False(validationContext.ModelState.ContainsKey("user.Password"));
@ -429,7 +488,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var testValidationContext = GetModelValidationContext(
user,
typeof(User),
"user",
new List<Type> { typeof(User) });
var validationContext = testValidationContext.ModelValidationContext;
@ -440,9 +498,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider);
var topLevelValidationNode =
new ModelValidationNode(
"user",
testValidationContext.ModelValidationContext.ModelExplorer.Metadata,
testValidationContext.ModelValidationContext.ModelExplorer.Model)
{
ValidateAllProperties = true
};
// Act
validator.Validate(validationContext);
validator.Validate(validationContext, topLevelValidationNode);
// Assert
var modelState = validationContext.ModelState["user.Password"];
@ -455,21 +521,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
[Fact]
public void NonRequestBoundModel_MarkedAsSkipped()
public void Validate_IfSuppressIsSet_MarkedAsSkipped()
{
// Arrange
var testValidationContext = GetModelValidationContext(
new TestServiceProvider(),
typeof(TestServiceProvider),
"serviceProvider");
typeof(TestServiceProvider));
var validationContext = testValidationContext.ModelValidationContext;
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider);
var modelExplorer = testValidationContext.ModelValidationContext.ModelExplorer;
var topLevelValidationNode = new ModelValidationNode(
"serviceProvider",
modelExplorer.Metadata,
modelExplorer.Model);
var propertyExplorer = modelExplorer.GetExplorerForProperty("TestService");
var childNode = new ModelValidationNode(
"serviceProvider.TestService",
propertyExplorer.Metadata,
propertyExplorer.Model)
{
SuppressValidation = true
};
topLevelValidationNode.ChildNodes.Add(childNode);
// Act
validator.Validate(validationContext);
validator.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.True(validationContext.ModelState.IsValid);
@ -496,7 +577,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var testValidationContext = GetModelValidationContext(
model,
type,
"items",
excludedTypes: null,
modelStateDictionary: modelStateDictionary);
@ -509,9 +589,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider);
var topLevelValidationNode =
new ModelValidationNode(
"items",
testValidationContext.ModelValidationContext.ModelExplorer.Metadata,
testValidationContext.ModelValidationContext.ModelExplorer.Model)
{
ValidateAllProperties = true
};
// Act
validator.Validate(validationContext);
validator.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.True(validationContext.ModelState.IsValid);
@ -538,7 +626,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var testValidationContext = GetModelValidationContext(
model,
typeof(Dictionary<string, string>),
"items",
excludedTypes: null,
modelStateDictionary: modelStateDictionary);
@ -552,8 +639,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
testValidationContext.ExcludeFilters,
testValidationContext.ModelMetadataProvider);
var topLevelValidationNode =
new ModelValidationNode(
"items",
testValidationContext.ModelValidationContext.ModelExplorer.Metadata,
testValidationContext.ModelValidationContext.ModelExplorer.Model)
{
ValidateAllProperties = true
};
// Act
validator.Validate(validationContext);
validator.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.True(validationContext.ModelState.IsValid);
@ -569,19 +665,88 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
}
private TestModelValidationContext GetModelValidationContext(
object model,
Type type,
string key = "",
List<Type> excludedTypes = null)
[Fact]
public void Validator_IfValidateAllPropertiesIsNotSet_DoesNotAutoExpand()
{
return GetModelValidationContext(model, type, key, excludedTypes, new ModelStateDictionary());
// 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);
// Act
validator.Validate(validationContext, topLevelValidationNode);
// Assert
Assert.True(validationContext.ModelState.IsValid);
var key = Assert.Single(validationContext.ModelState.Keys);
Assert.Equal("person", key);
}
[Fact]
public void Validator_IfValidateAllPropertiesSet_WithChildNodes_DoesNotAutoExpand()
{
// 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)
{
ValidateAllProperties = true
};
var propertyExplorer = modelExplorer.GetExplorerForProperty("Profession");
var childNode = new ModelValidationNode(
"person.Profession",
propertyExplorer.Metadata,
propertyExplorer.Model);
topLevelValidationNode.ChildNodes.Add(childNode);
// Act
validator.Validate(validationContext, topLevelValidationNode);
// Assert
var modelState = validationContext.ModelState;
Assert.False(modelState.IsValid);
// 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);
}
private TestModelValidationContext GetModelValidationContext(
object model,
Type type,
List<Type> excludedTypes = null)
{
return GetModelValidationContext(model, type, excludedTypes, new ModelStateDictionary());
}
private TestModelValidationContext GetModelValidationContext(
object model,
Type type,
string key,
List<Type> excludedTypes,
ModelStateDictionary modelStateDictionary)
{
@ -603,7 +768,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return new TestModelValidationContext
{
ModelValidationContext = new ModelValidationContext(
key,
null,
TestModelValidatorProvider.CreateDefaultProvider(),
modelStateDictionary,

View File

@ -154,7 +154,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()))
.Verifiable();
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
@ -164,7 +164,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Assert
mockValidatorProvider.Verify(
o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Once());
o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()), Times.Once());
}
[Fact]
@ -197,8 +197,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test
};
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()))
.Verifiable();
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
// Act
@ -206,7 +207,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
mockValidatorProvider.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Never());
mockValidatorProvider.Verify(
o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()),
Times.Never());
}
[Fact]
@ -226,7 +229,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()))
.Verifiable();
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
@ -236,7 +239,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Assert
mockValidatorProvider.Verify(
o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Once());
o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()), Times.Once());
}
[Fact]
@ -268,8 +271,10 @@ namespace Microsoft.AspNet.Mvc.Core.Test
};
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()))
.Verifiable();
var argumentBinder = GetArgumentBinder(mockValidatorProvider.Object);
// Act
@ -277,7 +282,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test
.BindActionArgumentsAsync(actionContext, actionBindingContext, new TestController());
// Assert
mockValidatorProvider.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Never());
mockValidatorProvider.Verify(
o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()),
Times.Never());
}
[Fact]
@ -596,7 +603,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
if (validator == null)
{
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
mockValidator.Setup(
o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<ModelValidationNode>()));
validator = mockValidator.Object;
}

View File

@ -281,7 +281,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
{
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
return Task.FromResult(new ModelBindingResult("Success", bindingContext.ModelName, true));
var model = "Success";
var modelValidationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
model);
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, true, modelValidationNode));
}
}

View File

@ -2,8 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit;
@ -536,5 +539,63 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
private class Person4
{
public IList<Address4> Addresses { get; set; }
}
private class Address4
{
public int Zip { get; set; }
public string Street { get; set; }
}
[Fact(Skip = "Extra ModelState key because of #2446")]
public async Task CollectionModelBinder_UsesCustomIndexes()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Person4)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
var formCollection = new FormCollection(new Dictionary<string, string[]>()
{
{ "Addresses.index", new [] { "Key1", "Key2" } },
{ "Addresses[Key1].Street", new [] { "Street1" } },
{ "Addresses[Key2].Street", new [] { "Street2" } },
});
request.Form = formCollection;
request.ContentType = "application/x-www-form-urlencoded";
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.IsType<Person4>(modelBindingResult.Model);
Assert.Equal(2, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "Addresses[Key1].Street").Value;
Assert.Equal("Street1", entry.Value.AttemptedValue);
Assert.Equal("Street1", entry.Value.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "Addresses[Key2].Street").Value;
Assert.Equal("Street2", entry.Value.AttemptedValue);
Assert.Equal("Street2", entry.Value.RawValue);
}
}
}