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:
parent
27f7f3d437
commit
07fabde92a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 '' 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 '' is invalid.</li>
|
||||
<li>The Password field is required.</li>
|
||||
</ul></div>
|
||||
<div class="order validation-summary-errors"><ul><li style="display:none"></li>
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
|
|||
private class Person4
|
||||
{
|
||||
[FromBody]
|
||||
[BindingBehavior(BindingBehavior.Optional)]
|
||||
[Required]
|
||||
public int Address { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue