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 /// <param name="isModelSet">A value that represents if the model has been set by the
/// <see cref="IModelBinder"/>.</param> /// <see cref="IModelBinder"/>.</param>
public ModelBindingResult(object model, string key, bool isModelSet) 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; Model = model;
Key = key; Key = key;
IsModelSet = isModelSet; IsModelSet = isModelSet;
ValidationNode = validationNode;
} }
/// <summary> /// <summary>
@ -47,5 +62,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </para> /// </para>
/// </summary> /// </summary>
public bool IsModelSet { get; } 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( public ModelValidationContext(
[NotNull] ModelBindingContext bindingContext, [NotNull] ModelBindingContext bindingContext,
[NotNull] ModelExplorer modelExplorer) [NotNull] ModelExplorer modelExplorer)
: this(bindingContext.ModelName, : this(
bindingContext.BindingSource, bindingContext.BindingSource,
bindingContext.OperationBindingContext.ValidatorProvider, bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState, bindingContext.ModelState,
modelExplorer) modelExplorer)
{ {
} }
public ModelValidationContext( public ModelValidationContext(
string rootPrefix,
BindingSource bindingSource, BindingSource bindingSource,
[NotNull] IModelValidatorProvider validatorProvider, [NotNull] IModelValidatorProvider validatorProvider,
[NotNull] ModelStateDictionary modelState, [NotNull] ModelStateDictionary modelState,
[NotNull] ModelExplorer modelExplorer) [NotNull] ModelExplorer modelExplorer)
{ {
ModelState = modelState; ModelState = modelState;
RootPrefix = rootPrefix;
ValidatorProvider = validatorProvider; ValidatorProvider = validatorProvider;
ModelExplorer = modelExplorer; ModelExplorer = modelExplorer;
BindingSource = bindingSource; BindingSource = bindingSource;
@ -45,7 +43,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
[NotNull] ModelExplorer modelExplorer) [NotNull] ModelExplorer modelExplorer)
{ {
return new ModelValidationContext( return new ModelValidationContext(
parentContext.RootPrefix,
modelExplorer.Metadata.BindingSource, modelExplorer.Metadata.BindingSource,
parentContext.ValidatorProvider, parentContext.ValidatorProvider,
parentContext.ModelState, parentContext.ModelState,
@ -56,8 +53,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelStateDictionary ModelState { get; } public ModelStateDictionary ModelState { get; }
public string RootPrefix { get; set; }
public BindingSource BindingSource { get; set; } public BindingSource BindingSource { get; set; }
public IModelValidatorProvider ValidatorProvider { get; } public IModelValidatorProvider ValidatorProvider { get; }

View File

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

View File

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

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await modelBinder.BindModelAsync(bindingContext); var result = await modelBinder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ? 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); new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// A model binder was specified by metadata and this binder handles all such cases. // 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 = var modelBindingResult =
result != null ? 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); new ModelBindingResult(model: null, key: context.ModelName, isModelSet: false);
// This model binder is the only handler for its binding source. // 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; 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 // 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. // consider the entire ModelName as a prefix.
var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName; 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) catch (Exception ex)
{ {

View File

@ -39,7 +39,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
try try
{ {
var model = Convert.FromBase64String(value); 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) catch (Exception ex)
{ {

View File

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

View File

@ -55,6 +55,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.OperationBindingContext.BodyBindingState = bindingContext.OperationBindingContext.BodyBindingState =
newBindingContext.OperationBindingContext.BodyBindingState; newBindingContext.OperationBindingContext.BodyBindingState;
var bindingKey = bindingContext.ModelName;
if (modelBindingResult.IsModelSet) if (modelBindingResult.IsModelSet)
{ {
// Update the model state key if we are bound using an empty prefix and it is a complex type. // 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. // 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). // (i.e here the prefix for the model key is empty).
// For the id parameter the key would be id. // 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( return new ModelBindingResult(
modelBindingResult.Model, modelBindingResult.Model,
bindingContext.ModelName, bindingKey,
modelBindingResult.IsModelSet); modelBindingResult.IsModelSet,
modelValidationNode);
} }
private async Task<ModelBindingResult> TryBind(ModelBindingContext bindingContext) 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 result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ? 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); new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
// Were able to resolve a binder type. // 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>), typeof(KeyValuePair<TKey, TValue>),
allowNullModel: true); allowNullModel: true);
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key"); var childNodes = new List<ModelValidationNode>();
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value"); var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key", childNodes);
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value", childNodes);
if (keyResult.IsModelSet && valueResult.IsModelSet) if (keyResult.IsModelSet && valueResult.IsModelSet)
{ {
@ -24,8 +25,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelBindingHelper.CastOrDefault<TKey>(keyResult.Model), ModelBindingHelper.CastOrDefault<TKey>(keyResult.Model),
ModelBindingHelper.CastOrDefault<TValue>(valueResult.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 // 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) else if (!keyResult.IsModelSet && valueResult.IsModelSet)
{ {
@ -55,8 +68,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
} }
internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(ModelBindingContext parentBindingContext, internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(
string propertyName) ModelBindingContext parentBindingContext,
string propertyName,
List<ModelValidationNode> childNodes)
{ {
var propertyModelMetadata = var propertyModelMetadata =
parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(typeof(TModel)); parentBindingContext.OperationBindingContext.MetadataProvider.GetMetadataForType(typeof(TModel));
@ -72,6 +87,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
propertyBindingContext); propertyBindingContext);
if (modelBindingResult != null) if (modelBindingResult != null)
{ {
if (modelBindingResult.ValidationNode != null)
{
childNodes.Add(modelBindingResult.ValidationNode);
}
return modelBindingResult; return modelBindingResult;
} }

View File

@ -44,12 +44,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
EnsureModel(bindingContext); EnsureModel(bindingContext);
var result = await CreateAndPopulateDto(bindingContext, mutableObjectBinderContext.PropertyMetadata); 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 // post-processing, e.g. property setters and hooking up validation
ProcessDto(bindingContext, (ComplexModelDto)result.Model); ProcessDto(bindingContext, (ComplexModelDto)result.Model, validationNode);
return new ModelBindingResult( return new ModelBindingResult(
bindingContext.Model, bindingContext.Model,
bindingContext.ModelName, bindingContext.ModelName,
isModelSet: true); isModelSet: true,
validationNode: validationNode);
} }
/// <summary> /// <summary>
@ -359,11 +365,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return validationInfo; 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 metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var modelExplorer = metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model); var modelExplorer = metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model);
var validationInfo = GetPropertyValidationInfo(bindingContext); var validationInfo = GetPropertyValidationInfo(bindingContext);
// Eliminate provided properties from requiredProperties; leaving just *missing* required properties. // Eliminate provided properties from requiredProperties; leaving just *missing* required properties.
@ -415,8 +424,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
out requiredValidator); out requiredValidator);
SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult, 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> /// <summary>

View File

@ -26,7 +26,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices; var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var model = requestServices.GetRequiredService(bindingContext.ModelType); 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); newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel); 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) catch (Exception ex)
{ {

View File

@ -19,7 +19,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var model = valueProviderResult.RawValue; var model = valueProviderResult.RawValue;
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model); 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) internal static async Task<ValueProviderResult> GetCompatibleValueProviderResult(ModelBindingContext context)

View File

@ -35,16 +35,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
} }
/// <inheritdoc /> /// <inheritdoc />
public void Validate([NotNull] ModelValidationContext modelValidationContext) public void Validate(
[NotNull] ModelValidationContext modelValidationContext,
[NotNull] ModelValidationNode validationNode)
{ {
var validationContext = new ValidationContext() var validationContext = new ValidationContext()
{ {
ModelValidationContext = modelValidationContext, ModelValidationContext = modelValidationContext,
Visited = new HashSet<object>(ReferenceEqualityComparer.Instance), Visited = new HashSet<object>(ReferenceEqualityComparer.Instance),
ValidationNode = validationNode
}; };
ValidateNonVisitedNodeAndChildren( ValidateNonVisitedNodeAndChildren(
modelValidationContext.RootPrefix, validationNode.Key,
validationContext, validationContext,
validators: null); validators: null);
} }
@ -54,19 +57,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
ValidationContext validationContext, ValidationContext validationContext,
IList<IModelValidator> validators) IList<IModelValidator> validators)
{ {
var modelValidationContext = validationContext.ModelValidationContext;
var modelExplorer = modelValidationContext.ModelExplorer;
// Recursion guard to avoid stack overflows // Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack(); RuntimeHelpers.EnsureSufficientExecutionStack();
var modelValidationContext = validationContext.ModelValidationContext;
var modelExplorer = modelValidationContext.ModelExplorer;
var modelState = modelValidationContext.ModelState; var modelState = modelValidationContext.ModelState;
var currentValidationNode = validationContext.ValidationNode;
var bindingSource = modelValidationContext.BindingSource; if (currentValidationNode.SuppressValidation)
if (bindingSource != null && !bindingSource.IsFromRequest)
{ {
// Short circuit if the metadata represents something that was not bound using request data. // Short circuit if the node is marked to be suppressed
// For example model bound using [FromServices]. Treat such objects as skipped.
var validationState = modelState.GetFieldValidationState(modelKey); var validationState = modelState.GetFieldValidationState(modelKey);
if (validationState == ModelValidationState.Unvalidated) 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 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, // 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. // instead we just pass them over.
var validatorProvider = modelValidationContext.ValidatorProvider; validators = GetValidators(modelValidationContext.ValidatorProvider, modelExplorer.Metadata);
var validatorProviderContext = new ModelValidatorProviderContext(modelExplorer.Metadata);
validatorProvider.GetValidators(validatorProviderContext);
validators = validatorProviderContext.Validators;
} }
// We don't need to recursively traverse the graph for null values // We don't need to recursively traverse the graph if there are no child nodes.
if (modelExplorer.Model == null) if (currentValidationNode.ChildNodes.Count == 0 && !currentValidationNode.ValidateAllProperties)
{ {
return ShallowValidate(modelKey, modelExplorer, validationContext, validators); return ShallowValidate(modelKey, modelExplorer, validationContext, validators);
} }
// We don't need to recursively traverse the graph for types that shouldn't be validated // 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)) if (IsTypeExcludedFromValidation(_excludeFilters, modelType))
{ {
var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators); 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. // 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)) if (validationContext.Visited.Contains(modelExplorer.Model))
{ {
return true; return true;
} }
validationContext.Visited.Add(modelExplorer.Model); validationContext.Visited.Add(modelExplorer.Model);
isValid = ValidateChildNodes(modelKey, modelExplorer, validationContext);
// 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);
}
if (isValid) if (isValid)
{ {
// Don't bother to validate this node if children failed. // 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, string currentModelKey,
ModelExplorer modelExplorer, ModelExplorer modelExplorer,
ValidationContext validationContext) ValidationContext validationContext)
{ {
var isValid = true; 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 enumerableModel = (IEnumerable)modelExplorer.Model;
var propertyMetadata = propertyExplorer.Metadata; 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() var propertyValidationContext = new ValidationContext()
{ {
ModelValidationContext = ModelValidationContext.GetChildValidationContext( ModelValidationContext = ModelValidationContext.GetChildValidationContext(
validationContext.ModelValidationContext, validationContext.ModelValidationContext,
propertyExplorer), childModelExplorer),
Visited = validationContext.Visited Visited = validationContext.Visited,
ValidationNode = childNode
}; };
var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
var childKey = ModelNames.CreatePropertyModelName(currentModelKey, propertyBindingName);
if (!ValidateNonVisitedNodeAndChildren( if (!ValidateNonVisitedNodeAndChildren(
childKey, childNode.Key,
propertyValidationContext, propertyValidationContext,
validators: null)) validators))
{ {
isValid = false; isValid = false;
} }
@ -200,51 +203,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return isValid; 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) // Validates a single node (not including children)
// Returns true if validation passes successfully // Returns true if validation passes successfully
private static bool ShallowValidate( private static bool ShallowValidate(
@ -312,6 +270,54 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return filters.Any(filter => filter.IsTypeExcluded(type)); 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) private static Type GetElementType(Type type)
{ {
Debug.Assert(typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())); Debug.Assert(typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()));
@ -337,6 +343,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelValidationContext ModelValidationContext { get; set; } public ModelValidationContext ModelValidationContext { get; set; }
public HashSet<object> Visited { 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> /// </summary>
/// <param name="validationContext">The <see cref="ModelValidationContext"/> associated with the current call. /// <param name="validationContext">The <see cref="ModelValidationContext"/> associated with the current call.
/// </param> /// </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 modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, modelBindingResult.Model);
var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer); var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer);
modelValidationContext.RootPrefix = prefix; objectModelValidator.Validate(
objectModelValidator.Validate(modelValidationContext); modelValidationContext,
new ModelValidationNode(prefix, modelBindingContext.ModelMetadata, modelBindingResult.Model)
{
ValidateAllProperties = true
});
return modelState.IsValid; return modelState.IsValid;
} }

View File

@ -417,13 +417,17 @@ namespace System.Web.Http
var modelExplorer = MetadataProvider.GetModelExplorerForType(typeof(TEntity), entity); var modelExplorer = MetadataProvider.GetModelExplorerForType(typeof(TEntity), entity);
var modelValidationContext = new ModelValidationContext( var modelValidationContext = new ModelValidationContext(
keyPrefix,
bindingSource: null, bindingSource: null,
validatorProvider: BindingContext.ValidatorProvider, validatorProvider: BindingContext.ValidatorProvider,
modelState: ModelState, modelState: ModelState,
modelExplorer: modelExplorer); modelExplorer: modelExplorer);
ObjectValidator.Validate(modelValidationContext); ObjectValidator.Validate(
modelValidationContext,
new ModelValidationNode(keyPrefix, modelExplorer.Metadata, entity)
{
ValidateAllProperties = true
});
} }
protected virtual void Dispose(bool disposing) 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 bindingContext = GetBindingContext(typeof(Person), binderType: typeof(TrueModelBinder));
var model = new Person(); var model = new Person();
var innerModelBinder = new TrueModelBinder();
var serviceProvider = new ServiceCollection() var serviceProvider = new ServiceCollection()
.AddSingleton(typeof(IModelBinder)) .AddSingleton(typeof(IModelBinder))
.BuildServiceProvider(); .BuildServiceProvider();
@ -67,6 +66,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var p = (Person)binderResult.Model; var p = (Person)binderResult.Model;
Assert.Equal(model.Age, p.Age); Assert.Equal(model.Age, p.Age);
Assert.Equal(model.Name, p.Name); 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] [Fact]
@ -138,7 +140,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext) 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); mockInputFormatter.Verify(v => v.ReadAsync(It.IsAny<InputFormatterContext>()), Times.Once);
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.True(binderResult.IsModelSet); 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] [Fact]
@ -71,6 +78,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type. // Returns true because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName")); Assert.True(bindingContext.ModelState.ContainsKey("someName"));
} }
@ -92,6 +100,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
} }
[Fact] [Fact]
@ -159,6 +168,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Returns true because it understands the metadata type. // Returns true because it understands the metadata type.
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.ValidationNode);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName")); Assert.True(bindingContext.ModelState.ContainsKey("someName"));
var errorMessage = bindingContext.ModelState["someName"].Errors[0].Exception.Message; var errorMessage = bindingContext.ModelState["someName"].Errors[0].Exception.Message;
@ -192,6 +202,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.NotNull(binderResult); Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet); Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.Model); Assert.Null(binderResult.Model);
Assert.Null(binderResult.ValidationNode);
Assert.True(bindingContext.ModelState.ContainsKey("someName")); Assert.True(bindingContext.ModelState.ContainsKey("someName"));
var errorMessage = bindingContext.ModelState["someName"].Errors[0].ErrorMessage; var errorMessage = bindingContext.ModelState["someName"].Errors[0].ErrorMessage;
Assert.Equal("Unsupported content type 'text/xyz'.", 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" }); var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" });
// Assert // 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] [Fact]
@ -53,7 +56,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null); var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
// Assert // 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] [Theory]
@ -193,8 +199,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var boundCollection = await binder.BindSimpleCollection(context, rawValue: new object[0], culture: null); var boundCollection = await binder.BindSimpleCollection(context, rawValue: new object[0], culture: null);
// Assert // Assert
Assert.NotNull(boundCollection); Assert.NotNull(boundCollection.Model);
Assert.Empty(boundCollection); Assert.Empty(boundCollection.Model);
} }
[Fact] [Fact]
@ -217,13 +223,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Arrange // Arrange
var culture = new CultureInfo("fr-FR"); var culture = new CultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider()); var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
ModelValidationNode childValidationNode = null;
Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder) Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder)
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) => .Returns((ModelBindingContext mbc) =>
{ {
Assert.Equal("someName", mbc.ModelName); 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>(); 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); var boundCollection = await modelBinder.BindSimpleCollection(bindingContext, new int[1], culture);
// Assert // 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( private static ModelBindingContext GetModelBindingContext(
@ -267,7 +275,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
if (value != null) if (value != null)
{ {
var model = value.ConvertTo(mbc.ModelType); 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; return null;

View File

@ -374,6 +374,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var model = Assert.IsType<SimplePropertiesModel>(result.Model); var model = Assert.IsType<SimplePropertiesModel>(result.Model);
Assert.Equal("firstName-value", model.FirstName); Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName); 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] [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); 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, private static ModelBindingContext CreateBindingContext(IModelBinder binder,
IValueProvider valueProvider, IValueProvider valueProvider,
Type type, Type type,

View File

@ -31,6 +31,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result); Assert.NotNull(result);
Assert.Null(result.Model); Assert.Null(result.Model);
Assert.False(bindingContext.ModelState.IsValid); Assert.False(bindingContext.ModelState.IsValid);
Assert.Null(result.ValidationNode);
Assert.Equal("someName", bindingContext.ModelName); Assert.Equal("someName", bindingContext.ModelName);
var error = Assert.Single(bindingContext.ModelState["someName.Key"].Errors); var error = Assert.Single(bindingContext.ModelState["someName.Key"].Errors);
Assert.Equal("A value is required.", error.ErrorMessage); Assert.Equal("A value is required.", error.ErrorMessage);
@ -53,6 +54,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.NotNull(result); Assert.NotNull(result);
Assert.Null(result.Model); Assert.Null(result.Model);
Assert.False(bindingContext.ModelState.IsValid); Assert.False(bindingContext.ModelState.IsValid);
Assert.Null(result.ValidationNode);
Assert.Equal("someName", bindingContext.ModelName); Assert.Equal("someName", bindingContext.ModelName);
Assert.Equal(bindingContext.ModelState["someName.Value"].Errors.First().ErrorMessage, "A value is required."); 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
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), result.Model); 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] [Fact]
public async Task TryBindStrongModel_BinderExists_BinderReturnsCorrectlyTypedObject_ReturnsTrue() public async Task TryBindStrongModel_BinderExists_BinderReturnsCorrectlyTypedObject_ReturnsTrue()
{ {
// Arrange // Arrange
ModelBindingContext bindingContext = GetBindingContext(new SimpleHttpValueProvider()); var bindingContext = GetBindingContext(new SimpleHttpValueProvider());
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>();
var modelValidationNodeList = new List<ModelValidationNode>();
// Act // Act
var result = await binder.TryBindStrongModel<int>(bindingContext, "key"); var result = await binder.TryBindStrongModel<int>(bindingContext, "key", modelValidationNodeList);
// Assert // Assert
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
@ -131,9 +147,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new KeyValuePairModelBinder<int, string>(); var binder = new KeyValuePairModelBinder<int, string>();
var modelValidationNodeList = new List<ModelValidationNode>();
// Act // Act
var result = await binder.TryBindStrongModel<int>(bindingContext, "key"); var result = await binder.TryBindStrongModel<int>(bindingContext, "key", modelValidationNodeList);
// Assert // Assert
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);

View File

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

View File

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

View File

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

View File

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

View File

@ -281,7 +281,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
{ {
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit; using Xunit;
@ -536,5 +539,63 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal(0, modelState.ErrorCount); Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid); 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);
}
} }
} }