Merging Model Validation for body and non body validation.

This also fixes #1503.

Currently all model binders except mutable object binder are independent of validation code. The mutable object binder which needs to do some validation ( for scenarios involving [BindRequired] and [BindNever]).
We would be going with an approach where required validaiton happens in input formatters and model binders.
This is needed as validation for value types can best be done at creation time.

Followup PRs:
Introduce support for skipping validation (and not binding) for a particular property/type etc.
This commit is contained in:
Harsh Gupta 2015-02-04 11:37:48 -08:00
parent cf36a601c6
commit f19c2e493d
89 changed files with 1810 additions and 2250 deletions

12
Mvc.sln
View File

@ -828,6 +828,18 @@ Global
{C651F432-4EBE-41A6-BAD2-3E07CCBA209C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C651F432-4EBE-41A6-BAD2-3E07CCBA209C}.Release|x86.ActiveCfg = Release|Any CPU
{C651F432-4EBE-41A6-BAD2-3E07CCBA209C}.Release|x86.Build.0 = Release|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|x86.ActiveCfg = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|x86.Build.0 = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Release|Any CPU.Build.0 = Release|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Release|x86.ActiveCfg = Release|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Release|x86.Build.0 = Release|Any CPU
{A19022EF-9BA3-4349-94E4-F48E13E1C8AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A19022EF-9BA3-4349-94E4-F48E13E1C8AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A19022EF-9BA3-4349-94E4-F48E13E1C8AE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU

View File

@ -135,6 +135,9 @@ namespace Microsoft.AspNet.Mvc
[FromServices]
public IUrlHelper Url { get; set; }
[FromServices]
public IObjectModelValidator ObjectValidator { get; set; }
/// <summary>
/// Gets or sets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
@ -938,6 +941,7 @@ namespace Microsoft.AspNet.Mvc
MetadataProvider,
BindingContext.ModelBinder,
valueProvider,
ObjectValidator,
BindingContext.ValidatorProvider);
}
@ -975,6 +979,7 @@ namespace Microsoft.AspNet.Mvc
MetadataProvider,
BindingContext.ModelBinder,
BindingContext.ValueProvider,
ObjectValidator,
BindingContext.ValidatorProvider,
includeExpressions);
}
@ -1012,6 +1017,7 @@ namespace Microsoft.AspNet.Mvc
MetadataProvider,
BindingContext.ModelBinder,
BindingContext.ValueProvider,
ObjectValidator,
BindingContext.ValidatorProvider,
predicate);
}
@ -1052,6 +1058,7 @@ namespace Microsoft.AspNet.Mvc
MetadataProvider,
BindingContext.ModelBinder,
valueProvider,
ObjectValidator,
BindingContext.ValidatorProvider,
includeExpressions);
}
@ -1091,6 +1098,7 @@ namespace Microsoft.AspNet.Mvc
MetadataProvider,
BindingContext.ModelBinder,
valueProvider,
ObjectValidator,
BindingContext.ValidatorProvider,
predicate);
}
@ -1128,21 +1136,15 @@ namespace Microsoft.AspNet.Mvc
modelAccessor: () => model,
modelType: model.GetType());
var modelName = prefix ?? string.Empty;
var validationContext = new ModelValidationContext(
MetadataProvider,
modelName,
BindingContext.ValidatorProvider,
ModelState,
modelMetadata,
containerMetadata: null);
var modelName = prefix ?? string.Empty;
var validationNode = new ModelValidationNode(modelMetadata, modelName)
{
ValidateAllProperties = true
};
validationNode.Validate(validationContext);
ObjectValidator.Validate(validationContext);
return ModelState.IsValid;
}

View File

@ -18,13 +18,16 @@ namespace Microsoft.AspNet.Mvc
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly MvcOptions _options;
private readonly IObjectModelValidator _validator;
public DefaultControllerActionArgumentBinder(
IModelMetadataProvider modelMetadataProvider,
IObjectModelValidator validator,
IOptions<MvcOptions> optionsAccessor)
{
_modelMetadataProvider = modelMetadataProvider;
_options = optionsAccessor.Options;
_validator = validator;
}
public async Task<IDictionary<string, object>> GetActionArgumentsAsync(
@ -91,15 +94,21 @@ namespace Microsoft.AspNet.Mvc
var modelState = actionContext.ModelState;
modelState.MaxAllowedErrors = _options.MaxModelValidationErrors;
foreach (var parameter in parameterMetadata)
{
var parameterType = parameter.ModelType;
var modelBindingContext = GetModelBindingContext(parameter, modelState, operationBindingContext);
if (await bindingContext.ModelBinder.BindModelAsync(modelBindingContext) &&
modelBindingContext.IsModelSet)
var modelBindingResult = await bindingContext.ModelBinder.BindModelAsync(modelBindingContext);
if (modelBindingResult != null && modelBindingResult.IsModelSet)
{
arguments[parameter.PropertyName] = modelBindingContext.Model;
arguments[parameter.PropertyName] = modelBindingResult.Model;
var validationContext = new ModelValidationContext(
modelBindingResult.Key,
bindingContext.ValidatorProvider,
actionContext.ModelState,
parameter,
containerMetadata: null);
_validator.Validate(validationContext);
}
}
}

View File

@ -19,7 +19,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private readonly ActionContext _actionContext;
private readonly IScopedInstance<ActionBindingContext> _bindingContext;
private readonly IInputFormatterSelector _formatterSelector;
private readonly IBodyModelValidator _bodyModelValidator;
private readonly IValidationExcludeFiltersProvider _bodyValidationExcludeFiltersProvider;
/// <summary>
@ -28,26 +27,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <param name="context">An accessor to the <see cref="ActionContext"/>.</param>
/// <param name="bindingContext">An accessor to the <see cref="ActionBindingContext"/>.</param>
/// <param name="selector">The <see cref="IInputFormatterSelector"/>.</param>
/// <param name="bodyModelValidator">The <see cref="IBodyModelValidator"/>.</param>
/// <param name="bodyValidationExcludeFiltersProvider">
/// The <see cref="IValidationExcludeFiltersProvider"/>.
/// </param>
public BodyModelBinder([NotNull] IScopedInstance<ActionContext> context,
[NotNull] IScopedInstance<ActionBindingContext> bindingContext,
[NotNull] IInputFormatterSelector selector,
[NotNull] IBodyModelValidator bodyModelValidator,
[NotNull] IValidationExcludeFiltersProvider bodyValidationExcludeFiltersProvider)
: base(BindingSource.Body)
{
_actionContext = context.Value;
_bindingContext = bindingContext;
_formatterSelector = selector;
_bodyModelValidator = bodyModelValidator;
_bodyValidationExcludeFiltersProvider = bodyValidationExcludeFiltersProvider;
}
/// <inheritdoc />
protected async override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
protected async override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
var formatters = _bindingContext.Value.InputFormatters;
@ -59,20 +55,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var unsupportedContentType = Resources.FormatUnsupportedContentType(
bindingContext.OperationBindingContext.HttpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, unsupportedContentType);
return;
return new ModelBindingResult(null, bindingContext.ModelName, isModelSet: false);
}
bindingContext.Model = await formatter.ReadAsync(formatterContext);
// Validate the deserialized object
var validationContext = new ModelValidationContext(
bindingContext.OperationBindingContext.MetadataProvider,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
bindingContext.ModelMetadata,
containerMetadata: null,
excludeFromValidationFilters: _bodyValidationExcludeFiltersProvider.ExcludeFilters);
_bodyModelValidator.Validate(validationContext, bindingContext.ModelName);
var model = await formatter.ReadAsync(formatterContext);
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
}
}
}

View File

@ -21,11 +21,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <inheritdoc />
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
bindingContext.Model = requestServices.GetRequiredService(bindingContext.ModelType);
return Task.FromResult(true);
var model = requestServices.GetRequiredService(bindingContext.ModelType);
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, true));
}
}
}

View File

@ -29,6 +29,8 @@ namespace Microsoft.AspNet.Mvc
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="objectModelValidator">The <see cref="IObjectModelValidator"/> used for validating the
/// bound values.</param>
/// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model instance.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
@ -40,6 +42,7 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider,
[NotNull] IObjectModelValidator objectModelValidator,
[NotNull] IModelValidatorProvider validatorProvider)
where TModel : class
{
@ -52,6 +55,7 @@ namespace Microsoft.AspNet.Mvc
metadataProvider,
modelBinder,
valueProvider,
objectModelValidator,
validatorProvider,
predicate: (context, propertyName) => true);
}
@ -71,6 +75,8 @@ namespace Microsoft.AspNet.Mvc
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="objectModelValidator">The <see cref="IObjectModelValidator"/> used for validating the
/// bound values.</param>
/// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model
/// instance.</param>
@ -85,6 +91,7 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider,
[NotNull] IObjectModelValidator objectModelValidator,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] params Expression<Func<TModel, object>>[] includeExpressions)
where TModel : class
@ -100,6 +107,7 @@ namespace Microsoft.AspNet.Mvc
metadataProvider,
modelBinder,
valueProvider,
objectModelValidator,
validatorProvider,
predicate: predicate);
}
@ -119,6 +127,8 @@ namespace Microsoft.AspNet.Mvc
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// /// <param name="objectModelValidator">The <see cref="IObjectModelValidator"/> used for validating the
/// bound values.</param>
/// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model instance.</param>
/// <param name="predicate">A predicate which can be used to
@ -132,12 +142,13 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider,
[NotNull] IObjectModelValidator objectModelValidator,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] Func<ModelBindingContext, string, bool> predicate)
where TModel : class
{
var modelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null,
modelAccessor: () => model,
modelType: model.GetType());
var operationBindingContext = new OperationBindingContext
@ -152,7 +163,6 @@ namespace Microsoft.AspNet.Mvc
{
ModelMetadata = modelMetadata,
ModelName = prefix,
Model = model,
ModelState = modelState,
ValueProvider = valueProvider,
FallbackToEmptyPrefix = true,
@ -160,8 +170,12 @@ namespace Microsoft.AspNet.Mvc
PropertyFilter = predicate
};
if (await modelBinder.BindModelAsync(modelBindingContext))
var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext);
if (modelBindingResult != null)
{
var modelValidationContext = new ModelValidationContext(modelBindingContext, modelMetadata);
modelValidationContext.RootPrefix = prefix;
objectModelValidator.Validate(modelValidationContext);
return modelState.IsValid;
}

View File

@ -9,21 +9,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
{
public override Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public override Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.IsReadOnly)
{
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
}
return base.BindModelAsync(bindingContext);
}
protected override bool CreateOrReplaceCollection(ModelBindingContext bindingContext,
IList<TElement> newCollection)
protected override object GetModel(IEnumerable<TElement> newCollection)
{
bindingContext.Model = newCollection.ToArray();
return true;
return newCollection.ToArray();
}
}
}

View File

@ -21,13 +21,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new ConcurrentDictionary<Type, ObjectFactory>();
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.BinderType == null)
{
// Return false so that we are able to continue with the default set of model binders,
// Return null so that we are able to continue with the default set of model binders,
// if there is no specific model binder provided.
return false;
return null;
}
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
@ -52,11 +52,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
await modelBinder.BindModelAsync(bindingContext);
var result = await modelBinder.BindModelAsync(bindingContext);
// return true here, because this binder will handle all cases where the model binder is
// specified by metadata.
return true;
var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
new ModelBindingResult(null, bindingContext.ModelName, false);
// return a non null modelbinding result here, because this binder will handle all cases where the
// model binder is specified by metadata.
return modelBindingResult;
}
}
}

View File

@ -58,10 +58,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <returns>
/// A <see cref="Task"/> which will complete when model binding has completed.
/// </returns>
protected abstract Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext);
protected abstract Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext);
/// <inheritdoc />
public async Task<bool> BindModelAsync(ModelBindingContext context)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext context)
{
var bindingSourceMetadata = context.ModelMetadata.BinderMetadata as IBindingSourceMetadata;
var allowedBindingSource = bindingSourceMetadata?.BindingSource;
@ -70,14 +70,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
// Binding Sources are opt-in. This model either didn't specify one or specified something
// incompatible so let other binders run.
return false;
return null;
}
await BindModelCoreAsync(context);
var result = await BindModelCoreAsync(context);
var modelBindingResult =
result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
new ModelBindingResult(null, context.ModelName, false);
// Prevent other model binders from running because this model binder is the only handler for
// its binding source.
return true;
return modelBindingResult;
}
}
}

View File

@ -13,11 +13,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class ByteArrayModelBinder : IModelBinder
{
/// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(byte[]))
{
return false;
return null;
}
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// case 1: there was no <input ... /> element containing this data
if (valueProviderResult == null)
{
return false;
return null;
}
var value = valueProviderResult.AttemptedValue;
@ -33,19 +33,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// case 2: there was an <input ... /> element but it was left blank
if (string.IsNullOrEmpty(value))
{
return false;
return null;
}
try
{
bindingContext.Model = Convert.FromBase64String(value);
var model = Convert.FromBase64String(value);
return new ModelBindingResult(model, bindingContext.ModelName, true);
}
catch (Exception ex)
{
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, ex);
}
return true;
return new ModelBindingResult(model: null, key: bindingContext.ModelName, isModelSet: false);
}
}
}

View File

@ -12,15 +12,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class CancellationTokenModelBinder : IModelBinder
{
/// <inheritdoc />
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(CancellationToken))
{
bindingContext.Model = bindingContext.OperationBindingContext.HttpContext.RequestAborted;
return Task.FromResult(true);
var model = bindingContext.OperationBindingContext.HttpContext.RequestAborted;
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, true));
}
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
}
}
}

View File

@ -13,13 +13,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CollectionModelBinder<TElement> : IModelBinder
{
public virtual async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public virtual async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName))
{
return false;
return null;
}
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
@ -27,13 +27,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
BindSimpleCollection(bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture) :
BindComplexCollection(bindingContext);
var boundCollection = await bindCollectionTask;
return CreateOrReplaceCollection(bindingContext, boundCollection);
var model = GetModel(boundCollection);
return new ModelBindingResult(model, bindingContext.ModelName, true);
}
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
// is [ "1", "2" ] and needs to be converted to an int[].
internal async Task<List<TElement>> BindSimpleCollection(ModelBindingContext bindingContext,
internal async Task<IEnumerable<TElement>> BindSimpleCollection(ModelBindingContext bindingContext,
object rawValue,
CultureInfo culture)
{
@ -62,10 +62,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
object boundValue = null;
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext))
var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext);
if (result != null)
{
boundValue = innerBindingContext.Model;
bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
boundValue = result.Model;
}
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
}
@ -74,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
// Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1].
private async Task<List<TElement>> BindComplexCollection(ModelBindingContext bindingContext)
private async Task<IEnumerable<TElement>> BindComplexCollection(ModelBindingContext bindingContext)
{
var indexPropertyName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "index");
var valueProviderResultIndex = await bindingContext.ValueProvider.GetValueAsync(indexPropertyName);
@ -82,7 +82,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
}
internal async Task<List<TElement>> BindComplexCollectionFromIndexes(ModelBindingContext bindingContext,
internal async Task<IEnumerable<TElement>> BindComplexCollectionFromIndexes(ModelBindingContext bindingContext,
IEnumerable<string> indexNames)
{
bool indexNamesIsFinite;
@ -110,13 +110,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var modelType = bindingContext.ModelType;
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext))
var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext);
if (result != null)
{
didBind = true;
boundValue = childBindingContext.Model;
// merge validation up
bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode);
boundValue = result.Model;
}
// infinite size collection stops on first bind failure
@ -133,11 +131,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Extensibility point that allows the bound collection to be manipulated or transformed before
// being returned from the binder.
protected virtual bool CreateOrReplaceCollection(ModelBindingContext bindingContext,
IList<TElement> newCollection)
protected virtual object GetModel(IEnumerable<TElement> newCollection)
{
CreateOrReplaceCollection(bindingContext, newCollection, () => new List<TElement>());
return true;
return newCollection;
}
internal static object[] RawValueToObjectArray(object rawValue)
@ -165,23 +161,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// fallback
return new[] { rawValue };
}
internal static void CreateOrReplaceCollection(ModelBindingContext bindingContext,
IEnumerable<TElement> incomingElements,
Func<ICollection<TElement>> creator)
{
var collection = bindingContext.Model as ICollection<TElement>;
if (collection == null || collection.IsReadOnly)
{
collection = creator();
bindingContext.Model = collection;
}
collection.Clear();
foreach (var element in incomingElements)
{
collection.Add(element);
}
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
ModelMetadata = modelMetadata;
PropertyMetadata = new Collection<ModelMetadata>(propertyMetadata.ToList());
Results = new Dictionary<ModelMetadata, ComplexModelDtoResult>();
Results = new Dictionary<ModelMetadata, ModelBindingResult>();
}
public ModelMetadata ModelMetadata { get; private set; }
@ -26,6 +26,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// attempted. If binding failed, the entry's value will be null. If binding
// was never attempted, this dictionary will not contain a corresponding
// entry.
public IDictionary<ModelMetadata, ComplexModelDtoResult> Results { get; private set; }
public IDictionary<ModelMetadata, ModelBindingResult> Results { get; private set; }
}
}

View File

@ -8,37 +8,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class ComplexModelDtoModelBinder : IModelBinder
{
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(ComplexModelDto))
if (bindingContext.ModelType != typeof(ComplexModelDto))
{
ModelBindingHelper.ValidateBindingContext(bindingContext,
typeof(ComplexModelDto),
allowNullModel: false);
var dto = (ComplexModelDto)bindingContext.Model;
foreach (var propertyMetadata in dto.PropertyMetadata)
{
var propertyModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
propertyMetadata.PropertyName);
var propertyBindingContext = new ModelBindingContext(bindingContext,
propertyModelName,
propertyMetadata);
// bind and propagate the values
// If we can't bind then leave the result missing (don't add a null).
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext))
{
var result = ComplexModelDtoResult.FromBindingContext(propertyBindingContext);
dto.Results[propertyMetadata] = result;
}
}
return true;
return null;
}
return false;
ModelBindingHelper.ValidateBindingContext(bindingContext,
typeof(ComplexModelDto),
allowNullModel: false);
var dto = (ComplexModelDto)bindingContext.ModelMetadata.Model;
foreach (var propertyMetadata in dto.PropertyMetadata)
{
var propertyModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
propertyMetadata.PropertyName);
var propertyBindingContext = new ModelBindingContext(bindingContext,
propertyModelName,
propertyMetadata);
// bind and propagate the values
// If we can't bind then leave the result missing (don't add a null).
var modelBindingResult =
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext);
if (modelBindingResult != null)
{
dto.Results[propertyMetadata] = modelBindingResult;
}
}
return new ModelBindingResult(dto, bindingContext.ModelName, isModelSet: true);
}
}
}

View File

@ -1,29 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class ComplexModelDtoResult
{
public static ComplexModelDtoResult FromBindingContext([NotNull] ModelBindingContext context)
{
return new ComplexModelDtoResult(context.Model, context.IsModelSet, context.ValidationNode);
}
public ComplexModelDtoResult(
object model,
bool isModelBound,
[NotNull] ModelValidationNode validationNode)
{
Model = model;
IsModelBound = isModelBound;
ValidationNode = validationNode;
}
public bool IsModelBound { get; }
public object Model { get; set; }
public ModelValidationNode ValidationNode { get; set; }
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -30,97 +31,86 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <inheritdoc />
public IReadOnlyList<IModelBinder> ModelBinders { get; }
public virtual async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
var newBindingContext = CreateNewBindingContext(bindingContext,
bindingContext.ModelName,
reuseValidationNode: true);
bindingContext.ModelName);
var boundSuccessfully = await TryBind(newBindingContext);
if (!boundSuccessfully && !string.IsNullOrEmpty(bindingContext.ModelName)
var modelBindingResult = await TryBind(newBindingContext);
if (modelBindingResult == null && !string.IsNullOrEmpty(bindingContext.ModelName)
&& bindingContext.FallbackToEmptyPrefix)
{
// fallback to empty prefix?
newBindingContext = CreateNewBindingContext(bindingContext,
modelName: string.Empty,
reuseValidationNode: false);
boundSuccessfully = await TryBind(newBindingContext);
modelName: string.Empty);
modelBindingResult = await TryBind(newBindingContext);
}
if (!boundSuccessfully)
if (modelBindingResult == null)
{
return false; // something went wrong
}
// Only perform validation at the root of the object graph. ValidationNode will recursively walk the graph.
// Ignore ComplexModelDto since it essentially wraps the primary object.
if (newBindingContext.IsModelSet && IsBindingAtRootOfObjectGraph(newBindingContext))
{
// run validation and return the model
// If we fell back to an empty prefix above and are dealing with simple types,
// propagate the non-blank model name through for user clarity in validation errors.
// Complex types will reveal their individual properties as model names and do not require this.
if (!newBindingContext.ModelMetadata.IsComplexType &&
string.IsNullOrEmpty(newBindingContext.ModelName))
{
newBindingContext.ValidationNode = new ModelValidationNode(newBindingContext.ModelMetadata,
bindingContext.ModelName);
}
var validationContext = new ModelValidationContext(
bindingContext.OperationBindingContext.MetadataProvider,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
bindingContext.ModelMetadata,
containerMetadata: null);
newBindingContext.ValidationNode.Validate(validationContext, parentNode: null);
return null; // something went wrong
}
bindingContext.OperationBindingContext.BodyBindingState =
newBindingContext.OperationBindingContext.BodyBindingState;
if (newBindingContext.IsModelSet)
if (modelBindingResult.IsModelSet)
{
bindingContext.Model = newBindingContext.Model;
bindingContext.ModelMetadata.Model = modelBindingResult.Model;
// Update the model state key if we are bound using an empty prefix and it is a complex type.
// This is needed as validation uses the model state key to log errors. The client validation expects
// the errors with property names rather than the full name.
if (newBindingContext.ModelMetadata.IsComplexType && string.IsNullOrEmpty(modelBindingResult.Key))
{
// For non-complex types, if we fell back to the empty prefix, we should still be using the name
// of the parameter/property. Complex types have their own property names which acts as model
// state keys and do not need special treatment.
// For example :
//
// public class Model
// {
// public int SimpleType { get; set; }
// }
// public void Action(int id, Model model)
// {
// }
//
// In this case, for the model parameter the key would be SimpleType instead of model.SimpleType.
// (i.e here the prefix for the model key is empty).
// For the id parameter the key would be id.
return modelBindingResult;
}
}
return true;
return new ModelBindingResult(
modelBindingResult.Model,
bindingContext.ModelName,
modelBindingResult.IsModelSet);
}
private async Task<bool> TryBind(ModelBindingContext bindingContext)
private async Task<ModelBindingResult> TryBind(ModelBindingContext bindingContext)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
foreach (var binder in ModelBinders)
{
if (await binder.BindModelAsync(bindingContext))
var result = await binder.BindModelAsync(bindingContext);
if (result != null)
{
return true;
return result;
}
}
// Either we couldn't find a binder, or the binder couldn't bind. Distinction is not important.
return false;
}
private static bool IsBindingAtRootOfObjectGraph(ModelBindingContext bindingContext)
{
// We're at the root of the object graph if the model does does not have a container.
// This statement is true for complex types at the root twice over - once with the actual model
// and once when when it is represented by a ComplexModelDto. Ignore the latter case.
return bindingContext.ModelMetadata.ContainerType == null &&
bindingContext.ModelMetadata.ModelType != typeof(ComplexModelDto);
return null;
}
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext,
string modelName,
bool reuseValidationNode)
string modelName)
{
var newBindingContext = new ModelBindingContext
{
IsModelSet = oldBindingContext.IsModelSet,
ModelMetadata = oldBindingContext.ModelMetadata,
ModelName = modelName,
ModelState = oldBindingContext.ModelState,
@ -129,12 +119,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
PropertyFilter = oldBindingContext.PropertyFilter,
};
// validation is expensive to create, so copy it over if we can
if (reuseValidationNode)
{
newBindingContext.ValidationNode = oldBindingContext.ValidationNode;
}
newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext);
// If the property has a specified data binding sources, we need to filter the set of value providers

View File

@ -3,37 +3,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
{
protected override bool CreateOrReplaceCollection(ModelBindingContext bindingContext,
IList<KeyValuePair<TKey, TValue>> newCollection)
protected override object GetModel(IEnumerable<KeyValuePair<TKey, TValue>> newCollection)
{
CreateOrReplaceDictionary(bindingContext, newCollection, () => new Dictionary<TKey, TValue>());
return true;
}
private static void CreateOrReplaceDictionary(ModelBindingContext bindingContext,
IEnumerable<KeyValuePair<TKey, TValue>> incomingElements,
Func<IDictionary<TKey, TValue>> creator)
{
var dictionary = bindingContext.Model as IDictionary<TKey, TValue>;
if (dictionary == null || dictionary.IsReadOnly)
{
dictionary = creator();
bindingContext.Model = dictionary;
}
dictionary.Clear();
foreach (var element in incomingElements)
{
if (element.Key != null)
{
dictionary[element.Key] = element.Value;
}
}
return newCollection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}
}

View File

@ -16,35 +16,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class FormCollectionModelBinder : IModelBinder
{
/// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(IFormCollection) &&
bindingContext.ModelType != typeof(FormCollection))
{
return false;
return null;
}
object model = null;
var request = bindingContext.OperationBindingContext.HttpContext.Request;
if (request.HasFormContentType)
{
var form = await request.ReadFormAsync();
if (bindingContext.ModelType.IsAssignableFrom(form.GetType()))
{
bindingContext.Model = form;
model = form;
}
else
{
var formValuesLookup = form.ToDictionary(p => p.Key,
p => p.Value);
bindingContext.Model = new FormCollection(formValuesLookup, form.Files);
model = new FormCollection(formValuesLookup, form.Files);
}
}
else
{
bindingContext.Model = new FormCollection(new Dictionary<string, string[]>());
model = new FormCollection(new Dictionary<string, string[]>());
}
return true;
return new ModelBindingResult(model, bindingContext.ModelName, true);
}
}
}

View File

@ -18,33 +18,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class FormFileModelBinder : IModelBinder
{
/// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(IFormFile))
{
var postedFiles = await GetFormFilesAsync(bindingContext);
var value = postedFiles.FirstOrDefault();
if (value != null)
{
bindingContext.Model = value;
}
return true;
return new ModelBindingResult(value, bindingContext.ModelName, value != null);
}
else if (typeof(IEnumerable<IFormFile>).GetTypeInfo().IsAssignableFrom(
bindingContext.ModelType.GetTypeInfo()))
{
var postedFiles = await GetFormFilesAsync(bindingContext);
var value = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, postedFiles);
if (value != null)
{
bindingContext.Model = value;
}
return true;
return new ModelBindingResult(value, bindingContext.ModelName, value != null);
}
return false;
return null;
}
private async Task<List<IFormFile>> GetFormFilesAsync(ModelBindingContext bindingContext)

View File

@ -12,20 +12,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class GenericModelBinder : IModelBinder
{
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
var binderType = ResolveBinderType(bindingContext.ModelType);
if (binderType != null)
{
var binder = (IModelBinder)Activator.CreateInstance(binderType);
await binder.BindModelAsync(bindingContext);
var result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != null ?
new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
new ModelBindingResult(null, bindingContext.ModelName, false);
// Was able to resolve a binder type, hence we should tell the model binding system to return
// true so that none of the other model binders participate.
return true;
return modelBindingResult;
}
return false;
return null;
}
private static Type ResolveBinderType(Type modelType)

View File

@ -23,19 +23,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <inheritdoc />
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
var request = bindingContext.OperationBindingContext.HttpContext.Request;
var modelMetadata = bindingContext.ModelMetadata;
// Property name can be null if the model metadata represents a type (rahter than a property or parameter).
var headerName = modelMetadata.BinderModelName ?? modelMetadata.PropertyName ?? bindingContext.ModelName;
object model = null;
if (bindingContext.ModelType == typeof(string))
{
var value = request.Headers.Get(headerName);
if (value != null)
{
bindingContext.Model = value;
model = value;
}
}
else if (typeof(IEnumerable<string>).GetTypeInfo().IsAssignableFrom(
@ -44,14 +45,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var values = request.Headers.GetCommaSeparatedValues(headerName);
if (values != null)
{
bindingContext.Model = ModelBindingHelper.ConvertValuesToCollectionType(
model = ModelBindingHelper.ConvertValuesToCollectionType(
bindingContext.ModelType,
values);
}
}
// Always return true as header model binder is supposed to always handle IHeaderBinderMetadata.
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, model != null));
}
}
}

View File

@ -14,7 +14,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// Async function to bind to a particular model.
/// </summary>
/// <param name="bindingContext">The binding context which has the object to be bound.</param>
/// <returns>A Task with a bool implying the success or failure of the operation.</returns>
Task<bool> BindModelAsync(ModelBindingContext bindingContext);
/// <returns>A Task which on completion returns a <see cref="ModelBindingResult"/> which represents the result
/// of the model binding process.
/// </returns>
/// <remarks>
/// A <c>null</c> return value means that this model binder was not able to handle the request.
/// Returning <c>null</c> ensures that subsequent model binders are run. If a non <c>null</c> value indicates
/// that the model binder was able to handle the request.
/// </remarks>
Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext);
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
@ -9,23 +10,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class KeyValuePairModelBinder<TKey, TValue> : IModelBinder
{
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext,
typeof(KeyValuePair<TKey, TValue>),
allowNullModel: true);
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "value");
if (keyResult.Success && valueResult.Success)
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value");
var model = bindingContext.ModelMetadata.Model;
var isModelSet = false;
if (keyResult.IsModelSet && valueResult.IsModelSet)
{
bindingContext.Model = new KeyValuePair<TKey, TValue>(keyResult.Model, valueResult.Model);
model = new KeyValuePair<TKey, TValue>(
ModelBindingHelper.CastOrDefault<TKey>(keyResult.Model),
ModelBindingHelper.CastOrDefault<TValue>(valueResult.Model));
isModelSet = true;
}
return keyResult.Success || valueResult.Success;
else if (!keyResult.IsModelSet && valueResult.IsModelSet)
{
bindingContext.ModelState.TryAddModelError(keyResult.Key,
Resources.KeyValuePair_BothKeyAndValueMustBePresent);
}
else if (keyResult.IsModelSet && !valueResult.IsModelSet)
{
bindingContext.ModelState.TryAddModelError(valueResult.Key,
Resources.KeyValuePair_BothKeyAndValueMustBePresent);
}
var result = new ModelBindingResult(model, bindingContext.ModelName, isModelSet);
return (keyResult.IsModelSet || valueResult.IsModelSet) ? result : null;
}
internal async Task<BindResult<TModel>> TryBindStrongModel<TModel>(ModelBindingContext parentBindingContext,
internal async Task<ModelBindingResult> TryBindStrongModel<TModel>(ModelBindingContext parentBindingContext,
string propertyName)
{
var propertyModelMetadata =
@ -35,29 +52,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName);
var propertyBindingContext =
new ModelBindingContext(parentBindingContext, propertyModelName, propertyModelMetadata);
if (await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext))
var modelBindingResult =
await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext);
if (modelBindingResult != null)
{
var untypedModel = propertyBindingContext.Model;
var model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel);
parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
return new BindResult<TModel>(success: true, model: model);
return modelBindingResult;
}
return new BindResult<TModel>(success: false, model: default(TModel));
}
internal sealed class BindResult<TModel>
{
public BindResult(bool success, TModel model)
{
Success = success;
Model = model;
}
public bool Success { get; private set; }
public TModel Model { get; private set; }
return new ModelBindingResult(model: default(TModel), key: propertyModelName, isModelSet: false);
}
}
}

View File

@ -14,12 +14,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class MutableObjectModelBinder : IModelBinder
{
public virtual async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public virtual async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelType))
{
return false;
return null;
}
var mutableObjectBinderContext = new MutableObjectBinderContext()
@ -30,17 +30,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (!(await CanCreateModel(mutableObjectBinderContext)))
{
return false;
return null;
}
EnsureModel(bindingContext);
var dto = await CreateAndPopulateDto(bindingContext, mutableObjectBinderContext.PropertyMetadata);
var result = await CreateAndPopulateDto(bindingContext, mutableObjectBinderContext.PropertyMetadata);
// post-processing, e.g. property setters and hooking up validation
ProcessDto(bindingContext, dto);
// complex models require full validation
bindingContext.ValidationNode.ValidateAllProperties = true;
return true;
ProcessDto(bindingContext, (ComplexModelDto)result.Model);
return
new ModelBindingResult(bindingContext.ModelMetadata.Model, bindingContext.ModelName, isModelSet: true);
}
protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
@ -236,7 +235,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return true;
}
private async Task<ComplexModelDto> CreateAndPopulateDto(ModelBindingContext bindingContext,
private async Task<ModelBindingResult> CreateAndPopulateDto(ModelBindingContext bindingContext,
IEnumerable<ModelMetadata> propertyMetadatas)
{
// create a DTO and call into the DTO binder
@ -247,8 +246,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dtoBindingContext =
new ModelBindingContext(bindingContext, bindingContext.ModelName, complexModelDtoMetadata);
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(dtoBindingContext);
return (ComplexModelDto)dtoBindingContext.Model;
return await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(dtoBindingContext);
}
protected virtual object CreateModel(ModelBindingContext bindingContext)
@ -258,36 +256,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return Activator.CreateInstance(bindingContext.ModelType);
}
// Called when the property setter null check failed, allows us to add our own error message to ModelState.
internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ModelMetadata modelMetadata,
object incomingValue)
{
return (sender, e) =>
{
var validationNode = (ModelValidationNode)sender;
var modelState = e.ValidationContext.ModelState;
var validationState = modelState.GetFieldValidationState(validationNode.ModelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
// TODO: https://github.com/aspnet/Mvc/issues/450 Revive ModelBinderConfig
// var errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ValidationContext,
// modelMetadata,
// incomingValue);
var errorMessage = Resources.ModelBinderConfig_ValueRequired;
if (errorMessage != null)
{
modelState.TryAddModelError(validationNode.ModelStateKey, errorMessage);
}
}
};
}
protected virtual void EnsureModel(ModelBindingContext bindingContext)
{
if (bindingContext.Model == null)
if (bindingContext.ModelMetadata.Model == null)
{
bindingContext.Model = CreateModel(bindingContext);
bindingContext.ModelMetadata.Model = CreateModel(bindingContext);
}
}
@ -378,14 +351,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var validationInfo = GetPropertyValidationInfo(bindingContext);
// Eliminate provided properties from requiredProperties; leaving just *missing* required properties.
var boundProperties = dto.Results.Where(p => p.Value.IsModelBound).Select(p => p.Key.PropertyName);
var boundProperties = dto.Results.Where(p => p.Value.IsModelSet).Select(p => p.Key.PropertyName);
validationInfo.RequiredProperties.ExceptWith(boundProperties);
foreach (var missingRequiredProperty in validationInfo.RequiredProperties)
{
var addedError = false;
var modelStateKey = ModelBindingHelper.CreatePropertyModelName(
bindingContext.ValidationNode.ModelStateKey, missingRequiredProperty);
bindingContext.ModelName, missingRequiredProperty);
// Update Model as SetProperty() would: Place null value where validator will check for non-null. This
// ensures a failure result from a required validator (if any) even for a non-nullable property.
@ -421,14 +394,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
validationInfo.RequiredValidators.TryGetValue(propertyMetadata.PropertyName,
out requiredValidator);
SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
}
}
}
protected virtual void SetProperty(ModelBindingContext bindingContext,
ModelMetadata propertyMetadata,
ComplexModelDtoResult dtoResult,
ModelBindingResult dtoResult,
IModelValidator requiredValidator)
{
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
@ -443,7 +415,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
object value;
var hasDefaultValue = false;
if (dtoResult.IsModelBound)
if (dtoResult.IsModelSet)
{
value = dtoResult.Model;
}
@ -458,7 +430,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// the property setters throw, e.g. if we're setting entity keys to null.
if (value == null)
{
var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
@ -473,7 +445,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
if (!dtoResult.IsModelBound && !hasDefaultValue)
if (!dtoResult.IsModelSet && !hasDefaultValue)
{
// If we don't have a value, don't set it on the model and trounce a pre-initialized
// value.
@ -484,7 +456,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
try
{
property.SetValue(bindingContext.Model, value);
property.SetValue(bindingContext.ModelMetadata.Model, value);
}
catch (Exception ex)
{
@ -495,7 +467,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
ex = targetInvocationException.InnerException;
}
var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
@ -506,11 +478,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
else
{
// trying to set a non-nullable value type to null, need to make sure there's a message
var modelStateKey = dtoResult.ValidationNode.ModelStateKey;
var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value);
var errorMessage = Resources.ModelBinderConfig_ValueRequired;
bindingContext.ModelState.TryAddModelError(modelStateKey, errorMessage);
}
}
}

View File

@ -9,20 +9,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class TypeConverterModelBinder : IModelBinder
{
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!TypeHelper.HasStringConverter(bindingContext.ModelType))
{
// this type cannot be converted
return false;
return null;
}
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
if (valueProviderResult == null)
{
return false; // no entry
return null; // no entry
}
object newModel;
@ -31,14 +31,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
bindingContext.Model = newModel;
return new ModelBindingResult(newModel, bindingContext.ModelName, true);
}
catch (Exception ex)
{
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, ex);
}
return true;
return new ModelBindingResult(null, bindingContext.ModelName, false);
}
}
}

View File

@ -8,21 +8,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class TypeMatchModelBinder : IModelBinder
{
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = await GetCompatibleValueProviderResult(bindingContext);
if (valueProviderResult == null)
{
// conversion would have failed
return false;
return null;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var model = valueProviderResult.RawValue;
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
bindingContext.Model = model;
return true;
return new ModelBindingResult(model, bindingContext.ModelName, true);
}
internal static async Task<ValueProviderResult> GetCompatibleValueProviderResult(ModelBindingContext context)

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Provides methods to validate an object graph.
/// </summary>
public interface IObjectModelValidator
{
/// <summary>
/// Validates the given model in <see cref="ModelValidationContext.ModelMetadata"/>.
/// </summary>
/// <param name="validationContext">The <see cref="ModelValidationContext"/> associated with the current call.
/// </param>
void Validate(ModelValidationContext validationContext);
}
}

View File

@ -19,25 +19,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
indexNames = indexes;
}
}
return indexNames;
}
public static void CreateOrReplaceCollection<TElement>(ModelBindingContext bindingContext,
IEnumerable<TElement> incomingElements,
Func<ICollection<TElement>> creator)
{
var collection = bindingContext.Model as ICollection<TElement>;
if (collection == null || collection.IsReadOnly)
{
collection = creator();
bindingContext.Model = collection;
}
collection.Clear();
foreach (var element in incomingElements)
{
collection.Add(element);
}
}
}
}

View File

@ -80,17 +80,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
throw new ArgumentException(message, "bindingContext");
}
if (!allowNullModel && bindingContext.Model == null)
if (!allowNullModel && bindingContext.ModelMetadata.Model == null)
{
var message = Resources.FormatModelBinderUtil_ModelCannotBeNull(requiredType);
throw new ArgumentException(message, "bindingContext");
}
if (bindingContext.Model != null &&
if (bindingContext.ModelMetadata.Model != null &&
!bindingContext.ModelType.GetTypeInfo().IsAssignableFrom(requiredType.GetTypeInfo()))
{
var message = Resources.FormatModelBinderUtil_ModelInstanceIsWrong(
bindingContext.Model.GetType(),
bindingContext.ModelMetadata.Model.GetType(),
requiredType);
throw new ArgumentException(message, "bindingContext");
}

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private string _modelName;
private ModelStateDictionary _modelState;
private ModelValidationNode _validationNode;
private Func<ModelBindingContext, string, bool> _propertyFilter;
/// <summary>
@ -55,36 +54,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public OperationBindingContext OperationBindingContext { get; set; }
/// <summary>
/// Gets or sets the model associated with this context.
/// </summary>
/// <remarks>
/// The <see cref="ModelMetadata"/> property must be set to access this property.
/// </remarks>
public object Model
{
get
{
EnsureModelMetadata();
return ModelMetadata.Model;
}
set
{
IsModelSet = true;
EnsureModelMetadata();
ModelMetadata.Model = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether or not the <see cref="Model"/> value has been set.
///
/// This property can be used to distinguish between a model binder which does not find a value and
/// the case where a model binder sets the <c>null</c> value.
/// </summary>
public bool IsModelSet { get; set; }
/// <summary>
/// Gets or sets the metadata for the model associated with this context.
/// </summary>
@ -164,23 +133,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
set { _propertyFilter = value; }
}
/// <summary>
/// Gets or sets the <see cref="ModelValidationNode"/> instance used as a container for
/// validation information.
/// </summary>
public ModelValidationNode ValidationNode
{
get
{
if (_validationNode == null)
{
_validationNode = new ModelValidationNode(ModelMetadata, ModelName);
}
return _validationNode;
}
set { _validationNode = value; }
}
private void EnsureModelMetadata()
{
if (ModelMetadata == null)

View File

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Contains the result of model binding.
/// </summary>
public class ModelBindingResult
{
/// <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>
public ModelBindingResult(object model, string key, bool isModelSet)
{
Model = model;
Key = key;
IsModelSet = isModelSet;
}
/// <summary>
/// Gets or sets the model associated with this context.
/// </summary>
public object Model { get; }
/// <summary>
/// Gets or sets the model name which was used to bind the model.
///
/// This property can be used during validation to add model state for a bound model.
/// </summary>
public string Key { get; }
/// <summary>
/// Gets or sets a value indicating whether or not the <see cref="Model"/> value has been set.
///
/// This property can be used to distinguish between a model binder which does not find a value and
/// the case where a model binder sets the <c>null</c> value.
/// </summary>
public bool IsModelSet { get; }
}
}

View File

@ -132,10 +132,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
get { return _innerDictionary.Values; }
}
/// <inheritdoc />
/// <summary>
/// Gets a value that indicates whether any model state values in this model state dictionary is invalid or not validated.
/// </summary>
public bool IsValid
{
get { return ValidationState == ModelValidationState.Valid; }
get
{
return ValidationState == ModelValidationState.Valid || ValidationState == ModelValidationState.Skipped;
}
}
/// <inheritdoc />
@ -284,6 +289,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetValidity(entries);
}
/// <summary>
/// Returns <see cref="ModelValidationState"/> for the <paramref name="key"/>.
/// </summary>
/// <param name="key">The key to look up model state errors for.</param>
/// <returns>Returns <see cref="ModelValidationState.Unvalidated"/> if no entry is found for the specified
/// key, <see cref="ModelValidationState.Invalid"/> if an instance is found with one or more model
/// state errors; <see cref="ModelValidationState.Valid"/> otherwise.</returns>
public ModelValidationState GetValidationState([NotNull] string key)
{
ModelState validationState;
if (TryGetValue(key, out validationState))
{
return validationState.ValidationState;
}
return ModelValidationState.Unvalidated;
}
/// <summary>
/// Marks the <see cref="ModelState.ValidationState"/> for the entry with the specified <paramref name="key"/>
/// as <see cref="ModelValidationState.Valid"/>.
@ -300,6 +323,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
modelState.ValidationState = ModelValidationState.Valid;
}
/// <summary>
/// Marks the <see cref="ModelState.ValidationState"/> for the entry with the specified <paramref name="key"/>
/// as <see cref="ModelValidationState.Skipped"/>.
/// </summary>
/// <param name="key">The key of the <see cref="ModelState"/> to mark as skipped.</param>
public void MarkFieldSkipped([NotNull] string key)
{
var modelState = GetModelStateForKey(key);
if (modelState.ValidationState == ModelValidationState.Invalid)
{
throw new InvalidOperationException(Resources.Validation_InvalidFieldCannotBeReset_ToSkipped);
}
modelState.ValidationState = ModelValidationState.Skipped;
}
/// <summary>
/// Copies the values from the specified <paramref name="dictionary"/> into this instance, overwriting
/// existing values if keys are the same.

View File

@ -8,5 +8,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Unvalidated,
Invalid,
Valid,
Skipped
}
}

View File

@ -42,6 +42,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("ArgumentCannotBeNullOrEmpty");
}
/// <summary>
/// A value is required.
/// </summary>
internal static string KeyValuePair_BothKeyAndValueMustBePresent
{
get { return GetString("KeyValuePair_BothKeyAndValueMustBePresent"); }
}
/// <summary>
/// A value is required.
/// </summary>
internal static string FormatKeyValuePair_BothKeyAndValueMustBePresent()
{
return GetString("KeyValuePair_BothKeyAndValueMustBePresent");
}
/// <summary>
/// The property {0}.{1} could not be found.
/// </summary>
@ -330,6 +346,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("Validation_InvalidFieldCannotBeReset");
}
/// <summary>
/// A field previously marked invalid should not be marked skipped.
/// </summary>
internal static string Validation_InvalidFieldCannotBeReset_ToSkipped
{
get { return GetString("Validation_InvalidFieldCannotBeReset_ToSkipped"); }
}
/// <summary>
/// A field previously marked invalid should not be marked skipped.
/// </summary>
internal static string FormatValidation_InvalidFieldCannotBeReset_ToSkipped()
{
return GetString("Validation_InvalidFieldCannotBeReset_ToSkipped");
}
/// <summary>
/// A value is required but was not present in the request.
/// </summary>

View File

@ -123,6 +123,9 @@
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or empty.</value>
</data>
<data name="KeyValuePair_BothKeyAndValueMustBePresent" xml:space="preserve">
<value>A value is required.</value>
</data>
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>
@ -177,6 +180,9 @@
<data name="Validation_InvalidFieldCannotBeReset" xml:space="preserve">
<value>A field previously marked invalid should not be marked valid.</value>
</data>
<data name="Validation_InvalidFieldCannotBeReset_ToSkipped" xml:space="preserve">
<value>A field previously marked invalid should not be marked skipped.</value>
</data>
<data name="Validation_ValueNotFound" xml:space="preserve">
<value>A value is required but was not present in the request.</value>
</data>

View File

@ -40,7 +40,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
foreach (var actualType in actualTypes)
{
var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType;
if (!IsSimpleType(underlyingType))
{
return false;

View File

@ -1,264 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Recursively validate an object.
/// </summary>
public class DefaultBodyModelValidator : IBodyModelValidator
{
/// <inheritdoc />
public bool Validate(
[NotNull] ModelValidationContext modelValidationContext,
string keyPrefix)
{
var metadata = modelValidationContext.ModelMetadata;
var validationContext = new ValidationContext()
{
ModelValidationContext = modelValidationContext,
Visited = new HashSet<object>(ReferenceEqualityComparer.Instance),
KeyBuilders = new Stack<IKeyBuilder>(),
RootPrefix = keyPrefix
};
return ValidateNonVisitedNodeAndChildren(metadata, validationContext, validators: null);
}
private bool ValidateNonVisitedNodeAndChildren(
ModelMetadata metadata, ValidationContext validationContext, IEnumerable<IModelValidator> validators)
{
// Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack();
var isValid = true;
if (validators == null)
{
// The validators are not null in the case of validating an array. Since the validators are
// the same for all the elements of the array, we do not do GetValidators for each element,
// instead we just pass them over. See ValidateElements function.
validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(metadata);
}
// We don't need to recursively traverse the graph for null values
if (metadata.Model == null)
{
return ShallowValidate(metadata, validationContext, validators);
}
// We don't need to recursively traverse the graph for types that shouldn't be validated
var modelType = metadata.Model.GetType();
if (IsTypeExcludedFromValidation(
validationContext.ModelValidationContext.ExcludeFromValidationFilters,
modelType))
{
return ShallowValidate(metadata, validationContext, validators);
}
// Check to avoid infinite recursion. This can happen with cycles in an object graph.
if (validationContext.Visited.Contains(metadata.Model))
{
return true;
}
validationContext.Visited.Add(metadata.Model);
// Validate the children first - depth-first traversal
var enumerableModel = metadata.Model as IEnumerable;
if (enumerableModel == null)
{
isValid = ValidateProperties(metadata, validationContext);
}
else
{
isValid = ValidateElements(enumerableModel, validationContext);
}
if (isValid)
{
// Don't bother to validate this node if children failed.
isValid = ShallowValidate(metadata, validationContext, validators);
}
// Pop the object so that it can be validated again in a different path
validationContext.Visited.Remove(metadata.Model);
return isValid;
}
private bool ValidateProperties(ModelMetadata metadata, ValidationContext validationContext)
{
var isValid = true;
var propertyScope = new PropertyScope();
validationContext.KeyBuilders.Push(propertyScope);
foreach (var childMetadata in
validationContext.ModelValidationContext.MetadataProvider.GetMetadataForProperties(
metadata.Model, metadata.RealModelType))
{
propertyScope.PropertyName = childMetadata.PropertyName;
if (!ValidateNonVisitedNodeAndChildren(childMetadata, validationContext, validators: null))
{
isValid = false;
}
}
validationContext.KeyBuilders.Pop();
return isValid;
}
private bool ValidateElements(IEnumerable model, ValidationContext validationContext)
{
var isValid = true;
var elementType = GetElementType(model.GetType());
var elementMetadata =
validationContext.ModelValidationContext.MetadataProvider.GetMetadataForType(
modelAccessor: null, modelType: elementType);
var elementScope = new ElementScope() { Index = 0 };
validationContext.KeyBuilders.Push(elementScope);
var validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(elementMetadata);
// 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();
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)
{
elementMetadata.Model = element;
if (!ValidateNonVisitedNodeAndChildren(elementMetadata, validationContext, validators))
{
isValid = false;
}
}
elementScope.Index++;
}
validationContext.KeyBuilders.Pop();
return isValid;
}
// Validates a single node (not including children)
// Returns true if validation passes successfully
private static bool ShallowValidate(
ModelMetadata metadata,
ValidationContext validationContext,
[NotNull] IEnumerable<IModelValidator> validators)
{
var isValid = true;
string modelKey = null;
// When the are no validators we bail quickly. This saves a GetEnumerator allocation.
// In a large array (tens of thousands or more) scenario it's very significant.
var validatorsAsCollection = validators as ICollection;
if (validatorsAsCollection != null && validatorsAsCollection.Count == 0)
{
return isValid;
}
var modelValidationContext =
new ModelValidationContext(validationContext.ModelValidationContext, metadata);
foreach (var validator in validators)
{
foreach (var error in validator.Validate(modelValidationContext))
{
if (modelKey == null)
{
modelKey = validationContext.RootPrefix;
// This constructs the object heirarchy
// Example: prefix.Parent.Child
foreach (var keyBuilder in validationContext.KeyBuilders.Reverse())
{
modelKey = keyBuilder.AppendTo(modelKey);
}
}
var errorKey = ModelBindingHelper.CreatePropertyModelName(modelKey, error.MemberName);
validationContext.ModelValidationContext.ModelState.AddModelError(errorKey, error.Message);
isValid = false;
}
}
return isValid;
}
private bool IsTypeExcludedFromValidation(
IReadOnlyList<IExcludeTypeValidationFilter> filters, Type type)
{
// This can be set to null in ModelBinding scenarios which does not flow through this path.
if (filters == null)
{
return false;
}
return filters.Any(filter => filter.IsTypeExcluded(type));
}
private static Type GetElementType(Type type)
{
Debug.Assert(typeof(IEnumerable).IsAssignableFrom(type));
if (type.IsArray)
{
return type.GetElementType();
}
foreach (var implementedInterface in type.GetInterfaces())
{
if (implementedInterface.IsGenericType() &&
implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return implementedInterface.GetGenericArguments()[0];
}
}
return typeof(object);
}
private interface IKeyBuilder
{
string AppendTo(string prefix);
}
private class PropertyScope : IKeyBuilder
{
public string PropertyName { get; set; }
public string AppendTo(string prefix)
{
return ModelBindingHelper.CreatePropertyModelName(prefix, PropertyName);
}
}
private class ElementScope : IKeyBuilder
{
public int Index { get; set; }
public string AppendTo(string prefix)
{
return ModelBindingHelper.CreateIndexModelName(prefix, Index);
}
}
private class ValidationContext
{
public ModelValidationContext ModelValidationContext { get; set; }
public HashSet<object> Visited { get; set; }
public Stack<IKeyBuilder> KeyBuilders { get; set; }
public string RootPrefix { get; set; }
}
}
}

View File

@ -0,0 +1,299 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Recursively validate an object.
/// </summary>
public class DefaultObjectValidator : IObjectModelValidator
{
private readonly IValidationExcludeFiltersProvider _excludeFilterProvider;
private readonly IModelMetadataProvider _modelMetadataProvider;
public DefaultObjectValidator(
IValidationExcludeFiltersProvider excludeFilterProvider,
IModelMetadataProvider modelMetadataProvider)
{
_excludeFilterProvider = excludeFilterProvider;
_modelMetadataProvider = modelMetadataProvider;
}
/// <inheritdoc />
public void Validate([NotNull] ModelValidationContext modelValidationContext)
{
var metadata = modelValidationContext.ModelMetadata;
var validationContext = new ValidationContext()
{
ModelValidationContext = modelValidationContext,
Visited = new HashSet<object>(ReferenceEqualityComparer.Instance),
};
ValidateNonVisitedNodeAndChildren(
modelValidationContext.RootPrefix, metadata, validationContext, validators: null);
}
private bool ValidateNonVisitedNodeAndChildren(string modelKey,
ModelMetadata metadata, ValidationContext validationContext, IEnumerable<IModelValidator> validators)
{
// Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack();
var modelState = validationContext.ModelValidationContext.ModelState;
var bindingSourceMetadata = metadata.BinderMetadata as IBindingSourceMetadata;
var bindingSource = bindingSourceMetadata?.BindingSource;
if (bindingSource != null && !bindingSource.IsFromRequest)
{
// Short circuit if the metadata represents something that was not bound using request data.
// For example model bound using [FromServices]. Treat such objects as skipped.
var validationState = modelState.GetFieldValidationState(modelKey);
if (validationState == ModelValidationState.Unvalidated)
{
validationContext.ModelValidationContext.ModelState.MarkFieldSkipped(modelKey);
}
// For validation purposes this model is valid.
return true;
}
if (modelState.HasReachedMaxErrors)
{
// Short circuit if max errors have been recorded. In which case we treat this as invalid.
return false;
}
var isValid = true;
if (validators == null)
{
// The validators are not null in the case of validating an array. Since the validators are
// the same for all the elements of the array, we do not do GetValidators for each element,
// instead we just pass them over. See ValidateElements function.
validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(metadata);
}
// We don't need to recursively traverse the graph for null values
if (metadata.Model == null)
{
return ShallowValidate(modelKey, metadata, validationContext, validators);
}
// We don't need to recursively traverse the graph for types that shouldn't be validated
var modelType = metadata.Model.GetType();
if (IsTypeExcludedFromValidation(_excludeFilterProvider.ExcludeFilters, modelType))
{
var result = ShallowValidate(modelKey, metadata, validationContext, validators);
MarkPropertiesAsSkipped(modelKey, metadata, validationContext);
return result;
}
// Check to avoid infinite recursion. This can happen with cycles in an object graph.
if (validationContext.Visited.Contains(metadata.Model))
{
return true;
}
validationContext.Visited.Add(metadata.Model);
// Validate the children first - depth-first traversal
var enumerableModel = metadata.Model as IEnumerable;
if (enumerableModel == null)
{
isValid = ValidateProperties(modelKey, metadata, validationContext);
}
else
{
isValid = ValidateElements(modelKey, enumerableModel, validationContext);
}
if (isValid)
{
// Don't bother to validate this node if children failed.
isValid = ShallowValidate(modelKey, metadata, validationContext, validators);
}
// Pop the object so that it can be validated again in a different path
validationContext.Visited.Remove(metadata.Model);
return isValid;
}
private void MarkPropertiesAsSkipped(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext)
{
var modelState = validationContext.ModelValidationContext.ModelState;
var fieldValidationState = modelState.GetFieldValidationState(currentModelKey);
// Since shallow validation is done, if the modelvalidation state is still marked as unvalidated,
// it is because some properties in the subtree are marked as unvalidated. Mark all such properties
// as skipped. Models which have their subtrees as Valid or Invalid do not need to be marked as skipped.
if (fieldValidationState != ModelValidationState.Unvalidated)
{
return;
}
foreach (var childMetadata in metadata.Properties)
{
var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, childMetadata.PropertyName);
var validationState = modelState.GetFieldValidationState(childKey);
if (validationState == ModelValidationState.Unvalidated)
{
validationContext.ModelValidationContext.ModelState.MarkFieldSkipped(childKey);
}
}
}
private bool ValidateProperties(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext)
{
var isValid = true;
foreach (var childMetadata in metadata.Properties)
{
var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, childMetadata.PropertyName);
if (!ValidateNonVisitedNodeAndChildren(childKey, childMetadata, validationContext, validators: null))
{
isValid = false;
}
}
return isValid;
}
private bool ValidateElements(string currentKey, IEnumerable model, ValidationContext validationContext)
{
var elementType = GetElementType(model.GetType());
var elementMetadata = _modelMetadataProvider.GetMetadataForType(
modelAccessor: null,
modelType: elementType);
var validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(elementMetadata);
// 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)
{
elementMetadata.Model = element;
var elementKey = ModelBindingHelper.CreateIndexModelName(currentKey, index);
if (!ValidateNonVisitedNodeAndChildren(elementKey, elementMetadata, validationContext, validators))
{
isValid = false;
}
}
index++;
}
return isValid;
}
// Validates a single node (not including children)
// Returns true if validation passes successfully
private static bool ShallowValidate(
string modelKey,
ModelMetadata metadata,
ValidationContext validationContext,
IEnumerable<IModelValidator> validators)
{
var isValid = true;
// When the are no validators we bail quickly. This saves a GetEnumerator allocation.
// In a large array (tens of thousands or more) scenario it's very significant.
var validatorsAsCollection = validators as ICollection;
if (validatorsAsCollection == null || validatorsAsCollection.Count > 0)
{
var modelValidationContext =
new ModelValidationContext(validationContext.ModelValidationContext, metadata);
var modelState = validationContext.ModelValidationContext.ModelState;
var modelValidationState = modelState.GetValidationState(modelKey);
var fieldValidationState = modelState.GetFieldValidationState(modelKey);
// If either the model or its properties are unvalidated, validate them now.
if (modelValidationState == ModelValidationState.Unvalidated ||
fieldValidationState == ModelValidationState.Unvalidated)
{
foreach (var validator in validators)
{
foreach (var error in validator.Validate(modelValidationContext))
{
var errorKey = ModelBindingHelper.CreatePropertyModelName(modelKey, error.MemberName);
if (!modelState.TryAddModelError(errorKey, error.Message) &&
modelState.GetFieldValidationState(errorKey) == ModelValidationState.Unvalidated)
{
// If we are not able to add a model error
// for instance when the max error count is reached, mark the model as skipped.
modelState.MarkFieldSkipped(errorKey);
}
isValid = false;
}
}
}
else if (fieldValidationState == ModelValidationState.Invalid)
{
isValid = false;
}
}
if (isValid)
{
validationContext.ModelValidationContext.ModelState.MarkFieldValid(modelKey);
}
return isValid;
}
private bool IsTypeExcludedFromValidation(IReadOnlyList<IExcludeTypeValidationFilter> filters, Type type)
{
// This can be set to null in ModelBinding scenarios which does not flow through this path.
if (filters == null)
{
return false;
}
return filters.Any(filter => filter.IsTypeExcluded(type));
}
private static Type GetElementType(Type type)
{
Debug.Assert(typeof(IEnumerable).IsAssignableFrom(type));
if (type.IsArray)
{
return type.GetElementType();
}
foreach (var implementedInterface in type.GetInterfaces())
{
if (implementedInterface.IsGenericType() &&
implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return implementedInterface.GetGenericArguments()[0];
}
}
return typeof(object);
}
private class ValidationContext
{
public ModelValidationContext ModelValidationContext { get; set; }
public HashSet<object> Visited { get; set; }
}
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Validates the body parameter of an action after the parameter
/// has been read by the Input Formatters.
/// </summary>
public interface IBodyModelValidator
{
/// <summary>
/// Determines whether the Model is valid
/// and adds any validation errors to the <see cref="ModelStateDictionary"/>
/// </summary>
/// <param name="modelValidaitonContext">The validation context which contains the model, metadata
/// and the validator providers.</param>
/// <param name="keyPrefix">The <see cref="string"/> to append to the key for any validation errors.</param>
/// <returns><c>true</c>if the model is valid, <c>false</c> otherwise.</returns>
bool Validate(ModelValidationContext modelValidationContext, string keyPrefix);
}
}

View File

@ -1,21 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class ModelValidatedEventArgs : EventArgs
{
public ModelValidatedEventArgs([NotNull] ModelValidationContext validationContext,
[NotNull] ModelValidationNode parentNode)
{
ValidationContext = validationContext;
ParentNode = parentNode;
}
public ModelValidationContext ValidationContext { get; private set; }
public ModelValidationNode ParentNode { get; private set; }
}
}

View File

@ -1,21 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public sealed class ModelValidatingEventArgs : CancelEventArgs
{
public ModelValidatingEventArgs([NotNull] ModelValidationContext validationContext,
[NotNull] ModelValidationNode parentNode)
{
ValidationContext = validationContext;
ParentNode = parentNode;
}
public ModelValidationContext ValidationContext { get; private set; }
public ModelValidationNode ParentNode { get; private set; }
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public ModelValidationContext([NotNull] ModelBindingContext bindingContext,
[NotNull] ModelMetadata metadata)
: this(bindingContext.OperationBindingContext.MetadataProvider,
: this(bindingContext.ModelName,
bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState,
metadata,
@ -17,33 +17,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
}
public ModelValidationContext([NotNull] IModelMetadataProvider metadataProvider,
public ModelValidationContext(string rootPrefix,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] ModelStateDictionary modelState,
[NotNull] ModelMetadata metadata,
ModelMetadata containerMetadata)
: this(metadataProvider,
validatorProvider,
modelState,
metadata,
containerMetadata,
excludeFromValidationFilters: null)
{
}
public ModelValidationContext([NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] ModelStateDictionary modelState,
[NotNull] ModelMetadata metadata,
ModelMetadata containerMetadata,
IReadOnlyList<IExcludeTypeValidationFilter> excludeFromValidationFilters)
{
ModelMetadata = metadata;
ModelState = modelState;
MetadataProvider = metadataProvider;
RootPrefix = rootPrefix;
ValidatorProvider = validatorProvider;
ContainerMetadata = containerMetadata;
ExcludeFromValidationFilters = excludeFromValidationFilters;
}
public ModelValidationContext([NotNull] ModelValidationContext parentContext,
@ -52,9 +36,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata = metadata;
ContainerMetadata = parentContext.ModelMetadata;
ModelState = parentContext.ModelState;
MetadataProvider = parentContext.MetadataProvider;
RootPrefix = parentContext.RootPrefix;
ValidatorProvider = parentContext.ValidatorProvider;
ExcludeFromValidationFilters = parentContext.ExcludeFromValidationFilters;
}
public ModelMetadata ModelMetadata { get; }
@ -63,10 +46,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public ModelStateDictionary ModelState { get; }
public IModelMetadataProvider MetadataProvider { get; }
public string RootPrefix { get; set; }
public IModelValidatorProvider ValidatorProvider { get; }
public IReadOnlyList<IExcludeTypeValidationFilter> ExcludeFromValidationFilters { get; }
}
}

View File

@ -8,213 +8,4 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ModelValidationNode
{
private readonly List<ModelValidationNode> _childNodes;
public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey)
: this(modelMetadata, modelStateKey, null)
{
}
public ModelValidationNode([NotNull] ModelMetadata modelMetadata,
[NotNull] string modelStateKey,
IEnumerable<ModelValidationNode> childNodes)
{
ModelMetadata = modelMetadata;
ModelStateKey = modelStateKey;
_childNodes = (childNodes != null) ? childNodes.ToList() : new List<ModelValidationNode>();
}
public event EventHandler<ModelValidatedEventArgs> Validated;
public event EventHandler<ModelValidatingEventArgs> Validating;
public ICollection<ModelValidationNode> ChildNodes
{
get { return _childNodes; }
}
public ModelMetadata ModelMetadata { get; private set; }
public string ModelStateKey { get; private set; }
public bool ValidateAllProperties { get; set; }
public bool SuppressValidation { get; set; }
public void CombineWith(ModelValidationNode otherNode)
{
if (otherNode != null && !otherNode.SuppressValidation)
{
Validated += otherNode.Validated;
Validating += otherNode.Validating;
var otherChildNodes = otherNode._childNodes;
for (var i = 0; i < otherChildNodes.Count; i++)
{
var childNode = otherChildNodes[i];
_childNodes.Add(childNode);
}
}
}
private void OnValidated(ModelValidatedEventArgs e)
{
if (Validated != null)
{
Validated(this, e);
}
}
private void OnValidating(ModelValidatingEventArgs e)
{
if (Validating != null)
{
Validating(this, e);
}
}
private object TryConvertContainerToMetadataType(ModelValidationNode parentNode)
{
if (parentNode != null)
{
var containerInstance = parentNode.ModelMetadata.Model;
if (containerInstance != null)
{
var expectedContainerType = ModelMetadata.ContainerType;
if (expectedContainerType != null)
{
if (expectedContainerType.IsCompatibleWith(containerInstance))
{
return containerInstance;
}
}
}
}
return null;
}
public void Validate(ModelValidationContext validationContext)
{
Validate(validationContext, parentNode: null);
}
public void Validate([NotNull] ModelValidationContext validationContext, ModelValidationNode parentNode)
{
if (SuppressValidation || validationContext.ModelState.HasReachedMaxErrors)
{
// Short circuit if validation does not need to be applied or if we've reached the max number of
// validation errors.
return;
}
// pre-validation steps
var validatingEventArgs = new ModelValidatingEventArgs(validationContext, parentNode);
OnValidating(validatingEventArgs);
if (validatingEventArgs.Cancel)
{
return;
}
ValidateChildren(validationContext);
ValidateThis(validationContext, parentNode);
// post-validation steps
var validatedEventArgs = new ModelValidatedEventArgs(validationContext, parentNode);
OnValidated(validatedEventArgs);
var modelState = validationContext.ModelState;
if (modelState.GetFieldValidationState(ModelStateKey) != ModelValidationState.Invalid)
{
// If a node or its subtree were not marked invalid, we can consider it valid at this point.
modelState.MarkFieldValid(ModelStateKey);
}
}
private void ValidateChildren(ModelValidationContext validationContext)
{
for (var i = 0; i < _childNodes.Count; i++)
{
var child = _childNodes[i];
var childValidationContext = new ModelValidationContext(validationContext, child.ModelMetadata);
child.Validate(childValidationContext, this);
}
if (ValidateAllProperties)
{
ValidateProperties(validationContext);
}
}
private void ValidateProperties(ModelValidationContext validationContext)
{
var modelState = validationContext.ModelState;
var model = ModelMetadata.Model;
var updatedMetadata = validationContext.MetadataProvider.GetMetadataForType(() => model,
ModelMetadata.ModelType);
foreach (var propertyMetadata in updatedMetadata.Properties)
{
// Only want to add errors to ModelState if something doesn't already exist for the property node,
// else we could end up with duplicate or irrelevant error messages.
var propertyKeyRoot = ModelBindingHelper.CreatePropertyModelName(ModelStateKey,
propertyMetadata.PropertyName);
if (modelState.GetFieldValidationState(propertyKeyRoot) == ModelValidationState.Unvalidated)
{
var propertyValidators = GetValidators(validationContext, propertyMetadata);
var propertyValidationContext = new ModelValidationContext(validationContext, propertyMetadata);
foreach (var propertyValidator in propertyValidators)
{
foreach (var propertyResult in propertyValidator.Validate(propertyValidationContext))
{
var thisErrorKey = ModelBindingHelper.CreatePropertyModelName(propertyKeyRoot,
propertyResult.MemberName);
modelState.TryAddModelError(thisErrorKey, propertyResult.Message);
}
}
}
}
}
private void ValidateThis(ModelValidationContext validationContext, ModelValidationNode parentNode)
{
var modelState = validationContext.ModelState;
if (modelState.GetFieldValidationState(ModelStateKey) == ModelValidationState.Invalid)
{
// If any item in the key's subtree has been identified as invalid, short-circuit
return;
}
// If the Model at the current node is null and there is no parent, we cannot validate, and the
// DataAnnotationsModelValidator will throw. So we intercept here to provide a catch-all value-required
// validation error
if (parentNode == null && ModelMetadata.Model == null)
{
modelState.TryAddModelError(ModelStateKey, Resources.Validation_ValueNotFound);
return;
}
var container = TryConvertContainerToMetadataType(parentNode);
var validators = GetValidators(validationContext, ModelMetadata).ToArray();
for (var i = 0; i < validators.Length; i++)
{
var validator = validators[i];
foreach (var validationResult in validator.Validate(validationContext))
{
var currentModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey,
validationResult.MemberName);
modelState.TryAddModelError(currentModelStateKey, validationResult.Message);
}
}
}
private static IEnumerable<IModelValidator> GetValidators(ModelValidationContext validationContext,
ModelMetadata metadata)
{
return validationContext.ValidatorProvider.GetValidators(metadata);
}
}
}

View File

@ -54,6 +54,10 @@ namespace System.Web.Http
[FromServices]
public IModelMetadataProvider MetadataProvider { get; set; }
[FromServices]
public IObjectModelValidator ObjectValidator { get; set; }
/// <summary>
/// Gets model state after the model binding process. This ModelState will be empty before model binding
/// happens.
@ -417,18 +421,14 @@ namespace System.Web.Http
{
var modelMetadata = MetadataProvider.GetMetadataForType(() => entity, typeof(TEntity));
var bodyValidationExcludeFiltersProvider = Context.RequestServices
.GetRequiredService<IValidationExcludeFiltersProvider>();
var validator = Context.RequestServices.GetRequiredService<IBodyModelValidator>();
var modelValidationContext = new ModelValidationContext(
MetadataProvider,
keyPrefix,
BindingContext.ValidatorProvider,
ModelState,
modelMetadata,
containerMetadata: null,
excludeFromValidationFilters: bodyValidationExcludeFiltersProvider.ExcludeFilters);
validator.Validate(modelValidationContext, keyPrefix);
containerMetadata: null);
ObjectValidator.Validate(modelValidationContext);
}
protected virtual void Dispose(bool disposing)

View File

@ -9,15 +9,15 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class HttpRequestMessageModelBinder : IModelBinder
{
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(HttpRequestMessage))
{
bindingContext.Model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
return Task.FromResult(true);
var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, true));
}
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
}
}
}

View File

@ -1,7 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Net.Http;
using System.Net.Http.Formatting;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
namespace Microsoft.Framework.DependencyInjection
@ -15,6 +17,10 @@ namespace Microsoft.Framework.DependencyInjection
// The constructors on DefaultContentNegotiator aren't DI friendly, so just
// new it up.
services.AddInstance<IContentNegotiator>(new DefaultContentNegotiator());
services.Configure<MvcOptions>(options =>
{
options.ValidationExcludeFilters.Add(typeof(HttpRequestMessage));
});
return services;
}

View File

@ -68,6 +68,12 @@ namespace Microsoft.AspNet.Mvc
options.ValidationExcludeFilters.Add(typeof(XObject));
options.ValidationExcludeFilters.Add(typeof(Type));
options.ValidationExcludeFilters.Add(typeof(JToken));
// Any 'known' types that we bind should be marked as excluded from validation.
options.ValidationExcludeFilters.Add(typeof(System.Threading.CancellationToken));
options.ValidationExcludeFilters.Add(typeof(Http.IFormFile));
options.ValidationExcludeFilters.Add(typeof(Http.IFormCollection));
options.ValidationExcludeFilters.Add(typeFullName: "System.Xml.XmlNode");
}
}

View File

@ -62,6 +62,7 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton<IActionSelectorDecisionTreeProvider, ActionSelectorDecisionTreeProvider>();
yield return describe.Scoped<IActionSelector, DefaultActionSelector>();
yield return describe.Transient<IControllerActionArgumentBinder, DefaultControllerActionArgumentBinder>();
yield return describe.Transient<IObjectModelValidator, DefaultObjectValidator>();
yield return describe.Transient<INestedProvider<ActionDescriptorProviderContext>,
ControllerActionDescriptorProvider>();
@ -91,7 +92,6 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Instance<JsonOutputFormatter>(new JsonOutputFormatter());
yield return describe.Transient<IModelValidatorProviderProvider, DefaultModelValidatorProviderProvider>();
yield return describe.Transient<IBodyModelValidator, DefaultBodyModelValidator>();
yield return describe.Transient<IValidationExcludeFiltersProvider,
DefaultValidationExcludeFiltersProvider>();

View File

@ -17,13 +17,9 @@ namespace Microsoft.AspNet.Mvc
public class BodyModelBinderTests
{
[Fact]
public async Task BindModel_CallsValidationAndSelectedInputFormatterOnce()
public async Task BindModel_CallsSelectedInputFormatterOnce()
{
// Arrange
var mockValidator = new Mock<IBodyModelValidator>();
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<string>()))
.Returns(true)
.Verifiable();
var mockInputFormatter = new Mock<IInputFormatter>();
mockInputFormatter.Setup(o => o.ReadAsync(It.IsAny<InputFormatterContext>()))
.Returns(Task.FromResult<object>(new Person()))
@ -32,13 +28,12 @@ namespace Microsoft.AspNet.Mvc
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: mockInputFormatter.Object);
bindingContext.ModelMetadata.BinderMetadata = new FromBodyAttribute();
var binder = GetBodyBinder(mockInputFormatter.Object, mockValidator.Object);
var binder = GetBodyBinder(mockInputFormatter.Object);
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
mockValidator.Verify(v => v.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<string>()), Times.Once);
mockInputFormatter.Verify(v => v.ReadAsync(It.IsAny<InputFormatterContext>()), Times.Once);
}
@ -57,8 +52,9 @@ namespace Microsoft.AspNet.Mvc
// Assert
// Returns true because it understands the metadata type.
Assert.True(binderResult);
Assert.Null(bindingContext.Model);
Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet);
Assert.Null(binderResult.Model);
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
}
@ -78,7 +74,8 @@ namespace Microsoft.AspNet.Mvc
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(binderResult);
Assert.NotNull(binderResult);
Assert.False(binderResult.IsModelSet);
}
[Fact]
@ -97,7 +94,7 @@ namespace Microsoft.AspNet.Mvc
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(binderResult);
}
[Fact]
@ -116,7 +113,7 @@ namespace Microsoft.AspNet.Mvc
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(binderResult);
}
private static ModelBindingContext GetBindingContext(Type modelType, IInputFormatter inputFormatter)
@ -124,7 +121,7 @@ namespace Microsoft.AspNet.Mvc
var metadataProvider = new EmptyModelMetadataProvider();
var operationBindingContext = new OperationBindingContext
{
ModelBinder = GetBodyBinder(inputFormatter, null),
ModelBinder = GetBodyBinder(inputFormatter),
MetadataProvider = metadataProvider,
HttpContext = new DefaultHttpContext(),
};
@ -141,8 +138,7 @@ namespace Microsoft.AspNet.Mvc
return bindingContext;
}
private static BodyModelBinder GetBodyBinder(
IInputFormatter inputFormatter, IBodyModelValidator validator)
private static BodyModelBinder GetBodyBinder(IInputFormatter inputFormatter)
{
var actionContext = CreateActionContext(new DefaultHttpContext());
var inputFormatterSelector = new Mock<IInputFormatterSelector>();
@ -152,15 +148,6 @@ namespace Microsoft.AspNet.Mvc
It.IsAny<InputFormatterContext>()))
.Returns(inputFormatter);
if (validator == null)
{
var mockValidator = new Mock<IBodyModelValidator>();
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>(), It.IsAny<string>()))
.Returns(true)
.Verifiable();
validator = mockValidator.Object;
}
var bodyValidationPredicatesProvider = new Mock<IValidationExcludeFiltersProvider>();
bodyValidationPredicatesProvider.SetupGet(o => o.ExcludeFilters)
.Returns(new List<IExcludeTypeValidationFilter>());
@ -179,7 +166,6 @@ namespace Microsoft.AspNet.Mvc
actionContext,
bindingContextAccessor,
inputFormatterSelector.Object,
validator,
bodyValidationPredicatesProvider.Object);
return binder;

View File

@ -1990,7 +1990,9 @@ namespace Microsoft.AspNet.Mvc
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var excludeFilterProvider = new Mock<IValidationExcludeFiltersProvider>();
excludeFilterProvider.SetupGet(o => o.ExcludeFilters)
.Returns(new List<IExcludeTypeValidationFilter>());
var invoker = new TestControllerActionInvoker(
actionContext,
filterProvider.Object,
@ -2029,7 +2031,7 @@ namespace Microsoft.AspNet.Mvc
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(result: false));
.Returns(Task.FromResult<ModelBindingResult>(result: null));
var context = new Mock<HttpContext>();
context.SetupGet(c => c.Items)
.Returns(new Dictionary<object, object>());
@ -2042,18 +2044,21 @@ namespace Microsoft.AspNet.Mvc
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var metadataProvider = new EmptyModelMetadataProvider();
var invoker = new ControllerActionInvoker(
actionContext,
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
controllerFactory.Object,
actionDescriptor,
inputFormattersProvider.Object,
new DefaultControllerActionArgumentBinder(new EmptyModelMetadataProvider(), new MockMvcOptionsAccessor()),
new MockModelBinderProvider() { ModelBinders = new List<IModelBinder>() { binder.Object } },
new MockModelValidatorProviderProvider(),
new MockValueProviderFactoryProvider(),
new MockScopedInstance<ActionBindingContext>());
new DefaultControllerActionArgumentBinder(
metadataProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider),
new MockMvcOptionsAccessor()),
new MockModelBinderProvider() { ModelBinders = new List<IModelBinder>() { binder.Object } },
new MockModelValidatorProviderProvider(),
new MockValueProviderFactoryProvider(),
new MockScopedInstance<ActionBindingContext>());
// Act
await invoker.InvokeAsync();

View File

@ -13,6 +13,8 @@ using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing;
using Microsoft.AspNet.WebUtilities;
using Microsoft.AspNet.Http.Core;
using Microsoft.Framework.DependencyInjection;
#if ASPNET50
using Moq;
#endif
@ -917,7 +919,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
})
.Returns(Task.FromResult(false))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
@ -950,7 +952,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
})
.Returns(Task.FromResult(false))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
@ -982,7 +984,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
})
.Returns(Task.FromResult(false))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var controller = GetController(binder.Object, provider: null);
@ -1019,7 +1021,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
@ -1056,7 +1058,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var controller = GetController(binder.Object, provider: null);
@ -1090,7 +1092,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
@ -1125,7 +1127,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var controller = GetController(binder.Object, provider: null);
@ -1336,7 +1338,8 @@ namespace Microsoft.AspNet.Mvc.Test
private static Controller GetController(IModelBinder binder, IValueProvider provider)
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var actionContext = new ActionContext(Mock.Of<HttpContext>(), new RouteData(), new ActionDescriptor());
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
@ -1344,6 +1347,7 @@ namespace Microsoft.AspNet.Mvc.Test
{
ModelBinder = binder,
ValueProvider = provider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider()
};
return new TestableController()
@ -1351,7 +1355,8 @@ namespace Microsoft.AspNet.Mvc.Test
ActionContext = actionContext,
BindingContext = bindingContext,
MetadataProvider = metadataProvider,
ViewData = viewData
ViewData = viewData,
ObjectValidator = new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider)
};
}

View File

@ -268,13 +268,16 @@ namespace Microsoft.AspNet.Mvc.Core
private IServiceProvider GetServices()
{
var metadataProvider = new EmptyModelMetadataProvider();
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(typeof(IUrlHelper)))
.Returns(Mock.Of<IUrlHelper>());
services.Setup(s => s.GetService(typeof(IModelMetadataProvider)))
.Returns(new EmptyModelMetadataProvider());
.Returns(metadataProvider);
services.Setup(s => s.GetService(typeof(TestService)))
.Returns(new TestService());
services.Setup(s => s.GetService(typeof(IObjectModelValidator)))
.Returns(new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider));
services
.Setup(s => s.GetService(typeof(IScopedInstance<ActionBindingContext>)))
.Returns(new MockScopedInstance<ActionBindingContext>());

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.OptionDescriptors
public ITestService Service { get; private set; }
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
throw new NotImplementedException();
}

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.OptionDescriptors
private class TestModelBinder : IModelBinder
{
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
throw new NotImplementedException();
}

View File

@ -169,12 +169,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(c =>
{
// This value won't go into the arguments, because we return false.
c.Model = "Hello";
})
.Returns(Task.FromResult(result: false));
.Returns(Task.FromResult<ModelBindingResult>(result: null));
var actionContext = new ActionContext(
new DefaultHttpContext(),
@ -186,13 +181,14 @@ namespace Microsoft.AspNet.Mvc.Core.Test
ModelBinder = binder.Object,
};
var modelMetadataProvider = new DataAnnotationsModelMetadataProvider();
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider
.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var invoker = new DefaultControllerActionArgumentBinder(
new DataAnnotationsModelMetadataProvider(),
modelMetadataProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), modelMetadataProvider),
new MockMvcOptionsAccessor());
// Act
@ -223,11 +219,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(c =>
{
Assert.False(c.IsModelSet);
})
.Returns(Task.FromResult(result: true));
.Returns(Task.FromResult(new ModelBindingResult(null, "", false)));
var actionContext = new ActionContext(
new DefaultHttpContext(),
@ -244,8 +236,10 @@ namespace Microsoft.AspNet.Mvc.Core.Test
.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var modelMetadataProvider = new DataAnnotationsModelMetadataProvider();
var invoker = new DefaultControllerActionArgumentBinder(
new DataAnnotationsModelMetadataProvider(),
modelMetadataProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), modelMetadataProvider),
new MockMvcOptionsAccessor());
// Act
@ -284,10 +278,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
context.ModelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null,
modelType: typeof(string));
context.Model = value;
})
.Returns(Task.FromResult(result: true));
.Returns(Task.FromResult(result: new ModelBindingResult(value, "", true)));
var actionContext = new ActionContext(
new DefaultHttpContext(),
@ -299,8 +291,12 @@ namespace Microsoft.AspNet.Mvc.Core.Test
ModelBinder = binder.Object,
};
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var invoker = new DefaultControllerActionArgumentBinder(
metadataProvider,
mockValidatorProvider.Object,
new MockMvcOptionsAccessor());
// Act
@ -311,6 +307,107 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Assert.Equal(value, result["foo"]);
}
[Fact]
public async Task GetActionArgumentsAsync_CallsValidator_IfModelBinderSucceeds()
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
}
}
};
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
actionDescriptor);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(result: new ModelBindingResult(
model: null,
key: string.Empty,
isModelSet: true)));
var actionBindingContext = new ActionBindingContext()
{
ModelBinder = binder.Object,
};
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
var invoker = new DefaultControllerActionArgumentBinder(
new DataAnnotationsModelMetadataProvider(),
mockValidatorProvider.Object,
new MockMvcOptionsAccessor());
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
// Assert
mockValidatorProvider.Verify(
o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Once());
}
[Fact]
public async Task GetActionArgumentsAsync_DoesNotCallValidator_IfModelBinderFails()
{
// Arrange
Func<object, int> method = foo => 1;
var actionDescriptor = new ControllerActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
}
}
};
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
actionDescriptor);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
var actionBindingContext = new ActionBindingContext()
{
ModelBinder = binder.Object,
};
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Verifiable();
var invoker = new DefaultControllerActionArgumentBinder(
new DataAnnotationsModelMetadataProvider(),
mockValidatorProvider.Object,
new MockMvcOptionsAccessor());
// Act
var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext);
// Assert
mockValidatorProvider.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Never());
}
[Fact]
public async Task GetActionArgumentsAsync_SetsMaxModelErrors()
{
@ -332,11 +429,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(c =>
{
c.Model = "Hello";
})
.Returns(Task.FromResult(result: true));
.Returns(Task.FromResult(
result: new ModelBindingResult(model: "Hello", key: string.Empty, isModelSet: true)));
var actionContext = new ActionContext(
new DefaultHttpContext(),
@ -355,9 +449,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var options = new MockMvcOptionsAccessor();
options.Options.MaxModelValidationErrors = 5;
var mockValidatorProvider = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidatorProvider.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var invoker = new DefaultControllerActionArgumentBinder(
new DataAnnotationsModelMetadataProvider(),
mockValidatorProvider.Object,
options);
// Act

View File

@ -24,25 +24,26 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>();
metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny<Type>()))
metadataProvider.Setup(m => m.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
.Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null))
.Verifiable();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false));
.Returns(Task.FromResult<ModelBindingResult>(null));
var model = new MyModel();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
null,
Mock.Of<HttpContext>(),
new ModelStateDictionary(),
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
Mock.Of<IModelValidatorProvider>());
model,
null,
Mock.Of<HttpContext>(),
new ModelStateDictionary(),
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider.Object),
Mock.Of<IModelValidatorProvider>());
// Assert
Assert.False(result);
@ -71,17 +72,19 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{ "", null }
};
var valueProvider = new TestValueProvider(values);
var modelMetadataProvider = new DataAnnotationsModelMetadataProvider();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator);
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
modelMetadataProvider,
GetCompositeBinder(binders),
valueProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), modelMetadataProvider),
validator);
// Assert
Assert.False(result);
@ -109,17 +112,19 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{ "MyProperty", "MyPropertyValue" }
};
var valueProvider = new TestValueProvider(values);
var metadataProvider = new DataAnnotationsModelMetadataProvider();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator);
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
metadataProvider,
GetCompositeBinder(binders),
valueProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider),
validator);
// Assert
Assert.True(result);
@ -131,27 +136,28 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>();
metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny<Type>()))
metadataProvider.Setup(m => m.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
.Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null))
.Verifiable();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false));
.Returns(Task.FromResult<ModelBindingResult>(null));
var model = new MyModel();
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) => true;
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
null,
Mock.Of<HttpContext>(),
new ModelStateDictionary(),
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
Mock.Of<IModelValidatorProvider>(),
includePredicate);
model,
null,
Mock.Of<HttpContext>(),
new ModelStateDictionary(),
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
Mock.Of<IObjectModelValidator>(),
Mock.Of<IModelValidatorProvider>(),
includePredicate);
// Assert
Assert.False(result);
@ -194,18 +200,20 @@ namespace Microsoft.AspNet.Mvc.Core.Test
string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
var valueProvider = new TestValueProvider(values);
var metadataProvider = new DataAnnotationsModelMetadataProvider();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator,
includePredicate);
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
metadataProvider,
GetCompositeBinder(binders),
valueProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider),
validator,
includePredicate);
// Assert
Assert.True(result);
@ -219,13 +227,13 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>();
metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny<Type>()))
metadataProvider.Setup(m => m.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
.Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null))
.Verifiable();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false));
.Returns(Task.FromResult<ModelBindingResult>(null));
var model = new MyModel();
// Act
@ -237,6 +245,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
Mock.Of<IObjectModelValidator>(),
Mock.Of<IModelValidatorProvider>(),
m => m.IncludedProperty );
@ -277,19 +286,21 @@ namespace Microsoft.AspNet.Mvc.Core.Test
};
var valueProvider = new TestValueProvider(values);
var metadataProvider = new DataAnnotationsModelMetadataProvider();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator,
m => m.IncludedProperty,
m => m.MyProperty);
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider),
validator,
m => m.IncludedProperty,
m => m.MyProperty);
// Assert
Assert.True(result);
@ -327,17 +338,19 @@ namespace Microsoft.AspNet.Mvc.Core.Test
};
var valueProvider = new TestValueProvider(values);
var metadataProvider = new DataAnnotationsModelMetadataProvider();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator);
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
metadataProvider,
GetCompositeBinder(binders),
valueProvider,
new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider),
validator);
// Assert
// Includes everything.

View File

@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent);
var errorCollection = Assert.Single(responseObject);
var errorCollection = Assert.Single(responseObject, modelState => modelState.Value.Errors.Any());
var error = Assert.Single(errorCollection.Value.Errors);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage);
@ -164,7 +164,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent);
var errorCollection = Assert.Single(responseObject);
var errorCollection = Assert.Single(responseObject, modelState => modelState.Value.Errors.Any());
var error = Assert.Single(errorCollection.Value.Errors);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage);
}

View File

@ -26,6 +26,45 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
private readonly IServiceProvider _services = TestHelper.CreateServices("ModelBindingWebSite");
private readonly Action<IApplicationBuilder> _app = new ModelBindingWebSite.Startup().Configure;
[Fact]
public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Make sure the body object gets created with an invalid zip.
var input = "{\"OfficeAddress.Zip\":\"45\"}";
var content = new StringContent(input, Encoding.UTF8, "application/json");
// Act
// Make sure the non body based object gets created with an invalid zip.
var response = await client.PostAsync(
"http://localhost/Validation/SkipValidation?ShippingAddresses[0].Zip=45&HomeAddress.Zip=46", content);
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue);
Assert.True(isModelStateValid);
}
[Fact]
public async Task ModelValidation_DoesNotValidate_AnAlreadyValidatedObject()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/Validation/AvoidRecursive?Name=selfish");
// Assert
var stringValue = await response.Content.ReadAsStringAsync();
var isModelStateValid = JsonConvert.DeserializeObject<bool>(stringValue);
Assert.True(isModelStateValid);
}
[Theory]
[InlineData("RestrictValueProvidersUsingFromRoute", "valueFromRoute")]
[InlineData("RestrictValueProvidersUsingFromQuery", "valueFromQuery")]
@ -1012,7 +1051,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"CreateAndUpdateUser" +
"?RegisterationMonth=March");
"?RegistedeburationMonth=March");
// Assert
var result = JsonConvert.DeserializeObject<bool>(response);

View File

@ -26,9 +26,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.NotNull(retVal);
int[] array = bindingContext.Model as int[];
int[] array = retVal.Model as int[];
Assert.Equal(new[] { 42, 84 }, array);
}
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bound = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bound);
Assert.Null(bound);
}
[Fact]
@ -62,7 +62,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bound = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bound);
Assert.Null(bound);
}
private static IModelBinder CreateIntBinder()
@ -75,10 +75,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var value = await mbc.ValueProvider.GetValueAsync(mbc.ModelName);
if (value != null)
{
mbc.Model = value.ConvertTo(mbc.ModelType);
return true;
var model = value.ConvertTo(mbc.ModelType);
return new ModelBindingResult(model, key: null, isModelSet: true);
}
return false;
return null;
});
return mockIntBinder.Object;
}

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(binderResult);
}
[Fact]
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(binderResult);
Assert.NotNull(binderResult);
}
[Fact]
@ -67,8 +67,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
var p = (Person)bindingContext.Model;
Assert.True(binderResult);
var p = (Person)binderResult.Model;
Assert.Equal(model.Age, p.Age);
Assert.Equal(model.Name, p.Name);
}
@ -95,8 +94,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
var p = (Person)bindingContext.Model;
Assert.True(binderResult);
var p = (Person)binderResult.Model;
Assert.Equal(model.Age, p.Age);
Assert.Equal(model.Name, p.Name);
}
@ -152,9 +150,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
private class FalseModelBinder : IModelBinder
{
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
}
}
@ -167,10 +165,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
_model = new Person();
}
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext.Model = _model;
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(_model, bindingContext.ModelName, true));
}
}

View File

@ -45,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(context);
// Assert
Assert.False(result);
Assert.Null(result);
Assert.False(binder.WasBindModelCoreCalled);
}
@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(context);
// Assert
Assert.False(result);
Assert.Null(result);
Assert.False(binder.WasBindModelCoreCalled);
}
@ -93,7 +93,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(context);
// Assert
Assert.True(result);
Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.True(binder.WasBindModelCoreCalled);
}
@ -106,10 +107,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
}
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
WasBindModelCoreCalled = true;
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(null, bindingContext.ModelName, true));
}
}
}

View File

@ -30,8 +30,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(bindingContext.Model);
Assert.Null(binderResult);
}
[Fact]
@ -50,8 +49,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(binderResult);
var bytes = Assert.IsType<byte[]>(bindingContext.Model);
Assert.NotNull(binderResult);
var bytes = Assert.IsType<byte[]>(binderResult.Model);
Assert.Equal(new byte[] { 23, 43, 53 }, bytes);
}
@ -75,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(binderResult);
Assert.NotNull(binderResult);
Assert.False(bindingContext.ModelState.IsValid);
var error = Assert.Single(bindingContext.ModelState["foo"].Errors);
Assert.Equal(expected, error.ErrorMessage);
@ -97,7 +96,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(binderResult);
}
[Fact]
@ -111,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(binderResult);
}
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, Type modelType)

View File

@ -22,8 +22,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bound = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bound);
Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, bindingContext.Model);
Assert.NotNull(bound);
Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, bound.Model);
}
[Theory]
@ -40,8 +40,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bound = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bound);
Assert.Null(bindingContext.Model);
Assert.Null(bound);
}
private static ModelBindingContext GetBindingContext(Type modelType)

View File

@ -34,7 +34,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert
Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray());
Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
}
[Fact]
@ -55,7 +54,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert
Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
}
[Fact]
@ -73,10 +71,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>();
// Act
bool retVal = await binder.BindModelAsync(bindingContext);
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)retVal.Model).ToArray());
}
[Fact]
@ -91,11 +89,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>();
// Act
bool retVal = await binder.BindModelAsync(bindingContext);
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
Assert.NotNull(retVal);
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)retVal.Model).ToArray());
}
#endif
@ -134,15 +132,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var culture = new CultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
ModelValidationNode childValidationNode = null;
Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder)
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) =>
{
Assert.Equal("someName", mbc.ModelName);
childValidationNode = mbc.ValidationNode;
mbc.Model = 42;
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(42, mbc.ModelName, true));
});
var modelBinder = new CollectionModelBinder<int>();
@ -151,7 +146,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert
Assert.Equal(new[] { 42 }, boundCollection.ToArray());
Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray());
}
private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider)
@ -181,10 +175,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var value = await mbc.ValueProvider.GetValueAsync(mbc.ModelName);
if (value != null)
{
mbc.Model = value.ConvertTo(mbc.ModelType);
return true;
var model = value.ConvertTo(mbc.ModelType);
return new ModelBindingResult(model, mbc.ModelName, true);
}
return false;
return null;
});
return mockIntBinder.Object;
}

View File

@ -1,35 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
public class ComplexModelDtoResultTest
{
[Fact]
public void Constructor_SetsProperties()
{
// Arrange
var validationNode = GetValidationNode();
// Act
var result = new ComplexModelDtoResult(
"some string",
isModelBound: true,
validationNode: validationNode);
// Assert
Assert.Equal("some string", result.Model);
Assert.True(result.IsModelBound);
Assert.Equal(validationNode, result.ValidationNode);
}
private static ModelValidationNode GetValidationNode()
{
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(null, typeof(object));
return new ModelValidationNode(metadata, "someKey");
}
}
}

View File

@ -15,11 +15,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public class CompositeModelBinderTest
{
[Fact]
public async Task BindModel_SuccessfulBind_RunsValidationAndReturnsModel()
public async Task BindModel_SuccessfulBind_ReturnsModel()
{
// Arrange
var validationCalled = false;
var bindingContext = new ModelBindingContext
{
FallbackToEmptyPrefix = true,
@ -46,28 +44,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal("someName", context.ModelName);
Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
context.Model = 42;
bindingContext.ValidationNode.Validating += delegate { validationCalled = true; };
return Task.FromResult(true);
return Task.FromResult(
new ModelBindingResult(model: 42, key: "someName", isModelSet: true));
});
var shimBinder = CreateCompositeBinder(mockIntBinder.Object);
// Act
var isBound = await shimBinder.BindModelAsync(bindingContext);
var result = await shimBinder.BindModelAsync(bindingContext);
// Assert
Assert.True(isBound);
Assert.Equal(42, bindingContext.Model);
Assert.True(validationCalled);
Assert.True(bindingContext.ModelState.IsValid);
Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.Equal(42, result.Model);
}
[Fact]
public async Task BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel()
public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel()
{
// Arrange
var validationCalled = false;
var expectedModel = new List<int> { 1, 2, 3, 4, 5 };
var bindingContext = new ModelBindingContext
@ -94,35 +89,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
if (!string.IsNullOrEmpty(mbc.ModelName))
{
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
}
Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
Assert.Equal("", mbc.ModelName);
Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
mbc.Model = expectedModel;
mbc.ValidationNode.Validating += delegate { validationCalled = true; };
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(expectedModel, string.Empty, true));
});
var shimBinder = CreateCompositeBinder(mockIntBinder.Object);
// Act
var isBound = await shimBinder.BindModelAsync(bindingContext);
var result = await shimBinder.BindModelAsync(bindingContext);
// Assert
Assert.True(isBound);
Assert.Equal(expectedModel, bindingContext.Model);
Assert.True(validationCalled);
Assert.True(bindingContext.ModelState.IsValid);
Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.Equal(string.Empty, result.Key);
Assert.Equal(expectedModel, result.Model);
}
[Fact]
public async Task ModelBinder_ReturnsTrue_WithoutSettingValue_SkipsValidation()
public async Task ModelBinder_ReturnsTrue_WithoutSettingValue()
{
// Arrange
var validationCalled = false;
var bindingContext = new ModelBindingContext
{
@ -143,31 +135,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var modelBinder = new Mock<IModelBinder>();
modelBinder
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(context =>
{
context.ValidationNode.Validating += delegate { validationCalled = true; };
})
.Returns(Task.FromResult(true));
.Returns(Task.FromResult(new ModelBindingResult(null, "someName", false)));
var composite = CreateCompositeBinder(modelBinder.Object);
// Act
var isBound = await composite.BindModelAsync(bindingContext);
var result = await composite.BindModelAsync(bindingContext);
// Assert
Assert.True(isBound);
Assert.Null(bindingContext.Model);
Assert.False(validationCalled);
Assert.False(bindingContext.IsModelSet);
Assert.True(bindingContext.ModelState.IsValid);
Assert.NotNull(result);
Assert.False(result.IsModelSet);
Assert.Equal("someName", result.Key);
Assert.Null(result.Model);
}
[Fact]
public async Task ModelBinder_ReturnsTrue_SetsNullValue_RunsValidation()
public async Task ModelBinder_ReturnsTrue_SetsNullValue_SetsModelStateKey()
{
// Arrange
var validationCalled = false;
var bindingContext = new ModelBindingContext
{
@ -188,25 +173,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var modelBinder = new Mock<IModelBinder>();
modelBinder
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(context =>
{
context.Model = null;
context.ValidationNode.Validating += delegate { validationCalled = true; };
})
.Returns(Task.FromResult(true));
.Returns(Task.FromResult(new ModelBindingResult(null, "someName", true)));
var composite = CreateCompositeBinder(modelBinder.Object);
// Act
var isBound = await composite.BindModelAsync(bindingContext);
var result = await composite.BindModelAsync(bindingContext);
// Assert
Assert.True(isBound);
Assert.Null(bindingContext.Model);
Assert.True(validationCalled);
Assert.True(bindingContext.IsModelSet);
Assert.False(bindingContext.ModelState.IsValid);
Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.Equal("someName", result.Key);
Assert.Null(result.Model);
}
[Fact]
@ -215,7 +193,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Arrange
var mockListBinder = new Mock<IModelBinder>();
mockListBinder.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false))
.Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable();
var shimBinder = mockListBinder.Object;
@ -227,11 +205,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
};
// Act
var isBound = await shimBinder.BindModelAsync(bindingContext);
var result = await shimBinder.BindModelAsync(bindingContext);
// Assert
Assert.False(isBound);
Assert.Null(bindingContext.Model);
Assert.Null(result);
Assert.True(bindingContext.ModelState.IsValid);
mockListBinder.Verify();
}
@ -252,11 +229,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
};
// Act
var isBound = await shimBinder.BindModelAsync(bindingContext);
var result = await shimBinder.BindModelAsync(bindingContext);
// Assert
Assert.False(isBound);
Assert.Null(bindingContext.Model);
Assert.Null(result);
}
[Fact]
@ -273,11 +249,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act
var isBound = await binder.BindModelAsync(bindingContext);
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(isBound);
var model = Assert.IsType<SimplePropertiesModel>(bindingContext.Model);
Assert.NotNull(result);
var model = Assert.IsType<SimplePropertiesModel>(result.Model);
Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName);
}
@ -302,11 +278,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(Person));
// Act
var isBound = await binder.BindModelAsync(bindingContext);
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(isBound);
var model = Assert.IsType<Person>(bindingContext.Model);
Assert.NotNull(result);
var model = Assert.IsType<Person>(result.Model);
Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName);
Assert.Equal(2, model.Friends.Count);
@ -319,81 +295,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal(new byte[] { 227, 233, 133, 121, 58, 119, 180, 241 }, model.Resume);
}
[Fact]
public async Task BindModel_WithDefaultValidators_ValidatesSubProperties()
{
// Arrange
var validatorProvider = new DataAnnotationsModelValidatorProvider();
var binder = CreateBinderWithDefaults();
var valueProvider = new SimpleHttpValueProvider
{
{ "user.password", "password-val" },
{ "user.confirmpassword", "not-password-val" },
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(User), validatorProvider);
bindingContext.ModelName = "user";
// Act
var isBound = await binder.BindModelAsync(bindingContext);
// Assert
var error = Assert.Single(bindingContext.ModelState["user.confirmpassword"].Errors);
Assert.Equal("'ConfirmPassword' and 'Password' do not match.", error.ErrorMessage);
}
[Fact]
public async Task BindModel_WithDefaultValidators_ValidatesInstance()
{
// Arrange
var validatorProvider = new DataAnnotationsModelValidatorProvider();
var binder = CreateBinderWithDefaults();
var valueProvider = new SimpleHttpValueProvider
{
{ "user.password", "password" },
{ "user.confirmpassword", "password" },
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(User), validatorProvider);
bindingContext.ModelName = "user";
// Act
var isBound = await binder.BindModelAsync(bindingContext);
// Assert
var error = Assert.Single(bindingContext.ModelState["user"].Errors);
Assert.Equal("Password does not meet complexity requirements.", error.ErrorMessage);
}
[Fact]
public async Task BindModel_UsesTryAddModelError()
{
// Arrange
var validatorProvider = new DataAnnotationsModelValidatorProvider();
var binder = CreateBinderWithDefaults();
var valueProvider = new SimpleHttpValueProvider
{
{ "user.password", "password" },
{ "user.confirmpassword", "password2" },
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(User), validatorProvider);
bindingContext.ModelState.MaxAllowedErrors = 2;
bindingContext.ModelState.AddModelError("key1", "error1");
bindingContext.ModelName = "user";
// Act
await binder.BindModelAsync(bindingContext);
// Assert
var modelState = bindingContext.ModelState["user.confirmpassword"];
Assert.Empty(modelState.Errors);
modelState = bindingContext.ModelState["user"];
Assert.Empty(modelState.Errors);
var error = Assert.Single(bindingContext.ModelState[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
}
private static ModelBindingContext CreateBindingContext(IModelBinder binder,
IValueProvider valueProvider,
Type type,

View File

@ -34,12 +34,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new DictionaryModelBinder<int, string>();
// Act
bool retVal = await binder.BindModelAsync(bindingContext);
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.NotNull(retVal);
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(bindingContext.Model);
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(retVal.Model);
Assert.NotNull(dictionary);
Assert.Equal(2, dictionary.Count);
Assert.Equal("forty-two", dictionary[42]);
@ -56,10 +56,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var value = await mbc.ValueProvider.GetValueAsync(mbc.ModelName);
if (value != null)
{
mbc.Model = value.ConvertTo(mbc.ModelType);
return true;
var model = value.ConvertTo(mbc.ModelType);
return new ModelBindingResult(model, key: null, isModelSet: true);
}
return false;
return null;
});
return mockKvpBinder.Object;
}

View File

@ -34,8 +34,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
var form = Assert.IsAssignableFrom<IFormCollection>(bindingContext.Model);
Assert.NotNull(result);
var form = Assert.IsAssignableFrom<IFormCollection>(result.Model);
Assert.Equal(2, form.Count);
Assert.Equal("value1", form["field1"]);
Assert.Equal("value2", form["field2"]);
@ -58,7 +58,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(result);
Assert.Null(result);
}
[Fact]
@ -73,9 +73,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
Assert.IsType(typeof(FormCollection), bindingContext.Model);
Assert.Empty((FormCollection)bindingContext.Model);
Assert.NotNull(result);
Assert.IsType(typeof(FormCollection), result.Model);
Assert.Empty((FormCollection)result.Model);
}
[Fact]
@ -95,8 +95,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
var form = Assert.IsAssignableFrom<IFormCollection>(bindingContext.Model);
Assert.NotNull(result);
var form = Assert.IsAssignableFrom<IFormCollection>(result.Model);
Assert.Equal(2, form.Count);
Assert.Equal("value1", form["field1"]);
Assert.Equal("value2", form["field2"]);

View File

@ -32,8 +32,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
var files = Assert.IsAssignableFrom<IList<IFormFile>>(bindingContext.Model);
Assert.NotNull(result);
var files = Assert.IsAssignableFrom<IList<IFormFile>>(result.Model);
Assert.Equal(2, files.Count);
}
@ -52,8 +52,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
var files = Assert.IsAssignableFrom<IList<IFormFile>>(bindingContext.Model);
Assert.NotNull(result);
var files = Assert.IsAssignableFrom<IList<IFormFile>>(result.Model);
Assert.Equal(2, files.Count);
}
@ -72,8 +72,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
var file = Assert.IsAssignableFrom<IFormFile>(bindingContext.Model);
Assert.NotNull(result);
var file = Assert.IsAssignableFrom<IFormFile>(result.Model);
Assert.Equal("form-data; name=file; filename=file1.txt",
file.ContentDisposition);
}
@ -91,8 +91,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
Assert.Null(bindingContext.Model);
Assert.NotNull(result);
Assert.Null(result.Model);
}
[Fact]
@ -109,8 +109,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
Assert.Null(bindingContext.Model);
Assert.NotNull(result);
Assert.Null(result.Model);
}
[Fact]
@ -127,8 +127,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
Assert.Null(bindingContext.Model);
Assert.NotNull(result);
Assert.Null(result.Model);
}
[Fact]
@ -145,8 +145,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(result);
Assert.Null(bindingContext.Model);
Assert.NotNull(result);
Assert.Null(result.Model);
}
private static ModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext)

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.True(result);
Assert.NotNull(result);
}
[Fact]
@ -47,8 +47,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.True(result);
Assert.Equal(headerValue.Split(','), modelBindingContext.Model);
Assert.NotNull(result);
Assert.Equal(headerValue.Split(','), result.Model);
}
[Fact]
@ -68,8 +68,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.True(result);
Assert.Equal(headerValue, modelBindingContext.Model);
Assert.NotNull(result);
Assert.Equal(headerValue, result.Model);
}
private static ModelBindingContext GetBindingContext(Type modelType)

View File

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if ASPNET50
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Moq;
@ -13,37 +15,70 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public class KeyValuePairModelBinderTest
{
[Fact]
public async Task BindModel_MissingKey_ReturnsFalse()
public async Task BindModel_MissingKey_ReturnsTrue_AndAddsModelValidationError()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider();
var bindingContext = GetBindingContext(valueProvider, Mock.Of<IModelBinder>());
// Create string binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider, CreateStringBinder());
var binder = new KeyValuePairModelBinder<int, string>();
// Act
bool retVal = await binder.BindModelAsync(bindingContext);
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(retVal);
Assert.Null(bindingContext.Model);
Assert.Empty(bindingContext.ValidationNode.ChildNodes);
Assert.NotNull(retVal);
Assert.Null(retVal.Model);
Assert.False(bindingContext.ModelState.IsValid);
Assert.Equal("someName", bindingContext.ModelName);
var error = Assert.Single(bindingContext.ModelState["someName.Key"].Errors);
Assert.Equal("A value is required.", error.ErrorMessage);
}
[Fact]
public async Task BindModel_MissingValue_ReturnsTrue()
public async Task BindModel_MissingValue_ReturnsTrue_AndAddsModelValidationError()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider();
var bindingContext = GetBindingContext(valueProvider);
// Create int binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider, CreateIntBinder());
var binder = new KeyValuePairModelBinder<int, string>();
// Act
bool retVal = await binder.BindModelAsync(bindingContext);
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.Null(bindingContext.Model);
Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
Assert.NotNull(retVal);
Assert.Null(retVal.Model);
Assert.False(bindingContext.ModelState.IsValid);
Assert.Equal("someName", bindingContext.ModelName);
Assert.Equal(bindingContext.ModelState["someName.Value"].Errors.First().ErrorMessage, "A value is required.");
}
[Fact]
public async Task BindModel_MissingKeyAndMissingValue_DoNotAddModelStateError()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider();
// Create int binder to create the value but not the key.
var bindingContext = GetBindingContext(valueProvider);
var mockBinder = new Mock<IModelBinder>();
mockBinder.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult<ModelBindingResult>(null));
bindingContext.OperationBindingContext.ModelBinder = mockBinder.Object;
var binder = new KeyValuePairModelBinder<int, string>();
// Act
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.Null(retVal);
Assert.True(bindingContext.ModelState.IsValid);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
[Fact]
@ -60,9 +95,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), bindingContext.Model);
Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
Assert.NotNull(retVal);
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), retVal.Model);
}
[Fact]
@ -76,9 +110,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.TryBindStrongModel<int>(bindingContext, "key");
// Assert
Assert.True(retVal.Success);
Assert.True(retVal.IsModelSet);
Assert.Equal(42, retVal.Model);
Assert.Single(bindingContext.ValidationNode.ChildNodes);
Assert.Empty(bindingContext.ModelState);
}
@ -92,7 +125,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
.Returns((ModelBindingContext mbc) =>
{
Assert.Equal("someName.key", mbc.ModelName);
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(null, string.Empty, true));
});
var bindingContext = GetBindingContext(new SimpleHttpValueProvider(), innerBinder.Object);
@ -103,25 +136,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.TryBindStrongModel<int>(bindingContext, "key");
// Assert
Assert.True(retVal.Success);
Assert.Equal(default(int), retVal.Model);
Assert.Single(bindingContext.ValidationNode.ChildNodes);
Assert.True(retVal.IsModelSet);
Assert.Null(retVal.Model);
Assert.Empty(bindingContext.ModelState);
}
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, IModelBinder innerBinder = null)
private static ModelBindingContext GetBindingContext(
IValueProvider valueProvider,
IModelBinder innerBinder = null,
Type keyValuePairType = null)
{
var metataProvider = new EmptyModelMetadataProvider();
var bindingContext = new ModelBindingContext
{
ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
ModelMetadata = metataProvider.GetMetadataForType(null, keyValuePairType ?? typeof(KeyValuePair<int, string>)),
ModelName = "someName",
ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext
{
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
ValidatorProvider = new DataAnnotationsModelValidatorProvider()
}
};
return bindingContext;
@ -136,10 +171,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
if (mbc.ModelType == typeof(int))
{
mbc.Model = 42;
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(42, mbc.ModelName, true));
}
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
});
return mockIntBinder.Object;
}
@ -153,10 +187,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
if (mbc.ModelType == typeof(string))
{
mbc.Model = "some-value";
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult("some-value", mbc.ModelName, true));
}
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
});
return mockStringBinder.Object;
}

View File

@ -34,19 +34,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}
[Fact]
public void ModelProperty_ThrowsIfModelMetadataDoesNotExist()
{
// Arrange
var bindingContext = new ModelBindingContext();
// Act & assert
ExceptionAssert.Throws<InvalidOperationException>(
() => bindingContext.Model = null,
"The ModelMetadata property must be set before accessing this property.");
}
[Fact]
public void ModelAndModelTypeAreFedFromModelMetadata()
public void ModelTypeAreFedFromModelMetadata()
{
// Act
var bindingContext = new ModelBindingContext
@ -55,27 +43,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
};
// Assert
Assert.Equal(42, bindingContext.Model);
Assert.Equal(typeof(int), bindingContext.ModelType);
}
[Fact]
public void ValidationNodeProperty_DefaultValues()
{
// Act
var bindingContext = new ModelBindingContext
{
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
ModelName = "theInt"
};
// Act
var validationNode = bindingContext.ValidationNode;
// Assert
Assert.NotNull(validationNode);
Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata);
Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey);
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
public class ModelBindingResultTest
{
[Fact]
public void Constructor_SetsProperties()
{
// Arrange
// Act
var result = new ModelBindingResult(
"some string",
isModelSet: true,
key: "someName");
// Assert
Assert.Equal("some string", result.Model);
Assert.True(result.IsModelSet);
Assert.Equal("someName", result.Key);
}
}
}

View File

@ -359,7 +359,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Returns((ModelBindingContext mbc) =>
{
// just return the DTO unchanged
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(mbc.ModelMetadata.Model, mbc.ModelName, true));
});
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
@ -371,9 +371,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var retValue = await testableBinder.Object.BindModelAsync(bindingContext);
// Assert
Assert.True(retValue);
Assert.IsType<Person>(bindingContext.Model);
Assert.True(bindingContext.ValidationNode.ValidateAllProperties);
Assert.NotNull(retValue);
Assert.True(retValue.IsModelSet);
Assert.NotNull(retValue.Model);
Assert.IsType<Person>(retValue.Model);
testableBinder.Verify();
}
@ -404,7 +405,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Returns((ModelBindingContext mbc) =>
{
// just return the DTO unchanged
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(mbc.ModelMetadata.Model, mbc.ModelName, true));
});
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
@ -416,9 +417,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var retValue = await testableBinder.Object.BindModelAsync(bindingContext);
// Assert
Assert.True(retValue);
Assert.IsType<Person>(bindingContext.Model);
Assert.True(bindingContext.ValidationNode.ValidateAllProperties);
Assert.NotNull(retValue);
Assert.True(retValue.IsModelSet);
Assert.NotNull(retValue.Model);
Assert.IsType<Person>(retValue.Model);
testableBinder.Verify();
}
@ -517,9 +519,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var testableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
// Act
var originalModel = bindingContext.Model;
var originalModel = bindingContext.ModelMetadata.Model;
testableBinder.Object.EnsureModelPublic(bindingContext);
var newModel = bindingContext.Model;
var newModel = bindingContext.ModelMetadata.Model;
// Assert
Assert.Same(originalModel, newModel);
@ -540,9 +542,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Returns(new Person()).Verifiable();
// Act
var originalModel = bindingContext.Model;
var originalModel = bindingContext.ModelMetadata.Model;
testableBinder.Object.EnsureModelPublic(bindingContext);
var newModel = bindingContext.Model;
var newModel = bindingContext.ModelMetadata.Model;
// Assert
Assert.Null(originalModel);
@ -685,53 +687,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties);
}
[Fact]
public void NullCheckFailedHandler_ModelStateAlreadyInvalid_DoesNothing()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError("foo.bar", "Some existing error.");
var modelMetadata = GetMetadataForType(typeof(Person));
var validationNode = new ModelValidationNode(modelMetadata, "foo");
var validationContext = new ModelValidationContext(new DataAnnotationsModelMetadataProvider(),
Mock.Of<IModelValidatorProvider>(),
modelState,
modelMetadata,
null);
var e = new ModelValidatedEventArgs(validationContext, parentNode: null);
// Act
var handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, incomingValue: null);
handler(validationNode, e);
// Assert
Assert.False(modelState.ContainsKey("foo"));
}
[Fact]
public void NullCheckFailedHandler_ModelStateValid_AddsErrorString()
{
// Arrange
var modelState = new ModelStateDictionary();
var modelMetadata = GetMetadataForType(typeof(Person));
var validationNode = new ModelValidationNode(modelMetadata, "foo");
var validationContext = new ModelValidationContext(new DataAnnotationsModelMetadataProvider(),
Mock.Of<IModelValidatorProvider>(),
modelState,
modelMetadata,
null);
var e = new ModelValidatedEventArgs(validationContext, parentNode: null);
// Act
var handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, incomingValue: null);
handler(validationNode, e);
// Assert
Assert.True(modelState.ContainsKey("foo"));
Assert.Equal("A value is required.", modelState["foo"].Errors[0].ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void ProcessDto_BindRequiredFieldMissing_RaisesModelError()
@ -756,10 +711,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name");
dto.Results[nameProperty] = new ComplexModelDtoResult(
dto.Results[nameProperty] = new ModelBindingResult(
"John Doe",
isModelBound: true,
validationNode: new ModelValidationNode(nameProperty, ""));
isModelSet: true,
key: "");
var testableBinder = new TestableMutableObjectModelBinder();
@ -802,7 +757,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ValidatorProvider = Mock.Of<IModelValidatorProvider>()
}
};
var validationContext = new ModelValidationContext(new EmptyModelMetadataProvider(),
var validationContext = new ModelValidationContext("theModel",
bindingContext.OperationBindingContext
.ValidatorProvider,
bindingContext.ModelState,
@ -813,35 +768,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var testableBinder = new TestableMutableObjectModelBinder();
var propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Name");
dto.Results[propertyMetadata] = new ComplexModelDtoResult(
dto.Results[propertyMetadata] = new ModelBindingResult(
"John Doe",
isModelBound: true,
validationNode: new ModelValidationNode(propertyMetadata, "theModel.Name"));
isModelSet: true,
key: "theModel.Name");
// Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this
// case because the binding exists.
propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Age");
dto.Results[propertyMetadata] = new ComplexModelDtoResult(
dto.Results[propertyMetadata] = new ModelBindingResult(
null,
isModelBound: true,
validationNode: new ModelValidationNode(propertyMetadata, "theModel.Age"));
isModelSet: true,
key: "theModel.Age");
// Act; must also Validate because null-check error handler is late-bound
// Act
testableBinder.ProcessDto(bindingContext, dto);
bindingContext.ValidationNode.Validate(validationContext);
// Assert
var modelStateDictionary = bindingContext.ModelState;
Assert.False(modelStateDictionary.IsValid);
Assert.Equal(2, modelStateDictionary.Count);
// Check Name field
ModelState modelState;
Assert.True(modelStateDictionary.TryGetValue("theModel.Name", out modelState));
Assert.Equal(0, modelState.Errors.Count);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
Assert.Equal(1, modelStateDictionary.Count);
// Check Age error.
ModelState modelState;
Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState));
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
@ -906,16 +855,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Make Age valid and City invalid.
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "Age");
dto.Results[propertyMetadata] = new ComplexModelDtoResult(
dto.Results[propertyMetadata] = new ModelBindingResult(
23,
isModelBound: true,
validationNode: new ModelValidationNode(propertyMetadata, "theModel.Age"));
isModelSet: true,
key: "theModel.Age");
propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "City");
dto.Results[propertyMetadata] = new ComplexModelDtoResult(
dto.Results[propertyMetadata] = new ModelBindingResult(
null,
isModelBound: true,
validationNode: new ModelValidationNode(propertyMetadata, "theModel.City"));
isModelSet: true,
key: "theModel.City");
// Act
testableBinder.ProcessDto(bindingContext, dto);
@ -980,10 +929,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Make ValueTypeRequired invalid.
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "ValueTypeRequired");
dto.Results[propertyMetadata] = new ComplexModelDtoResult(
dto.Results[propertyMetadata] = new ModelBindingResult(
null,
isModelBound: true,
validationNode: new ModelValidationNode(propertyMetadata, "theModel.ValueTypeRequired"));
isModelSet: true,
key: "theModel.ValueTypeRequired");
// Act
testableBinder.ProcessDto(bindingContext, dto);
@ -1018,16 +967,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName");
dto.Results[firstNameProperty] = new ComplexModelDtoResult(
dto.Results[firstNameProperty] = new ModelBindingResult(
"John",
isModelBound: true,
validationNode: new ModelValidationNode(firstNameProperty, ""));
isModelSet: true,
key: "");
var lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName");
dto.Results[lastNameProperty] = new ComplexModelDtoResult(
dto.Results[lastNameProperty] = new ModelBindingResult(
"Doe",
isModelBound: true,
validationNode: new ModelValidationNode(lastNameProperty, ""));
isModelSet: true,
key: "");
var dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth");
dto.Results[dobProperty] = null;
@ -1051,12 +1000,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
var propertyMetadata = bindingContext.ModelMetadata.Properties.First(o => o.PropertyName == "PropertyWithDefaultValue");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: null,
isModelBound: false,
validationNode: validationNode);
isModelSet: false,
key: "foo");
var requiredValidator = bindingContext.OperationBindingContext
.ValidatorProvider
@ -1069,7 +1017,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
// Assert
var person = Assert.IsType<Person>(bindingContext.Model);
var person = Assert.IsType<Person>(bindingContext.ModelMetadata.Model);
Assert.Equal(123.456m, person.PropertyWithDefaultValue);
Assert.True(bindingContext.ModelState.IsValid);
}
@ -1082,13 +1030,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(
o => o.PropertyName == "PropertyWithInitializedValue");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
// This value won't be used because IsModelBound = false.
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: "bad-value",
isModelBound: false,
validationNode: validationNode);
isModelSet: false,
key: "foo");
var testableBinder = new TestableMutableObjectModelBinder();
@ -1096,7 +1043,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator: null);
// Assert
var person = Assert.IsType<Person>(bindingContext.Model);
var person = Assert.IsType<Person>(bindingContext.ModelMetadata.Model);
Assert.Equal("preinitialized", person.PropertyWithInitializedValue);
Assert.True(bindingContext.ModelState.IsValid);
}
@ -1109,13 +1056,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(
o => o.PropertyName == "PropertyWithInitializedValueAndDefault");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
// This value won't be used because IsModelBound = false.
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: "bad-value",
isModelBound: false,
validationNode: validationNode);
isModelSet: false,
key: "foo");
var testableBinder = new TestableMutableObjectModelBinder();
@ -1123,7 +1069,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator: null);
// Assert
var person = Assert.IsType<Person>(bindingContext.Model);
var person = Assert.IsType<Person>(bindingContext.ModelMetadata.Model);
Assert.Equal("default", person.PropertyWithInitializedValueAndDefault);
Assert.True(bindingContext.ModelState.IsValid);
}
@ -1134,12 +1080,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Arrange
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NonUpdateableProperty");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: null,
isModelBound: false,
validationNode: validationNode);
isModelSet: false,
key: "foo");
var testableBinder = new TestableMutableObjectModelBinder();
@ -1158,12 +1103,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForObject(model));
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
new DateTime(2001, 1, 1),
isModelBound: true,
validationNode: validationNode);
key: "foo",
isModelSet: true);
var requiredValidator = bindingContext.OperationBindingContext
.ValidatorProvider
@ -1177,7 +1122,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
// Assert
validationNode.Validate(validationContext);
Assert.True(bindingContext.ModelState.IsValid);
Assert.Equal(new DateTime(2001, 1, 1), model.DateOfBirth);
}
@ -1194,11 +1138,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForObject(model));
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfDeath");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
new DateTime(1800, 1, 1),
isModelBound: true,
validationNode: validationNode);
isModelSet: true,
key: "foo");
var testableBinder = new TestableMutableObjectModelBinder();
@ -1218,11 +1161,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var bindingContext = CreateContext(GetMetadataForObject(new Person()));
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
var validationNode = new ModelValidationNode(propertyMetadata, "foo");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: null,
isModelBound: true,
validationNode: validationNode);
isModelSet: true,
key: "foo");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
@ -1233,8 +1175,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
// Assert
Assert.True(bindingContext.ModelState.IsValid);
validationNode.Validate(validationContext, bindingContext.ValidationNode);
Assert.False(bindingContext.ModelState.IsValid);
}
@ -1246,11 +1186,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName = " foo";
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "ValueTypeRequired");
var validationNode = new ModelValidationNode(propertyMetadata, "foo.ValueTypeRequired");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: null,
isModelBound: true,
validationNode: validationNode);
isModelSet: true,
key: "foo.ValueTypeRequired");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
@ -1273,11 +1212,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName = "foo";
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NameNoAttribute");
var validationNode = new ModelValidationNode(propertyMetadata, "foo.NameNoAttribute");
var dtoResult = new ComplexModelDtoResult(
var dtoResult = new ModelBindingResult(
model: null,
isModelBound: true,
validationNode: validationNode);
isModelSet: true,
key: "foo.NameNoAttribute");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
@ -1302,10 +1240,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName = "foo";
var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "Name");
var validationNode = new ModelValidationNode(propertyMetadata, "foo.Name");
var dtoResult = new ComplexModelDtoResult(model: null,
isModelBound: true,
validationNode: validationNode);
var dtoResult = new ModelBindingResult(model: null,
isModelSet: true,
key: "foo.Name");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
@ -1613,7 +1550,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public new void SetProperty(ModelBindingContext bindingContext,
ModelMetadata propertyMetadata,
ComplexModelDtoResult dtoResult,
ModelBindingResult dtoResult,
IModelValidator requiredValidator)
{
base.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(retVal);
Assert.Null(retVal);
}
[Theory]
@ -64,7 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.NotNull(retVal);
}
[Fact]
@ -84,8 +84,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.Null(bindingContext.Model);
Assert.NotNull(retVal);
Assert.Null(retVal.Model);
Assert.False(bindingContext.ModelState.IsValid);
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
Assert.Equal(message, error.ErrorMessage);
@ -102,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(retVal, "BindModel should have returned null.");
Assert.Null(retVal);
Assert.Empty(bindingContext.ModelState);
}
@ -122,8 +122,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.Null(bindingContext.Model);
Assert.NotNull(retVal);
Assert.Null(retVal.Model);
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
}
@ -143,8 +143,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(retVal);
Assert.Equal(42, bindingContext.Model);
Assert.NotNull(retVal);
Assert.Equal(42, retVal.Model);;
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
}

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
@ -15,11 +16,11 @@ using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class DefaultBodyModelValidatorTests
public class DefaultObjectValidatorTests
{
private static Person LonelyPerson;
static DefaultBodyModelValidatorTests()
static DefaultObjectValidatorTests()
{
LonelyPerson = new Person() { Name = "Reallllllllly Long Name" };
LonelyPerson.Friend = LonelyPerson;
@ -212,14 +213,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public void ExpectedValidationErrorsRaised(object model, Type type, Dictionary<string, string> expectedErrors)
{
// Arrange
var validationContext = GetModelValidationContext(model, type);
var testValidationContext = GetModelValidationContext(model, type);
// Act (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
// Assert
var actualErrors = new Dictionary<string, string>();
foreach (var keyStatePair in validationContext.ModelState)
foreach (var keyStatePair in testValidationContext.ModelValidationContext.ModelState)
{
foreach (var error in keyStatePair.Value.Errors)
{
@ -237,17 +241,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Fact]
[ReplaceCulture]
public void BodyValidator_Throws_IfPropertyAccessorThrows()
public void Validator_Throws_IfPropertyAccessorThrows()
{
// Arrange
var validationContext = GetModelValidationContext(new Uri("/api/values", UriKind.Relative), typeof(Uri));
var testValidationContext = GetModelValidationContext(new Uri("/api/values", UriKind.Relative), typeof(Uri));
// Act & Assert
Assert.Throws(
typeof(InvalidOperationException),
() =>
{
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
});
}
@ -270,30 +277,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Theory]
[MemberData(nameof(ObjectsWithPropertiesWhichThrowOnGet))]
[ReplaceCulture]
public void BodyValidator_DoesNotThrow_IfExcludedPropertyAccessorsThrow(
public void Validator_DoesNotThrow_IfExcludedPropertyAccessorsThrow(
object input, Type type, List<Type> excludedTypes)
{
// Arrange
var validationContext = GetModelValidationContext(input, type, excludedTypes);
var testValidationContext = GetModelValidationContext(input, type, string.Empty, excludedTypes);
// Act & Assert (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
Assert.True(validationContext.ModelState.IsValid);
new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
Assert.True(testValidationContext.ModelValidationContext.ModelState.IsValid);
}
[Fact]
[ReplaceCulture]
public void BodyValidator_Throws_IfPropertyGetterThrows()
public void Validator_Throws_IfPropertyGetterThrows()
{
// Arrange
var validationContext = GetModelValidationContext(
new Uri("/api/values", UriKind.Relative), typeof(Uri), new List<Type>());
var testValidationContext = GetModelValidationContext(
new Uri("/api/values", UriKind.Relative), typeof(Uri));
var validationContext = testValidationContext.ModelValidationContext;
// Act & Assert
Assert.Throws<InvalidOperationException>(
() =>
{
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(validationContext);
});
Assert.True(validationContext.ModelState.IsValid);
}
@ -304,10 +318,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
// Arrange
var model = new Address() { Street = "Microsoft Way" };
var validationContext = GetModelValidationContext(model, model.GetType());
var testValidationContext = GetModelValidationContext(model, model.GetType());
var validationContext = testValidationContext.ModelValidationContext;
// Act (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(validationContext);
// Assert
Assert.Contains("Street", validationContext.ModelState.Keys);
@ -326,14 +344,146 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new TypeThatOverridesEquals { Funny = "hehe" },
new TypeThatOverridesEquals { Funny = "hehe" }
};
var validationContext = GetModelValidationContext(instance, typeof(TypeThatOverridesEquals[]));
var testValidationContext = GetModelValidationContext(instance, typeof(TypeThatOverridesEquals[]));
// Act & Assert (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
}
private ModelValidationContext GetModelValidationContext(
object model, Type type, List<Type> excludedTypes = null)
[Fact]
public void Validation_ShortCircuit_WhenMaxErrorCountIsSet()
{
// Arrange
var user = new User()
{
Password = "password-val",
ConfirmPassword = "not-password-val"
};
var testValidationContext = GetModelValidationContext(
user,
typeof(User),
"user",
new List<Type> { typeof(string) });
var validationContext = testValidationContext.ModelValidationContext;
validationContext.ModelState.MaxAllowedErrors = 2;
validationContext.ModelState.AddModelError("key1", "error1");
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider);
// Act
validator.Validate(validationContext);
// Assert
Assert.Equal(new[] { "key1", "user.Password", "", "user.ConfirmPassword" },
validationContext.ModelState.Keys.ToArray());
var modelState = validationContext.ModelState["user.ConfirmPassword"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
var error = Assert.Single(validationContext.ModelState[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
}
[Fact]
public void ForExcludedNonModelBoundType_Properties_NotMarkedAsSkiped()
{
// Arrange
var user = new User()
{
Password = "password-val",
ConfirmPassword = "not-password-val"
};
var testValidationContext = GetModelValidationContext(
user,
typeof(User),
"user",
new List<Type> { typeof(User) });
var validationContext = testValidationContext.ModelValidationContext;
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider);
// Act
validator.Validate(validationContext);
// Assert
Assert.False(validationContext.ModelState.ContainsKey("user.Password"));
Assert.False(validationContext.ModelState.ContainsKey("user.ConfirmPassword"));
var modelState = validationContext.ModelState["user"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Valid);
}
[Fact]
public void ForExcludedModelBoundTypes_Properties_MarkedAsSkipped()
{
// Arrange
var user = new User()
{
Password = "password-val",
ConfirmPassword = "not-password-val"
};
var testValidationContext = GetModelValidationContext(
user,
typeof(User),
"user",
new List<Type> { typeof(User) });
var validationContext = testValidationContext.ModelValidationContext;
// Set the value on model state as a model binder would.
validationContext.ModelState.SetModelValue(
"user.Password",
Mock.Of<ValueProviderResult>());
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider);
// Act
validator.Validate(validationContext);
// Assert
var modelState = validationContext.ModelState["user.Password"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
modelState = validationContext.ModelState["user.ConfirmPassword"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
}
[Fact]
public void NonRequestBoundModel_MarkedAsSkipped()
{
// Arrange
var testValidationContext = GetModelValidationContext(
new TestServiceProvider(),
typeof(TestServiceProvider),
"serviceProvider");
var validationContext = testValidationContext.ModelValidationContext;
var validator = new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider);
// Act
validator.Validate(validationContext);
// Assert
var modelState = validationContext.ModelState["serviceProvider.TestService"];
Assert.Empty(modelState.Errors);
Assert.Equal(modelState.ValidationState, ModelValidationState.Skipped);
}
private TestModelValidationContext GetModelValidationContext(
object model, Type type, string key = "", List<Type> excludedTypes = null)
{
var modelStateDictionary = new ModelStateDictionary();
var providers = new IModelValidatorProvider[]
@ -341,9 +491,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new DataAnnotationsModelValidatorProvider(),
new DataMemberModelValidatorProvider()
};
var modelMetadataProvider = new EmptyModelMetadataProvider();
var modelMetadataProvider = new DataAnnotationsModelMetadataProvider();
var excludedValidationTypesPredicate =
new List<IExcludeTypeValidationFilter>();
if (excludedTypes != null)
{
var mockExcludeTypeFilter = new Mock<IExcludeTypeValidationFilter>();
@ -354,18 +505,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
excludedValidationTypesPredicate.Add(mockExcludeTypeFilter.Object);
}
return new ModelValidationContext(
modelMetadataProvider,
new CompositeModelValidatorProvider(providers),
modelStateDictionary,
new ModelMetadata(
provider: modelMetadataProvider,
containerType: typeof(object),
modelAccessor: () => model,
modelType: type,
propertyName: null),
containerMetadata: null,
excludeFromValidationFilters: excludedValidationTypesPredicate);
var mockValidationExcludeFiltersProvider = new Mock<IValidationExcludeFiltersProvider>();
mockValidationExcludeFiltersProvider.SetupGet(o => o.ExcludeFilters)
.Returns(excludedValidationTypesPredicate);
return new TestModelValidationContext
{
ModelValidationContext = new ModelValidationContext(
key,
new CompositeModelValidatorProvider(providers),
modelStateDictionary,
new ModelMetadata(
provider: modelMetadataProvider,
containerType: typeof(object),
modelAccessor: () => model,
modelType: type,
propertyName: null),
containerMetadata: null),
ModelMetadataProvider = modelMetadataProvider,
ExcludeFiltersProvider = mockValidationExcludeFiltersProvider.Object
};
}
public class Person
@ -468,6 +626,42 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public Team Test { get; set; }
}
private class User : IValidatableObject
{
public string Password { get; set; }
[Compare("Password")]
public string ConfirmPassword { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Password == "password")
{
yield return new ValidationResult("Password does not meet complexity requirements.");
}
}
}
public class TestServiceProvider
{
[FromServices]
[Required]
public ITestService TestService { get; set; }
}
public interface ITestService
{
}
private class TestModelValidationContext
{
public ModelValidationContext ModelValidationContext { get; set; }
public IModelMetadataProvider ModelMetadataProvider { get; set; }
public IValidationExcludeFiltersProvider ExcludeFiltersProvider { get; set; }
}
}
}
#endif

View File

@ -9,6 +9,140 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ModelStateDictionaryTest
{
[Theory]
[InlineData(ModelValidationState.Valid)]
[InlineData(ModelValidationState.Unvalidated)]
public void MarkFieldSkipped_MarksFieldAsSkipped_IfStateIsNotInValid(ModelValidationState validationState)
{
// Arrange
var modelState = new ModelState
{
Value = GetValueProviderResult("value"),
ValidationState = validationState
};
var source = new ModelStateDictionary
{
{ "key", modelState }
};
// Act
source.MarkFieldSkipped("key");
// Assert
Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState);
}
[Fact]
public void MarkFieldSkipped_MarksFieldAsSkipped_IfKeyIsNotPresent()
{
// Arrange
var modelState = new ModelState
{
Value = GetValueProviderResult("value"),
ValidationState = ModelValidationState.Valid
};
var source = new ModelStateDictionary
{
};
// Act
source.MarkFieldSkipped("key");
// Assert
Assert.Equal(0, source.ErrorCount);
Assert.Equal(1, source.Count);
Assert.Equal(ModelValidationState.Skipped, source["key"].ValidationState);
}
[Fact]
public void MarkFieldSkipped_Throws_IfStateIsInvalid()
{
// Arrange
var modelState = new ModelState
{
Value = GetValueProviderResult("value"),
ValidationState = ModelValidationState.Invalid
};
var source = new ModelStateDictionary
{
{ "key", modelState }
};
// Act
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped("key"));
// Assert
Assert.Equal(
"A field previously marked invalid should not be marked skipped.",
exception.Message);
}
[Theory]
[InlineData(ModelValidationState.Skipped)]
[InlineData(ModelValidationState.Unvalidated)]
public void MarkFieldValid_MarksFieldAsValid_IfStateIsNotInvalid(ModelValidationState validationState)
{
// Arrange
var modelState = new ModelState
{
Value = GetValueProviderResult("value"),
ValidationState = validationState
};
var source = new ModelStateDictionary
{
{ "key", modelState }
};
// Act
source.MarkFieldValid("key");
// Assert
Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
}
[Fact]
public void MarkFieldValid_MarksFieldAsValid_IfKeyIsNotPresent()
{
// Arrange
var source = new ModelStateDictionary();
// Act
source.MarkFieldValid("key");
// Assert
Assert.Equal(0, source.ErrorCount);
Assert.Equal(1, source.Count);
Assert.Equal(ModelValidationState.Valid, source["key"].ValidationState);
}
[Fact]
public void MarkFieldValid_Throws_IfStateIsInvalid()
{
// Arrange
var modelState = new ModelState
{
Value = GetValueProviderResult("value"),
ValidationState = ModelValidationState.Invalid
};
var source = new ModelStateDictionary
{
{ "key", modelState }
};
// Act
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid("key"));
// Assert
Assert.Equal(
"A field previously marked invalid should not be marked valid.",
exception.Message);
}
[Fact]
public void CopyConstructor_CopiesModelStateData()
{
@ -100,6 +234,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(ModelValidationState.Unvalidated, validationState);
}
[Fact]
public void GetValidationState_ReturnsValidationStateForKey_IgnoresChildren()
{
// Arrange
var msd = new ModelStateDictionary();
msd.AddModelError("foo.bar", "error text");
// Act
var validationState = msd.GetValidationState("foo");
// Assert
Assert.Equal(ModelValidationState.Unvalidated, validationState);
}
[Fact]
public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors()
{
@ -193,7 +341,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
},
{ "baz", new ModelState
{
ValidationState = ModelValidationState.Valid,
ValidationState = ModelValidationState.Skipped,
Value = GetValueProviderResult("quux", "bar")
}
}

View File

@ -1,568 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Testing;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ModelValidationNodeTest
{
[Fact]
public void ConstructorSetsCollectionInstance()
{
// Arrange
var metadata = GetModelMetadata();
var modelStateKey = "someKey";
var childNodes = new[]
{
new ModelValidationNode(metadata, "someKey0"),
new ModelValidationNode(metadata, "someKey1")
};
// Act
var node = new ModelValidationNode(metadata, modelStateKey, childNodes);
// Assert
Assert.Equal(childNodes, node.ChildNodes.ToArray());
}
[Fact]
public void PropertiesAreSet()
{
// Arrange
var metadata = GetModelMetadata();
var modelStateKey = "someKey";
// Act
var node = new ModelValidationNode(metadata, modelStateKey);
// Assert
Assert.Equal(metadata, node.ModelMetadata);
Assert.Equal(modelStateKey, node.ModelStateKey);
Assert.NotNull(node.ChildNodes);
Assert.Empty(node.ChildNodes);
}
[Fact]
public void CombineWith()
{
// Arrange
var expected = new[]
{
"Validating parent1.",
"Validating parent2.",
"Validated parent1.",
"Validated parent2."
};
var log = new List<string>();
var allChildNodes = new[]
{
new ModelValidationNode(GetModelMetadata(), "key1"),
new ModelValidationNode(GetModelMetadata(), "key2"),
new ModelValidationNode(GetModelMetadata(), "key3"),
};
var parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1");
parentNode1.ChildNodes.Add(allChildNodes[0]);
parentNode1.Validating += (sender, e) => log.Add("Validating parent1.");
parentNode1.Validated += (sender, e) => log.Add("Validated parent1.");
var parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2");
parentNode2.ChildNodes.Add(allChildNodes[1]);
parentNode2.ChildNodes.Add(allChildNodes[2]);
parentNode2.Validating += (sender, e) => log.Add("Validating parent2.");
parentNode2.Validated += (sender, e) => log.Add("Validated parent2.");
var context = CreateContext();
// Act
parentNode1.CombineWith(parentNode2);
parentNode1.Validate(context);
// Assert
Assert.Equal(expected, log);
Assert.Equal(allChildNodes, parentNode1.ChildNodes.ToArray());
}
[Fact]
public void CombineWith_OtherNodeIsSuppressed_DoesNothing()
{
// Arrange
var log = new List<string>();
var allChildNodes = new[]
{
new ModelValidationNode(GetModelMetadata(), "key1"),
new ModelValidationNode(GetModelMetadata(), "key2"),
new ModelValidationNode(GetModelMetadata(), "key3"),
};
var expectedChildNodes = new[]
{
allChildNodes[0]
};
var parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1");
parentNode1.ChildNodes.Add(allChildNodes[0]);
parentNode1.Validating += (sender, e) => log.Add("Validating parent1.");
parentNode1.Validated += (sender, e) => log.Add("Validated parent1.");
var parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2");
parentNode2.ChildNodes.Add(allChildNodes[1]);
parentNode2.ChildNodes.Add(allChildNodes[2]);
parentNode2.Validating += (sender, e) => log.Add("Validating parent2.");
parentNode2.Validated += (sender, e) => log.Add("Validated parent2.");
parentNode2.SuppressValidation = true;
var context = CreateContext();
// Act
parentNode1.CombineWith(parentNode2);
parentNode1.Validate(context);
// Assert
Assert.Equal(new[] { "Validating parent1.", "Validated parent1." }, log.ToArray());
Assert.Equal(expectedChildNodes, parentNode1.ChildNodes.ToArray());
}
[Fact]
public void Validate_Ordering()
{
// Proper order of invocation:
// 1. OnValidating()
// 2. Child validators
// 3. This validator
// 4. OnValidated()
// Arrange
var expected = new[]
{
"In OnValidating()",
"In LoggingValidatonAttribute.IsValid()",
"In IValidatableObject.Validate()",
"In OnValidated()"
};
var log = new List<string>();
var model = new LoggingValidatableObject(log);
var modelMetadata = GetModelMetadata(model);
var provider = new EmptyModelMetadataProvider();
var childMetadata = provider.GetMetadataForProperty(() => model, model.GetType(), "ValidStringProperty");
var node = new ModelValidationNode(modelMetadata, "theKey");
node.Validating += (sender, e) => log.Add("In OnValidating()");
node.Validated += (sender, e) => log.Add("In OnValidated()");
node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.ValidStringProperty"));
var context = CreateContext(modelMetadata);
// Act
node.Validate(context);
// Assert
Assert.Equal(expected, log);
}
// Validation order is primarily important when MaxAllowedErrors has been overridden.
[Fact]
public void Validate_OrdersUsingModelMetadata()
{
// Proper order of invocation:
// 1. OnValidating()
// 2. Child validators -- ordered using ModelMetadata.Order.
// 3. OnValidated()
// Arrange
var expected = new[]
{
"In OnValidating()",
"In LoggingValidatonAttribute.IsValid(OrderedProperty3)",
"In LoggingValidatonAttribute.IsValid(OrderedProperty2)",
"In LoggingValidatonAttribute.IsValid(OrderedProperty1)",
"In LoggingValidatonAttribute.IsValid(Property3)",
"In LoggingValidatonAttribute.IsValid(Property1)",
"In LoggingValidatonAttribute.IsValid(Property2)",
"In LoggingValidatonAttribute.IsValid(LastProperty)",
"In OnValidated()"
};
var log = new List<string>();
var model = new LoggingNonValidatableObject(log);
var provider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = provider.GetMetadataForType(() => model, model.GetType());
var node = new ModelValidationNode(modelMetadata, "theKey")
{
ValidateAllProperties = true,
};
node.Validating += (sender, e) => log.Add("In OnValidating()");
node.Validated += (sender, e) => log.Add("In OnValidated()");
var context = CreateContext(modelMetadata, provider);
// Act
node.Validate(context);
// Assert
Assert.Equal(expected, log);
}
[Fact]
public void Validate_ChildNodes_OverridesOrdering()
{
// Proper order of invocation:
// 1. OnValidating()
// 2. Child validators -- ordered using ChildNodes, then ModelMetadata.Order.
// 3. OnValidated()
// Arrange
var expected = new[]
{
"In OnValidating()",
"In LoggingValidatonAttribute.IsValid(LastProperty)",
"In LoggingValidatonAttribute.IsValid(OrderedProperty3)",
"In LoggingValidatonAttribute.IsValid(OrderedProperty2)",
"In LoggingValidatonAttribute.IsValid(OrderedProperty1)",
"In LoggingValidatonAttribute.IsValid(Property3)",
"In LoggingValidatonAttribute.IsValid(Property1)",
"In LoggingValidatonAttribute.IsValid(Property2)",
"In OnValidated()"
};
var log = new List<string>();
var model = new LoggingNonValidatableObject(log);
var provider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = provider.GetMetadataForType(() => model, model.GetType());
var childMetadata = modelMetadata.Properties.FirstOrDefault(
property => property.PropertyName == "LastProperty");
Assert.NotNull(childMetadata); // Guard
var node = new ModelValidationNode(modelMetadata, "theKey")
{
ChildNodes =
{
new ModelValidationNode(childMetadata, "theKey.LastProperty")
},
ValidateAllProperties = true,
};
node.Validating += (sender, e) => log.Add("In OnValidating()");
node.Validated += (sender, e) => log.Add("In OnValidated()");
var context = CreateContext(modelMetadata, provider);
// Act
node.Validate(context);
// Assert
Assert.Equal(expected, log);
}
[Fact]
public void Validate_SkipsRemainingValidationIfModelStateIsInvalid()
{
// Because a property validator fails, the model validator shouldn't run
// Arrange
var expected = new[]
{
"In OnValidating()",
"In IValidatableObject.Validate()",
"In OnValidated()"
};
var log = new List<string>();
var model = new LoggingValidatableObject(log);
var modelMetadata = GetModelMetadata(model);
var provider = new EmptyModelMetadataProvider();
var childMetadata = provider.GetMetadataForProperty(() => model,
model.GetType(),
"InvalidStringProperty");
var node = new ModelValidationNode(modelMetadata, "theKey");
node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.InvalidStringProperty"));
node.Validating += (sender, e) => log.Add("In OnValidating()");
node.Validated += (sender, e) => log.Add("In OnValidated()");
var context = CreateContext(modelMetadata);
// Act
node.Validate(context);
// Assert
Assert.Equal(expected, log);
Assert.Equal("Sample error message",
context.ModelState["theKey.InvalidStringProperty"].Errors[0].ErrorMessage);
}
[Fact]
public void Validate_SkipsValidationIfHandlerCancels()
{
// Arrange
var log = new List<string>();
var model = new LoggingValidatableObject(log);
var modelMetadata = GetModelMetadata(model);
var node = new ModelValidationNode(modelMetadata, "theKey");
node.Validating += (sender, e) =>
{
log.Add("In OnValidating()");
e.Cancel = true;
};
node.Validated += (sender, e) => log.Add("In OnValidated()");
var context = CreateContext(modelMetadata);
// Act
node.Validate(context);
// Assert
Assert.Equal(new[] { "In OnValidating()" }, log.ToArray());
}
[Fact]
public void Validate_SkipsValidationIfSuppressed()
{
// Arrange
var log = new List<string>();
var model = new LoggingValidatableObject(log);
var modelMetadata = GetModelMetadata(model);
var node = new ModelValidationNode(modelMetadata, "theKey")
{
SuppressValidation = true
};
node.Validating += (sender, e) => log.Add("In OnValidating()");
node.Validated += (sender, e) => log.Add("In OnValidated()");
var context = CreateContext();
// Act
node.Validate(context);
// Assert
Assert.Empty(log);
}
[Fact]
public void Validate_ValidatesIfModelIsNull()
{
// Arrange
var modelMetadata = GetModelMetadata(typeof(ValidateAllPropertiesModel));
var node = new ModelValidationNode(modelMetadata, "theKey");
var context = CreateContext(modelMetadata);
// Act
node.Validate(context);
// Assert
var modelState = Assert.Single(context.ModelState);
Assert.Equal("theKey", modelState.Key);
Assert.Equal(ModelValidationState.Invalid, modelState.Value.ValidationState);
var error = Assert.Single(modelState.Value.Errors);
Assert.Equal("A value is required but was not present in the request.", error.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void Validate_ValidateAllProperties_AddsValidationErrors()
{
// Arrange
var model = new ValidateAllPropertiesModel
{
RequiredString = null /* error */,
RangedInt = 0 /* error */,
ValidString = "dog"
};
var modelMetadata = GetModelMetadata(model);
var node = new ModelValidationNode(modelMetadata, "theKey")
{
ValidateAllProperties = true
};
var context = CreateContext(modelMetadata);
context.ModelState.AddModelError("theKey.RequiredString.Dummy", "existing Error Text");
// Act
node.Validate(context);
// Assert
var modelState = context.ModelState["theKey.RequiredString.Dummy"];
Assert.NotNull(modelState);
var error = Assert.Single(modelState.Errors);
Assert.Equal("existing Error Text", error.ErrorMessage);
modelState = context.ModelState["theKey.RangedInt"];
Assert.NotNull(modelState);
error = Assert.Single(modelState.Errors);
Assert.Equal("The field RangedInt must be between 10 and 30.", error.ErrorMessage);
Assert.DoesNotContain("theKey.RequiredString", context.ModelState.Keys);
Assert.DoesNotContain("theKey.ValidString", context.ModelState.Keys);
Assert.DoesNotContain("theKey", context.ModelState.Keys);
}
[Fact]
[ReplaceCulture]
public void Validate_ShortCircuits_IfModelStateHasReachedMaxNumberOfErrors()
{
// Arrange
var model = new ValidateAllPropertiesModel
{
RequiredString = null /* error */,
RangedInt = 0 /* error */,
ValidString = "cat" /* error */
};
var expectedMessage = ValidationAttributeUtil.GetRequiredErrorMessage("RequiredString");
var modelMetadata = GetModelMetadata(model);
var node = new ModelValidationNode(modelMetadata, "theKey")
{
ValidateAllProperties = true
};
var context = CreateContext(modelMetadata);
context.ModelState.MaxAllowedErrors = 3;
context.ModelState.AddModelError("somekey", "error text");
// Act
node.Validate(context);
// Assert
Assert.Equal(3, context.ModelState.Count);
var modelState = context.ModelState[string.Empty];
Assert.NotNull(modelState);
var error = Assert.Single(modelState.Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
// RequiredString is validated first due to ModelMetadata.Properties ordering (Reflection-based).
modelState = context.ModelState["theKey.RequiredString"];
Assert.NotNull(modelState);
error = Assert.Single(modelState.Errors);
Assert.Equal(expectedMessage, error.ErrorMessage);
// No room for the other validation errors.
Assert.DoesNotContain("theKey.RangedInt", context.ModelState.Keys);
Assert.DoesNotContain("theKey.ValidString", context.ModelState.Keys);
}
private static ModelMetadata GetModelMetadata()
{
return new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object));
}
private static ModelMetadata GetModelMetadata(object model)
{
return new DataAnnotationsModelMetadataProvider().GetMetadataForType(() => model, model.GetType());
}
private static ModelMetadata GetModelMetadata(Type type)
{
return new DataAnnotationsModelMetadataProvider().GetMetadataForType(modelAccessor: null, modelType: type);
}
private static ModelValidationContext CreateContext(
ModelMetadata metadata = null,
IModelMetadataProvider metadataProvider = null)
{
var providers = new IModelValidatorProvider[]
{
new DataAnnotationsModelValidatorProvider(),
new DataMemberModelValidatorProvider()
};
if (metadataProvider == null)
{
metadataProvider = new EmptyModelMetadataProvider();
}
return new ModelValidationContext(metadataProvider,
new CompositeModelValidatorProvider(providers),
new ModelStateDictionary(),
metadata,
null);
}
private sealed class LoggingValidatableObject : IValidatableObject
{
private readonly IList<string> _log;
public LoggingValidatableObject(IList<string> log)
{
_log = log;
}
[LoggingValidation]
public string ValidStringProperty { get; set; }
public string InvalidStringProperty { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
Assert.Null(validationContext.MemberName);
_log.Add("In IValidatableObject.Validate()");
yield return new ValidationResult("Sample error message", new[] { "InvalidStringProperty" });
}
private sealed class LoggingValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var validatableObject = Assert.IsType<LoggingValidatableObject>(value);
Assert.NotNull(validationContext);
Assert.Equal("ValidStringProperty", validationContext.MemberName);
validatableObject._log.Add("In LoggingValidatonAttribute.IsValid()");
return ValidationResult.Success;
}
}
}
private sealed class LoggingNonValidatableObject
{
private readonly IList<string> _log;
public LoggingNonValidatableObject(IList<string> log)
{
_log = log;
}
[LoggingValidation]
[Display(Order = 10001)]
public string LastProperty { get; set; }
[LoggingValidation]
public string Property3 { get; set; }
[LoggingValidation]
public string Property1 { get; set; }
[LoggingValidation]
public string Property2 { get; set; }
[LoggingValidation]
[Display(Order = 23)]
public string OrderedProperty3 { get; set; }
[LoggingValidation]
[Display(Order = 23)]
public string OrderedProperty2 { get; set; }
[LoggingValidation]
[Display(Order = 23)]
public string OrderedProperty1 { get; set; }
private sealed class LoggingValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Assert.Null(value);
Assert.NotNull(validationContext);
var nonValidatableObject =
Assert.IsType<LoggingNonValidatableObject>(validationContext.ObjectInstance);
nonValidatableObject._log.Add(
string.Format("In LoggingValidatonAttribute.IsValid({0})", validationContext.MemberName));
return ValidationResult.Success;
}
}
}
private class ValidateAllPropertiesModel
{
[Required]
public string RequiredString { get; set; }
[Range(10, 30)]
public int RangedInt { get; set; }
[RegularExpression("dog")]
public string ValidString { get; set; }
}
}
}

View File

@ -147,28 +147,46 @@ namespace Microsoft.AspNet.Mvc
setup.Configure(mvcOptions);
// Assert
Assert.Equal(5, mvcOptions.ValidationExcludeFilters.Count);
Assert.Equal(8, mvcOptions.ValidationExcludeFilters.Count);
var i = 0;
// Verify if the delegates registered by default exclude the given types.
Assert.Equal(typeof(SimpleTypesExcludeFilter), mvcOptions.ValidationExcludeFilters[0].OptionType);
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[1].OptionType);
Assert.Equal(typeof(SimpleTypesExcludeFilter), mvcOptions.ValidationExcludeFilters[i++].OptionType);
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var xObjectFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[1].Instance);
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(xObjectFilter.ExcludedType, typeof(XObject));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[2].OptionType);
var typeFilter =
Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[2].Instance);
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var typeFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(typeFilter.ExcludedType, typeof(Type));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[3].OptionType);
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var jTokenFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[3].Instance);
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(jTokenFilter.ExcludedType, typeof(JToken));
Assert.Equal(typeof(DefaultTypeNameBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[4].OptionType);
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var cancellationTokenFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(cancellationTokenFilter.ExcludedType, typeof(System.Threading.CancellationToken));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var formFileFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(formFileFilter.ExcludedType, typeof(Http.IFormFile));
Assert.Equal(
typeof(DefaultTypeBasedExcludeFilter),
mvcOptions.ValidationExcludeFilters[i].OptionType);
var formCollectionFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(formCollectionFilter.ExcludedType, typeof(Http.IFormCollection));
Assert.Equal(typeof(DefaultTypeNameBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var xmlNodeFilter =
Assert.IsType<DefaultTypeNameBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[4].Instance);
Assert.IsType<DefaultTypeNameBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode");
}
}

View File

@ -81,7 +81,7 @@ namespace ModelBindingWebSite.Controllers
private class OrderStatusBinder : IModelBinder
{
public Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (typeof(OrderStatus).IsAssignableFrom(bindingContext.ModelType))
{
@ -90,21 +90,17 @@ namespace ModelBindingWebSite.Controllers
// Doing something slightly different here to make sure we don't get accidentally bound
// by the type converter binder.
OrderStatus model;
if (Enum.TryParse<OrderStatus>("Status" + request.Query.Get("status"), out model))
{
bindingContext.Model = model;
}
return Task.FromResult(true);
var isModelSet = Enum.TryParse<OrderStatus>("Status" + request.Query.Get("status"), out model);
return Task.FromResult(new ModelBindingResult(model, "status", isModelSet));
}
return Task.FromResult(false);
return Task.FromResult<ModelBindingResult>(null);
}
}
private class ProductModelBinder : IModelBinder
{
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext)
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (typeof(Product).IsAssignableFrom(bindingContext.ModelType))
{
@ -120,11 +116,10 @@ namespace ModelBindingWebSite.Controllers
var value = await bindingContext.ValueProvider.GetValueAsync(key);
model.ProductId = (int)value.ConvertTo(typeof(int));
bindingContext.Model = model;
return true;
return new ModelBindingResult(model, key, true);
}
return false;
return null;
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc;
namespace ModelBindingWebSite.Controllers
{
[Route("Validation/[Action]")]
public class ValidationController : Controller
{
public bool SkipValidation(Resident resident)
{
return ModelState.IsValid;
}
public bool AvoidRecursive(SelfishPerson selfishPerson)
{
return ModelState.IsValid;
}
}
public class SelfishPerson
{
public string Name { get; set; }
public SelfishPerson MySelf { get { return this; } }
}
}

View File

@ -1,12 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite
{
public class Address
{
public int Street { get; set; }
public string State { get; set; }
[Range(10000, 99999)]
public int Zip { get; set; }
public Country Country { get; set; }

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Mvc;
namespace ModelBindingWebSite
{
public class Resident : Person
{
public IEnumerable<Address> ShippingAddresses { get; set; }
public Address HomeAddress { get; set; }
[FromBody]
public Address OfficeAddress { get; set; }
}
}

View File

@ -30,6 +30,7 @@ namespace ModelBindingWebSite
m.ModelBinders.Insert(0, typeof(TestBindingSourceModelBinder));
m.AddXmlDataContractSerializerFormatter();
m.ValidationExcludeFilters.Add(typeof(Address));
});
services.AddSingleton<ICalculator, DefaultCalculator>();

View File

@ -15,17 +15,17 @@ namespace ModelBindingWebSite
{
}
protected override Task BindModelCoreAsync(ModelBindingContext bindingContext)
protected override Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext)
{
var metadata = (FromTestAttribute)bindingContext.ModelMetadata.BinderMetadata;
bindingContext.Model = metadata.Value;
var model = metadata.Value;
if (!IsSimpleType(bindingContext.ModelType))
{
bindingContext.Model = Activator.CreateInstance(bindingContext.ModelType);
model = Activator.CreateInstance(bindingContext.ModelType);
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, true));
}
return Task.FromResult(true);
return Task.FromResult(new ModelBindingResult(null, bindingContext.ModelName, false));
}
private bool IsSimpleType(Type type)

View File

@ -1 +1 @@
index:@GetType().GetTypeInfo().Assembly.GetName()
index:@GetType().GetTypeInfo().Assembly.GetName()

View File

@ -1,2 +1,2 @@
Layout:@GetType().GetTypeInfo().Assembly.FullName
@RenderBody()
@RenderBody()

View File

@ -1,3 +1,4 @@
@using System.Reflection
@{ Layout = "/Views/Home/Layout.cshtml";}
_viewstart:@GetType().GetTypeInfo().Assembly.FullName