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|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.ActiveCfg = Release|Any CPU
{C651F432-4EBE-41A6-BAD2-3E07CCBA209C}.Release|x86.Build.0 = 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.ActiveCfg = Debug|Any CPU
{A19022EF-9BA3-4349-94E4-F48E13E1C8AE}.Debug|Any CPU.Build.0 = 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 {A19022EF-9BA3-4349-94E4-F48E13E1C8AE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU

View File

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

View File

@ -18,13 +18,16 @@ namespace Microsoft.AspNet.Mvc
{ {
private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly MvcOptions _options; private readonly MvcOptions _options;
private readonly IObjectModelValidator _validator;
public DefaultControllerActionArgumentBinder( public DefaultControllerActionArgumentBinder(
IModelMetadataProvider modelMetadataProvider, IModelMetadataProvider modelMetadataProvider,
IObjectModelValidator validator,
IOptions<MvcOptions> optionsAccessor) IOptions<MvcOptions> optionsAccessor)
{ {
_modelMetadataProvider = modelMetadataProvider; _modelMetadataProvider = modelMetadataProvider;
_options = optionsAccessor.Options; _options = optionsAccessor.Options;
_validator = validator;
} }
public async Task<IDictionary<string, object>> GetActionArgumentsAsync( public async Task<IDictionary<string, object>> GetActionArgumentsAsync(
@ -91,15 +94,21 @@ namespace Microsoft.AspNet.Mvc
var modelState = actionContext.ModelState; var modelState = actionContext.ModelState;
modelState.MaxAllowedErrors = _options.MaxModelValidationErrors; modelState.MaxAllowedErrors = _options.MaxModelValidationErrors;
foreach (var parameter in parameterMetadata) foreach (var parameter in parameterMetadata)
{ {
var parameterType = parameter.ModelType; var parameterType = parameter.ModelType;
var modelBindingContext = GetModelBindingContext(parameter, modelState, operationBindingContext); var modelBindingContext = GetModelBindingContext(parameter, modelState, operationBindingContext);
if (await bindingContext.ModelBinder.BindModelAsync(modelBindingContext) && var modelBindingResult = await bindingContext.ModelBinder.BindModelAsync(modelBindingContext);
modelBindingContext.IsModelSet) 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 ActionContext _actionContext;
private readonly IScopedInstance<ActionBindingContext> _bindingContext; private readonly IScopedInstance<ActionBindingContext> _bindingContext;
private readonly IInputFormatterSelector _formatterSelector; private readonly IInputFormatterSelector _formatterSelector;
private readonly IBodyModelValidator _bodyModelValidator;
private readonly IValidationExcludeFiltersProvider _bodyValidationExcludeFiltersProvider; private readonly IValidationExcludeFiltersProvider _bodyValidationExcludeFiltersProvider;
/// <summary> /// <summary>
@ -28,26 +27,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <param name="context">An accessor to the <see cref="ActionContext"/>.</param> /// <param name="context">An accessor to the <see cref="ActionContext"/>.</param>
/// <param name="bindingContext">An accessor to the <see cref="ActionBindingContext"/>.</param> /// <param name="bindingContext">An accessor to the <see cref="ActionBindingContext"/>.</param>
/// <param name="selector">The <see cref="IInputFormatterSelector"/>.</param> /// <param name="selector">The <see cref="IInputFormatterSelector"/>.</param>
/// <param name="bodyModelValidator">The <see cref="IBodyModelValidator"/>.</param>
/// <param name="bodyValidationExcludeFiltersProvider"> /// <param name="bodyValidationExcludeFiltersProvider">
/// The <see cref="IValidationExcludeFiltersProvider"/>. /// The <see cref="IValidationExcludeFiltersProvider"/>.
/// </param> /// </param>
public BodyModelBinder([NotNull] IScopedInstance<ActionContext> context, public BodyModelBinder([NotNull] IScopedInstance<ActionContext> context,
[NotNull] IScopedInstance<ActionBindingContext> bindingContext, [NotNull] IScopedInstance<ActionBindingContext> bindingContext,
[NotNull] IInputFormatterSelector selector, [NotNull] IInputFormatterSelector selector,
[NotNull] IBodyModelValidator bodyModelValidator,
[NotNull] IValidationExcludeFiltersProvider bodyValidationExcludeFiltersProvider) [NotNull] IValidationExcludeFiltersProvider bodyValidationExcludeFiltersProvider)
: base(BindingSource.Body) : base(BindingSource.Body)
{ {
_actionContext = context.Value; _actionContext = context.Value;
_bindingContext = bindingContext; _bindingContext = bindingContext;
_formatterSelector = selector; _formatterSelector = selector;
_bodyModelValidator = bodyModelValidator;
_bodyValidationExcludeFiltersProvider = bodyValidationExcludeFiltersProvider; _bodyValidationExcludeFiltersProvider = bodyValidationExcludeFiltersProvider;
} }
/// <inheritdoc /> /// <inheritdoc />
protected async override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext) protected async override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{ {
var formatters = _bindingContext.Value.InputFormatters; var formatters = _bindingContext.Value.InputFormatters;
@ -59,20 +55,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var unsupportedContentType = Resources.FormatUnsupportedContentType( var unsupportedContentType = Resources.FormatUnsupportedContentType(
bindingContext.OperationBindingContext.HttpContext.Request.ContentType); bindingContext.OperationBindingContext.HttpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, unsupportedContentType); bindingContext.ModelState.AddModelError(bindingContext.ModelName, unsupportedContentType);
return; return new ModelBindingResult(null, bindingContext.ModelName, isModelSet: false);
} }
bindingContext.Model = await formatter.ReadAsync(formatterContext); var model = await formatter.ReadAsync(formatterContext);
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
// 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);
} }
} }
} }

View File

@ -21,11 +21,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext) protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{ {
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices; var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
bindingContext.Model = requestServices.GetRequiredService(bindingContext.ModelType); var model = requestServices.GetRequiredService(bindingContext.ModelType);
return Task.FromResult(true); 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="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="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</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 /// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model instance.</param> /// on the model instance.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns> /// <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] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder, [NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider, [NotNull] IValueProvider valueProvider,
[NotNull] IObjectModelValidator objectModelValidator,
[NotNull] IModelValidatorProvider validatorProvider) [NotNull] IModelValidatorProvider validatorProvider)
where TModel : class where TModel : class
{ {
@ -52,6 +55,7 @@ namespace Microsoft.AspNet.Mvc
metadataProvider, metadataProvider,
modelBinder, modelBinder,
valueProvider, valueProvider,
objectModelValidator,
validatorProvider, validatorProvider,
predicate: (context, propertyName) => true); 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="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="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</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 /// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model /// on the model
/// instance.</param> /// instance.</param>
@ -85,6 +91,7 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder, [NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider, [NotNull] IValueProvider valueProvider,
[NotNull] IObjectModelValidator objectModelValidator,
[NotNull] IModelValidatorProvider validatorProvider, [NotNull] IModelValidatorProvider validatorProvider,
[NotNull] params Expression<Func<TModel, object>>[] includeExpressions) [NotNull] params Expression<Func<TModel, object>>[] includeExpressions)
where TModel : class where TModel : class
@ -100,6 +107,7 @@ namespace Microsoft.AspNet.Mvc
metadataProvider, metadataProvider,
modelBinder, modelBinder,
valueProvider, valueProvider,
objectModelValidator,
validatorProvider, validatorProvider,
predicate: predicate); 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="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="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</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 /// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model instance.</param> /// on the model instance.</param>
/// <param name="predicate">A predicate which can be used to /// <param name="predicate">A predicate which can be used to
@ -132,12 +142,13 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder, [NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider, [NotNull] IValueProvider valueProvider,
[NotNull] IObjectModelValidator objectModelValidator,
[NotNull] IModelValidatorProvider validatorProvider, [NotNull] IModelValidatorProvider validatorProvider,
[NotNull] Func<ModelBindingContext, string, bool> predicate) [NotNull] Func<ModelBindingContext, string, bool> predicate)
where TModel : class where TModel : class
{ {
var modelMetadata = metadataProvider.GetMetadataForType( var modelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null, modelAccessor: () => model,
modelType: model.GetType()); modelType: model.GetType());
var operationBindingContext = new OperationBindingContext var operationBindingContext = new OperationBindingContext
@ -152,7 +163,6 @@ namespace Microsoft.AspNet.Mvc
{ {
ModelMetadata = modelMetadata, ModelMetadata = modelMetadata,
ModelName = prefix, ModelName = prefix,
Model = model,
ModelState = modelState, ModelState = modelState,
ValueProvider = valueProvider, ValueProvider = valueProvider,
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
@ -160,8 +170,12 @@ namespace Microsoft.AspNet.Mvc
PropertyFilter = predicate 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; return modelState.IsValid;
} }

View File

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

View File

@ -21,13 +21,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new ConcurrentDictionary<Type, ObjectFactory>(); new ConcurrentDictionary<Type, ObjectFactory>();
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelMetadata.BinderType == null) 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. // if there is no specific model binder provided.
return false; return null;
} }
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices; 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 var modelBindingResult = result != null ?
// specified by metadata. new ModelBindingResult(result.Model, result.Key, result.IsModelSet) :
return true; 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> /// <returns>
/// A <see cref="Task"/> which will complete when model binding has completed. /// A <see cref="Task"/> which will complete when model binding has completed.
/// </returns> /// </returns>
protected abstract Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext); protected abstract Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext);
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> BindModelAsync(ModelBindingContext context) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext context)
{ {
var bindingSourceMetadata = context.ModelMetadata.BinderMetadata as IBindingSourceMetadata; var bindingSourceMetadata = context.ModelMetadata.BinderMetadata as IBindingSourceMetadata;
var allowedBindingSource = bindingSourceMetadata?.BindingSource; 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 // Binding Sources are opt-in. This model either didn't specify one or specified something
// incompatible so let other binders run. // 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 // Prevent other model binders from running because this model binder is the only handler for
// its binding source. // its binding source.
return true; return modelBindingResult;
} }
} }
} }

View File

@ -13,11 +13,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class ByteArrayModelBinder : IModelBinder public class ByteArrayModelBinder : IModelBinder
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelType != typeof(byte[])) if (bindingContext.ModelType != typeof(byte[]))
{ {
return false; return null;
} }
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName); 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 // case 1: there was no <input ... /> element containing this data
if (valueProviderResult == null) if (valueProviderResult == null)
{ {
return false; return null;
} }
var value = valueProviderResult.AttemptedValue; 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 // case 2: there was an <input ... /> element but it was left blank
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
return false; return null;
} }
try try
{ {
bindingContext.Model = Convert.FromBase64String(value); var model = Convert.FromBase64String(value);
return new ModelBindingResult(model, bindingContext.ModelName, true);
} }
catch (Exception ex) catch (Exception ex)
{ {
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, 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 public class CancellationTokenModelBinder : IModelBinder
{ {
/// <inheritdoc /> /// <inheritdoc />
public Task<bool> BindModelAsync(ModelBindingContext bindingContext) public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelType == typeof(CancellationToken)) if (bindingContext.ModelType == typeof(CancellationToken))
{ {
bindingContext.Model = bindingContext.OperationBindingContext.HttpContext.RequestAborted; var model = bindingContext.OperationBindingContext.HttpContext.RequestAborted;
return Task.FromResult(true); 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 class CollectionModelBinder<TElement> : IModelBinder
{ {
public virtual async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public virtual async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
ModelBindingHelper.ValidateBindingContext(bindingContext); ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName)) if (!await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName))
{ {
return false; return null;
} }
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName); var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
@ -27,13 +27,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
BindSimpleCollection(bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture) : BindSimpleCollection(bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture) :
BindComplexCollection(bindingContext); BindComplexCollection(bindingContext);
var boundCollection = await bindCollectionTask; var boundCollection = await bindCollectionTask;
var model = GetModel(boundCollection);
return CreateOrReplaceCollection(bindingContext, 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 // Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
// is [ "1", "2" ] and needs to be converted to an int[]. // is [ "1", "2" ] and needs to be converted to an int[].
internal async Task<List<TElement>> BindSimpleCollection(ModelBindingContext bindingContext, internal async Task<IEnumerable<TElement>> BindSimpleCollection(ModelBindingContext bindingContext,
object rawValue, object rawValue,
CultureInfo culture) CultureInfo culture)
{ {
@ -62,10 +62,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}; };
object boundValue = null; object boundValue = null;
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext)) var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext);
if (result != null)
{ {
boundValue = innerBindingContext.Model; boundValue = result.Model;
bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
} }
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue)); 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]. // 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 indexPropertyName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "index");
var valueProviderResultIndex = await bindingContext.ValueProvider.GetValueAsync(indexPropertyName); var valueProviderResultIndex = await bindingContext.ValueProvider.GetValueAsync(indexPropertyName);
@ -82,7 +82,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return await BindComplexCollectionFromIndexes(bindingContext, indexNames); return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
} }
internal async Task<List<TElement>> BindComplexCollectionFromIndexes(ModelBindingContext bindingContext, internal async Task<IEnumerable<TElement>> BindComplexCollectionFromIndexes(ModelBindingContext bindingContext,
IEnumerable<string> indexNames) IEnumerable<string> indexNames)
{ {
bool indexNamesIsFinite; bool indexNamesIsFinite;
@ -110,13 +110,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var modelType = bindingContext.ModelType; var modelType = bindingContext.ModelType;
if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext)) var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext);
if (result != null)
{ {
didBind = true; didBind = true;
boundValue = childBindingContext.Model; boundValue = result.Model;
// merge validation up
bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode);
} }
// infinite size collection stops on first bind failure // 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 // Extensibility point that allows the bound collection to be manipulated or transformed before
// being returned from the binder. // being returned from the binder.
protected virtual bool CreateOrReplaceCollection(ModelBindingContext bindingContext, protected virtual object GetModel(IEnumerable<TElement> newCollection)
IList<TElement> newCollection)
{ {
CreateOrReplaceCollection(bindingContext, newCollection, () => new List<TElement>()); return newCollection;
return true;
} }
internal static object[] RawValueToObjectArray(object rawValue) internal static object[] RawValueToObjectArray(object rawValue)
@ -165,23 +161,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// fallback // fallback
return new[] { rawValue }; 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; ModelMetadata = modelMetadata;
PropertyMetadata = new Collection<ModelMetadata>(propertyMetadata.ToList()); PropertyMetadata = new Collection<ModelMetadata>(propertyMetadata.ToList());
Results = new Dictionary<ModelMetadata, ComplexModelDtoResult>(); Results = new Dictionary<ModelMetadata, ModelBindingResult>();
} }
public ModelMetadata ModelMetadata { get; private set; } 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 // attempted. If binding failed, the entry's value will be null. If binding
// was never attempted, this dictionary will not contain a corresponding // was never attempted, this dictionary will not contain a corresponding
// entry. // 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 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, return null;
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 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.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
@ -30,97 +31,86 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<IModelBinder> ModelBinders { get; } 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, var newBindingContext = CreateNewBindingContext(bindingContext,
bindingContext.ModelName, bindingContext.ModelName);
reuseValidationNode: true);
var boundSuccessfully = await TryBind(newBindingContext); var modelBindingResult = await TryBind(newBindingContext);
if (!boundSuccessfully && !string.IsNullOrEmpty(bindingContext.ModelName) if (modelBindingResult == null && !string.IsNullOrEmpty(bindingContext.ModelName)
&& bindingContext.FallbackToEmptyPrefix) && bindingContext.FallbackToEmptyPrefix)
{ {
// fallback to empty prefix? // fallback to empty prefix?
newBindingContext = CreateNewBindingContext(bindingContext, newBindingContext = CreateNewBindingContext(bindingContext,
modelName: string.Empty, modelName: string.Empty);
reuseValidationNode: false); modelBindingResult = await TryBind(newBindingContext);
boundSuccessfully = await TryBind(newBindingContext);
} }
if (!boundSuccessfully) if (modelBindingResult == null)
{ {
return false; // something went wrong return null; // 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);
} }
bindingContext.OperationBindingContext.BodyBindingState = bindingContext.OperationBindingContext.BodyBindingState =
newBindingContext.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(); RuntimeHelpers.EnsureSufficientExecutionStack();
foreach (var binder in ModelBinders) 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. // Either we couldn't find a binder, or the binder couldn't bind. Distinction is not important.
return false; return null;
}
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);
} }
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext,
string modelName, string modelName)
bool reuseValidationNode)
{ {
var newBindingContext = new ModelBindingContext var newBindingContext = new ModelBindingContext
{ {
IsModelSet = oldBindingContext.IsModelSet,
ModelMetadata = oldBindingContext.ModelMetadata, ModelMetadata = oldBindingContext.ModelMetadata,
ModelName = modelName, ModelName = modelName,
ModelState = oldBindingContext.ModelState, ModelState = oldBindingContext.ModelState,
@ -129,12 +119,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
PropertyFilter = oldBindingContext.PropertyFilter, 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); newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext);
// If the property has a specified data binding sources, we need to filter the set of value providers // 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>> public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
{ {
protected override bool CreateOrReplaceCollection(ModelBindingContext bindingContext, protected override object GetModel(IEnumerable<KeyValuePair<TKey, TValue>> newCollection)
IList<KeyValuePair<TKey, TValue>> newCollection)
{ {
CreateOrReplaceDictionary(bindingContext, newCollection, () => new Dictionary<TKey, TValue>()); return newCollection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
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;
}
}
} }
} }
} }

View File

@ -16,35 +16,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class FormCollectionModelBinder : IModelBinder public class FormCollectionModelBinder : IModelBinder
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelType != typeof(IFormCollection) && if (bindingContext.ModelType != typeof(IFormCollection) &&
bindingContext.ModelType != typeof(FormCollection)) bindingContext.ModelType != typeof(FormCollection))
{ {
return false; return null;
} }
object model = null;
var request = bindingContext.OperationBindingContext.HttpContext.Request; var request = bindingContext.OperationBindingContext.HttpContext.Request;
if (request.HasFormContentType) if (request.HasFormContentType)
{ {
var form = await request.ReadFormAsync(); var form = await request.ReadFormAsync();
if (bindingContext.ModelType.IsAssignableFrom(form.GetType())) if (bindingContext.ModelType.IsAssignableFrom(form.GetType()))
{ {
bindingContext.Model = form; model = form;
} }
else else
{ {
var formValuesLookup = form.ToDictionary(p => p.Key, var formValuesLookup = form.ToDictionary(p => p.Key,
p => p.Value); p => p.Value);
bindingContext.Model = new FormCollection(formValuesLookup, form.Files); model = new FormCollection(formValuesLookup, form.Files);
} }
} }
else 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 public class FormFileModelBinder : IModelBinder
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelType == typeof(IFormFile)) if (bindingContext.ModelType == typeof(IFormFile))
{ {
var postedFiles = await GetFormFilesAsync(bindingContext); var postedFiles = await GetFormFilesAsync(bindingContext);
var value = postedFiles.FirstOrDefault(); var value = postedFiles.FirstOrDefault();
if (value != null) return new ModelBindingResult(value, bindingContext.ModelName, value != null);
{
bindingContext.Model = value;
}
return true;
} }
else if (typeof(IEnumerable<IFormFile>).GetTypeInfo().IsAssignableFrom( else if (typeof(IEnumerable<IFormFile>).GetTypeInfo().IsAssignableFrom(
bindingContext.ModelType.GetTypeInfo())) bindingContext.ModelType.GetTypeInfo()))
{ {
var postedFiles = await GetFormFilesAsync(bindingContext); var postedFiles = await GetFormFilesAsync(bindingContext);
var value = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, postedFiles); var value = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, postedFiles);
if (value != null) return new ModelBindingResult(value, bindingContext.ModelName, value != null);
{
bindingContext.Model = value;
}
return true;
} }
return false; return null;
} }
private async Task<List<IFormFile>> GetFormFilesAsync(ModelBindingContext bindingContext) private async Task<List<IFormFile>> GetFormFilesAsync(ModelBindingContext bindingContext)

View File

@ -12,20 +12,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
public class GenericModelBinder : IModelBinder public class GenericModelBinder : IModelBinder
{ {
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
var binderType = ResolveBinderType(bindingContext.ModelType); var binderType = ResolveBinderType(bindingContext.ModelType);
if (binderType != null) if (binderType != null)
{ {
var binder = (IModelBinder)Activator.CreateInstance(binderType); 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 // 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. // true so that none of the other model binders participate.
return true; return modelBindingResult;
} }
return false; return null;
} }
private static Type ResolveBinderType(Type modelType) private static Type ResolveBinderType(Type modelType)

View File

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

View File

@ -14,7 +14,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// Async function to bind to a particular model. /// Async function to bind to a particular model.
/// </summary> /// </summary>
/// <param name="bindingContext">The binding context which has the object to be bound.</param> /// <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> /// <returns>A Task which on completion returns a <see cref="ModelBindingResult"/> which represents the result
Task<bool> BindModelAsync(ModelBindingContext bindingContext); /// 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Internal; using Microsoft.AspNet.Mvc.ModelBinding.Internal;
@ -9,23 +10,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
public sealed class KeyValuePairModelBinder<TKey, TValue> : IModelBinder public sealed class KeyValuePairModelBinder<TKey, TValue> : IModelBinder
{ {
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
ModelBindingHelper.ValidateBindingContext(bindingContext, ModelBindingHelper.ValidateBindingContext(bindingContext,
typeof(KeyValuePair<TKey, TValue>), typeof(KeyValuePair<TKey, TValue>),
allowNullModel: true); allowNullModel: true);
var keyResult = await TryBindStrongModel<TKey>(bindingContext, "key"); var keyResult = await TryBindStrongModel<TKey>(bindingContext, "Key");
var valueResult = await TryBindStrongModel<TValue>(bindingContext, "value"); var valueResult = await TryBindStrongModel<TValue>(bindingContext, "Value");
var model = bindingContext.ModelMetadata.Model;
if (keyResult.Success && valueResult.Success) 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) string propertyName)
{ {
var propertyModelMetadata = var propertyModelMetadata =
@ -35,29 +52,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName); ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName);
var propertyBindingContext = var propertyBindingContext =
new ModelBindingContext(parentBindingContext, propertyModelName, propertyModelMetadata); new ModelBindingContext(parentBindingContext, propertyModelName, propertyModelMetadata);
var modelBindingResult =
if (await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext)) await propertyBindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext);
if (modelBindingResult != null)
{ {
var untypedModel = propertyBindingContext.Model; return modelBindingResult;
var model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel);
parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
return new BindResult<TModel>(success: true, model: model);
} }
return new BindResult<TModel>(success: false, model: default(TModel)); return new ModelBindingResult(model: default(TModel), key: propertyModelName, isModelSet: false);
}
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; }
} }
} }
} }

View File

@ -14,12 +14,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
public class MutableObjectModelBinder : IModelBinder public class MutableObjectModelBinder : IModelBinder
{ {
public virtual async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public virtual async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
ModelBindingHelper.ValidateBindingContext(bindingContext); ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelType)) if (!CanBindType(bindingContext.ModelType))
{ {
return false; return null;
} }
var mutableObjectBinderContext = new MutableObjectBinderContext() var mutableObjectBinderContext = new MutableObjectBinderContext()
@ -30,17 +30,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (!(await CanCreateModel(mutableObjectBinderContext))) if (!(await CanCreateModel(mutableObjectBinderContext)))
{ {
return false; return null;
} }
EnsureModel(bindingContext); 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 // post-processing, e.g. property setters and hooking up validation
ProcessDto(bindingContext, dto); ProcessDto(bindingContext, (ComplexModelDto)result.Model);
// complex models require full validation return
bindingContext.ValidationNode.ValidateAllProperties = true; new ModelBindingResult(bindingContext.ModelMetadata.Model, bindingContext.ModelName, isModelSet: true);
return true;
} }
protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata) protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
@ -236,7 +235,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return true; return true;
} }
private async Task<ComplexModelDto> CreateAndPopulateDto(ModelBindingContext bindingContext, private async Task<ModelBindingResult> CreateAndPopulateDto(ModelBindingContext bindingContext,
IEnumerable<ModelMetadata> propertyMetadatas) IEnumerable<ModelMetadata> propertyMetadatas)
{ {
// create a DTO and call into the DTO binder // create a DTO and call into the DTO binder
@ -247,8 +246,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dtoBindingContext = var dtoBindingContext =
new ModelBindingContext(bindingContext, bindingContext.ModelName, complexModelDtoMetadata); new ModelBindingContext(bindingContext, bindingContext.ModelName, complexModelDtoMetadata);
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(dtoBindingContext); return await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(dtoBindingContext);
return (ComplexModelDto)dtoBindingContext.Model;
} }
protected virtual object CreateModel(ModelBindingContext bindingContext) protected virtual object CreateModel(ModelBindingContext bindingContext)
@ -258,36 +256,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return Activator.CreateInstance(bindingContext.ModelType); 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) 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); var validationInfo = GetPropertyValidationInfo(bindingContext);
// Eliminate provided properties from requiredProperties; leaving just *missing* required properties. // 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); validationInfo.RequiredProperties.ExceptWith(boundProperties);
foreach (var missingRequiredProperty in validationInfo.RequiredProperties) foreach (var missingRequiredProperty in validationInfo.RequiredProperties)
{ {
var addedError = false; var addedError = false;
var modelStateKey = ModelBindingHelper.CreatePropertyModelName( 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 // 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. // 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, validationInfo.RequiredValidators.TryGetValue(propertyMetadata.PropertyName,
out requiredValidator); out requiredValidator);
SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator);
bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
} }
} }
} }
protected virtual void SetProperty(ModelBindingContext bindingContext, protected virtual void SetProperty(ModelBindingContext bindingContext,
ModelMetadata propertyMetadata, ModelMetadata propertyMetadata,
ComplexModelDtoResult dtoResult, ModelBindingResult dtoResult,
IModelValidator requiredValidator) IModelValidator requiredValidator)
{ {
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
@ -443,7 +415,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
object value; object value;
var hasDefaultValue = false; var hasDefaultValue = false;
if (dtoResult.IsModelBound) if (dtoResult.IsModelSet)
{ {
value = dtoResult.Model; 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. // the property setters throw, e.g. if we're setting entity keys to null.
if (value == null) if (value == null)
{ {
var modelStateKey = dtoResult.ValidationNode.ModelStateKey; var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey); var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated) 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 // If we don't have a value, don't set it on the model and trounce a pre-initialized
// value. // value.
@ -484,7 +456,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
try try
{ {
property.SetValue(bindingContext.Model, value); property.SetValue(bindingContext.ModelMetadata.Model, value);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -495,7 +467,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
ex = targetInvocationException.InnerException; ex = targetInvocationException.InnerException;
} }
var modelStateKey = dtoResult.ValidationNode.ModelStateKey; var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey); var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated) if (validationState == ModelValidationState.Unvalidated)
{ {
@ -506,11 +478,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
else else
{ {
// trying to set a non-nullable value type to null, need to make sure there's a message // 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); var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated) 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 sealed class TypeConverterModelBinder : IModelBinder
{ {
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
ModelBindingHelper.ValidateBindingContext(bindingContext); ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!TypeHelper.HasStringConverter(bindingContext.ModelType)) if (!TypeHelper.HasStringConverter(bindingContext.ModelType))
{ {
// this type cannot be converted // this type cannot be converted
return false; return null;
} }
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName); var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
if (valueProviderResult == null) if (valueProviderResult == null)
{ {
return false; // no entry return null; // no entry
} }
object newModel; object newModel;
@ -31,14 +31,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
newModel = valueProviderResult.ConvertTo(bindingContext.ModelType); newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel); ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
bindingContext.Model = newModel; return new ModelBindingResult(newModel, bindingContext.ModelName, true);
} }
catch (Exception ex) catch (Exception ex)
{ {
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, 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 sealed class TypeMatchModelBinder : IModelBinder
{ {
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
var valueProviderResult = await GetCompatibleValueProviderResult(bindingContext); var valueProviderResult = await GetCompatibleValueProviderResult(bindingContext);
if (valueProviderResult == null) if (valueProviderResult == null)
{ {
// conversion would have failed // conversion would have failed
return false; return null;
} }
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var model = valueProviderResult.RawValue; var model = valueProviderResult.RawValue;
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model); ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
bindingContext.Model = model; return new ModelBindingResult(model, bindingContext.ModelName, true);
return true;
} }
internal static async Task<ValueProviderResult> GetCompatibleValueProviderResult(ModelBindingContext context) 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; indexNames = indexes;
} }
} }
return indexNames; 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"); throw new ArgumentException(message, "bindingContext");
} }
if (!allowNullModel && bindingContext.Model == null) if (!allowNullModel && bindingContext.ModelMetadata.Model == null)
{ {
var message = Resources.FormatModelBinderUtil_ModelCannotBeNull(requiredType); var message = Resources.FormatModelBinderUtil_ModelCannotBeNull(requiredType);
throw new ArgumentException(message, "bindingContext"); throw new ArgumentException(message, "bindingContext");
} }
if (bindingContext.Model != null && if (bindingContext.ModelMetadata.Model != null &&
!bindingContext.ModelType.GetTypeInfo().IsAssignableFrom(requiredType.GetTypeInfo())) !bindingContext.ModelType.GetTypeInfo().IsAssignableFrom(requiredType.GetTypeInfo()))
{ {
var message = Resources.FormatModelBinderUtil_ModelInstanceIsWrong( var message = Resources.FormatModelBinderUtil_ModelInstanceIsWrong(
bindingContext.Model.GetType(), bindingContext.ModelMetadata.Model.GetType(),
requiredType); requiredType);
throw new ArgumentException(message, "bindingContext"); throw new ArgumentException(message, "bindingContext");
} }

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private string _modelName; private string _modelName;
private ModelStateDictionary _modelState; private ModelStateDictionary _modelState;
private ModelValidationNode _validationNode;
private Func<ModelBindingContext, string, bool> _propertyFilter; private Func<ModelBindingContext, string, bool> _propertyFilter;
/// <summary> /// <summary>
@ -55,36 +54,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public OperationBindingContext OperationBindingContext { get; set; } 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> /// <summary>
/// Gets or sets the metadata for the model associated with this context. /// Gets or sets the metadata for the model associated with this context.
/// </summary> /// </summary>
@ -164,23 +133,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
set { _propertyFilter = value; } 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() private void EnsureModelMetadata()
{ {
if (ModelMetadata == null) 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; } 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 public bool IsValid
{ {
get { return ValidationState == ModelValidationState.Valid; } get
{
return ValidationState == ModelValidationState.Valid || ValidationState == ModelValidationState.Skipped;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -284,6 +289,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetValidity(entries); 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> /// <summary>
/// Marks the <see cref="ModelState.ValidationState"/> for the entry with the specified <paramref name="key"/> /// Marks the <see cref="ModelState.ValidationState"/> for the entry with the specified <paramref name="key"/>
/// as <see cref="ModelValidationState.Valid"/>. /// as <see cref="ModelValidationState.Valid"/>.
@ -300,6 +323,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
modelState.ValidationState = ModelValidationState.Valid; 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> /// <summary>
/// Copies the values from the specified <paramref name="dictionary"/> into this instance, overwriting /// Copies the values from the specified <paramref name="dictionary"/> into this instance, overwriting
/// existing values if keys are the same. /// existing values if keys are the same.

View File

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

View File

@ -42,6 +42,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("ArgumentCannotBeNullOrEmpty"); 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> /// <summary>
/// The property {0}.{1} could not be found. /// The property {0}.{1} could not be found.
/// </summary> /// </summary>
@ -330,6 +346,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetString("Validation_InvalidFieldCannotBeReset"); 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> /// <summary>
/// A value is required but was not present in the request. /// A value is required but was not present in the request.
/// </summary> /// </summary>

View File

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

View File

@ -40,7 +40,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
foreach (var actualType in actualTypes) foreach (var actualType in actualTypes)
{ {
var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType; var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType;
if (!IsSimpleType(underlyingType)) if (!IsSimpleType(underlyingType))
{ {
return false; 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, public ModelValidationContext([NotNull] ModelBindingContext bindingContext,
[NotNull] ModelMetadata metadata) [NotNull] ModelMetadata metadata)
: this(bindingContext.OperationBindingContext.MetadataProvider, : this(bindingContext.ModelName,
bindingContext.OperationBindingContext.ValidatorProvider, bindingContext.OperationBindingContext.ValidatorProvider,
bindingContext.ModelState, bindingContext.ModelState,
metadata, metadata,
@ -17,33 +17,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
} }
public ModelValidationContext([NotNull] IModelMetadataProvider metadataProvider, public ModelValidationContext(string rootPrefix,
[NotNull] IModelValidatorProvider validatorProvider, [NotNull] IModelValidatorProvider validatorProvider,
[NotNull] ModelStateDictionary modelState, [NotNull] ModelStateDictionary modelState,
[NotNull] ModelMetadata metadata, [NotNull] ModelMetadata metadata,
ModelMetadata containerMetadata) 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; ModelMetadata = metadata;
ModelState = modelState; ModelState = modelState;
MetadataProvider = metadataProvider; RootPrefix = rootPrefix;
ValidatorProvider = validatorProvider; ValidatorProvider = validatorProvider;
ContainerMetadata = containerMetadata; ContainerMetadata = containerMetadata;
ExcludeFromValidationFilters = excludeFromValidationFilters;
} }
public ModelValidationContext([NotNull] ModelValidationContext parentContext, public ModelValidationContext([NotNull] ModelValidationContext parentContext,
@ -52,9 +36,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata = metadata; ModelMetadata = metadata;
ContainerMetadata = parentContext.ModelMetadata; ContainerMetadata = parentContext.ModelMetadata;
ModelState = parentContext.ModelState; ModelState = parentContext.ModelState;
MetadataProvider = parentContext.MetadataProvider; RootPrefix = parentContext.RootPrefix;
ValidatorProvider = parentContext.ValidatorProvider; ValidatorProvider = parentContext.ValidatorProvider;
ExcludeFromValidationFilters = parentContext.ExcludeFromValidationFilters;
} }
public ModelMetadata ModelMetadata { get; } public ModelMetadata ModelMetadata { get; }
@ -63,10 +46,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public ModelStateDictionary ModelState { get; } public ModelStateDictionary ModelState { get; }
public IModelMetadataProvider MetadataProvider { get; } public string RootPrefix { get; set; }
public IModelValidatorProvider ValidatorProvider { get; } 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 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] [FromServices]
public IModelMetadataProvider MetadataProvider { get; set; } public IModelMetadataProvider MetadataProvider { get; set; }
[FromServices]
public IObjectModelValidator ObjectValidator { get; set; }
/// <summary> /// <summary>
/// Gets model state after the model binding process. This ModelState will be empty before model binding /// Gets model state after the model binding process. This ModelState will be empty before model binding
/// happens. /// happens.
@ -417,18 +421,14 @@ namespace System.Web.Http
{ {
var modelMetadata = MetadataProvider.GetMetadataForType(() => entity, typeof(TEntity)); var modelMetadata = MetadataProvider.GetMetadataForType(() => entity, typeof(TEntity));
var bodyValidationExcludeFiltersProvider = Context.RequestServices
.GetRequiredService<IValidationExcludeFiltersProvider>();
var validator = Context.RequestServices.GetRequiredService<IBodyModelValidator>();
var modelValidationContext = new ModelValidationContext( var modelValidationContext = new ModelValidationContext(
MetadataProvider, keyPrefix,
BindingContext.ValidatorProvider, BindingContext.ValidatorProvider,
ModelState, ModelState,
modelMetadata, modelMetadata,
containerMetadata: null, containerMetadata: null);
excludeFromValidationFilters: bodyValidationExcludeFiltersProvider.ExcludeFilters);
validator.Validate(modelValidationContext, keyPrefix); ObjectValidator.Validate(modelValidationContext);
} }
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

View File

@ -9,15 +9,15 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{ {
public class HttpRequestMessageModelBinder : IModelBinder public class HttpRequestMessageModelBinder : IModelBinder
{ {
public Task<bool> BindModelAsync(ModelBindingContext bindingContext) public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
if (bindingContext.ModelType == typeof(HttpRequestMessage)) if (bindingContext.ModelType == typeof(HttpRequestMessage))
{ {
bindingContext.Model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage(); var model = bindingContext.OperationBindingContext.HttpContext.GetHttpRequestMessage();
return Task.FromResult(true); 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. // 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. // 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 System.Net.Http.Formatting;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.WebApiCompatShim; using Microsoft.AspNet.Mvc.WebApiCompatShim;
namespace Microsoft.Framework.DependencyInjection namespace Microsoft.Framework.DependencyInjection
@ -15,6 +17,10 @@ namespace Microsoft.Framework.DependencyInjection
// The constructors on DefaultContentNegotiator aren't DI friendly, so just // The constructors on DefaultContentNegotiator aren't DI friendly, so just
// new it up. // new it up.
services.AddInstance<IContentNegotiator>(new DefaultContentNegotiator()); services.AddInstance<IContentNegotiator>(new DefaultContentNegotiator());
services.Configure<MvcOptions>(options =>
{
options.ValidationExcludeFilters.Add(typeof(HttpRequestMessage));
});
return services; return services;
} }

View File

@ -68,6 +68,12 @@ namespace Microsoft.AspNet.Mvc
options.ValidationExcludeFilters.Add(typeof(XObject)); options.ValidationExcludeFilters.Add(typeof(XObject));
options.ValidationExcludeFilters.Add(typeof(Type)); options.ValidationExcludeFilters.Add(typeof(Type));
options.ValidationExcludeFilters.Add(typeof(JToken)); 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"); 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.Singleton<IActionSelectorDecisionTreeProvider, ActionSelectorDecisionTreeProvider>();
yield return describe.Scoped<IActionSelector, DefaultActionSelector>(); yield return describe.Scoped<IActionSelector, DefaultActionSelector>();
yield return describe.Transient<IControllerActionArgumentBinder, DefaultControllerActionArgumentBinder>(); yield return describe.Transient<IControllerActionArgumentBinder, DefaultControllerActionArgumentBinder>();
yield return describe.Transient<IObjectModelValidator, DefaultObjectValidator>();
yield return describe.Transient<INestedProvider<ActionDescriptorProviderContext>, yield return describe.Transient<INestedProvider<ActionDescriptorProviderContext>,
ControllerActionDescriptorProvider>(); ControllerActionDescriptorProvider>();
@ -91,7 +92,6 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Instance<JsonOutputFormatter>(new JsonOutputFormatter()); yield return describe.Instance<JsonOutputFormatter>(new JsonOutputFormatter());
yield return describe.Transient<IModelValidatorProviderProvider, DefaultModelValidatorProviderProvider>(); yield return describe.Transient<IModelValidatorProviderProvider, DefaultModelValidatorProviderProvider>();
yield return describe.Transient<IBodyModelValidator, DefaultBodyModelValidator>();
yield return describe.Transient<IValidationExcludeFiltersProvider, yield return describe.Transient<IValidationExcludeFiltersProvider,
DefaultValidationExcludeFiltersProvider>(); DefaultValidationExcludeFiltersProvider>();

View File

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

View File

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

View File

@ -13,6 +13,8 @@ using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.AspNet.WebUtilities; using Microsoft.AspNet.WebUtilities;
using Microsoft.AspNet.Http.Core;
using Microsoft.Framework.DependencyInjection;
#if ASPNET50 #if ASPNET50
using Moq; using Moq;
#endif #endif
@ -917,7 +919,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.True(context.PropertyFilter(context, "Property1")); Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2")); Assert.True(context.PropertyFilter(context, "Property2"));
}) })
.Returns(Task.FromResult(false)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, valueProvider); 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, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2")); Assert.True(context.PropertyFilter(context, "Property2"));
}) })
.Returns(Task.FromResult(false)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, valueProvider); 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, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2")); Assert.True(context.PropertyFilter(context, "Property2"));
}) })
.Returns(Task.FromResult(false)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, provider: null); 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, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2")); Assert.False(context.PropertyFilter(context, "exclude2"));
}) })
.Returns(Task.FromResult(true)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, valueProvider); 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, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2")); Assert.False(context.PropertyFilter(context, "exclude2"));
}) })
.Returns(Task.FromResult(true)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, provider: null); 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, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2")); Assert.False(context.PropertyFilter(context, "exclude2"));
}) })
.Returns(Task.FromResult(true)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
@ -1125,7 +1127,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.False(context.PropertyFilter(context, "exclude1")); Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2")); Assert.False(context.PropertyFilter(context, "exclude2"));
}) })
.Returns(Task.FromResult(true)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, provider: null); var controller = GetController(binder.Object, provider: null);
@ -1336,7 +1338,8 @@ namespace Microsoft.AspNet.Mvc.Test
private static Controller GetController(IModelBinder binder, IValueProvider provider) private static Controller GetController(IModelBinder binder, IValueProvider provider)
{ {
var metadataProvider = new DataAnnotationsModelMetadataProvider(); 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()); var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
@ -1344,6 +1347,7 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
ModelBinder = binder, ModelBinder = binder,
ValueProvider = provider, ValueProvider = provider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider()
}; };
return new TestableController() return new TestableController()
@ -1351,7 +1355,8 @@ namespace Microsoft.AspNet.Mvc.Test
ActionContext = actionContext, ActionContext = actionContext,
BindingContext = bindingContext, BindingContext = bindingContext,
MetadataProvider = metadataProvider, 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() private IServiceProvider GetServices()
{ {
var metadataProvider = new EmptyModelMetadataProvider();
var services = new Mock<IServiceProvider>(); var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(typeof(IUrlHelper))) services.Setup(s => s.GetService(typeof(IUrlHelper)))
.Returns(Mock.Of<IUrlHelper>()); .Returns(Mock.Of<IUrlHelper>());
services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) services.Setup(s => s.GetService(typeof(IModelMetadataProvider)))
.Returns(new EmptyModelMetadataProvider()); .Returns(metadataProvider);
services.Setup(s => s.GetService(typeof(TestService))) services.Setup(s => s.GetService(typeof(TestService)))
.Returns(new TestService()); .Returns(new TestService());
services.Setup(s => s.GetService(typeof(IObjectModelValidator)))
.Returns(new DefaultObjectValidator(Mock.Of<IValidationExcludeFiltersProvider>(), metadataProvider));
services services
.Setup(s => s.GetService(typeof(IScopedInstance<ActionBindingContext>))) .Setup(s => s.GetService(typeof(IScopedInstance<ActionBindingContext>)))
.Returns(new MockScopedInstance<ActionBindingContext>()); .Returns(new MockScopedInstance<ActionBindingContext>());

View File

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

View File

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

View File

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

View File

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

View File

@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent); 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); var error = Assert.Single(errorCollection.Value.Errors);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage); Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage);
@ -164,7 +164,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<Dictionary<string, ErrorCollection>>(responseContent); 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); var error = Assert.Single(errorCollection.Value.Errors);
Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage); 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 IServiceProvider _services = TestHelper.CreateServices("ModelBindingWebSite");
private readonly Action<IApplicationBuilder> _app = new ModelBindingWebSite.Startup().Configure; 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] [Theory]
[InlineData("RestrictValueProvidersUsingFromRoute", "valueFromRoute")] [InlineData("RestrictValueProvidersUsingFromRoute", "valueFromRoute")]
[InlineData("RestrictValueProvidersUsingFromQuery", "valueFromQuery")] [InlineData("RestrictValueProvidersUsingFromQuery", "valueFromQuery")]
@ -1012,7 +1051,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act // Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" + var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"CreateAndUpdateUser" + "CreateAndUpdateUser" +
"?RegisterationMonth=March"); "?RegistedeburationMonth=March");
// Assert // Assert
var result = JsonConvert.DeserializeObject<bool>(response); var result = JsonConvert.DeserializeObject<bool>(response);

View File

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

View File

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

View File

@ -45,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(context); var result = await binder.BindModelAsync(context);
// Assert // Assert
Assert.False(result); Assert.Null(result);
Assert.False(binder.WasBindModelCoreCalled); Assert.False(binder.WasBindModelCoreCalled);
} }
@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(context); var result = await binder.BindModelAsync(context);
// Assert // Assert
Assert.False(result); Assert.Null(result);
Assert.False(binder.WasBindModelCoreCalled); Assert.False(binder.WasBindModelCoreCalled);
} }
@ -93,7 +93,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(context); var result = await binder.BindModelAsync(context);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.True(binder.WasBindModelCoreCalled); 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; 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); var binderResult = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(binderResult); Assert.Null(binderResult);
Assert.Null(bindingContext.Model);
} }
[Fact] [Fact]
@ -50,8 +49,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext); var binderResult = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(binderResult); Assert.NotNull(binderResult);
var bytes = Assert.IsType<byte[]>(bindingContext.Model); var bytes = Assert.IsType<byte[]>(binderResult.Model);
Assert.Equal(new byte[] { 23, 43, 53 }, bytes); Assert.Equal(new byte[] { 23, 43, 53 }, bytes);
} }
@ -75,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext); var binderResult = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(binderResult); Assert.NotNull(binderResult);
Assert.False(bindingContext.ModelState.IsValid); Assert.False(bindingContext.ModelState.IsValid);
var error = Assert.Single(bindingContext.ModelState["foo"].Errors); var error = Assert.Single(bindingContext.ModelState["foo"].Errors);
Assert.Equal(expected, error.ErrorMessage); Assert.Equal(expected, error.ErrorMessage);
@ -97,7 +96,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext); var binderResult = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(binderResult); Assert.Null(binderResult);
} }
[Fact] [Fact]
@ -111,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binderResult = await binder.BindModelAsync(bindingContext); var binderResult = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(binderResult); Assert.Null(binderResult);
} }
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, Type modelType) 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); var bound = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(bound); Assert.NotNull(bound);
Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, bindingContext.Model); Assert.Equal(bindingContext.OperationBindingContext.HttpContext.RequestAborted, bound.Model);
} }
[Theory] [Theory]
@ -40,8 +40,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bound = await binder.BindModelAsync(bindingContext); var bound = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(bound); Assert.Null(bound);
Assert.Null(bindingContext.Model);
} }
private static ModelBindingContext GetBindingContext(Type modelType) private static ModelBindingContext GetBindingContext(Type modelType)

View File

@ -34,7 +34,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray()); Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray());
Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
} }
[Fact] [Fact]
@ -55,7 +54,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray()); Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
} }
[Fact] [Fact]
@ -73,10 +71,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>();
// Act // Act
bool retVal = await binder.BindModelAsync(bindingContext); var retVal = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray()); Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)retVal.Model).ToArray());
} }
[Fact] [Fact]
@ -91,11 +89,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binder = new CollectionModelBinder<int>(); var binder = new CollectionModelBinder<int>();
// Act // Act
bool retVal = await binder.BindModelAsync(bindingContext); var retVal = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(retVal); Assert.NotNull(retVal);
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray()); Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)retVal.Model).ToArray());
} }
#endif #endif
@ -134,15 +132,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var culture = new CultureInfo("fr-FR"); var culture = new CultureInfo("fr-FR");
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider()); var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
ModelValidationNode childValidationNode = null;
Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder) Mock.Get<IModelBinder>(bindingContext.OperationBindingContext.ModelBinder)
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>())) .Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns((ModelBindingContext mbc) => .Returns((ModelBindingContext mbc) =>
{ {
Assert.Equal("someName", mbc.ModelName); Assert.Equal("someName", mbc.ModelName);
childValidationNode = mbc.ValidationNode; return Task.FromResult(new ModelBindingResult(42, mbc.ModelName, true));
mbc.Model = 42;
return Task.FromResult(true);
}); });
var modelBinder = new CollectionModelBinder<int>(); var modelBinder = new CollectionModelBinder<int>();
@ -151,7 +146,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Assert // Assert
Assert.Equal(new[] { 42 }, boundCollection.ToArray()); Assert.Equal(new[] { 42 }, boundCollection.ToArray());
Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray());
} }
private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider) private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider)
@ -181,10 +175,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var value = await mbc.ValueProvider.GetValueAsync(mbc.ModelName); var value = await mbc.ValueProvider.GetValueAsync(mbc.ModelName);
if (value != null) if (value != null)
{ {
mbc.Model = value.ConvertTo(mbc.ModelType); var model = value.ConvertTo(mbc.ModelType);
return true; return new ModelBindingResult(model, mbc.ModelName, true);
} }
return false;
return null;
}); });
return mockIntBinder.Object; 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 public class CompositeModelBinderTest
{ {
[Fact] [Fact]
public async Task BindModel_SuccessfulBind_RunsValidationAndReturnsModel() public async Task BindModel_SuccessfulBind_ReturnsModel()
{ {
// Arrange // Arrange
var validationCalled = false;
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
@ -46,28 +44,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal("someName", context.ModelName); Assert.Equal("someName", context.ModelName);
Assert.Same(bindingContext.ValueProvider, context.ValueProvider); Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
context.Model = 42;
bindingContext.ValidationNode.Validating += delegate { validationCalled = true; }; return Task.FromResult(
return Task.FromResult(true); new ModelBindingResult(model: 42, key: "someName", isModelSet: true));
}); });
var shimBinder = CreateCompositeBinder(mockIntBinder.Object); var shimBinder = CreateCompositeBinder(mockIntBinder.Object);
// Act // Act
var isBound = await shimBinder.BindModelAsync(bindingContext); var result = await shimBinder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.NotNull(result);
Assert.Equal(42, bindingContext.Model); Assert.True(result.IsModelSet);
Assert.Equal(42, result.Model);
Assert.True(validationCalled);
Assert.True(bindingContext.ModelState.IsValid);
} }
[Fact] [Fact]
public async Task BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel() public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel()
{ {
// Arrange // Arrange
var validationCalled = false;
var expectedModel = new List<int> { 1, 2, 3, 4, 5 }; var expectedModel = new List<int> { 1, 2, 3, 4, 5 };
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
@ -94,35 +89,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
if (!string.IsNullOrEmpty(mbc.ModelName)) if (!string.IsNullOrEmpty(mbc.ModelName))
{ {
return Task.FromResult(false); return Task.FromResult<ModelBindingResult>(null);
} }
Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata); Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
Assert.Equal("", mbc.ModelName); Assert.Equal("", mbc.ModelName);
Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
mbc.Model = expectedModel; return Task.FromResult(new ModelBindingResult(expectedModel, string.Empty, true));
mbc.ValidationNode.Validating += delegate { validationCalled = true; };
return Task.FromResult(true);
}); });
var shimBinder = CreateCompositeBinder(mockIntBinder.Object); var shimBinder = CreateCompositeBinder(mockIntBinder.Object);
// Act // Act
var isBound = await shimBinder.BindModelAsync(bindingContext); var result = await shimBinder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.NotNull(result);
Assert.Equal(expectedModel, bindingContext.Model); Assert.True(result.IsModelSet);
Assert.True(validationCalled); Assert.Equal(string.Empty, result.Key);
Assert.True(bindingContext.ModelState.IsValid); Assert.Equal(expectedModel, result.Model);
} }
[Fact] [Fact]
public async Task ModelBinder_ReturnsTrue_WithoutSettingValue_SkipsValidation() public async Task ModelBinder_ReturnsTrue_WithoutSettingValue()
{ {
// Arrange // Arrange
var validationCalled = false;
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
@ -143,31 +135,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var modelBinder = new Mock<IModelBinder>(); var modelBinder = new Mock<IModelBinder>();
modelBinder modelBinder
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>())) .Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(context => .Returns(Task.FromResult(new ModelBindingResult(null, "someName", false)));
{
context.ValidationNode.Validating += delegate { validationCalled = true; };
})
.Returns(Task.FromResult(true));
var composite = CreateCompositeBinder(modelBinder.Object); var composite = CreateCompositeBinder(modelBinder.Object);
// Act // Act
var isBound = await composite.BindModelAsync(bindingContext); var result = await composite.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.NotNull(result);
Assert.False(result.IsModelSet);
Assert.Null(bindingContext.Model); Assert.Equal("someName", result.Key);
Assert.False(validationCalled); Assert.Null(result.Model);
Assert.False(bindingContext.IsModelSet);
Assert.True(bindingContext.ModelState.IsValid);
} }
[Fact] [Fact]
public async Task ModelBinder_ReturnsTrue_SetsNullValue_RunsValidation() public async Task ModelBinder_ReturnsTrue_SetsNullValue_SetsModelStateKey()
{ {
// Arrange // Arrange
var validationCalled = false;
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
@ -188,25 +173,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var modelBinder = new Mock<IModelBinder>(); var modelBinder = new Mock<IModelBinder>();
modelBinder modelBinder
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>())) .Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback<ModelBindingContext>(context => .Returns(Task.FromResult(new ModelBindingResult(null, "someName", true)));
{
context.Model = null;
context.ValidationNode.Validating += delegate { validationCalled = true; };
})
.Returns(Task.FromResult(true));
var composite = CreateCompositeBinder(modelBinder.Object); var composite = CreateCompositeBinder(modelBinder.Object);
// Act // Act
var isBound = await composite.BindModelAsync(bindingContext); var result = await composite.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.NotNull(result);
Assert.True(result.IsModelSet);
Assert.Null(bindingContext.Model); Assert.Equal("someName", result.Key);
Assert.True(validationCalled); Assert.Null(result.Model);
Assert.True(bindingContext.IsModelSet);
Assert.False(bindingContext.ModelState.IsValid);
} }
[Fact] [Fact]
@ -215,7 +193,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Arrange // Arrange
var mockListBinder = new Mock<IModelBinder>(); var mockListBinder = new Mock<IModelBinder>();
mockListBinder.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>())) mockListBinder.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false)) .Returns(Task.FromResult<ModelBindingResult>(null))
.Verifiable(); .Verifiable();
var shimBinder = mockListBinder.Object; var shimBinder = mockListBinder.Object;
@ -227,11 +205,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}; };
// Act // Act
var isBound = await shimBinder.BindModelAsync(bindingContext); var result = await shimBinder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(isBound); Assert.Null(result);
Assert.Null(bindingContext.Model);
Assert.True(bindingContext.ModelState.IsValid); Assert.True(bindingContext.ModelState.IsValid);
mockListBinder.Verify(); mockListBinder.Verify();
} }
@ -252,11 +229,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
}; };
// Act // Act
var isBound = await shimBinder.BindModelAsync(bindingContext); var result = await shimBinder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(isBound); Assert.Null(result);
Assert.Null(bindingContext.Model);
} }
[Fact] [Fact]
@ -273,11 +249,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel)); var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel));
// Act // Act
var isBound = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.NotNull(result);
var model = Assert.IsType<SimplePropertiesModel>(bindingContext.Model); var model = Assert.IsType<SimplePropertiesModel>(result.Model);
Assert.Equal("firstName-value", model.FirstName); Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName); Assert.Equal("lastName-value", model.LastName);
} }
@ -302,11 +278,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(Person)); var bindingContext = CreateBindingContext(binder, valueProvider, typeof(Person));
// Act // Act
var isBound = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(isBound); Assert.NotNull(result);
var model = Assert.IsType<Person>(bindingContext.Model); var model = Assert.IsType<Person>(result.Model);
Assert.Equal("firstName-value", model.FirstName); Assert.Equal("firstName-value", model.FirstName);
Assert.Equal("lastName-value", model.LastName); Assert.Equal("lastName-value", model.LastName);
Assert.Equal(2, model.Friends.Count); 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); 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, private static ModelBindingContext CreateBindingContext(IModelBinder binder,
IValueProvider valueProvider, IValueProvider valueProvider,
Type type, Type type,

View File

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

View File

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

View File

@ -32,8 +32,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
var files = Assert.IsAssignableFrom<IList<IFormFile>>(bindingContext.Model); var files = Assert.IsAssignableFrom<IList<IFormFile>>(result.Model);
Assert.Equal(2, files.Count); Assert.Equal(2, files.Count);
} }
@ -52,8 +52,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
var files = Assert.IsAssignableFrom<IList<IFormFile>>(bindingContext.Model); var files = Assert.IsAssignableFrom<IList<IFormFile>>(result.Model);
Assert.Equal(2, files.Count); Assert.Equal(2, files.Count);
} }
@ -72,8 +72,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
var file = Assert.IsAssignableFrom<IFormFile>(bindingContext.Model); var file = Assert.IsAssignableFrom<IFormFile>(result.Model);
Assert.Equal("form-data; name=file; filename=file1.txt", Assert.Equal("form-data; name=file; filename=file1.txt",
file.ContentDisposition); file.ContentDisposition);
} }
@ -91,8 +91,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.Null(bindingContext.Model); Assert.Null(result.Model);
} }
[Fact] [Fact]
@ -109,8 +109,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.Null(bindingContext.Model); Assert.Null(result.Model);
} }
[Fact] [Fact]
@ -127,8 +127,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.Null(bindingContext.Model); Assert.Null(result.Model);
} }
[Fact] [Fact]
@ -145,8 +145,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var result = await binder.BindModelAsync(bindingContext); var result = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.Null(bindingContext.Model); Assert.Null(result.Model);
} }
private static ModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext) 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); var result = await binder.BindModelAsync(modelBindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
} }
[Fact] [Fact]
@ -47,8 +47,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = await binder.BindModelAsync(modelBindingContext); var result = await binder.BindModelAsync(modelBindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.Equal(headerValue.Split(','), modelBindingContext.Model); Assert.Equal(headerValue.Split(','), result.Model);
} }
[Fact] [Fact]
@ -68,8 +68,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = await binder.BindModelAsync(modelBindingContext); var result = await binder.BindModelAsync(modelBindingContext);
// Assert // Assert
Assert.True(result); Assert.NotNull(result);
Assert.Equal(headerValue, modelBindingContext.Model); Assert.Equal(headerValue, result.Model);
} }
private static ModelBindingContext GetBindingContext(Type modelType) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if ASPNET50 #if ASPNET50
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Moq; using Moq;
@ -13,37 +15,70 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public class KeyValuePairModelBinderTest public class KeyValuePairModelBinderTest
{ {
[Fact] [Fact]
public async Task BindModel_MissingKey_ReturnsFalse() public async Task BindModel_MissingKey_ReturnsTrue_AndAddsModelValidationError()
{ {
// Arrange // Arrange
var valueProvider = new SimpleHttpValueProvider(); 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>(); var binder = new KeyValuePairModelBinder<int, string>();
// Act // Act
bool retVal = await binder.BindModelAsync(bindingContext); var retVal = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.False(retVal); Assert.NotNull(retVal);
Assert.Null(bindingContext.Model); Assert.Null(retVal.Model);
Assert.Empty(bindingContext.ValidationNode.ChildNodes); 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] [Fact]
public async Task BindModel_MissingValue_ReturnsTrue() public async Task BindModel_MissingValue_ReturnsTrue_AndAddsModelValidationError()
{ {
// Arrange // Arrange
var valueProvider = new SimpleHttpValueProvider(); 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>(); var binder = new KeyValuePairModelBinder<int, string>();
// Act // Act
bool retVal = await binder.BindModelAsync(bindingContext); var retVal = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(retVal); Assert.NotNull(retVal);
Assert.Null(bindingContext.Model); Assert.Null(retVal.Model);
Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray()); 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] [Fact]
@ -60,9 +95,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.BindModelAsync(bindingContext); var retVal = await binder.BindModelAsync(bindingContext);
// Assert // Assert
Assert.True(retVal); Assert.NotNull(retVal);
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), bindingContext.Model); Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), retVal.Model);
Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
} }
[Fact] [Fact]
@ -76,9 +110,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var retVal = await binder.TryBindStrongModel<int>(bindingContext, "key"); var retVal = await binder.TryBindStrongModel<int>(bindingContext, "key");
// Assert // Assert
Assert.True(retVal.Success); Assert.True(retVal.IsModelSet);
Assert.Equal(42, retVal.Model); Assert.Equal(42, retVal.Model);
Assert.Single(bindingContext.ValidationNode.ChildNodes);
Assert.Empty(bindingContext.ModelState); Assert.Empty(bindingContext.ModelState);
} }
@ -92,7 +125,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
.Returns((ModelBindingContext mbc) => .Returns((ModelBindingContext mbc) =>
{ {
Assert.Equal("someName.key", mbc.ModelName); 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); 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"); var retVal = await binder.TryBindStrongModel<int>(bindingContext, "key");
// Assert // Assert
Assert.True(retVal.Success); Assert.True(retVal.IsModelSet);
Assert.Equal(default(int), retVal.Model); Assert.Null(retVal.Model);
Assert.Single(bindingContext.ValidationNode.ChildNodes);
Assert.Empty(bindingContext.ModelState); 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 metataProvider = new EmptyModelMetadataProvider();
var bindingContext = new ModelBindingContext var bindingContext = new ModelBindingContext
{ {
ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair<int, string>)), ModelMetadata = metataProvider.GetMetadataForType(null, keyValuePairType ?? typeof(KeyValuePair<int, string>)),
ModelName = "someName", ModelName = "someName",
ValueProvider = valueProvider, ValueProvider = valueProvider,
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
{ {
ModelBinder = innerBinder ?? CreateIntBinder(), ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider, MetadataProvider = metataProvider,
ValidatorProvider = Mock.Of<IModelValidatorProvider>() ValidatorProvider = new DataAnnotationsModelValidatorProvider()
} }
}; };
return bindingContext; return bindingContext;
@ -136,10 +171,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
if (mbc.ModelType == typeof(int)) if (mbc.ModelType == typeof(int))
{ {
mbc.Model = 42; return Task.FromResult(new ModelBindingResult(42, mbc.ModelName, true));
return Task.FromResult(true);
} }
return Task.FromResult(false); return Task.FromResult<ModelBindingResult>(null);
}); });
return mockIntBinder.Object; return mockIntBinder.Object;
} }
@ -153,10 +187,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
if (mbc.ModelType == typeof(string)) if (mbc.ModelType == typeof(string))
{ {
mbc.Model = "some-value"; return Task.FromResult(new ModelBindingResult("some-value", mbc.ModelName, true));
return Task.FromResult(true);
} }
return Task.FromResult(false); return Task.FromResult<ModelBindingResult>(null);
}); });
return mockStringBinder.Object; return mockStringBinder.Object;
} }

View File

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

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
@ -15,11 +16,11 @@ using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
public class DefaultBodyModelValidatorTests public class DefaultObjectValidatorTests
{ {
private static Person LonelyPerson; private static Person LonelyPerson;
static DefaultBodyModelValidatorTests() static DefaultObjectValidatorTests()
{ {
LonelyPerson = new Person() { Name = "Reallllllllly Long Name" }; LonelyPerson = new Person() { Name = "Reallllllllly Long Name" };
LonelyPerson.Friend = LonelyPerson; LonelyPerson.Friend = LonelyPerson;
@ -212,14 +213,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public void ExpectedValidationErrorsRaised(object model, Type type, Dictionary<string, string> expectedErrors) public void ExpectedValidationErrorsRaised(object model, Type type, Dictionary<string, string> expectedErrors)
{ {
// Arrange // Arrange
var validationContext = GetModelValidationContext(model, type); var testValidationContext = GetModelValidationContext(model, type);
// Act (does not throw) // Act (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty); new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
// Assert // Assert
var actualErrors = new Dictionary<string, string>(); 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) foreach (var error in keyStatePair.Value.Errors)
{ {
@ -237,17 +241,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void BodyValidator_Throws_IfPropertyAccessorThrows() public void Validator_Throws_IfPropertyAccessorThrows()
{ {
// Arrange // 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 // Act & Assert
Assert.Throws( Assert.Throws(
typeof(InvalidOperationException), 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] [Theory]
[MemberData(nameof(ObjectsWithPropertiesWhichThrowOnGet))] [MemberData(nameof(ObjectsWithPropertiesWhichThrowOnGet))]
[ReplaceCulture] [ReplaceCulture]
public void BodyValidator_DoesNotThrow_IfExcludedPropertyAccessorsThrow( public void Validator_DoesNotThrow_IfExcludedPropertyAccessorsThrow(
object input, Type type, List<Type> excludedTypes) object input, Type type, List<Type> excludedTypes)
{ {
// Arrange // Arrange
var validationContext = GetModelValidationContext(input, type, excludedTypes); var testValidationContext = GetModelValidationContext(input, type, string.Empty, excludedTypes);
// Act & Assert (does not throw) // Act & Assert (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty); new DefaultObjectValidator(
Assert.True(validationContext.ModelState.IsValid); testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
Assert.True(testValidationContext.ModelValidationContext.ModelState.IsValid);
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void BodyValidator_Throws_IfPropertyGetterThrows() public void Validator_Throws_IfPropertyGetterThrows()
{ {
// Arrange // Arrange
var validationContext = GetModelValidationContext( var testValidationContext = GetModelValidationContext(
new Uri("/api/values", UriKind.Relative), typeof(Uri), new List<Type>()); new Uri("/api/values", UriKind.Relative), typeof(Uri));
var validationContext = testValidationContext.ModelValidationContext;
// Act & Assert // Act & Assert
Assert.Throws<InvalidOperationException>( Assert.Throws<InvalidOperationException>(
() => () =>
{ {
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty); new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(validationContext);
}); });
Assert.True(validationContext.ModelState.IsValid); Assert.True(validationContext.ModelState.IsValid);
} }
@ -304,10 +318,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
// Arrange // Arrange
var model = new Address() { Street = "Microsoft Way" }; 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) // Act (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty); new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(validationContext);
// Assert // Assert
Assert.Contains("Street", validationContext.ModelState.Keys); Assert.Contains("Street", validationContext.ModelState.Keys);
@ -326,14 +344,146 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new TypeThatOverridesEquals { Funny = "hehe" }, new TypeThatOverridesEquals { Funny = "hehe" },
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) // Act & Assert (does not throw)
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty); new DefaultObjectValidator(
testValidationContext.ExcludeFiltersProvider,
testValidationContext.ModelMetadataProvider)
.Validate(testValidationContext.ModelValidationContext);
} }
private ModelValidationContext GetModelValidationContext( [Fact]
object model, Type type, List<Type> excludedTypes = null) 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 modelStateDictionary = new ModelStateDictionary();
var providers = new IModelValidatorProvider[] var providers = new IModelValidatorProvider[]
@ -341,9 +491,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new DataAnnotationsModelValidatorProvider(), new DataAnnotationsModelValidatorProvider(),
new DataMemberModelValidatorProvider() new DataMemberModelValidatorProvider()
}; };
var modelMetadataProvider = new EmptyModelMetadataProvider(); var modelMetadataProvider = new DataAnnotationsModelMetadataProvider();
var excludedValidationTypesPredicate = var excludedValidationTypesPredicate =
new List<IExcludeTypeValidationFilter>(); new List<IExcludeTypeValidationFilter>();
if (excludedTypes != null) if (excludedTypes != null)
{ {
var mockExcludeTypeFilter = new Mock<IExcludeTypeValidationFilter>(); var mockExcludeTypeFilter = new Mock<IExcludeTypeValidationFilter>();
@ -354,18 +505,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
excludedValidationTypesPredicate.Add(mockExcludeTypeFilter.Object); excludedValidationTypesPredicate.Add(mockExcludeTypeFilter.Object);
} }
return new ModelValidationContext( var mockValidationExcludeFiltersProvider = new Mock<IValidationExcludeFiltersProvider>();
modelMetadataProvider, mockValidationExcludeFiltersProvider.SetupGet(o => o.ExcludeFilters)
new CompositeModelValidatorProvider(providers), .Returns(excludedValidationTypesPredicate);
modelStateDictionary, return new TestModelValidationContext
new ModelMetadata( {
provider: modelMetadataProvider, ModelValidationContext = new ModelValidationContext(
containerType: typeof(object), key,
modelAccessor: () => model, new CompositeModelValidatorProvider(providers),
modelType: type, modelStateDictionary,
propertyName: null), new ModelMetadata(
containerMetadata: null, provider: modelMetadataProvider,
excludeFromValidationFilters: excludedValidationTypesPredicate); containerType: typeof(object),
modelAccessor: () => model,
modelType: type,
propertyName: null),
containerMetadata: null),
ModelMetadataProvider = modelMetadataProvider,
ExcludeFiltersProvider = mockValidationExcludeFiltersProvider.Object
};
} }
public class Person public class Person
@ -468,6 +626,42 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public Team Test { get; set; } 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 #endif

View File

@ -9,6 +9,140 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
public class ModelStateDictionaryTest 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] [Fact]
public void CopyConstructor_CopiesModelStateData() public void CopyConstructor_CopiesModelStateData()
{ {
@ -100,6 +234,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(ModelValidationState.Unvalidated, validationState); 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] [Fact]
public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors() public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors()
{ {
@ -193,7 +341,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}, },
{ "baz", new ModelState { "baz", new ModelState
{ {
ValidationState = ModelValidationState.Valid, ValidationState = ModelValidationState.Skipped,
Value = GetValueProviderResult("quux", "bar") 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); setup.Configure(mvcOptions);
// Assert // 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. // Verify if the delegates registered by default exclude the given types.
Assert.Equal(typeof(SimpleTypesExcludeFilter), mvcOptions.ValidationExcludeFilters[0].OptionType); Assert.Equal(typeof(SimpleTypesExcludeFilter), mvcOptions.ValidationExcludeFilters[i++].OptionType);
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[1].OptionType); Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var xObjectFilter var xObjectFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[1].Instance); = Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(xObjectFilter.ExcludedType, typeof(XObject)); Assert.Equal(xObjectFilter.ExcludedType, typeof(XObject));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[2].OptionType); Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var typeFilter = var typeFilter
Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[2].Instance); = Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(typeFilter.ExcludedType, typeof(Type)); Assert.Equal(typeFilter.ExcludedType, typeof(Type));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[3].OptionType); Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[i].OptionType);
var jTokenFilter var jTokenFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[3].Instance); = Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(jTokenFilter.ExcludedType, typeof(JToken)); 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 = var xmlNodeFilter =
Assert.IsType<DefaultTypeNameBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[4].Instance); Assert.IsType<DefaultTypeNameBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[i++].Instance);
Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode"); Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode");
} }
} }

View File

@ -81,7 +81,7 @@ namespace ModelBindingWebSite.Controllers
private class OrderStatusBinder : IModelBinder private class OrderStatusBinder : IModelBinder
{ {
public Task<bool> BindModelAsync(ModelBindingContext bindingContext) public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
if (typeof(OrderStatus).IsAssignableFrom(bindingContext.ModelType)) 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 // Doing something slightly different here to make sure we don't get accidentally bound
// by the type converter binder. // by the type converter binder.
OrderStatus model; OrderStatus model;
if (Enum.TryParse<OrderStatus>("Status" + request.Query.Get("status"), out model)) var isModelSet = Enum.TryParse<OrderStatus>("Status" + request.Query.Get("status"), out model);
{ return Task.FromResult(new ModelBindingResult(model, "status", isModelSet));
bindingContext.Model = model;
}
return Task.FromResult(true);
} }
return Task.FromResult(false); return Task.FromResult<ModelBindingResult>(null);
} }
} }
private class ProductModelBinder : IModelBinder private class ProductModelBinder : IModelBinder
{ {
public async Task<bool> BindModelAsync(ModelBindingContext bindingContext) public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ {
if (typeof(Product).IsAssignableFrom(bindingContext.ModelType)) if (typeof(Product).IsAssignableFrom(bindingContext.ModelType))
{ {
@ -120,11 +116,10 @@ namespace ModelBindingWebSite.Controllers
var value = await bindingContext.ValueProvider.GetValueAsync(key); var value = await bindingContext.ValueProvider.GetValueAsync(key);
model.ProductId = (int)value.ConvertTo(typeof(int)); model.ProductId = (int)value.ConvertTo(typeof(int));
bindingContext.Model = model; return new ModelBindingResult(model, key, true);
return 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. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite namespace ModelBindingWebSite
{ {
public class Address public class Address
{ {
public int Street { get; set; } public int Street { get; set; }
public string State { get; set; } public string State { get; set; }
[Range(10000, 99999)]
public int Zip { get; set; } public int Zip { get; set; }
public Country Country { 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.ModelBinders.Insert(0, typeof(TestBindingSourceModelBinder));
m.AddXmlDataContractSerializerFormatter(); m.AddXmlDataContractSerializerFormatter();
m.ValidationExcludeFilters.Add(typeof(Address));
}); });
services.AddSingleton<ICalculator, DefaultCalculator>(); 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; var metadata = (FromTestAttribute)bindingContext.ModelMetadata.BinderMetadata;
bindingContext.Model = metadata.Value; var model = metadata.Value;
if (!IsSimpleType(bindingContext.ModelType)) 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) 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 Layout:@GetType().GetTypeInfo().Assembly.FullName
@RenderBody() @RenderBody()

View File

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