Fix #2407 - Part 1 - Make model binding behavior for [Required] compatible

with MVC5.

This change removes the behavior in model binding to validate values 'on
the wire' for requiredness instead of the looking at the model. This
restores the behavior of [Required] for model binding to the MVC5
semantics.
This commit is contained in:
Ryan Nowak 2015-05-18 15:27:56 -07:00
parent 48e0b3261c
commit fa56df93c3
7 changed files with 240 additions and 272 deletions

View File

@ -349,17 +349,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
validationInfo.RequiredProperties.Add(propertyName); validationInfo.RequiredProperties.Add(propertyName);
} }
var validatorProviderContext = new ModelValidatorProviderContext(propertyMetadata);
bindingContext.OperationBindingContext.ValidatorProvider.GetValidators(validatorProviderContext);
var requiredValidator = validatorProviderContext.Validators
.FirstOrDefault(v => v != null && v.IsRequired);
if (requiredValidator != null)
{
validationInfo.RequiredValidators[propertyName] = requiredValidator;
validationInfo.RequiredProperties.Add(propertyName);
}
} }
return validationInfo; return validationInfo;
@ -393,15 +382,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName, bindingContext.ModelName,
propertyName); propertyName);
// Execute validator (if any) to get custom error message. // Get the first 'required' validator (if any) to get custom error message.
IModelValidator validator; var validatorProviderContext = new ModelValidatorProviderContext(propertyExplorer.Metadata);
if (validationInfo.RequiredValidators.TryGetValue(missingRequiredProperty, out validator)) bindingContext.OperationBindingContext.ValidatorProvider.GetValidators(validatorProviderContext);
var validator = validatorProviderContext.Validators.FirstOrDefault(v => v.IsRequired);
if (validator != null)
{ {
addedError = RunValidator(validator, bindingContext, propertyExplorer, modelStateKey); addedError = RunValidator(validator, bindingContext, propertyExplorer, modelStateKey);
} }
// Fall back to default message if BindingBehaviorAttribute required this property or validator // Fall back to default message if BindingBehaviorAttribute required this property and we have no
// (oddly) succeeded. // actual validator for it.
if (!addedError) if (!addedError)
{ {
bindingContext.ModelState.TryAddModelError( bindingContext.ModelState.TryAddModelError(
@ -418,12 +410,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (dtoResult != null) if (dtoResult != null)
{ {
var propertyMetadata = entry.Key; var propertyMetadata = entry.Key;
IModelValidator requiredValidator; SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult);
validationInfo.RequiredValidators.TryGetValue(
propertyMetadata.PropertyName,
out requiredValidator);
SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult, requiredValidator);
var dtoValidationNode = dtoResult.ValidationNode; var dtoValidationNode = dtoResult.ValidationNode;
if (dtoValidationNode == null) if (dtoValidationNode == null)
@ -458,8 +445,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[NotNull] ModelBindingContext bindingContext, [NotNull] ModelBindingContext bindingContext,
[NotNull] ModelExplorer modelExplorer, [NotNull] ModelExplorer modelExplorer,
[NotNull] ModelMetadata propertyMetadata, [NotNull] ModelMetadata propertyMetadata,
[NotNull] ModelBindingResult dtoResult, [NotNull] ModelBindingResult dtoResult)
IModelValidator requiredValidator)
{ {
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
var property = bindingContext.ModelType.GetProperty( var property = bindingContext.ModelType.GetProperty(
@ -485,26 +471,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
value = dtoResult.Model; value = dtoResult.Model;
} }
// 'Required' validators need to run first so that we can provide useful error messages if
// the property setters throw, e.g. if we're setting entity keys to null.
if (value == null)
{
var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
if (requiredValidator != null)
{
var propertyExplorer = modelExplorer.GetExplorerForExpression(propertyMetadata, model: null);
var validationContext = new ModelValidationContext(bindingContext, propertyExplorer);
foreach (var validationResult in requiredValidator.Validate(validationContext))
{
bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message);
}
}
}
}
if (!dtoResult.IsModelSet) if (!dtoResult.IsModelSet)
{ {
// 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
@ -512,31 +478,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return; return;
} }
if (value != null || TypeHelper.AllowsNullValue(property.PropertyType)) try
{ {
try propertyMetadata.PropertySetter(bindingContext.Model, value);
{
propertyMetadata.PropertySetter(bindingContext.Model, value);
}
catch (Exception exception)
{
AddModelError(exception, bindingContext, dtoResult);
}
} }
else catch (Exception exception)
{ {
// trying to set a non-nullable value type to null, need to make sure there's a message AddModelError(exception, bindingContext, dtoResult);
var modelStateKey = dtoResult.Key;
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
if (validationState == ModelValidationState.Unvalidated)
{
var errorMessage = Resources.ModelBinding_ValueRequired;
bindingContext.ModelState.TryAddModelError(modelStateKey, errorMessage);
}
} }
} }
// Neither [DefaultValue] nor [Required] is relevant for a non-settable collection.
private void AddToProperty( private void AddToProperty(
ModelBindingContext bindingContext, ModelBindingContext bindingContext,
ModelExplorer modelExplorer, ModelExplorer modelExplorer,
@ -642,14 +593,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public PropertyValidationInfo() public PropertyValidationInfo()
{ {
RequiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase); RequiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
RequiredValidators = new Dictionary<string, IModelValidator>(StringComparer.OrdinalIgnoreCase);
SkipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase); SkipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
} }
public HashSet<string> RequiredProperties { get; private set; } public HashSet<string> RequiredProperties { get; private set; }
public Dictionary<string, IModelValidator> RequiredValidators { get; private set; }
public HashSet<string> SkipProperties { get; private set; } public HashSet<string> SkipProperties { get; private set; }
} }
} }

View File

@ -153,7 +153,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
var validatorProviderContext = new ModelValidatorProviderContext(metadata); var validatorProviderContext = new ModelValidatorProviderContext(metadata);
provider.GetValidators(validatorProviderContext); provider.GetValidators(validatorProviderContext);
return validatorProviderContext.Validators;
return validatorProviderContext
.Validators
.OrderBy(v => v, ValidatorOrderComparer.Instance)
.ToList();
} }
private bool ValidateChildNodes( private bool ValidateChildNodes(
@ -345,5 +349,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ModelValidationNode ValidationNode { get; set; } public ModelValidationNode ValidationNode { get; set; }
} }
// Sorts validators based on whether or not they are 'required'. We want to run
// 'required' validators first so that we get the best possible error message.
private class ValidatorOrderComparer : IComparer<IModelValidator>
{
public static readonly ValidatorOrderComparer Instance = new ValidatorOrderComparer();
public int Compare(IModelValidator x, IModelValidator y)
{
var xScore = x.IsRequired ? 0 : 1;
var yScore = y.IsRequired ? 0 : 1;
return xScore.CompareTo(yScore);
}
}
} }
} }

View File

@ -849,7 +849,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ProcessDto_BindRequiredFieldNull_RaisesModelError() public void ProcessDto_ValueTypePropertyWithBindRequired_SetToNull_CapturesException()
{ {
// Arrange // Arrange
var model = new ModelWithBindRequired var model = new ModelWithBindRequired
@ -905,14 +905,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState); Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var modelError = Assert.Single(modelState.Errors); var modelError = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception); Assert.Equal(string.Empty, modelError.ErrorMessage);
Assert.NotNull(modelError.ErrorMessage); Assert.IsType<NullReferenceException>(modelError.Exception);
Assert.Equal("A value is required.", modelError.ErrorMessage);
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ProcessDto_RequiredFieldMissing_RaisesModelError() public void ProcessDto_MissingDataForRequiredFields_NoErrors()
{ {
// Arrange // Arrange
var model = new ModelWithRequired(); var model = new ModelWithRequired();
@ -930,32 +929,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
var modelStateDictionary = bindingContext.ModelState; var modelStateDictionary = bindingContext.ModelState;
Assert.False(modelStateDictionary.IsValid); Assert.True(modelStateDictionary.IsValid);
Assert.Equal(2, modelStateDictionary.Count); Assert.Empty(modelStateDictionary);
// Check Age error.
ModelState modelState;
Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.Age), out modelState));
var modelError = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
var expected = ValidationAttributeUtil.GetRequiredErrorMessage(nameof(ModelWithRequired.Age));
Assert.Equal(expected, modelError.ErrorMessage);
// Check City error.
Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.City), out modelState));
modelError = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
expected = ValidationAttributeUtil.GetRequiredErrorMessage(nameof(ModelWithRequired.City));
Assert.Equal(expected, modelError.ErrorMessage);
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ProcessDto_RequiredFieldNull_RaisesModelError() public void ProcessDto_ValueTypeProperty_WithRequiredAttribute_SetToNull_NoError()
{ {
// Arrange // Arrange
var model = new ModelWithRequired(); var model = new ModelWithRequired();
@ -984,22 +964,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
var modelStateDictionary = bindingContext.ModelState; var modelStateDictionary = bindingContext.ModelState;
Assert.False(modelStateDictionary.IsValid); Assert.True(modelStateDictionary.IsValid);
Assert.Single(modelStateDictionary); Assert.Empty(modelStateDictionary);
// Check City error.
ModelState modelState;
Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.City), out modelState));
var modelError = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
var expected = ValidationAttributeUtil.GetRequiredErrorMessage(nameof(ModelWithRequired.City));
Assert.Equal(expected, modelError.ErrorMessage);
} }
[Fact] [Fact]
public void ProcessDto_RequiredFieldMissing_RaisesModelErrorWithMessage() public void ProcessDto_PropertyWithRequiredAttribute_NoPropertiesSet_NoError()
{ {
// Arrange // Arrange
var model = new Person(); var model = new Person();
@ -1016,42 +986,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert // Assert
var modelStateDictionary = bindingContext.ModelState; var modelStateDictionary = bindingContext.ModelState;
Assert.False(modelStateDictionary.IsValid); Assert.True(modelStateDictionary.IsValid);
Assert.Equal(2, modelStateDictionary.Count);
// Check ValueTypeRequired error.
var modelStateEntry = Assert.Single(
modelStateDictionary,
entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequired));
Assert.Equal("theModel." + nameof(Person.ValueTypeRequired), modelStateEntry.Key);
var modelState = modelStateEntry.Value;
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var modelError = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
Assert.Equal("Sample message", modelError.ErrorMessage);
// Check ValueTypeRequiredWithDefaultValue error.
modelStateEntry = Assert.Single(
modelStateDictionary,
entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
Assert.Equal("theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue), modelStateEntry.Key);
modelState = modelStateEntry.Value;
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
modelError = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
Assert.Equal("Another sample message", modelError.ErrorMessage);
} }
[Theory] [Fact]
[InlineData(false)] public void ProcessDto_ValueTypeProperty_TriesToSetNullModel_CapturesException()
[InlineData(true)]
public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage(bool isModelSet)
{ {
// Arrange // Arrange
var model = new Person(); var model = new Person();
@ -1070,7 +1009,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired));
dto.Results[propertyMetadata] = new ModelBindingResult( dto.Results[propertyMetadata] = new ModelBindingResult(
null, null,
isModelSet: isModelSet, isModelSet: true,
key: "theModel." + nameof(Person.ValueTypeRequired)); key: "theModel." + nameof(Person.ValueTypeRequired));
// Make ValueTypeRequiredWithDefaultValue invalid // Make ValueTypeRequiredWithDefaultValue invalid
@ -1078,8 +1017,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
.Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue)); .Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue));
dto.Results[propertyMetadata] = new ModelBindingResult( dto.Results[propertyMetadata] = new ModelBindingResult(
model: null, model: null,
isModelSet: isModelSet, isModelSet: true,
key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model); var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act // Act
@ -1097,10 +1037,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var modelState = modelStateEntry.Value; var modelState = modelStateEntry.Value;
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState); Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var modelError = Assert.Single(modelState.Errors); var error = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception); Assert.Equal(string.Empty, error.ErrorMessage);
Assert.NotNull(modelError.ErrorMessage); Assert.IsType<NullReferenceException>(error.Exception);
Assert.Equal("Sample message", modelError.ErrorMessage);
// Check ValueTypeRequiredWithDefaultValue error. // Check ValueTypeRequiredWithDefaultValue error.
modelStateEntry = Assert.Single( modelStateEntry = Assert.Single(
@ -1111,15 +1050,55 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
modelState = modelStateEntry.Value; modelState = modelStateEntry.Value;
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState); Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
modelError = Assert.Single(modelState.Errors); error = Assert.Single(modelState.Errors);
Assert.Null(modelError.Exception); Assert.Equal(string.Empty, error.ErrorMessage);
Assert.NotNull(modelError.ErrorMessage); Assert.IsType<NullReferenceException>(error.Exception);
Assert.Equal("Another sample message", modelError.ErrorMessage);
Assert.Equal(0, model.ValueTypeRequired); Assert.Equal(0, model.ValueTypeRequired);
Assert.Equal(expectedValue, model.ValueTypeRequiredWithDefaultValue); Assert.Equal(expectedValue, model.ValueTypeRequiredWithDefaultValue);
} }
[Fact]
public void ProcessDto_ValueTypeProperty_NoValue_NoError()
{
// Arrange
var model = new Person();
var containerMetadata = GetMetadataForType(model.GetType());
var bindingContext = CreateContext(containerMetadata, model);
var modelStateDictionary = bindingContext.ModelState;
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder();
// The [DefaultValue] on ValueTypeRequiredWithDefaultValue is ignored by model binding.
var expectedValue = 0;
// Make ValueTypeRequired invalid.
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired));
dto.Results[propertyMetadata] = new ModelBindingResult(
null,
isModelSet: false,
key: "theModel." + nameof(Person.ValueTypeRequired));
// Make ValueTypeRequiredWithDefaultValue invalid
propertyMetadata = dto.PropertyMetadata
.Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue));
dto.Results[propertyMetadata] = new ModelBindingResult(
model: null,
isModelSet: false,
key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
Assert.True(modelStateDictionary.IsValid);
Assert.Empty(modelStateDictionary);
}
[Fact] [Fact]
public void ProcessDto_ProvideRequiredFields_Success() public void ProcessDto_ProvideRequiredFields_Success()
{ {
@ -1171,6 +1150,109 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(0m, model.PropertyWithDefaultValue); // [DefaultValue] has no effect Assert.Equal(0m, model.PropertyWithDefaultValue); // [DefaultValue] has no effect
} }
// This uses [Required] with [BindRequired] to provide a custom validation messsage.
[Fact]
public void ProcessDto_ValueTypePropertyWithBindRequired_CustomValidationMessage()
{
// Arrange
var model = new ModelWithBindRequiredAndRequiredAttribute();
var containerMetadata = GetMetadataForType(model.GetType());
var bindingContext = CreateContext(containerMetadata, model);
var modelStateDictionary = bindingContext.ModelState;
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder();
// Make ValueTypeProperty not have a value.
var propertyMetadata = containerMetadata
.Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)];
dto.Results[propertyMetadata] = new ModelBindingResult(
null,
isModelSet: false,
key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty));
// Make ReferenceTypeProperty have a value.
propertyMetadata = containerMetadata
.Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)];
dto.Results[propertyMetadata] = new ModelBindingResult(
model: "value",
isModelSet: true,
key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty));
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
Assert.False(modelStateDictionary.IsValid);
var entry = Assert.Single(
modelStateDictionary,
kvp => kvp.Key == "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty))
.Value;
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("Custom Message ValueTypeProperty", error.ErrorMessage);
// Model gets provided values.
Assert.Equal(0, model.ValueTypeProperty);
Assert.Equal("value", model.ReferenceTypeProperty);
}
// This uses [Required] with [BindRequired] to provide a custom validation messsage.
[Fact]
public void ProcessDto_ReferenceTypePropertyWithBindRequired_CustomValidationMessage()
{
// Arrange
var model = new ModelWithBindRequiredAndRequiredAttribute();
var containerMetadata = GetMetadataForType(model.GetType());
var bindingContext = CreateContext(containerMetadata, model);
var modelStateDictionary = bindingContext.ModelState;
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
var testableBinder = new TestableMutableObjectModelBinder();
// Make ValueTypeProperty have a value.
var propertyMetadata = containerMetadata
.Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty)];
dto.Results[propertyMetadata] = new ModelBindingResult(
17,
isModelSet: true,
key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ValueTypeProperty));
// Make ReferenceTypeProperty not have a value.
propertyMetadata = containerMetadata
.Properties[nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty)];
dto.Results[propertyMetadata] = new ModelBindingResult(
model: null,
isModelSet: false,
key: "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty));
var modelValidationNode = new ModelValidationNode(string.Empty, containerMetadata, model);
// Act
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
Assert.False(modelStateDictionary.IsValid);
var entry = Assert.Single(
modelStateDictionary,
kvp => kvp.Key == "theModel." + nameof(ModelWithBindRequiredAndRequiredAttribute.ReferenceTypeProperty))
.Value;
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("Custom Message ReferenceTypeProperty", error.ErrorMessage);
// Model gets provided values.
Assert.Equal(17, model.ValueTypeProperty);
Assert.Null(model.ReferenceTypeProperty);
}
[Fact] [Fact]
public void ProcessDto_Success() public void ProcessDto_Success()
{ {
@ -1241,12 +1323,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
isModelSet: false, isModelSet: false,
key: "foo"); key: "foo");
var validatorProvider = bindingContext.OperationBindingContext.ValidatorProvider;
var validatorProviderContext = new ModelValidatorProviderContext(propertyMetadata);
validatorProvider.GetValidators(validatorProviderContext);
var requiredValidator = validatorProviderContext.Validators.FirstOrDefault(v => v.IsRequired);
var testableBinder = new TestableMutableObjectModelBinder(); var testableBinder = new TestableMutableObjectModelBinder();
// Act // Act
@ -1254,8 +1330,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator);
// Assert // Assert
var person = Assert.IsType<Person>(bindingContext.Model); var person = Assert.IsType<Person>(bindingContext.Model);
@ -1287,8 +1362,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator: null);
// Assert // Assert
var person = Assert.IsType<Person>(bindingContext.Model); var person = Assert.IsType<Person>(bindingContext.Model);
@ -1320,8 +1394,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator: null);
// Assert // Assert
var person = Assert.IsType<Person>(bindingContext.Model); var person = Assert.IsType<Person>(bindingContext.Model);
@ -1352,8 +1425,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator: null);
// Assert // Assert
// If didn't throw, success! // If didn't throw, success!
@ -1406,8 +1478,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator: null);
// Assert // Assert
Assert.Equal("Joe", propertAccessor(model)); Assert.Equal("Joe", propertAccessor(model));
@ -1486,8 +1557,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator: null);
// Assert // Assert
Assert.Equal(collection, propertyAccessor(model)); Assert.Equal(collection, propertyAccessor(model));
@ -1511,12 +1581,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
key: "foo", key: "foo",
isModelSet: true); isModelSet: true);
var validatorProvider = bindingContext.OperationBindingContext.ValidatorProvider;
var validatorProviderContext = new ModelValidatorProviderContext(propertyMetadata);
validatorProvider.GetValidators(validatorProviderContext);
var requiredValidator = validatorProviderContext.Validators.FirstOrDefault(v => v.IsRequired);
var testableBinder = new TestableMutableObjectModelBinder(); var testableBinder = new TestableMutableObjectModelBinder();
// Act // Act
@ -1524,8 +1588,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator);
// Assert // Assert
Assert.True(bindingContext.ModelState.IsValid); Assert.True(bindingContext.ModelState.IsValid);
@ -1560,8 +1623,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator: null);
// Assert // Assert
Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine
@ -1569,8 +1631,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState["foo"].Errors[0].Exception.Message); bindingContext.ModelState["foo"].Errors[0].Exception.Message);
} }
// This can only really be done by writing an invalid model binder and returning 'isModelSet: true'
// with a null model for a value type.
[Fact] [Fact]
public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorNotPresent_RegistersValidationCallback() public void SetProperty_SettingNonNullableValueTypeToNull_CapturesException()
{ {
// Arrange // Arrange
var model = new Person(); var model = new Person();
@ -1583,9 +1647,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dtoResult = new ModelBindingResult( var dtoResult = new ModelBindingResult(
model: null, model: null,
isModelSet: true, isModelSet: true,
key: "foo"); key: "foo.DateOfBirth");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
var testableBinder = new TestableMutableObjectModelBinder(); var testableBinder = new TestableMutableObjectModelBinder();
@ -1594,50 +1656,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator);
// Assert // Assert
Assert.False(bindingContext.ModelState.IsValid); Assert.False(bindingContext.ModelState.IsValid);
}
[Fact] var entry = Assert.Single(bindingContext.ModelState, kvp => kvp.Key == "foo.DateOfBirth").Value;
public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorPresent_AddsModelError() var error = Assert.Single(entry.Errors);
{ Assert.Equal(string.Empty, error.ErrorMessage);
// Arrange Assert.IsType<NullReferenceException>(error.Exception);
var model = new Person();
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
bindingContext.ModelName = " foo";
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model);
var propertyMetadata = bindingContext.ModelMetadata.Properties["ValueTypeRequired"];
var dtoResult = new ModelBindingResult(
model: null,
isModelSet: true,
key: "foo.ValueTypeRequired");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
var testableBinder = new TestableMutableObjectModelBinder();
// Act
testableBinder.SetProperty(
bindingContext,
modelExplorer,
propertyMetadata,
dtoResult,
requiredValidator);
// Assert
Assert.False(bindingContext.ModelState.IsValid);
Assert.Equal("Sample message", bindingContext.ModelState["foo.ValueTypeRequired"].Errors[0].ErrorMessage);
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void SetProperty_SettingNullableTypeToNull_RequiredValidatorNotPresent_PropertySetterThrows_AddsRequiredMessageString() public void SetProperty_PropertySetterThrows_CapturesException()
{ {
// Arrange // Arrange
var model = new ModelWhosePropertySetterThrows(); var model = new ModelWhosePropertySetterThrows();
@ -1645,7 +1677,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName = "foo"; bindingContext.ModelName = "foo";
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model); var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(ModelWhosePropertySetterThrows), model);
var propertyMetadata = bindingContext.ModelMetadata.Properties["NameNoAttribute"]; var propertyMetadata = bindingContext.ModelMetadata.Properties["NameNoAttribute"];
var dtoResult = new ModelBindingResult( var dtoResult = new ModelBindingResult(
@ -1653,8 +1685,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
isModelSet: true, isModelSet: true,
key: "foo.NameNoAttribute"); key: "foo.NameNoAttribute");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
var testableBinder = new TestableMutableObjectModelBinder(); var testableBinder = new TestableMutableObjectModelBinder();
// Act // Act
@ -1662,8 +1692,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator);
// Assert // Assert
Assert.False(bindingContext.ModelState.IsValid); Assert.False(bindingContext.ModelState.IsValid);
@ -1673,41 +1702,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message); bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message);
} }
[Fact]
public void SetProperty_SettingNullableTypeToNull_RequiredValidatorPresent_PropertySetterThrows_AddsRequiredMessageString()
{
// Arrange
var model = new ModelWhosePropertySetterThrows();
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
bindingContext.ModelName = "foo";
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(Person), model);
var propertyMetadata = bindingContext.ModelMetadata.Properties["Name"];
var dtoResult = new ModelBindingResult(
model: null,
isModelSet: true,
key: "foo.Name");
var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata);
var testableBinder = new TestableMutableObjectModelBinder();
// Act
testableBinder.SetProperty(
bindingContext,
modelExplorer,
propertyMetadata,
dtoResult,
requiredValidator);
// Assert
Assert.False(bindingContext.ModelState.IsValid);
var error = Assert.Single(bindingContext.ModelState["foo.Name"].Errors);
Assert.Equal("This message comes from the [Required] attribute.", error.ErrorMessage);
}
private static ModelBindingContext CreateContext(ModelMetadata metadata, object model) private static ModelBindingContext CreateContext(ModelMetadata metadata, object model)
{ {
return new ModelBindingContext return new ModelBindingContext
@ -1841,6 +1835,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public string Optional { get; set; } public string Optional { get; set; }
} }
[BindRequired]
private class ModelWithBindRequiredAndRequiredAttribute
{
[Range(5, 20)]
[Required(ErrorMessage = "Custom Message {0}")]
public int ValueTypeProperty { get; set; }
[StringLength(25)]
[Required(ErrorMessage = "Custom Message {0}")]
public string ReferenceTypeProperty { get; set; }
}
private sealed class MyModelTestingCanUpdateProperty private sealed class MyModelTestingCanUpdateProperty
{ {
public int ReadOnlyInt { get; private set; } public int ReadOnlyInt { get; private set; }
@ -2030,15 +2036,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelBindingContext bindingContext, ModelBindingContext bindingContext,
ModelExplorer modelExplorer, ModelExplorer modelExplorer,
ModelMetadata propertyMetadata, ModelMetadata propertyMetadata,
ModelBindingResult dtoResult, ModelBindingResult dtoResult)
IModelValidator requiredValidator)
{ {
base.SetProperty( base.SetProperty(
bindingContext, bindingContext,
modelExplorer, modelExplorer,
propertyMetadata, propertyMetadata,
dtoResult, dtoResult);
requiredValidator);
} }
} }
} }

View File

@ -295,9 +295,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// This model sets a value for 'Title', and the model binder won't trounce it. // This model sets a value for 'Title', and the model binder won't trounce it.
// //
// There's a validation error because we validate the value in the request (not present). // There's no validation error because we validate the value on the model.
[Fact] [Fact]
public async Task FromHeader_BindHeader_ToModel_NoValues_InitializedValue_ValidationError() public async Task FromHeader_BindHeader_ToModel_NoValues_InitializedValue_NoValidationError()
{ {
// Arrange // Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices); var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
@ -321,8 +321,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("How to Make Soup", result.HeaderValue); Assert.Equal("How to Make Soup", result.HeaderValue);
Assert.Equal<string>(new[] { "Cooking" }, result.HeaderValues); Assert.Equal<string>(new[] { "Cooking" }, result.HeaderValues);
var error = Assert.Single(result.ModelStateErrors); Assert.Empty(result.ModelStateErrors);
Assert.Equal("Title", error);
} }
private class Result private class Result

View File

@ -7,7 +7,7 @@
<div> <div>
<label class="order" for="Number">Number</label> <label class="order" for="Number">Number</label>
<input class="form-control input-validation-error" type="number" data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" id="Number" name="Number" value="" /> <input class="form-control input-validation-error" type="number" data-val="true" data-val-range="The field Number must be between 1 and 100." data-val-range-max="100" data-val-range-min="1" id="Number" name="Number" value="" />
<span class="field-validation-error" data-valmsg-for="Number" data-valmsg-replace="true">A value is required.</span> <span class="field-validation-error" data-valmsg-for="Number" data-valmsg-replace="true">The value &#x27;&#x27; is invalid.</span>
</div> </div>
<div> <div>
<label class="order" for="Name">Name</label> <label class="order" for="Name">Name</label>
@ -33,8 +33,7 @@
<input type="radio" value="Female" id="Gender" name="Gender" /> Female <input type="radio" value="Female" id="Gender" name="Gender" /> Female
<span class="field-validation-valid" data-valmsg-for="Gender" data-valmsg-replace="true"></span> <span class="field-validation-valid" data-valmsg-for="Gender" data-valmsg-replace="true"></span>
</div> </div>
<div class="order validation-summary-errors" data-valmsg-summary="true"><ul><li>A value is required.</li> <div class="order validation-summary-errors" data-valmsg-summary="true"><ul><li>The Password field is required.</li>
<li>The Password field is required.</li>
</ul></div> </ul></div>
<div class="order validation-summary-errors"><ul><li style="display:none"></li> <div class="order validation-summary-errors"><ul><li style="display:none"></li>
</ul></div> </ul></div>

View File

@ -55,7 +55,7 @@
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="JoinDate">JoinDate</label> <label class="control-label col-md-2" for="JoinDate">JoinDate</label>
<div class="col-md-10"> <div class="col-md-10">
<input class="form-control input-validation-error" type="date" data-val="true" data-val-required="The JoinDate field is required." id="JoinDate" name="JoinDate" value="0001-01-01" /> <input class="form-control input-validation-error" type="date" data-val="true" data-val-required="The JoinDate field is required." id="JoinDate" name="JoinDate" value="" />
<span class="field-validation-error" data-valmsg-for="JoinDate" data-valmsg-replace="true">The JoinDate field is required.</span> <span class="field-validation-error" data-valmsg-for="JoinDate" data-valmsg-replace="true">The JoinDate field is required.</span>
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@ namespace TagHelpersWebSite.Models
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTimeOffset JoinDate { get; set; } public DateTimeOffset? JoinDate { get; set; }
[DataType(DataType.EmailAddress)] [DataType(DataType.EmailAddress)]
public string Email { get; set; } public string Email { get; set; }