diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 3b4310e33e..3f6b1d99f1 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -92,16 +92,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return true; } - // 3. The model name is not prefixed and a value provider can directly provide a value for the model name. - // The fact that it is not prefixed means that the containsPrefixAsync call checks for the exact - // model name instead of doing a prefix match. - if (!bindingContext.ModelName.Contains(".") && - await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName)) - { - return true; - } - - // 4. Any of the model properties can be bound using a value provider. + // 3. Any of the model properties can be bound using a value provider. if (await CanValueBindAnyModelProperties(context)) { return true; diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs index 06821e8825..be33f0526a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs @@ -718,7 +718,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } [Fact] - public async Task ModelBinding_LimitsErrorsToMaxErrorCount() + public async Task ModelBinding_LimitsErrorsToMaxErrorCount_DoesNotValidateMembersOfMissingProperties() { // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); @@ -730,20 +730,21 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests //Assert var json = JsonConvert.DeserializeObject>(response); + // 8 is the value of MaxModelValidationErrors for the application being tested. Assert.Equal(8, json.Count); - Assert.Equal("The Field1 field is required.", json["Field1.Field1"]); - Assert.Equal("The Field2 field is required.", json["Field1.Field2"]); - Assert.Equal("The Field3 field is required.", json["Field1.Field3"]); - Assert.Equal("The Field1 field is required.", json["Field2.Field1"]); - Assert.Equal("The Field2 field is required.", json["Field2.Field2"]); - Assert.Equal("The Field3 field is required.", json["Field2.Field3"]); - Assert.Equal("The Field1 field is required.", json["Field3.Field1"]); + Assert.Equal("The Field1 field is required.", json["Field1"]); + Assert.Equal("The Field2 field is required.", json["Field2"]); + Assert.Equal("The Field3 field is required.", json["Field3"]); + Assert.Equal("The Field4 field is required.", json["Field4"]); + Assert.Equal("The Field5 field is required.", json["Field5"]); + Assert.Equal("The Field6 field is required.", json["Field6"]); + Assert.Equal("The Field7 field is required.", json["Field7"]); Assert.Equal("The maximum number of allowed model errors has been reached.", json[""]); } [Fact] - public async Task ModelBinding_ValidatesAllPropertiesInModel() + public async Task ModelBinding_FallsBackAndValidatesAllPropertiesInModel() { // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); @@ -752,12 +753,72 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Act var response = await client.GetStringAsync("http://localhost/Home/ModelWithFewValidationErrors?model="); - //Assert + // Assert var json = JsonConvert.DeserializeObject>(response); Assert.Equal(3, json.Count); - Assert.Equal("The Field1 field is required.", json["model.Field1"]); - Assert.Equal("The Field2 field is required.", json["model.Field2"]); - Assert.Equal("The Field3 field is required.", json["model.Field3"]); + Assert.Equal("The Field1 field is required.", json["Field1"]); + Assert.Equal("The Field2 field is required.", json["Field2"]); + Assert.Equal("The Field3 field is required.", json["Field3"]); + } + + [Fact] + public async Task ModelBinding_FallsBackAndSuccessfullyBindsStructCollection() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + var contentDictionary = new Dictionary + { + { "[0]", "23" }, + { "[1]", "97" }, + { "[2]", "103" }, + }; + var requestContent = new FormUrlEncodedContent(contentDictionary); + + // Act + var response = await client.PostAsync("http://localhost/integers", requestContent); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var responseContent = await response.Content.ReadAsStringAsync(); + var array = JsonConvert.DeserializeObject(responseContent); + + Assert.Equal(3, array.Length); + Assert.Equal(23, array[0]); + Assert.Equal(97, array[1]); + Assert.Equal(103, array[2]); + } + + [Fact] + public async Task ModelBinding_FallsBackAndSuccessfullyBindsPOCOCollection() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + var contentDictionary = new Dictionary + { + { "[0].CityCode", "YYZ" }, + { "[0].CityName", "Toronto" }, + { "[1].CityCode", "SEA" }, + { "[1].CityName", "Seattle" }, + }; + var requestContent = new FormUrlEncodedContent(contentDictionary); + + // Act + var response = await client.PostAsync("http://localhost/cities", requestContent); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var responseContent = await response.Content.ReadAsStringAsync(); + var list = JsonConvert.DeserializeObject>(responseContent); + + Assert.Equal(2, list.Count); + Assert.Equal(contentDictionary["[0].CityCode"], list[0].CityCode); + Assert.Equal(contentDictionary["[0].CityName"], list[0].CityName); + Assert.Equal(contentDictionary["[1].CityCode"], list[1].CityCode); + Assert.Equal(contentDictionary["[1].CityName"], list[1].CityName); } [Fact] @@ -1918,7 +1979,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } [Fact] - public async Task BindModelAsync_WithIncorrectlyFormattedNestedCollectionValue() + public async Task BindModelAsync_WithIncorrectlyFormattedNestedCollectionValue_BindsSingleNullEntry() { // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); @@ -1935,9 +1996,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert var result = await ReadValue(response); + + // Though posted content did not contain any valid Addresses, it is bound as a single-element List + // containing null. Slightly odd behavior is specific to this unusual error case: CollectionModelBinder + // attempted to bind a comma-separated string as a collection and Address lacks a from-string conversion. + // MutableObjectModelBinder does not create model when value providers have no data (unless at top level). var address = Assert.Single(result.Addresses); - Assert.Null(address.AddressLines); - Assert.Null(address.ZipCode); + Assert.Null(address); } [Fact] @@ -1979,7 +2044,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } [Fact] - public async Task BindModelAsync_WithNestedCollectionContainingRecursiveRelation_WithMalformedValue() + public async Task + BindModelAsync_WithNestedCollectionContainingRecursiveRelation_WithMalformedValue_BindsSingleNullEntry() { // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); @@ -1996,9 +2062,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert var result = await ReadValue(response); + + // Though posted content did not contain any valid People, it is bound as a single-element List + // containing null. Slightly odd behavior is specific to this unusual error case: CollectionModelBinder + // attempted to bind a comma-separated string as a collection and Address lacks a from-string conversion. + // MutableObjectModelBinder does not create model when value providers have no data (unless at top level). var person = Assert.Single(result.People); - Assert.Null(person.Name); - Assert.Null(person.Parent); + Assert.Null(person); } [Theory] diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs index 7f0f0a978f..5fc3692480 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/CompositeModelBinderTest.cs @@ -145,6 +145,127 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Null(result); } + [Fact] + public async Task ModelBinder_FallsBackToEmpty_IfBinderMatchesButDoesNotSetModel() + { + // Arrange + var bindingContext = new ModelBindingContext + { + FallbackToEmptyPrefix = true, + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), + ModelName = "someName", + ModelState = new ModelStateDictionary(), + ValueProvider = new SimpleHttpValueProvider + { + { "someOtherName", "dummyValue" } + }, + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = GetValidatorProvider() + } + }; + + var count = 0; + var modelBinder = new Mock(); + modelBinder + .Setup(mb => mb.BindModelAsync(It.IsAny())) + .Callback(context => + { + // Expect two calls; the second with empty ModelName. + Assert.InRange(count, 0, 1); + count++; + if (count == 1) + { + Assert.Equal("someName", context.ModelName); + } + else + { + Assert.Empty(context.ModelName); + } + }) + .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))); + + var composite = CreateCompositeBinder(modelBinder.Object); + + // Act & Assert + var result = await composite.BindModelAsync(bindingContext); + } + + [Fact] + public async Task ModelBinder_DoesNotFallBackToEmpty_IfFallbackToEmptyPrefixFalse() + { + // Arrange + var bindingContext = new ModelBindingContext + { + FallbackToEmptyPrefix = false, + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), + ModelName = "someName", + ModelState = new ModelStateDictionary(), + ValueProvider = new SimpleHttpValueProvider + { + { "someOtherName", "dummyValue" } + }, + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = GetValidatorProvider() + } + }; + + var modelBinder = new Mock(); + modelBinder + .Setup(mb => mb.BindModelAsync(It.IsAny())) + .Callback(context => + { + Assert.Equal("someName", context.ModelName); + }) + .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))) + .Verifiable(); + + var composite = CreateCompositeBinder(modelBinder.Object); + + // Act & Assert + var result = await composite.BindModelAsync(bindingContext); + modelBinder.Verify(mb => mb.BindModelAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ModelBinder_DoesNotFallBackToEmpty_IfErrorsAreAdded() + { + // Arrange + var bindingContext = new ModelBindingContext + { + FallbackToEmptyPrefix = false, + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), + ModelName = "someName", + ModelState = new ModelStateDictionary(), + ValueProvider = new SimpleHttpValueProvider + { + { "someOtherName", "dummyValue" } + }, + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = GetValidatorProvider() + } + }; + + var modelBinder = new Mock(); + modelBinder + .Setup(mb => mb.BindModelAsync(It.IsAny())) + .Callback(context => + { + Assert.Equal("someName", context.ModelName); + context.ModelState.AddModelError(context.ModelName, "this is an error message"); + }) + .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))) + .Verifiable(); + + var composite = CreateCompositeBinder(modelBinder.Object); + + // Act & Assert + var result = await composite.BindModelAsync(bindingContext); + modelBinder.Verify(mb => mb.BindModelAsync(It.IsAny()), Times.Once); + } + [Fact] public async Task ModelBinder_ReturnsTrue_SetsNullValue_SetsModelStateKey() { diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs index 7e7a689c4a..4039322c45 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs @@ -193,6 +193,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.True(retModel); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CanCreateModel_ReturnsTrue_ForNonTopLevelModel_BasedOnValueAvailability(bool valueAvailable) + { + // Arrange + var mockValueProvider = new Mock(MockBehavior.Strict); + mockValueProvider + .Setup(provider => provider.ContainsPrefixAsync("SimpleContainer.Simple.Name")) + .Returns(Task.FromResult(valueAvailable)); + + var typeMetadata = GetMetadataForType(typeof(SimpleContainer)); + var modelMetadata = typeMetadata.Properties[nameof(SimpleContainer.Simple)]; + var bindingContext = new MutableObjectBinderContext + { + ModelBindingContext = new ModelBindingContext + { + ModelMetadata = modelMetadata, + ModelName = "SimpleContainer.Simple", + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + ValueProvider = mockValueProvider.Object, + MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), + }, + ValueProvider = mockValueProvider.Object, + }, + PropertyMetadata = modelMetadata.Properties, + }; + + var mutableBinder = new MutableObjectModelBinder(); + + // Act + var result = await mutableBinder.CanCreateModel(bindingContext); + + // Assert + // Result matches whether first Simple property can bind. + Assert.Equal(valueAvailable, result); + } + [Theory] [InlineData(typeof(TypeWithNoBinderMetadata), false)] [InlineData(typeof(TypeWithNoBinderMetadata), true)] @@ -603,14 +643,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Arrange var expectedPropertyNames = new[] { - "DateOfBirth", - "DateOfDeath", - "ValueTypeRequired", - "FirstName", - "LastName", - "PropertyWithDefaultValue", - "PropertyWithInitializedValue", - "PropertyWithInitializedValueAndDefault", + nameof(Person.DateOfBirth), + nameof(Person.DateOfDeath), + nameof(Person.ValueTypeRequired), + nameof(Person.ValueTypeRequiredWithDefaultValue), + nameof(Person.FirstName), + nameof(Person.LastName), + nameof(Person.PropertyWithDefaultValue), + nameof(Person.PropertyWithInitializedValue), + nameof(Person.PropertyWithInitializedValueAndDefault), }; var bindingContext = new ModelBindingContext { @@ -707,6 +748,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties); } + [Fact] + public void GetPropertyValidationInfo_WithIndexerProperties_Succeeds() + { + // Arrange + var bindingContext = new ModelBindingContext + { + // Any type, even an otherwise-simple POCO with an indexer property, would do here. + ModelMetadata = GetMetadataForType(typeof(List)), + OperationBindingContext = new OperationBindingContext + { + ValidatorProvider = Mock.Of(), + }, + }; + + // Act + var validationInfo = MutableObjectModelBinder.GetPropertyValidationInfo(bindingContext); + + // Assert + Assert.Equal(Enumerable.Empty(), validationInfo.RequiredProperties); + Assert.Equal(Enumerable.Empty(), validationInfo.SkipProperties); + } + [Fact] [ReplaceCulture] public void ProcessDto_BindRequiredFieldMissing_RaisesModelError() @@ -842,7 +905,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Check Age error. ModelState modelState; - Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState)); + Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.Age), out modelState)); var modelError = Assert.Single(modelState.Errors); Assert.Null(modelError.Exception); @@ -851,7 +914,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Equal(expected, modelError.ErrorMessage); // Check City error. - Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState)); + Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.City), out modelState)); modelError = Assert.Single(modelState.Errors); Assert.Null(modelError.Exception); @@ -895,7 +958,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Check City error. ModelState modelState; - Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState)); + Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.City), out modelState)); var modelError = Assert.Single(modelState.Errors); Assert.Null(modelError.Exception); @@ -922,53 +985,156 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Assert var modelStateDictionary = bindingContext.ModelState; Assert.False(modelStateDictionary.IsValid); - Assert.Single(modelStateDictionary); + Assert.Equal(2, modelStateDictionary.Count); // Check ValueTypeRequired error. - ModelState modelState; - Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState)); + 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); } - [Fact] - public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage(bool isModelSet) { // 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(); + // ValueTypeRequiredWithDefaultValue value comes from [DefaultValue] when !isModelSet. + var expectedValue = isModelSet ? 0 : 42; + // Make ValueTypeRequired invalid. - var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "ValueTypeRequired"); + var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); dto.Results[propertyMetadata] = new ModelBindingResult( null, - isModelSet: true, - key: "theModel.ValueTypeRequired"); + isModelSet: isModelSet, + 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: isModelSet, + key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); // Act testableBinder.ProcessDto(bindingContext, dto); // Assert - var modelStateDictionary = bindingContext.ModelState; Assert.False(modelStateDictionary.IsValid); - Assert.Single(modelStateDictionary); // Check ValueTypeRequired error. - ModelState modelState; - Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState)); + 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); + + Assert.Equal(0, model.ValueTypeRequired); + Assert.Equal(expectedValue, model.ValueTypeRequiredWithDefaultValue); + } + + [Fact] + public void ProcessDto_ProvideRequiredFields_Success() + { + // 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(); + + // Make ValueTypeRequired valid. + var propertyMetadata = dto.PropertyMetadata + .Single(p => p.PropertyName == nameof(Person.ValueTypeRequired)); + dto.Results[propertyMetadata] = new ModelBindingResult( + 41, + isModelSet: true, + key: "theModel." + nameof(Person.ValueTypeRequired)); + + // Make ValueTypeRequiredWithDefaultValue valid. + propertyMetadata = dto.PropertyMetadata + .Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue)); + dto.Results[propertyMetadata] = new ModelBindingResult( + model: 57, + isModelSet: true, + key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue)); + + // Also remind ProcessDto about PropertyWithDefaultValue -- as ComplexModelDtoModelBinder would. + propertyMetadata = dto.PropertyMetadata + .Single(p => p.PropertyName == nameof(Person.PropertyWithDefaultValue)); + dto.Results[propertyMetadata] = new ModelBindingResult( + model: null, + isModelSet: false, + key: "theModel." + nameof(Person.PropertyWithDefaultValue)); + + // Act + testableBinder.ProcessDto(bindingContext, dto); + + // Assert + Assert.True(modelStateDictionary.IsValid); + Assert.Empty(modelStateDictionary); + + // Model gets provided values. + Assert.Equal(41, model.ValueTypeRequired); + Assert.Equal(57, model.ValueTypeRequiredWithDefaultValue); + Assert.Equal(123.456m, model.PropertyWithDefaultValue); // from [DefaultValue] } [Fact] @@ -1426,6 +1592,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [Required(ErrorMessage = "Sample message")] public int ValueTypeRequired { get; set; } + [Required(ErrorMessage = "Another sample message")] + [DefaultValue(42)] + public int ValueTypeRequiredWithDefaultValue { get; set; } + public string FirstName { get; set; } public string LastName { get; set; } public string NonUpdateableProperty { get; private set; } @@ -1593,6 +1763,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + public class SimpleContainer + { + public Simple Simple { get; set; } + } + + public class Simple + { + public string Name { get; set; } + } + private IServiceProvider CreateServices() { var services = new Mock(MockBehavior.Strict); diff --git a/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs b/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs index 37f8b13bbf..cb713a790b 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/HomeController.cs @@ -18,6 +18,18 @@ namespace ModelBindingWebSite.Controllers return Content(System.Text.Encoding.UTF8.GetString(byteValues)); } + [HttpPost("/integers")] + public IActionResult CollectionToJson(int[] model) + { + return Json(model); + } + + [HttpPost("/cities")] + public IActionResult PocoCollectionToJson(List model) + { + return Json(model); + } + public object ModelWithTooManyValidationErrors(LargeModelWithValidation model) { return CreateValidationDictionary(); diff --git a/test/WebSites/ModelBindingWebSite/Models/LargeModelWithValidation.cs b/test/WebSites/ModelBindingWebSite/Models/LargeModelWithValidation.cs index 62d08e2df1..71df957bf5 100644 --- a/test/WebSites/ModelBindingWebSite/Models/LargeModelWithValidation.cs +++ b/test/WebSites/ModelBindingWebSite/Models/LargeModelWithValidation.cs @@ -18,5 +18,17 @@ namespace ModelBindingWebSite.Models [Required] public ModelWithValidation Field4 { get; set; } + + [Required] + public ModelWithValidation Field5 { get; set; } + + [Required] + public ModelWithValidation Field6 { get; set; } + + [Required] + public ModelWithValidation Field7 { get; set; } + + [Required] + public ModelWithValidation Field8 { get; set; } } } \ No newline at end of file