Part 3 of #2776 - revert a6ce9abab1 and add

some more tests.

This change reverts the behavior change from
a6ce9abab1 and adds more tests around the
scneario that was actually broken.

The right behavior is that unconvertable values result in a validation
error. There's no special behavior around value types and required values.
This commit is contained in:
Ryan Nowak 2015-08-14 15:35:04 -07:00
parent 27f7f3d437
commit 07fabde92a
14 changed files with 162 additions and 89 deletions

View File

@ -38,11 +38,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
/// <summary>
/// Gets or sets a value indicating whether or not the request must contain a value for the model.
/// Will be ignored if the model metadata being created does not represent a property.
/// See <see cref="ModelMetadata.IsBindingRequired"/>. If <c>null</c>, the value of
/// <see cref="ModelMetadata.IsBindingRequired"/> will be computed based on
/// <see cref="ModelMetadata.ModelType"/>.
/// See <see cref="ModelMetadata.IsBindingRequired"/>.
/// </summary>
public bool? IsBindingRequired { get; set; }
public bool IsBindingRequired { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the model is read-only. Will be ignored

View File

@ -341,14 +341,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
_isBindingRequired = false;
}
else if (BindingMetadata.IsBindingRequired.HasValue)
{
_isBindingRequired = BindingMetadata.IsBindingRequired;
}
else
{
// Default to IsBindingRequired = true for value types.
_isBindingRequired = !AllowsNullValue(ModelType);
_isBindingRequired = BindingMetadata.IsBindingRequired;
}
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
return;
}
if (context.BindingMetadata.IsBindingRequired == true)
if (context.BindingMetadata.IsBindingRequired)
{
// This value is already required, no need to look at attributes.
return;

View File

@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
Assert.False(metadata.HideSurroundingHtml);
Assert.True(metadata.HtmlEncode);
Assert.True(metadata.IsBindingAllowed);
Assert.False(metadata.IsBindingRequired); // Defaults to false for reference types
Assert.False(metadata.IsBindingRequired);
Assert.False(metadata.IsComplexType);
Assert.False(metadata.IsCollectionType);
Assert.False(metadata.IsEnum);
@ -223,49 +223,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
Assert.False(isBindingRequired);
}
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(IDisposable))]
[InlineData(typeof(Nullable<int>))]
public void IsBindingRequired_ReturnsFalse_ForNullablePropertyTypes(Type modelType)
{
// Arrange
var provider = new EmptyModelMetadataProvider();
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForProperty(modelType, "Test", typeof(string));
var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0]));
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
// Act
var isBindingRequired = metadata.IsBindingRequired;
// Assert
Assert.False(isBindingRequired);
}
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(DayOfWeek))]
public void IsBindingRequired_ReturnsTrue_ForNonNullablePropertyTypes(Type modelType)
{
// Arrange
var provider = new EmptyModelMetadataProvider();
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForProperty(modelType, "Test", typeof(string));
var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0]));
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
// Act
var isBindingRequired = metadata.IsBindingRequired;
// Assert
Assert.True(isBindingRequired);
}
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(IDisposable))]

View File

@ -1050,7 +1050,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
[Fact]
public void ProcessDto_ValueTypeProperty_NoValue_Error()
public void ProcessDto_ValueTypeProperty_NoValue_NoError()
{
// Arrange
var model = new Person();
@ -1083,14 +1083,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
testableBinder.ProcessDto(bindingContext, dto, modelValidationNode);
// Assert
Assert.False(modelState.IsValid);
var entry = modelState["theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)];
var error = Assert.Single(entry.Errors);
Assert.Equal(
$"A value for the '{nameof(Person.ValueTypeRequiredWithDefaultValue)}' property was not provided.",
error.ErrorMessage);
Assert.Null(error.Exception);
Assert.True(modelState.IsValid);
}
[Fact]

View File

@ -196,12 +196,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Customer/HtmlGeneration_Customer");
var nameValueCollection = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string,string>("Number", "0"),
new KeyValuePair<string,string>("Number", string.Empty),
new KeyValuePair<string,string>("Name", string.Empty),
new KeyValuePair<string,string>("Email", string.Empty),
new KeyValuePair<string,string>("PhoneNumber", string.Empty),
new KeyValuePair<string,string>("Password", string.Empty),
new KeyValuePair<string, string>("Gender", "Female"),
new KeyValuePair<string,string>("Password", string.Empty)
};
request.Content = new FormUrlEncodedContent(nameValueCollection);

View File

@ -122,12 +122,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(body);
Assert.Collection(
result.ModelStateErrors,
e => Assert.Equal("TestEmployees[0].EmployeeId", e),
e => Assert.Equal("TestEmployees[0].EmployeeTaxId", e),
e => Assert.Equal("TestEmployees[0].Age", e));
var error = Assert.Single(result.ModelStateErrors);
Assert.Equal("TestEmployees[0].EmployeeId", error);
}
}
}

View File

@ -2218,7 +2218,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
var url = "http://localhost/TryUpdateModel/TryUpdateModel_ClearsModelStateEntries?id=5&price=1";
var url = "http://localhost/TryUpdateModel/TryUpdateModel_ClearsModelStateEntries";
// Act
var response = await client.GetAsync(url);

View File

@ -3,8 +3,8 @@
<form action="/Customer/HtmlGeneration_Customer" method="post">
<div>
<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" data-val-required="The Number field is required." id="Number" name="Number" value="0" />
<span class="field-validation-error" data-valmsg-for="Number" data-valmsg-replace="true">The field Number must be between 1 and 100.</span>
<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" data-val-required="The Number field is required." id="Number" name="Number" value="" />
<span class="field-validation-error" data-valmsg-for="Number" data-valmsg-replace="true">The value &#x27;&#x27; is invalid.</span>
</div>
<div>
<label class="order" for="Name">Name</label>
@ -27,10 +27,10 @@
<div>
<label class="order" for="Gender">Gender</label>
<input type="radio" value="Male" data-val="true" data-val-required="The Gender field is required." id="Gender" name="Gender" /> Male
<input type="radio" value="Female" checked="checked" 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>
</div>
<div class="order validation-summary-errors" data-valmsg-summary="true"><ul><li>The field Number must be between 1 and 100.</li>
<div class="order validation-summary-errors" data-valmsg-summary="true"><ul><li>The value &#x27;&#x27; is invalid.</li>
<li>The Password field is required.</li>
</ul></div>
<div class="order validation-summary-errors"><ul><li style="display:none"></li>

View File

@ -108,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Person4
{
[FromBody]
[BindingBehavior(BindingBehavior.Optional)]
[Required]
public int Address { get; set; }
}

View File

@ -552,7 +552,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Address4
{
[BindingBehavior(BindingBehavior.Optional)]
public int Zip { get; set; }
public string Street { get; set; }
@ -611,7 +610,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Address5
{
public int? Zip { get; set; }
public int Zip { get; set; }
[StringLength(3)]
public string Street { get; set; }

View File

@ -26,7 +26,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Order1
{
[BindingBehavior(BindingBehavior.Optional)]
public int ProductId { get; set; }
public Person1 Customer { get; set; }
@ -262,7 +261,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Order2
{
public int? ProductId { get; set; }
public int ProductId { get; set; }
public Person2 Customer { get; set; }
}
@ -436,7 +435,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Order3
{
[BindingBehavior(BindingBehavior.Optional)]
public int ProductId { get; set; }
public Person3 Customer { get; set; }
@ -580,7 +578,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Order4
{
public int? ProductId { get; set; }
public int ProductId { get; set; }
public Person4 Customer { get; set; }
}
@ -1343,7 +1341,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
{
public string Name { get; set; }
[BindingBehavior(BindingBehavior.Optional)]
public KeyValuePair<string, int> ProductId { get; set; }
}
@ -2033,6 +2030,93 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal("123", entry.Value.AttemptedValue);
}
private class Order14
{
public int ProductId { get; set; }
}
// This covers the case where a key is present, but has an empty value. The type converter
// will report an error.
[Fact]
public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyNonConvertableValue_GetsError()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Order14)
};
// Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.ProductId=");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Order14>(modelBindingResult.Model);
Assert.NotNull(model);
Assert.Equal(0, model.ProductId);
Assert.Equal(1, modelState.Count);
Assert.Equal(1, modelState.ErrorCount);
Assert.False(modelState.IsValid);
var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
Assert.Equal(string.Empty, entry.Value.AttemptedValue);
Assert.Equal(string.Empty, entry.Value.RawValue);
var error = Assert.Single(entry.Errors);
Assert.Equal("The value '' is invalid.", error.ErrorMessage);
Assert.Null(error.Exception);
}
// This covers the case where a key is present, but has no value. The type converter
// will report an error.
[Fact]
public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyWithNoValue_NoError()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Order14)
};
// Need to have a key here so that the MutableObjectModelBinder will recurse to bind elements.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter.ProductId");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Order14>(modelBindingResult.Model);
Assert.NotNull(model);
Assert.Equal(0, model.ProductId);
Assert.Equal(0, modelState.Count);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
private static void SetJsonBodyContent(HttpRequest request, string content)
{
var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content));

View File

@ -159,6 +159,58 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
}
[Fact]
public async Task BindParameter_NonConvertableValue_GetsError()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = typeof(int)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = QueryString.Create("Parameter1", "abcd");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
// ModelBindingResult
Assert.NotNull(modelBindingResult);
Assert.False(modelBindingResult.IsModelSet);
// Model
Assert.Null(modelBindingResult.Model);
// ModelState
Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.Count);
Assert.Equal(1, modelState.ErrorCount);
var key = Assert.Single(modelState.Keys);
Assert.Equal("Parameter1", key);
var entry = modelState[key];
Assert.NotNull(entry.Value);
Assert.Equal("abcd", entry.Value.AttemptedValue);
Assert.Equal("abcd", entry.Value.RawValue);
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception);
Assert.Equal("The value 'abcd' is not valid for Parameter1.", error.ErrorMessage);
}
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(bool))]

View File

@ -195,7 +195,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Person3
{
public int? Age { get; set; }
public int Age { get; set; }
[Required]
public string Name { get; set; }
@ -1018,12 +1018,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Address
{
public int? Street { get; set; }
public int Street { 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; }
}