[Partial fix for #1372]Added SimpleTypesExcludeFilter to exlcude validation on simple types and also added tests to cover scenarios.

This commit is contained in:
Kiran Challa 2014-11-10 14:46:46 -08:00 committed by ianhong
parent d788e87a94
commit 00b61ec1e6
13 changed files with 402 additions and 30 deletions

View File

@ -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
{
/// <summary>
/// Adds a descriptor to the specified <paramref name="excludeBodyValidationDescriptorCollection" />
/// that excludes the properties of the <see cref="Type"/> specified and it's derived types from validaton.
/// Adds a descriptor to the specified <paramref name="descriptorCollection" /> that excludes the properties of
/// the <see cref="Type"/> specified and its derived types from validaton.
/// </summary>
/// <param name="excludeBodyValidationDescriptorCollection">A list of <see cref="ExcludeValidationDescriptor"/>
/// which are used to get a collection of exclude filters to be applied for filtering model properties during validation.
/// <param name="descriptorCollection">A list of <see cref="ExcludeValidationDescriptor"/> which are used to
/// get a collection of exclude filters to be applied for filtering model properties during validation.
/// </param>
/// <param name="type"><see cref="Type"/> which should be excluded from validation.</param>
public static void Add(this IList<ExcludeValidationDescriptor> excludeBodyValidationDescriptorCollection,
Type type)
public static void Add(this IList<ExcludeValidationDescriptor> descriptorCollection, Type type)
{
var typeBasedExcludeFilter = new DefaultTypeBasedExcludeFilter(type);
excludeBodyValidationDescriptorCollection.Add(new ExcludeValidationDescriptor(typeBasedExcludeFilter));
descriptorCollection.Add(new ExcludeValidationDescriptor(typeBasedExcludeFilter));
}
/// <summary>
/// Adds a descriptor to the specified <paramref name="excludeBodyValidationDescriptorCollection" />
/// that excludes the properties of the type specified and it's derived types from validaton.
/// Adds a descriptor to the specified <paramref name="descriptorCollection" /> that excludes the properties of
/// the type specified and its derived types from validaton.
/// </summary>
/// <param name="excludeBodyValidationDescriptorCollection">A list of <see cref="ExcludeValidationDescriptor"/>
/// which are used to get a collection of exclude filters to be applied for filtering model properties during validation.
/// <param name="descriptorCollection">A list of <see cref="ExcludeValidationDescriptor"/> which are used to
/// get a collection of exclude filters to be applied for filtering model properties during validation.
/// </param>
/// <param name="typeFullName">Full name of the type which should be excluded from validation.</param>
public static void Add(this IList<ExcludeValidationDescriptor> excludeBodyValidationDescriptorCollection,
string typeFullName)
public static void Add(this IList<ExcludeValidationDescriptor> descriptorCollection, string typeFullName)
{
var filter = new DefaultTypeNameBasedExcludeFilter(typeFullName);
excludeBodyValidationDescriptorCollection.Add(new ExcludeValidationDescriptor(filter));
descriptorCollection.Add(new ExcludeValidationDescriptor(filter));
}
/// <summary>
/// Adds a descriptor to the specified <paramref name="descriptorCollection" /> that excludes the properties of
/// the type specified and its derived types from validaton.
/// </summary>
/// <param name="descriptorCollection">A list of <see cref="ExcludeValidationDescriptor"/> which are used to
/// get a collection of exclude filters to be applied for filtering model properties during validation.
/// </param>
/// <param name="filter"><see cref="IExcludeTypeValidationFilter"/> which should be excluded from validation.
/// </param>
public static void Add(this IList<ExcludeValidationDescriptor> descriptorCollection,
IExcludeTypeValidationFilter filter)
{
descriptorCollection.Add(new ExcludeValidationDescriptor(filter));
}
}
}

View File

@ -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
{
/// <summary>
/// Identifies the simple types that the default model binding validation will exclude.
/// </summary>
public class SimpleTypesExcludeFilter : IExcludeTypeValidationFilter
{
/// <summary>
/// Returns true if the given type will be excluded from the default model validation.
/// </summary>
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<KeyValuePair<K,V>>,
// supertype of IDictionary<K,V>, and IReadOnlyDictionary<K,V>.
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;
}
/// <summary>
/// Returns true if the given type is the underlying types that <see cref="IsTypeExcluded"/> will exclude.
/// </summary>
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;
}
}
}

View File

@ -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);
}

View File

@ -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");
}

View File

@ -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<IApplicationBuilder> _app = new FormatterWebSite.Startup().Configure;
// Parameters: Request Content, Expected status code, Expected model state error message
public static IEnumerable<object[]> 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<Dictionary<string, ErrorCollection>>(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<Dictionary<string, ErrorCollection>>(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<Error> Errors
{
get;
set;
}
}
private class Error
{
public string ErrorMessage
{
get;
set;
}
}
}
}

View File

@ -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<Type> ExcludedTypes
{
get
{
return new TheoryData<Type>()
{
// Simple types
typeof(int[]),
typeof(int),
typeof(List<decimal>),
typeof(SortedSet<int>),
// Nullable types
typeof(ICollection<string>),
typeof(int?[]),
typeof(SortedSet<int?>),
typeof(HashSet<Uri>),
typeof(HashSet<string>),
// Value types
typeof(IList<DateTime>),
// KeyValue types
typeof(Dictionary<int, string>),
typeof(IReadOnlyDictionary<int?, char>)
};
}
}
public static TheoryData<Type> IncludedTypes
{
get
{
return new TheoryData<Type>()
{
typeof(TestType),
typeof(TestType[]),
typeof(SortedSet<TestType>),
typeof(Dictionary<int, TestType>),
typeof(Dictionary<TestType, int>),
typeof(Dictionary<TestType, TestType>)
};
}
}
}
}

View File

@ -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<DefaultTypeBasedExcludeFilter>(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<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[1].Instance);
Assert.Equal(instance2.ExcludedType, typeof(Type));
var xObjectFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[1].Instance);
Assert.Equal(xObjectFilter.ExcludedType, typeof(XObject));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[2].OptionType);
var instance3 = Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[2].Instance);
Assert.Equal(instance3.ExcludedType, typeof(byte[]));
var typeFilter =
Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[2].Instance);
Assert.Equal(typeFilter.ExcludedType, typeof(Type));
Assert.Equal(typeof(DefaultTypeBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[3].OptionType);
var instance4 = Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[3].Instance);
Assert.Equal(instance4.ExcludedType, typeof(JToken));
var jTokenFilter
= Assert.IsType<DefaultTypeBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[3].Instance);
Assert.Equal(jTokenFilter.ExcludedType, typeof(JToken));
Assert.Equal(typeof(DefaultTypeNameBasedExcludeFilter), mvcOptions.ValidationExcludeFilters[4].OptionType);
var instance5 = Assert.IsType<DefaultTypeNameBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[4].Instance);
Assert.Equal(instance5.ExcludedTypeName, "System.Xml.XmlNode");
var xmlNodeFilter =
Assert.IsType<DefaultTypeNameBasedExcludeFilter>(mvcOptions.ValidationExcludeFilters[4].Instance);
Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -23,6 +23,7 @@ namespace FormatterWebSite
services.Configure<MvcOptions>(options =>
{
options.ValidationExcludeFilters.Add(typeof(Developer));
options.ValidationExcludeFilters.Add(typeof(Supplier));
});
});