diff --git a/Mvc.sln b/Mvc.sln index 82f2d7b3b8..3e4c137962 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22810.0 +VisualStudioVersion = 14.0.22808.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -166,6 +166,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExp EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExplorer.Test", "test\Microsoft.AspNet.Mvc.ApiExplorer.Test\Microsoft.AspNet.Mvc.ApiExplorer.Test.xproj", "{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Abstractions.Test", "test\Microsoft.AspNet.Mvc.Abstractions.Test\Microsoft.AspNet.Mvc.Abstractions.Test.xproj", "{DA000953-7532-4DF5-8DB9-8143DF98D999}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -996,6 +998,18 @@ Global {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|x86.ActiveCfg = Release|Any CPU {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}.Release|x86.Build.0 = Release|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|x86.Build.0 = Debug|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Any CPU.Build.0 = Release|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.ActiveCfg = Release|Any CPU + {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1077,5 +1091,6 @@ Global {1154203C-7579-4525-906E-BC55268421C1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {A2B72833-5D70-4C42-AE85-E0319926FB8A} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {DA000953-7532-4DF5-8DB9-8143DF98D999} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs index c0a822a7fc..abca9b58a5 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs @@ -281,7 +281,7 @@ public bool TryAddModelError([NotNull] string key, [NotNull] string errorMessage /// state errors; otherwise. public ModelValidationState GetFieldValidationState([NotNull] string key) { - var entries = FindKeysWithPrefix(this, key); + var entries = FindKeysWithPrefix(key); if (!entries.Any()) { return ModelValidationState.Unvalidated; @@ -378,7 +378,7 @@ public void ClearValidationState(string key) // If key is null or empty, clear all entries in the dictionary // else just clear the ones that have key as prefix var entries = (string.IsNullOrEmpty(key)) ? - _innerDictionary : FindKeysWithPrefix(this, key); + _innerDictionary : FindKeysWithPrefix(key); foreach (var entry in entries) { @@ -502,17 +502,15 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } - private static IEnumerable> FindKeysWithPrefix( - [NotNull] IDictionary dictionary, - [NotNull] string prefix) + public IEnumerable> FindKeysWithPrefix([NotNull] string prefix) { - TValue exactMatchValue; - if (dictionary.TryGetValue(prefix, out exactMatchValue)) + ModelState exactMatchValue; + if (_innerDictionary.TryGetValue(prefix, out exactMatchValue)) { - yield return new KeyValuePair(prefix, exactMatchValue); + yield return new KeyValuePair(prefix, exactMatchValue); } - foreach (var entry in dictionary) + foreach (var entry in _innerDictionary) { var key = entry.Key; @@ -521,21 +519,32 @@ private static IEnumerable> FindKeysWithPrefix(stringValue); - Assert.True(isModelStateValid); - } - - [Fact] - public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels() - { - // Arrange - var server = TestHelper.CreateServer(_app, SiteName, _configureServices); - var client = server.CreateClient(); - - // Make sure the body object gets created with an invalid zip. - var input = "{\"OfficeAddress.Zip\":\"45\"}"; - var content = new StringContent(input, Encoding.UTF8, "application/json"); - - // Act - // Make sure the non body based object gets created with an invalid zip. - var response = await client.PostAsync( - "http://localhost/Validation/SkipValidation?ShippingAddresses[0].Zip=45&HomeAddress.Zip=46", content); - - // Assert - var stringValue = await response.Content.ReadAsStringAsync(); - var isModelStateValid = JsonConvert.DeserializeObject(stringValue); - Assert.True(isModelStateValid); + var modelState = JsonConvert.DeserializeObject(response); + Assert.Empty(modelState); + Assert.True(modelState.IsValid); } [Fact] @@ -76,8 +56,8 @@ public async Task ModelValidation_DoesNotValidate_AnAlreadyValidatedObject() // Assert var stringValue = await response.Content.ReadAsStringAsync(); - var isModelStateValid = JsonConvert.DeserializeObject(stringValue); - Assert.True(isModelStateValid); + var json = JsonConvert.DeserializeObject(stringValue); + Assert.True(json.IsValid); } [Theory] diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs index 23f7c529aa..458de1dfe0 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs @@ -45,7 +45,7 @@ public async Task FromBodyAndRequiredOnProperty_EmptyBody_AddsModelStateError() var operationContext = ModelBindingTestHelper.GetOperationBindingContext( request => { - request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }")); + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)); request.ContentType = "application/json"; }); @@ -67,7 +67,7 @@ public async Task FromBodyAndRequiredOnProperty_EmptyBody_AddsModelStateError() } [Fact] - public async Task FromBodyOnActionParameter_EmptyBody_AddsModelStateError() + public async Task FromBodyOnActionParameter_EmptyBody_BindsToNullValue() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -85,7 +85,7 @@ public async Task FromBodyOnActionParameter_EmptyBody_AddsModelStateError() var operationContext = ModelBindingTestHelper.GetOperationBindingContext( request => { - request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }")); + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)); request.ContentType = "application/json"; }); @@ -98,13 +98,9 @@ public async Task FromBodyOnActionParameter_EmptyBody_AddsModelStateError() // Assert Assert.NotNull(modelBindingResult); Assert.True(modelBindingResult.IsModelSet); - var boundPerson = Assert.IsType(modelBindingResult.Model); - Assert.NotNull(boundPerson); - var key = Assert.Single(modelState.Keys); - Assert.Equal("Address", key); - Assert.False(modelState.IsValid); - var error = Assert.Single(modelState[key].Errors); - Assert.Equal("The Address field is required.",error.ErrorMessage); + Assert.Null(modelBindingResult.Model); + Assert.Empty(modelState.Keys); + Assert.True(modelState.IsValid); } private class Person4 diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs index 167cea94eb..30a25e1dd0 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Text; using System.Threading.Tasks; @@ -597,5 +598,60 @@ public async Task CollectionModelBinder_UsesCustomIndexes() Assert.Equal("Street2", entry.Value.AttemptedValue); Assert.Equal("Street2", entry.Value.RawValue); } + + private class Person5 + { + public IList Addresses { get; set; } + } + + private class Address5 + { + public int Zip { get; set; } + + [StringLength(3)] + public string Street { get; set; } + } + + [Fact] + public async Task CollectionModelBinder_UsesCustomIndexes_AddsErrorsWithCorrectKeys() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Person5) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => + { + var formCollection = new FormCollection(new Dictionary() + { + { "Addresses.index", new [] { "Key1" } }, + { "Addresses[Key1].Street", new [] { "Street1" } }, + }); + + request.Form = formCollection; + request.ContentType = "application/x-www-form-urlencoded"; + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + Assert.IsType(modelBindingResult.Model); + + Assert.Equal(1, modelState.Count); + Assert.Equal(1, modelState.ErrorCount); + Assert.False(modelState.IsValid); + + var entry = Assert.Single(modelState, kvp => kvp.Key == "Addresses[Key1].Street").Value; + var error = Assert.Single(entry.Errors); + Assert.Equal("The field Street must be a string with a maximum length of 3.", error.ErrorMessage); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs index 76f2d124e0..65ec766e19 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -49,21 +49,21 @@ public static OperationBindingContext GetOperationBindingContext( }; } - public static DefaultControllerActionArgumentBinder GetArgumentBinder() + public static DefaultControllerActionArgumentBinder GetArgumentBinder(MvcOptions options = null) { var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); return new DefaultControllerActionArgumentBinder( metadataProvider, - GetObjectValidator()); + GetObjectValidator(options)); } - public static IObjectModelValidator GetObjectValidator() + public static IObjectModelValidator GetObjectValidator(MvcOptions options = null) { - var options = new TestMvcOptions(); - options.Options.MaxModelValidationErrors = 5; + options = options ?? new TestMvcOptions().Options; + options.MaxModelValidationErrors = 5; var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); return new DefaultObjectValidator( - options.Options.ValidationExcludeFilters, + options.ValidationExcludeFilters, metadataProvider); } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs index c8f3d1407d..6d93979e37 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs @@ -273,7 +273,7 @@ private class Person2 public IScopedInstance BindingContext { get; set; } } - [Fact(Skip = "FromServices should not have an entry in model state #2464.")] + [Fact] public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithPrefix_Success() { // Arrange @@ -313,7 +313,7 @@ public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBind Assert.Equal("bill", entry.Value.RawValue); } - [Fact(Skip = "FromServices should not have an entry in model state #2464.")] + [Fact] public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithEmptyPrefix_Success() { // Arrange @@ -355,7 +355,7 @@ public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBind // We don't provide enough data in this test for the 'Person' model to be created. So even though there is // a [FromServices], it won't be used. - [Fact(Skip = "FromServices should not have an entry in model state #2464.")] + [Fact] public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBinder_WithPrefix_PartialData() { // Arrange @@ -427,7 +427,7 @@ public async Task MutableObjectModelBinder_BindsNestedPOCO_WithServicesModelBind var model = Assert.IsType(modelBindingResult.Model); Assert.Null(model.Customer); - Assert.Equal(0, modelState.Count); // Fails due to #2464 + Assert.Equal(0, modelState.Count); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs index e37516da6b..7cfb29dcd7 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs @@ -60,15 +60,10 @@ public async Task BindPropertyFromService_WithData_WithPrefix_GetsBound() // ModelState Assert.True(modelState.IsValid); - - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.OutputFormatter"); - Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); - Assert.Null(modelState[key].Value); - Assert.Empty(modelState[key].Errors); + Assert.Empty(modelState.Keys); } - [Fact(Skip = "Should be no entry for model bound using services. #2464")] + [Fact] public async Task BindPropertyFromService_WithData_WithEmptyPrefix_GetsBound() { // Arrange @@ -99,13 +94,12 @@ public async Task BindPropertyFromService_WithData_WithEmptyPrefix_GetsBound() // ModelState Assert.True(modelState.IsValid); - Assert.Equal(1, modelState.Keys.Count); - var key = Assert.Single(modelState.Keys, k => k == "Address"); - Assert.Null(modelState[key].Value); // For non user bound models there should be no value. - Assert.Empty(modelState[key].Errors); + + // For non user bound models there should be no entry in model state. + Assert.Empty(modelState); } - [Fact(Skip = "#2464 ModelState should not have entry for non request bound models.")] + [Fact] public async Task BindParameterFromService_WithData_GetsBound() { // Arrange diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs index b84112dcef..6b832ca281 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; @@ -1005,6 +1007,82 @@ public async Task Validation_StringLengthAttribute_OnProperyOfCollectionElement_ Assert.False(modelState.IsValid); } + private class Order11 + { + public IEnumerable
ShippingAddresses { get; set; } + + public Address HomeAddress { get; set; } + + [FromBody] + public Address OfficeAddress { get; set; } + } + + private class Address + { + public int Street { get; set; } + public string State { get; set; } + + [Range(10000, 99999)] + public int Zip { get; set; } + + public Country Country { get; set; } + } + + private class Country + { + public string Name { get; set; } + } + [Fact] + public async Task TypeBasedExclusion_ForBodyAndNonBodyBoundModels() + { + // Arrange + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Order11) + }; + + MvcOptions testOptions = null; + var input = "{\"OfficeAddress.Zip\":\"45\"}"; + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => + { + request.QueryString = + new QueryString("?HomeAddress.Country.Name=US&ShippingAddresses[0].Zip=45&HomeAddress.Zip=46"); + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input)); + request.ContentType = "application/json"; + }, + options => { + + options.ValidationExcludeFilters.Add(typeof(Address)); + testOptions = options; + }); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(testOptions); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + Assert.Equal(3, modelState.Count); + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Country.Name").Value; + Assert.Equal("US", entry.Value.AttemptedValue); + Assert.Equal("US", entry.Value.RawValue); + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + + entry = Assert.Single(modelState, e => e.Key == "ShippingAddresses[0].Zip").Value; + Assert.Equal("45", entry.Value.AttemptedValue); + Assert.Equal("45", entry.Value.RawValue); + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + + entry = Assert.Single(modelState, e => e.Key == "HomeAddress.Zip").Value; + Assert.Equal("46", entry.Value.AttemptedValue); + Assert.Equal("46", entry.Value.RawValue); + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + } + private static void AssertRequiredError(string key, ModelError error) { Assert.Equal(string.Format("The {0} field is required.", key), error.ErrorMessage); diff --git a/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs b/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs index 6994af1378..452b440f06 100644 --- a/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs +++ b/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs @@ -46,7 +46,6 @@ public string GetDeveloperName([FromBody] Developer developer) [HttpPost] public string GetDeveloperAlias(Developer developer) { - // Since validation exclusion is currently only effective in case of body bound models. if (ModelState.IsValid) { return developer.Alias; diff --git a/test/WebSites/ModelBindingWebSite/Controllers/ValidationController.cs b/test/WebSites/ModelBindingWebSite/Controllers/ValidationController.cs index db32d0bbd1..99f6b4904e 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/ValidationController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/ValidationController.cs @@ -14,22 +14,60 @@ public class ValidationController : Controller [FromServices] public ITestService ControllerService { get; set; } - public bool SkipValidation(Resident resident) + public object AvoidRecursive(SelfishPerson selfishPerson) { - return ModelState.IsValid; + return new SerializableModelStateDictionary(ModelState); } - public bool AvoidRecursive(SelfishPerson selfishPerson) + public object DoNotValidateParameter([FromServices] ITestService service) { - return ModelState.IsValid; + return ModelState; } + } + + public class SerializableModelStateDictionary : Dictionary + { + public bool IsValid { get; set; } + + public int ErrorCount { get; set; } - public bool DoNotValidateParameter([FromServices] ITestService service) + public SerializableModelStateDictionary(ModelStateDictionary modelState) { - return ModelState.IsValid; + var errorCount = 0; + foreach (var keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + var value = keyModelStatePair.Value; + errorCount += value.Errors.Count; + var entry = new Entry() + { + Errors = value.Errors, + RawValue = value.Value.RawValue, + AttemptedValue = value.Value.AttemptedValue, + ValidationState = value.ValidationState + }; + + Add(key, entry); + } + + IsValid = modelState.IsValid; + ErrorCount = errorCount; } } + public class Entry + { + public ModelValidationState ValidationState { get; set; } + + public ModelErrorCollection Errors { get; set; } + + public object RawValue { get; set; } + + public string AttemptedValue { get; set; } + + } + + public class SelfishPerson { public string Name { get; set; }