From 7b18d1d3f1f04ba8952b13fd2b0f903e684bf78d Mon Sep 17 00:00:00 2001 From: Kirthi Krishnamraju Date: Mon, 2 Mar 2015 22:56:34 -0800 Subject: [PATCH] Clear ModelState errors of model before TryValidateModel or TryUpdateModel --- src/Microsoft.AspNet.Mvc.Core/Controller.cs | 10 +- .../ParameterBinding/ModelBindingHelper.cs | 63 ++++++ .../Internal/DictionaryHelper.cs | 11 ++ .../ModelStateDictionary.cs | 18 ++ .../ModelBindingHelperTest.cs | 140 ++++++++++++++ .../ModelBindingTest.cs | 17 ++ .../TryValidateModelTest.cs | 34 +++- .../Validation/ModelStateDictionaryTest.cs | 179 +++++++++++++++--- .../Controllers/TryUpdateModelController.cs | 45 ++++- .../ModelMetadataTypeAttributeController.cs | 10 +- 10 files changed, 500 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index fbca4e2fea..f33342ef2c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -910,7 +910,7 @@ namespace Microsoft.AspNet.Mvc public virtual Task TryUpdateModelAsync([NotNull] TModel model) where TModel : class { - return TryUpdateModelAsync(model, prefix: null); + return TryUpdateModelAsync(model, prefix: string.Empty); } /// @@ -1240,6 +1240,14 @@ namespace Microsoft.AspNet.Mvc var modelExplorer = MetadataProvider.GetModelExplorerForType(model.GetType(), model); var modelName = prefix ?? string.Empty; + + // Clear ModelStateDictionary entries for the model so that it will be re-validated. + ModelBindingHelper.ClearValidationStateForModel( + model.GetType(), + ModelState, + MetadataProvider, + modelName); + var validationContext = new ModelValidationContext( modelName, BindingContext.ValidatorProvider, diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs index b4954a3fe4..d27d5217ed 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs @@ -2,6 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -254,6 +257,9 @@ namespace Microsoft.AspNet.Mvc var modelMetadata = metadataProvider.GetMetadataForType(modelType); + // Clear ModelStateDictionary entries for the model so that it will be re-validated. + ClearValidationStateForModel(modelType, modelState, metadataProvider, prefix); + var operationBindingContext = new OperationBindingContext { ModelBinder = modelBinder, @@ -381,5 +387,62 @@ namespace Microsoft.AspNet.Mvc return prefix + "." + propertyName; } } + + /// + /// Clears entries for . + /// + /// The . + /// The entry to clear. + /// The . + public static void ClearValidationStateForModel( + [NotNull] Type modelType, + [NotNull] ModelStateDictionary modelstate, + [NotNull] IModelMetadataProvider metadataProvider, + string modelKey) + { + // If modelkey is empty, we need to iterate through properties (obtained from ModelMetadata) and + // clear validation state for all entries in ModelStateDictionary that start with each property name. + // If modelkey is non-empty, clear validation state for all entries in ModelStateDictionary + // that start with modelKey + if (string.IsNullOrEmpty(modelKey)) + { + var modelMetadata = metadataProvider.GetMetadataForType(modelType); + if (modelMetadata.IsCollectionType) + { + var elementType = GetElementType(modelMetadata.ModelType); + modelMetadata = metadataProvider.GetMetadataForType(elementType); + } + + foreach (var property in modelMetadata.Properties) + { + var childKey = property.BinderModelName ?? property.PropertyName; + modelstate.ClearValidationState(childKey); + } + } + else + { + modelstate.ClearValidationState(modelKey); + } + } + + private static Type GetElementType(Type type) + { + Debug.Assert(typeof(IEnumerable).IsAssignableFrom(type)); + if (type.IsArray) + { + return type.GetElementType(); + } + + foreach (var implementedInterface in type.GetInterfaces()) + { + if (implementedInterface.IsGenericType() && + implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return implementedInterface.GetGenericArguments()[0]; + } + } + + return typeof(object); + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs index 6aeb6aa6f5..6f4726b720 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/DictionaryHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding.Internal @@ -28,6 +29,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal continue; } + if (key.StartsWith("[", StringComparison.OrdinalIgnoreCase)) + { + key = key.Substring(key.IndexOf('.') + 1); + if (string.Equals(prefix, key, StringComparison.Ordinal)) + { + yield return entry; + continue; + } + } + if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { continue; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs index c2f2c15f30..6dbd77bc6b 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelStateDictionary.cs @@ -369,6 +369,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetModelStateForKey(key).Value = value; } + /// + /// Clears entries that match the key that is passed as parameter. + /// + /// The key of to clear. + 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 : DictionaryHelper.FindKeysWithPrefix(this, key); + + foreach (var entry in entries) + { + entry.Value.Errors.Clear(); + entry.Value.ValidationState = ModelValidationState.Unvalidated; + } + } + private ModelState GetModelStateForKey([NotNull] string key) { ModelState modelState; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs index d08091a7d1..32d474f71f 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs @@ -6,6 +6,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; @@ -667,6 +668,126 @@ namespace Microsoft.AspNet.Mvc.Core.Test Assert.Equal(expectedMessage, exception.Message); } + [Theory] + [InlineData("")] + [InlineData(null)] + public void ClearValidationStateForModel_EmtpyModelKey(string modelKey) + { + // Arrange + var metadataProvider = new EmptyModelMetadataProvider(); + var dictionary = new ModelStateDictionary(); + dictionary["Name"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Name", "MyProperty invalid."); + dictionary["Id"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Id", "Id invalid."); + dictionary.AddModelError("Id", "Id is required."); + dictionary["Category"] = new ModelState { ValidationState = ModelValidationState.Valid }; + + // Act + ModelBindingHelper.ClearValidationStateForModel( + typeof(Product), + dictionary, + metadataProvider, + modelKey); + + // Assert + Assert.Equal(0, dictionary["Name"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Name"].ValidationState); + Assert.Equal(0, dictionary["Id"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Id"].ValidationState); + Assert.Equal(0, dictionary["Category"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Category"].ValidationState); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void ClearValidationStateForCollectionsModel_EmtpyModelKey(string modelKey) + { + // Arrange + var metadataProvider = new EmptyModelMetadataProvider(); + var dictionary = new ModelStateDictionary(); + dictionary["[0].Name"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("[0].Name", "Name invalid."); + dictionary["[0].Id"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("[0].Id", "Id invalid."); + dictionary.AddModelError("[0].Id", "Id required."); + dictionary["[0].Category"] = new ModelState { ValidationState = ModelValidationState.Valid }; + + dictionary["[1].Name"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary["[1].Id"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary["[1].Category"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("[1].Category", "Category invalid."); + + // Act + ModelBindingHelper.ClearValidationStateForModel( + typeof(List), + dictionary, + metadataProvider, + modelKey); + + // Assert + Assert.Equal(0, dictionary["[0].Name"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["[0].Name"].ValidationState); + Assert.Equal(0, dictionary["[0].Id"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["[0].Id"].ValidationState); + Assert.Equal(0, dictionary["[0].Category"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["[0].Category"].ValidationState); + Assert.Equal(0, dictionary["[1].Name"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["[1].Name"].ValidationState); + Assert.Equal(0, dictionary["[1].Id"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["[1].Id"].ValidationState); + Assert.Equal(0, dictionary["[1].Category"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["[1].Category"].ValidationState); + } + + [Theory] + [InlineData("product")] + [InlineData("product.Name")] + [InlineData("product.Order[0].Name")] + [InlineData("product.Order[0].Address.Street")] + [InlineData("product.Category.Name")] + [InlineData("product.Order")] + public void ClearValidationStateForModel_NonEmtpyModelKey(string prefix) + { + // Arrange + var metadataProvider = new TestModelMetadataProvider(); + + var dictionary = new ModelStateDictionary(); + dictionary["product.Name"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("product.Name", "Name invalid."); + dictionary["product.Id"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("product.Id", "Id invalid."); + dictionary.AddModelError("product.Id", "Id required."); + dictionary["product.Category"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary["product.Category.Name"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary["product.Order[0].Name"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("product.Order[0].Name", "Order name invalid."); + dictionary["product.Order[0].Address.Street"] = + new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("product.Order[0].Address.Street", "Street invalid."); + dictionary["product.Order[1].Name"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary["product.Order[0]"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("product.Order[0]", "Order invalid."); + + // Act + ModelBindingHelper.ClearValidationStateForModel( + typeof(Product), + dictionary, + metadataProvider, + prefix); + + // Assert + foreach (var entry in dictionary.Keys) + { + if (entry.StartsWith(prefix)) + { + Assert.Equal(0, dictionary[entry].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary[entry].ValidationState); + } + } + } + private static IModelBinder GetCompositeBinder(params IModelBinder[] binders) { return new CompositeModelBinder(binders); @@ -710,6 +831,25 @@ namespace Microsoft.AspNet.Mvc.Core.Test public string ExcludedProperty { get; set; } } + + private class Product + { + public string Name { get; set; } + public int Id { get; set; } + public Category Category { get; set; } + public List Orders { get; set; } + } + + public class Category + { + public string Name { get; set; } + } + + public class Order + { + public string Name { get; set; } + public Address Address { get; set; } + } } } #endif diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs index 18d9bf2a49..e550a7c1d1 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs @@ -2066,6 +2066,23 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.False(result[1].Checked); } + [Fact] + public async Task TryUpdateModel_ClearsModelStateEntries() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + var url = "http://localhost/TryUpdateModel/TryUpdateModel_ClearsModelStateEntries"; + + // Act + var response = await client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(string.Empty, body); + } + private async Task ReadValue(HttpResponseMessage response) { Assert.True(response.IsSuccessStatusCode); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs index b048491c34..e60ed11c6a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("CompanyName cannot be null or empty.", json["product.CompanyName"]); Assert.Equal("The field Price must be between 20 and 100.", json["product.Price"]); Assert.Equal("The Category field is required.", json["product.Category"]); - Assert.Equal("The field Contact Us must be a string with a maximum length of 20."+ + Assert.Equal("The field Contact Us must be a string with a maximum length of 20." + "The field Contact Us must match the regular expression '^[0-9]*$'.", json["product.Contact"]); Assert.Equal("CompanyName cannot be null or empty.", json["CompanyName"]); Assert.Equal("The field Price must be between 20 and 100.", json["Price"]); @@ -86,5 +86,37 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await response.Content.ReadAsStringAsync(); Assert.Equal("{}", body); } + + [Fact] + public async Task TryValidateModel_CollectionsModel_ReturnsErrorsForInvalidProperties() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + var input = "[ { \"Price\": 2, \"Contact\": \"acvrdzersaererererfdsfdsfdsfsdf\", " + + "\"ProductDetails\": {\"Detail1\": \"d1\", \"Detail2\": \"d2\", \"Detail3\": \"d3\"} }," + + "{\"Price\": 2, \"Contact\": \"acvrdzersaererererfdsfdsfdsfsdf\", " + + "\"ProductDetails\": {\"Detail1\": \"d1\", \"Detail2\": \"d2\", \"Detail3\": \"d3\"} }]"; + var content = new StringContent(input, Encoding.UTF8, "application/json"); + var url = + "http://localhost/ModelMetadataTypeValidation/TryValidateModelWithCollectionsModel"; + + // Act + var response = await client.PostAsync(url, content); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + var json = JsonConvert.DeserializeObject>(body); + Assert.Equal("CompanyName cannot be null or empty.", json["[0].CompanyName"]); + Assert.Equal("The field Price must be between 20 and 100.", json["[0].Price"]); + Assert.Equal("The Category field is required.", json["[0].Category"]); + Assert.Equal("The field Contact Us must be a string with a maximum length of 20." + + "The field Contact Us must match the regular expression '^[0-9]*$'.", json["[0].Contact"]); + Assert.Equal("CompanyName cannot be null or empty.", json["[1].CompanyName"]); + Assert.Equal("The field Price must be between 20 and 100.", json["[1].Price"]); + Assert.Equal("The Category field is required.", json["[1].Category"]); + Assert.Equal("The field Contact Us must be a string with a maximum length of 20." + + "The field Contact Us must match the regular expression '^[0-9]*$'.", json["[1].Contact"]); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelStateDictionaryTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelStateDictionaryTest.cs index 46eedc3ad3..09ab10d0eb 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelStateDictionaryTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/ModelStateDictionaryTest.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; +using System.Collections.Generic; using System.Globalization; using Microsoft.Framework.Internal; using Xunit; @@ -238,7 +239,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [Fact] public void GetValidationState_ReturnsValidationStateForKey_IgnoresChildren() { - // Arrange + // Arrange var msd = new ModelStateDictionary(); msd.AddModelError("foo.bar", "error text"); @@ -249,12 +250,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Equal(ModelValidationState.Unvalidated, validationState); } - [Fact] - public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors() + [Theory] + [InlineData("foo")] + [InlineData("foo.bar")] + [InlineData("[0].foo.bar")] + [InlineData("[0].foo.bar[0]")] + public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors(string key) { // Arrange var msd = new ModelStateDictionary(); - msd.AddModelError("foo.bar", "error text"); + msd.AddModelError(key, "error text"); // Act var validationState = msd.GetFieldValidationState("foo"); @@ -263,22 +268,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Equal(ModelValidationState.Invalid, validationState); } - [Fact] - public void GetFieldValidationState_ReturnsInvalidIfKeyContainsErrors() - { - // Arrange - var msd = new ModelStateDictionary(); - msd.AddModelError("foo", "error text"); - - // Act - var validationState = msd.GetFieldValidationState("foo"); - - // Assert - Assert.Equal(ModelValidationState.Invalid, validationState); - } - - [Fact] - public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors() + [Theory] + [InlineData("foo")] + [InlineData("foo.bar")] + [InlineData("[0].foo.bar")] + [InlineData("[0].foo.bar[0]")] + public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key) { // Arrange var validState = new ModelState @@ -288,7 +283,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var msd = new ModelStateDictionary { - { "foo", validState } + { key, validState } }; // Act @@ -487,6 +482,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [Theory] [InlineData("user")] [InlineData("user.Age")] + [InlineData("product")] public void GetFieldValidity_ReturnsInvalid_IfAllKeysAreValidatedAndAnyEntryIsInvalid(string key) { // Arrange @@ -494,6 +490,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding dictionary["user.Address"] = new ModelState { ValidationState = ModelValidationState.Valid }; dictionary["user.Name"] = new ModelState { ValidationState = ModelValidationState.Valid }; dictionary.AddModelError("user.Age", "Age is not a valid int"); + dictionary["[0].product.Name"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary["[0].product.Age[0]"] = new ModelState { ValidationState = ModelValidationState.Valid }; + dictionary.AddModelError("[1].product.Name", "Name is invalid"); // Act var validationState = dictionary.GetFieldValidationState(key); @@ -733,6 +732,142 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Empty(error.ErrorMessage); } + [Fact] + public void ModelStateDictionary_ClearEntriesThatMatchWithKey_NonEmptyKey() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + dictionary["Property1"] = new ModelState { ValidationState = ModelValidationState.Valid }; + + dictionary["Property2"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Property2", "Property2 invalid."); + + dictionary["Property3"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Property3", "Property invalid."); + + dictionary["Property4"] = new ModelState { ValidationState = ModelValidationState.Skipped }; + + // Act + dictionary.ClearValidationState("Property1"); + dictionary.ClearValidationState("Property2"); + dictionary.ClearValidationState("Property4"); + + // Assert + Assert.Equal(0, dictionary["Property1"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property1"].ValidationState); + Assert.Equal(0, dictionary["Property2"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property2"].ValidationState); + Assert.Equal(1, dictionary["Property3"].Errors.Count); + Assert.Equal(ModelValidationState.Invalid, dictionary["Property3"].ValidationState); + Assert.Equal(0, dictionary["Property4"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState); + } + + [Fact] + public void ModelStateDictionary_ClearEntriesPrefixedWithKey_NonEmptyKey() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + dictionary["Product"] = new ModelState { ValidationState = ModelValidationState.Valid }; + + dictionary["Product.Detail1"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid."); + + dictionary["Product.Detail2[0]"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Product.Detail2[0]", "Product Detail2[0] invalid."); + + dictionary["Product.Detail2[1]"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Product.Detail2[1]", "Product Detail2[1] invalid."); + + dictionary["Product.Detail2[2]"] = new ModelState { ValidationState = ModelValidationState.Skipped }; + + dictionary["Product.Detail3"] = new ModelState { ValidationState = ModelValidationState.Skipped }; + + dictionary["ProductName"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("ProductName", "ProductName invalid."); + + // Act + dictionary.ClearValidationState("Product"); + + // Assert + Assert.Equal(0, dictionary["Product"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail1"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail1"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail2[0]"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail2[0]"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail2[1]"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail2[1]"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail2[2]"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail2[2]"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail3"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail3"].ValidationState); + Assert.Equal(1, dictionary["ProductName"].Errors.Count); + Assert.Equal(ModelValidationState.Invalid, dictionary["ProductName"].ValidationState); + } + + [Fact] + public void ModelStateDictionary_ClearEntries_KeyHasDot_NonEmptyKey() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + dictionary["Product"] = new ModelState { ValidationState = ModelValidationState.Valid }; + + dictionary["Product.Detail1"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Product.Detail1", "Product Detail1 invalid."); + + dictionary["Product.Detail1.Name"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Product.Detail1.Name", "Product Detail1 Name invalid."); + + dictionary["Product.Detail1Name"] = new ModelState { ValidationState = ModelValidationState.Skipped }; + + // Act + dictionary.ClearValidationState("Product.Detail1"); + + // Assert + Assert.Equal(ModelValidationState.Valid, dictionary["Product"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail1"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail1"].ValidationState); + Assert.Equal(0, dictionary["Product.Detail1.Name"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Product.Detail1.Name"].ValidationState); + Assert.Equal(ModelValidationState.Skipped, dictionary["Product.Detail1Name"].ValidationState); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void ModelStateDictionary_ClearsAllEntries_EmptyKey(string modelKey) + { + // Arrange + var dictionary = new ModelStateDictionary(); + + dictionary["Property1"] = new ModelState { ValidationState = ModelValidationState.Valid }; + + dictionary["Property2"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Property2", "Property2 invalid."); + + dictionary["Property3"] = new ModelState { ValidationState = ModelValidationState.Invalid }; + dictionary.AddModelError("Property3", "Property invalid."); + + dictionary["Property4"] = new ModelState { ValidationState = ModelValidationState.Skipped }; + + // Act + dictionary.ClearValidationState(modelKey); + + // Assert + Assert.Equal(0, dictionary["Property1"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property1"].ValidationState); + Assert.Equal(0, dictionary["Property2"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property2"].ValidationState); + Assert.Equal(0, dictionary["Property3"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property3"].ValidationState); + Assert.Equal(0, dictionary["Property4"].Errors.Count); + Assert.Equal(ModelValidationState.Unvalidated, dictionary["Property4"].ValidationState); + } + private static ValueProviderResult GetValueProviderResult(object rawValue = null, string attemptedValue = null) { return new ValueProviderResult(rawValue ?? "some value", @@ -740,4 +875,4 @@ namespace Microsoft.AspNet.Mvc.ModelBinding CultureInfo.InvariantCulture); } } -} +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs b/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs index 29a697cc3c..e0cc8730e4 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs @@ -2,12 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Threading.Tasks; +using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ModelBinding; -using System.Collections.Generic; -using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.WebUtilities; using ModelBindingWebSite.Models; namespace ModelBindingWebSite.Controllers @@ -158,6 +160,37 @@ namespace ModelBindingWebSite.Controllers return user; } + public async Task TryUpdateModel_ClearsModelStateEntries() + { + var result = new ObjectResult(null); + + // Invalid model. + var model = new MyModel + { + Id = 1, + Price = -1 + }; + + // Validate model first and subsequent TryUpdateModel should remove + //modelstate entries for model and re-validate. + TryValidateModel(model); + + // Update Name to a valid value and call TryUpdateModel + model.Price = 1; + await TryUpdateModelAsync(model); + + if (ModelState.IsValid) + { + result.StatusCode = StatusCodes.Status204NoContent; + } + else + { + result.StatusCode = StatusCodes.Status500InternalServerError; + } + + return result; + } + private User GetUser(int id) { return new User @@ -168,6 +201,14 @@ namespace ModelBindingWebSite.Controllers }; } + private class MyModel + { + public int Id { get; set; } + + [Range(0,10)] + public double Price { get; set; } + } + public class CustomValueProvider : IValueProvider { public Task ContainsPrefixAsync(string prefix) diff --git a/test/WebSites/ValidationWebSite/Controllers/ModelMetadataTypeAttributeController.cs b/test/WebSites/ValidationWebSite/Controllers/ModelMetadataTypeAttributeController.cs index 1bc77e607f..b09ef3ea57 100644 --- a/test/WebSites/ValidationWebSite/Controllers/ModelMetadataTypeAttributeController.cs +++ b/test/WebSites/ValidationWebSite/Controllers/ModelMetadataTypeAttributeController.cs @@ -31,13 +31,21 @@ namespace ValidationWebSite.Controllers { // Clear ModelState entry. TryValidateModel should not add entries except those found within the // passed model. - ModelState["theImpossibleString"].Errors.Clear(); + ModelState.ClearValidationState("theImpossibleString"); TryValidateModel(product); return CreateValidationDictionary(); } + [HttpPost] + public object TryValidateModelWithCollectionsModel([FromBody] List products) + { + TryValidateModel(products); + + return CreateValidationDictionary(); + } + [HttpGet] public object TryValidateModelSoftwareViewModelWithPrefix() {