diff --git a/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/ValidationExcludeFiltersExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/ValidationExcludeFiltersExtensions.cs index ff644186ea..09d346918c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/ValidationExcludeFiltersExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/OptionDescriptors/ValidationExcludeFiltersExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.OptionDescriptors; namespace Microsoft.AspNet.Mvc @@ -13,33 +14,46 @@ namespace Microsoft.AspNet.Mvc public static class ValidationExcludeFiltersExtensions { /// - /// Adds a descriptor to the specified - /// that excludes the properties of the specified and it's derived types from validaton. + /// Adds a descriptor to the specified that excludes the properties of + /// the specified and its derived types from validaton. /// - /// A list of - /// which are used to get a collection of exclude filters to be applied for filtering model properties during validation. + /// A list of which are used to + /// get a collection of exclude filters to be applied for filtering model properties during validation. /// /// which should be excluded from validation. - public static void Add(this IList excludeBodyValidationDescriptorCollection, - Type type) + public static void Add(this IList descriptorCollection, Type type) { var typeBasedExcludeFilter = new DefaultTypeBasedExcludeFilter(type); - excludeBodyValidationDescriptorCollection.Add(new ExcludeValidationDescriptor(typeBasedExcludeFilter)); + descriptorCollection.Add(new ExcludeValidationDescriptor(typeBasedExcludeFilter)); } /// - /// Adds a descriptor to the specified - /// that excludes the properties of the type specified and it's derived types from validaton. + /// Adds a descriptor to the specified that excludes the properties of + /// the type specified and its derived types from validaton. /// - /// A list of - /// which are used to get a collection of exclude filters to be applied for filtering model properties during validation. + /// A list of which are used to + /// get a collection of exclude filters to be applied for filtering model properties during validation. /// /// Full name of the type which should be excluded from validation. - public static void Add(this IList excludeBodyValidationDescriptorCollection, - string typeFullName) + public static void Add(this IList descriptorCollection, string typeFullName) { var filter = new DefaultTypeNameBasedExcludeFilter(typeFullName); - excludeBodyValidationDescriptorCollection.Add(new ExcludeValidationDescriptor(filter)); + descriptorCollection.Add(new ExcludeValidationDescriptor(filter)); + } + + /// + /// Adds a descriptor to the specified that excludes the properties of + /// the type specified and its derived types from validaton. + /// + /// A list of which are used to + /// get a collection of exclude filters to be applied for filtering model properties during validation. + /// + /// which should be excluded from validation. + /// + public static void Add(this IList descriptorCollection, + IExcludeTypeValidationFilter filter) + { + descriptorCollection.Add(new ExcludeValidationDescriptor(filter)); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/SimpleTypesExcludeFilter.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/SimpleTypesExcludeFilter.cs new file mode 100644 index 0000000000..85316acd77 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/SimpleTypesExcludeFilter.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// 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.Reflection; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Identifies the simple types that the default model binding validation will exclude. + /// + public class SimpleTypesExcludeFilter : IExcludeTypeValidationFilter + { + /// + /// Returns true if the given type will be excluded from the default model validation. + /// + public bool IsTypeExcluded(Type type) + { + Type[] actualTypes; + + var enumerable = type.ExtractGenericInterface(typeof(IEnumerable<>)); + if (enumerable == null) + { + actualTypes = new Type[] { type }; + } + else + { + actualTypes = enumerable.GenericTypeArguments; + // The following special case is for IEnumerable>, + // supertype of IDictionary, and IReadOnlyDictionary. + if (actualTypes.Length == 1 + && actualTypes[0].IsGenericType() + && actualTypes[0].GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) + { + actualTypes = actualTypes[0].GenericTypeArguments; + } + } + + foreach (var actualType in actualTypes) + { + var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType; + + if (!IsSimpleType(underlyingType)) + { + return false; + } + } + + return true; + } + + /// + /// Returns true if the given type is the underlying types that will exclude. + /// + protected virtual bool IsSimpleType(Type type) + { + var result = type.GetTypeInfo().IsPrimitive || + type.Equals(typeof(decimal)) || + type.Equals(typeof(string)) || + type.Equals(typeof(DateTime)) || + type.Equals(typeof(Guid)) || + type.Equals(typeof(DateTimeOffset)) || + type.Equals(typeof(TimeSpan)) || + type.Equals(typeof(Uri)); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultBodyModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultBodyModelValidator.cs index d9ae190cab..ca3a9a7f0c 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultBodyModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultBodyModelValidator.cs @@ -56,9 +56,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // We don't need to recursively traverse the graph for types that shouldn't be validated var modelType = metadata.Model.GetType(); - if (TypeHelper.IsSimpleType(modelType) || - IsTypeExcludedFromValidation( - validationContext.ModelValidationContext.ExcludeFromValidationFilters, modelType)) + if (IsTypeExcludedFromValidation(validationContext.ModelValidationContext.ExcludeFromValidationFilters, modelType)) { return ShallowValidate(metadata, validationContext, validators); } diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 9f9e0da1ee..2be43e3715 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -58,9 +58,9 @@ namespace Microsoft.AspNet.Mvc options.ModelValidatorProviders.Add(new DataMemberModelValidatorProvider()); // Add types to be excluded from Validation + options.ValidationExcludeFilters.Add(new SimpleTypesExcludeFilter()); options.ValidationExcludeFilters.Add(typeof(XObject)); options.ValidationExcludeFilters.Add(typeof(Type)); - options.ValidationExcludeFilters.Add(typeof(byte[])); options.ValidationExcludeFilters.Add(typeof(JToken)); options.ValidationExcludeFilters.Add(typeFullName: "System.Xml.XmlNode"); } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputObjectValidationTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputObjectValidationTests.cs index 2291ba9081..600f899cd8 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputObjectValidationTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputObjectValidationTests.cs @@ -2,12 +2,15 @@ // 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.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNet.Mvc.FunctionalTests @@ -17,6 +20,28 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests private readonly IServiceProvider _services = TestHelper.CreateServices("FormatterWebSite"); private readonly Action _app = new FormatterWebSite.Startup().Configure; + // Parameters: Request Content, Expected status code, Expected model state error message + public static IEnumerable SimpleTypePropertiesModelRequestData + { + get + { + yield return new object[] { + "{\"ByteProperty\":1, \"NullableByteProperty\":5, \"ByteArrayProperty\":[1,2,3]}", + 400, + "The field ByteProperty must be between 2 and 8."}; + + yield return new object[] { + "{\"ByteProperty\":8, \"NullableByteProperty\":1, \"ByteArrayProperty\":[1,2,3]}", + 400, + "The field NullableByteProperty must be between 2 and 8."}; + + yield return new object[] { + "{\"ByteProperty\":8, \"NullableByteProperty\":2, \"ByteArrayProperty\":[1]}", + 400, + "The field ByteArrayProperty must be a string or array type with a minimum length of '2'."}; + } + } + [Fact] public async Task CheckIfObjectIsDeserializedWithoutErrors() { @@ -84,9 +109,64 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests //Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("No model validation for developer, even though developer.Name is empty.", await response.Content.ReadAsStringAsync()); + Assert.Equal("No model validation for developer, even though developer.Name is empty.", + await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task ShallowValidation_HappensOnExcluded_ComplexTypeProperties() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var requestData = "{\"Name\":\"Library Manager\", \"Suppliers\": [{\"Name\":\"Contoso Corp\"}]}"; + var content = new StringContent(requestData, Encoding.UTF8, "application/json"); + var expectedModelStateErrorMessage + = "The field Suppliers must be a string or array type with a minimum length of '2'."; + var shouldNotContainMessage + = "The field Name must be a string or array type with a maximum length of '5'."; + + // Act + var response = await client.PostAsync("http://localhost/Validation/CreateProject", content); + + //Assert + Assert.Equal(400, (int)response.StatusCode); + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObject = JsonConvert.DeserializeObject>(responseContent); + var errorCollection = Assert.Single(responseObject); + var error = Assert.Single(errorCollection.Value.Errors); + Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage); + + // verifies that the excluded type is not validated + Assert.NotEqual(shouldNotContainMessage, error.ErrorMessage); + } + + [Theory] + [MemberData(nameof(SimpleTypePropertiesModelRequestData))] + public async Task ShallowValidation_HappensOnExlcuded_SimpleTypeProperties( + string requestContent, + int expectedStatusCode, + string expectedModelStateErrorMessage) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var content = new StringContent(requestContent, Encoding.UTF8, "application/json"); + + // Act + var response = await client.PostAsync("http://localhost/Validation/CreateSimpleTypePropertiesModel", + content); + + //Assert + Assert.Equal(expectedStatusCode, (int)response.StatusCode); + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObject = JsonConvert.DeserializeObject>(responseContent); + var errorCollection = Assert.Single(responseObject); + var error = Assert.Single(errorCollection.Value.Errors); + Assert.Equal(expectedModelStateErrorMessage, error.ErrorMessage); + } [Fact] public async Task CheckIfExcludedField_IsValidatedForNonBodyBoundModels() @@ -103,5 +183,23 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("The Name field is required.", await response.Content.ReadAsStringAsync()); } + + private class ErrorCollection + { + public IEnumerable Errors + { + get; + set; + } + } + + private class Error + { + public string ErrorMessage + { + get; + set; + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/SimpleTypeExcludeFilterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/SimpleTypeExcludeFilterTest.cs new file mode 100644 index 0000000000..5918bc629d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/SimpleTypeExcludeFilterTest.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// 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 Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class SimpleTypeExcluceFilterTest + { + [Theory] + [MemberData(nameof(ExcludedTypes))] + public void SimpleTypeExcluceFilter_ExcludedTypes(Type type) + { + // Arrange + var filter = new SimpleTypesExcludeFilter(); + + // Act & Assert + Assert.True(filter.IsTypeExcluded(type)); + } + + [Theory] + [MemberData(nameof(IncludedTypes))] + public void SimpleTypeExcluceFilter_IncludedTypes(Type type) + { + // Arrange + var filter = new SimpleTypesExcludeFilter(); + + // Act & Assert + Assert.False(filter.IsTypeExcluded(type)); + } + + private class TestType + { + + } + + public static TheoryData ExcludedTypes + { + get + { + return new TheoryData() + { + // Simple types + typeof(int[]), + typeof(int), + typeof(List), + typeof(SortedSet), + + // Nullable types + typeof(ICollection), + typeof(int?[]), + typeof(SortedSet), + typeof(HashSet), + typeof(HashSet), + + // Value types + typeof(IList), + + // KeyValue types + typeof(Dictionary), + typeof(IReadOnlyDictionary) + }; + } + } + + public static TheoryData IncludedTypes + { + get + { + return new TheoryData() + { + typeof(TestType), + typeof(TestType[]), + typeof(SortedSet), + typeof(Dictionary), + typeof(Dictionary), + typeof(Dictionary) + }; + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index f6cdc38906..3e73de6703 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -133,21 +133,26 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(5, mvcOptions.ValidationExcludeFilters.Count); // Verify if the delegates registered by default exclude the given types. - Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[0].OptionType); - var instance = Assert.IsType(mvcOptions.ValidationExcludeFilters[0].Instance); - Assert.Equal(instance.ExcludedType, typeof(XObject)); + Assert.Equal(typeof(SimpleTypesExcludeFilter), mvcOptions.ValidationExcludeFilters[0].OptionType); Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[1].OptionType); - var instance2 = Assert.IsType(mvcOptions.ValidationExcludeFilters[1].Instance); - Assert.Equal(instance2.ExcludedType, typeof(Type)); + var xObjectFilter + = Assert.IsType(mvcOptions.ValidationExcludeFilters[1].Instance); + Assert.Equal(xObjectFilter.ExcludedType, typeof(XObject)); + Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[2].OptionType); - var instance3 = Assert.IsType(mvcOptions.ValidationExcludeFilters[2].Instance); - Assert.Equal(instance3.ExcludedType, typeof(byte[])); + var typeFilter = + Assert.IsType(mvcOptions.ValidationExcludeFilters[2].Instance); + Assert.Equal(typeFilter.ExcludedType, typeof(Type)); + Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[3].OptionType); - var instance4 = Assert.IsType(mvcOptions.ValidationExcludeFilters[3].Instance); - Assert.Equal(instance4.ExcludedType, typeof(JToken)); + var jTokenFilter + = Assert.IsType(mvcOptions.ValidationExcludeFilters[3].Instance); + Assert.Equal(jTokenFilter.ExcludedType, typeof(JToken)); + Assert.Equal(typeof(DefaultTypeNameBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[4].OptionType); - var instance5 = Assert.IsType(mvcOptions.ValidationExcludeFilters[4].Instance); - Assert.Equal(instance5.ExcludedTypeName, "System.Xml.XmlNode"); + var xmlNodeFilter = + Assert.IsType(mvcOptions.ValidationExcludeFilters[4].Instance); + Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode"); } } } \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/ActionResults/CustomObjectResult.cs b/test/WebSites/FormatterWebSite/ActionResults/CustomObjectResult.cs new file mode 100644 index 0000000000..f0ce75cd6c --- /dev/null +++ b/test/WebSites/FormatterWebSite/ActionResults/CustomObjectResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace FormatterWebSite +{ + public class CustomObjectResult : ObjectResult + { + public CustomObjectResult(object value, int statusCode) : base(value) + { + StatusCode = statusCode; + } + + public int StatusCode { get; private set; } + + public override Task ExecuteResultAsync(ActionContext context) + { + context.HttpContext.Response.StatusCode = StatusCode; + + return base.ExecuteResultAsync(context); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs b/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs index db3ffabd21..29778035e9 100644 --- a/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs +++ b/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using Microsoft.AspNet.Mvc; namespace FormatterWebSite @@ -54,5 +56,19 @@ namespace FormatterWebSite return ModelState["Name"].Errors[0].ErrorMessage; } } + + // 'Developer' type is excluded but the shallow validation on the + // property Developers should happen + [ModelStateValidationFilter] + public IActionResult CreateProject([FromBody]Project project) + { + return Json(project); + } + + [ModelStateValidationFilter] + public IActionResult CreateSimpleTypePropertiesModel([FromBody] SimpleTypePropertiesModel simpleTypePropertiesModel) + { + return Json(simpleTypePropertiesModel); + } } } \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Filters/ModelStateValidationFilterAttribute.cs b/test/WebSites/FormatterWebSite/Filters/ModelStateValidationFilterAttribute.cs new file mode 100644 index 0000000000..5f25ca5951 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Filters/ModelStateValidationFilterAttribute.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace FormatterWebSite +{ + public class ModelStateValidationFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new CustomObjectResult(context.ModelState, 400); + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Models/Project.cs b/test/WebSites/FormatterWebSite/Models/Project.cs new file mode 100644 index 0000000000..d3e53f0d17 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/Project.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace FormatterWebSite +{ + public class Project + { + public int Id { get; set; } + + public string Name { get; set; } + + [MinLength(2)] + [MaxLength(5)] + public Supplier[] Suppliers { get; set; } + } + + public class Supplier + { + public int Id { get; set; } + + [MaxLength(5)] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Models/SimpleTypePropertiesModel.cs b/test/WebSites/FormatterWebSite/Models/SimpleTypePropertiesModel.cs new file mode 100644 index 0000000000..878e465186 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/SimpleTypePropertiesModel.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace FormatterWebSite +{ + public class SimpleTypePropertiesModel + { + [Range(2, 8)] + public byte ByteProperty { get; set; } + + [Range(2, 8)] + public byte? NullableByteProperty { get; set; } + + [MinLength(2)] + public byte[] ByteArrayProperty { get; set; } + } + +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs index b70f5f4c6b..389e4c4ccf 100644 --- a/test/WebSites/FormatterWebSite/Startup.cs +++ b/test/WebSites/FormatterWebSite/Startup.cs @@ -23,6 +23,7 @@ namespace FormatterWebSite services.Configure(options => { options.ValidationExcludeFilters.Add(typeof(Developer)); + options.ValidationExcludeFilters.Add(typeof(Supplier)); }); });