diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs index 3dd1e7940d..ff87152d6d 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/ModelMetadata.cs @@ -289,6 +289,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public abstract string TemplateHint { get; } + /// + /// Gets a value that indicates whether properties or elements of the model should be validated. + /// + public abstract bool ValidateChildren { get; } + /// /// Gets a collection of metadata items for validators. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 598c730495..8b0ad72750 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -134,12 +134,7 @@ namespace Microsoft.Extensions.DependencyInjection var options = serviceProvider.GetRequiredService>().Value; return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders); })); - services.TryAdd(ServiceDescriptor.Singleton(serviceProvider => - { - var options = serviceProvider.GetRequiredService>().Value; - var modelMetadataProvider = serviceProvider.GetRequiredService(); - return new DefaultObjectValidator(options.ValidationExcludeFilters, modelMetadataProvider); - })); + services.TryAddSingleton(); // // Random Infrastructure diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index e20fee051a..b89a939b77 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.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.Threading; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Formatters; @@ -63,13 +64,11 @@ namespace Microsoft.AspNet.Mvc.Internal options.ModelValidatorProviders.Add(new DefaultModelValidatorProvider()); // Add types to be excluded from Validation - options.ValidationExcludeFilters.Add(new SimpleTypesExcludeFilter()); - options.ValidationExcludeFilters.Add(typeof(Type)); - - // Any 'known' types that we bind should be marked as excluded from validation. - options.ValidationExcludeFilters.Add(typeof(System.Threading.CancellationToken)); - options.ValidationExcludeFilters.Add(typeof(IFormFile)); - options.ValidationExcludeFilters.Add(typeof(IFormCollection)); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(Type))); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(Uri))); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(CancellationToken))); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(IFormFile))); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(IFormCollection))); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs index 57de7ad847..48943e456e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs @@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata private bool? _isReadOnly; private bool? _isRequired; private ModelPropertyCollection _properties; + private bool? _validateChildren; private ReadOnlyCollection _validatorMetadata; /// @@ -491,6 +492,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata } } + /// + public override bool ValidateChildren + { + get + { + if (!_validateChildren.HasValue) + { + if (ValidationMetadata.ValidateChildren.HasValue) + { + _validateChildren = ValidationMetadata.ValidateChildren.Value; + } + else if (IsComplexType || IsEnumerableType) + { + _validateChildren = true; + } + else + { + _validateChildren = false; + } + } + + return _validateChildren.Value; + } + } + /// public override IReadOnlyList ValidatorMetadata { diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ValidationMetadata.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ValidationMetadata.cs index 3f9ee9905b..89058ed55b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ValidationMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Metadata/ValidationMetadata.cs @@ -18,6 +18,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata /// public bool? IsRequired { get; set; } + /// + /// Gets or sets a value that indicates whether children of the model should be validated. If null + /// then will be true if either of + /// or is true; + /// false otherwise. + /// + public bool? ValidateChildren { get; set; } + /// /// Gets a list of metadata items for validators. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs index 5ea43b5d95..3913a4288c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { @@ -11,31 +10,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// public class DefaultObjectValidator : IObjectModelValidator { - private readonly IList _excludeFilters; private readonly IModelMetadataProvider _modelMetadataProvider; /// /// Initializes a new instance of . /// - /// s that determine - /// types to exclude from validation. /// The . public DefaultObjectValidator( - IList excludeFilters, IModelMetadataProvider modelMetadataProvider) { - if (excludeFilters == null) - { - throw new ArgumentNullException(nameof(excludeFilters)); - } - if (modelMetadataProvider == null) { throw new ArgumentNullException(nameof(modelMetadataProvider)); } _modelMetadataProvider = modelMetadataProvider; - _excludeFilters = excludeFilters; } /// @@ -59,7 +48,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var visitor = new ValidationVisitor( actionContext, validatorProvider, - _excludeFilters, validationState); var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType()); diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultTypeBasedExcludeFilter.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultTypeBasedExcludeFilter.cs deleted file mode 100644 index 0ea1d04225..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultTypeBasedExcludeFilter.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Reflection; - -namespace Microsoft.AspNet.Mvc.ModelBinding.Validation -{ - /// - /// Provides an implementation of which can filter - /// based on a type. - /// - public class DefaultTypeBasedExcludeFilter : IExcludeTypeValidationFilter - { - /// - /// Creates a new instance of . - /// - /// The type which needs to be excluded. - public DefaultTypeBasedExcludeFilter(Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - ExcludedType = type; - } - - /// - /// Gets the type which is excluded from validation. - /// - public Type ExcludedType { get; } - - /// - public bool IsTypeExcluded(Type propertyType) - { - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } - - return ExcludedType.GetTypeInfo().IsAssignableFrom(propertyType.GetTypeInfo()); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultTypeNameBasedExcludeFilter.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultTypeNameBasedExcludeFilter.cs deleted file mode 100644 index df933f04a0..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultTypeNameBasedExcludeFilter.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Reflection; - -namespace Microsoft.AspNet.Mvc.ModelBinding.Validation -{ - /// - /// Provides an implementation of which can filter - /// based on a type full name. - /// - public class DefaultTypeNameBasedExcludeFilter : IExcludeTypeValidationFilter - { - /// - /// Creates a new instance of - /// - /// Fully qualified name of the type which needs to be excluded. - public DefaultTypeNameBasedExcludeFilter(string typeFullName) - { - if (typeFullName == null) - { - throw new ArgumentNullException(nameof(typeFullName)); - } - - ExcludedTypeName = typeFullName; - } - - /// - /// Gets the type full name which is excluded from validation. - /// - public string ExcludedTypeName { get; } - - /// - public bool IsTypeExcluded(Type propertyType) - { - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } - - return CheckIfTypeNameMatches(propertyType); - } - - private bool CheckIfTypeNameMatches(Type type) - { - if (type == null) - { - return false; - } - - if (string.Equals(type.FullName, ExcludedTypeName, StringComparison.Ordinal)) - { - return true; - } - - return CheckIfTypeNameMatches(type.GetTypeInfo().BaseType); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ExcludeTypeValidationFilterCollection.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ExcludeTypeValidationFilterCollection.cs deleted file mode 100644 index 1a507f656b..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ExcludeTypeValidationFilterCollection.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) .NET Foundation. 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.ObjectModel; - -namespace Microsoft.AspNet.Mvc.ModelBinding.Validation -{ - public class ExcludeTypeValidationFilterCollection : Collection - { - /// - /// Adds an that excludes the properties of - /// the specified and its derived types from validaton. - /// - /// which should be excluded from validation. - public void Add(Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - var typeBasedExcludeFilter = new DefaultTypeBasedExcludeFilter(type); - Add(typeBasedExcludeFilter); - } - - /// - /// Adds an that excludes the properties of - /// the specified and its derived types from validaton. - /// - /// Full name of the type which should be excluded from validation. - public void Add(string typeFullName) - { - if (typeFullName == null) - { - throw new ArgumentNullException(nameof(typeFullName)); - } - - var filter = new DefaultTypeNameBasedExcludeFilter(typeFullName); - Add(filter); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IExcludeTypeValidationFilter.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IExcludeTypeValidationFilter.cs deleted file mode 100644 index 6b013b4311..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/IExcludeTypeValidationFilter.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNet.Mvc.ModelBinding.Validation -{ - /// - /// Provides an interface which is used to determine if s are excluded from model validation. - /// - public interface IExcludeTypeValidationFilter - { - /// - /// Determines if the given type is excluded from validation. - /// - /// The for which the check is to be performed. - /// True if the type is to be excluded. False otherwise. - bool IsTypeExcluded(Type type); - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/SimpleTypesExcludeFilter.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/SimpleTypesExcludeFilter.cs deleted file mode 100644 index bb68f6d065..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/SimpleTypesExcludeFilter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Validation -{ - /// - /// 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) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - Type[] actualTypes; - - if (type.GetTypeInfo().IsGenericType && - type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) - { - actualTypes = type.GenericTypeArguments; - } - else - { - actualTypes = new Type[] { type }; - } - - 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) - { - return 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)); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationExcludeFilter.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationExcludeFilter.cs new file mode 100644 index 0000000000..3bcdd3912f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationExcludeFilter.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. 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.Diagnostics; +using System.Reflection; +using Microsoft.AspNet.Mvc.ModelBinding.Metadata; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Validation +{ + /// + /// An which configures to + /// false for matching types. + /// + public class ValidationExcludeFilter : IValidationMetadataProvider + { + /// + /// Creates a new for the given . + /// + /// + /// The . This and all assignable values will have + /// set to false. + /// + public ValidationExcludeFilter(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + Type = type; + } + + /// + /// Creates a new for the given . + /// + /// + /// The type full name. This type and all of its subclasses will have + /// set to false. + /// + public ValidationExcludeFilter(string fullTypeName) + { + if (fullTypeName == null) + { + throw new ArgumentNullException(nameof(fullTypeName)); + } + + FullTypeName = fullTypeName; + } + + /// + /// Gets the for which to suppress validation of children. + /// + public Type Type { get; } + + /// + /// Gets the full name of a type for which to suppress validation of children. + /// + public string FullTypeName { get; } + + /// + public void GetValidationMetadata(ValidationMetadataProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (Type != null) + { + if (Type.GetTypeInfo().IsAssignableFrom(context.Key.ModelType.GetTypeInfo())) + { + context.ValidationMetadata.ValidateChildren = false; + } + + return; + } + + if (FullTypeName != null) + { + if (IsMatchingName(context.Key.ModelType)) + { + context.ValidationMetadata.ValidateChildren = false; + } + + return; + } + + Debug.Fail("We shouldn't get here."); + } + + private bool IsMatchingName(Type type) + { + Debug.Assert(FullTypeName != null); + + if (type == null) + { + return false; + } + + if (string.Equals(type.FullName, FullTypeName, StringComparison.Ordinal)) + { + return true; + } + + return IsMatchingName(type.GetTypeInfo().BaseType); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs index 3956b60314..c4c38442f5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -14,7 +14,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation public class ValidationVisitor { private readonly IModelValidatorProvider _validatorProvider; - private readonly IList _excludeFilters; private readonly ActionContext _actionContext; private readonly ModelStateDictionary _modelState; private readonly ValidationStateDictionary _validationState; @@ -33,12 +32,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// /// The associated with the current request. /// The . - /// The list of . /// The . public ValidationVisitor( ActionContext actionContext, IModelValidatorProvider validatorProvider, - IList excludeFilters, ValidationStateDictionary validationState) { if (actionContext == null) @@ -51,14 +48,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(validatorProvider)); } - if (excludeFilters == null) - { - throw new ArgumentNullException(nameof(excludeFilters)); - } - _actionContext = actionContext; _validatorProvider = validatorProvider; - _excludeFilters = excludeFilters; _validationState = validationState; _modelState = actionContext.ModelState; @@ -191,11 +182,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { var isValid = true; - if (_model != null) + if (_model != null && _metadata.ValidateChildren) { var strategy = _strategy ?? DefaultCollectionValidationStrategy.Instance; isValid = VisitChildren(strategy); } + else if (_model != null) + { + // Suppress validation for the entries matching this prefix. This will temporarily set + // the current node to 'skipped' but we're going to visit it right away, so subsequent + // code will set it to 'valid' or 'invalid' + SuppressValidation(_key); + } // Double-checking HasReachedMaxErrors just in case this model has no elements. if (isValid && !_modelState.HasReachedMaxErrors) @@ -210,14 +208,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { var isValid = true; - if (_model != null && ShouldValidateProperties(_metadata)) + if (_model != null && _metadata.ValidateChildren) { var strategy = _strategy ?? DefaultComplexObjectValidationStrategy.Instance; isValid = VisitChildren(strategy); } else if (_model != null) { - // Suppress validation for the prefix, but we still want to validate the object. + // Suppress validation for the entries matching this prefix. This will temporarily set + // the current node to 'skipped' but we're going to visit it right away, so subsequent + // code will set it to 'valid' or 'invalid' SuppressValidation(_key); } @@ -286,20 +286,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation } } - private bool ShouldValidateProperties(ModelMetadata metadata) - { - var count = _excludeFilters.Count; - for (var i = 0; i < _excludeFilters.Count; i++) - { - if (_excludeFilters[i].IsTypeExcluded(metadata.UnderlyingOrModelType)) - { - return false; - } - } - - return true; - } - private ValidationStateEntry GetValidationEntry(object model) { if (model == null || _validationState == null) diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs index fc4046d675..381e62d31a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs @@ -31,7 +31,6 @@ namespace Microsoft.AspNet.Mvc ModelBindingMessageProvider = new ModelBindingMessageProvider(); ModelMetadataDetailsProviders = new List(); ModelValidatorProviders = new List(); - ValidationExcludeFilters = new ExcludeTypeValidationFilterCollection(); ValueProviderFactories = new List(); } @@ -125,11 +124,6 @@ namespace Microsoft.AspNet.Mvc /// public bool RespectBrowserAcceptHeader { get; set; } - /// - /// Gets a collection of s that are used by this application. - /// - public ExcludeTypeValidationFilterCollection ValidationExcludeFilters { get; } - /// /// Gets a list of used by this application. /// diff --git a/src/Microsoft.AspNet.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs index 2241518322..0fa8dae25d 100644 --- a/src/Microsoft.AspNet.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. 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.ModelBinding.Validation; using Microsoft.Extensions.Logging; using Microsoft.Extensions.OptionsModel; using Microsoft.Net.Http.Headers; @@ -30,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.Formatters.Json.Internal options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json")); - options.ValidationExcludeFilters.Add(typeof(JToken)); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(JToken))); } } } diff --git a/src/Microsoft.AspNet.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs index 72e12f7e0b..acc6ffa975 100644 --- a/src/Microsoft.AspNet.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Mvc.ModelBinding.Metadata; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Extensions.OptionsModel; namespace Microsoft.AspNet.Mvc.Formatters.Xml.Internal @@ -31,8 +32,8 @@ namespace Microsoft.AspNet.Mvc.Formatters.Xml.Internal options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter()); - options.ValidationExcludeFilters.Add(typeFullName: "System.Xml.Linq.XObject"); - options.ValidationExcludeFilters.Add(typeFullName: "System.Xml.XmlNode"); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter("System.Xml.Linq.XObject")); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter("System.Xml.XmlNode")); } } } diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index f01010b376..4808dc40a3 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.Net.Http.Formatting; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.OptionsModel; @@ -32,8 +33,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim // Add a formatter to write out an HttpResponseMessage to the response options.OutputFormatters.Insert(0, new HttpResponseMessageOutputFormatter()); - options.ValidationExcludeFilters.Add(typeof(HttpRequestMessage)); - options.ValidationExcludeFilters.Add(typeof(HttpResponseMessage)); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(HttpRequestMessage))); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(HttpResponseMessage))); } public void Configure(WebApiCompatShimOptions options) diff --git a/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs index 60cca2c443..f2d1adf19c 100644 --- a/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.Abstractions.Test/ModelBinding/ModelMetadataTest.cs @@ -568,6 +568,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + public override bool ValidateChildren + { + get + { + throw new NotImplementedException(); + } + } + public override IReadOnlyList ValidatorMetadata { get diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionInvokerTest.cs index 91b3db1159..78fd2b34d4 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/ControllerActionInvokerTest.cs @@ -2112,7 +2112,7 @@ namespace Microsoft.AspNet.Mvc.Controllers new IInputFormatter[0], new DefaultControllerActionArgumentBinder( metadataProvider, - new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider)), + new DefaultObjectValidator(metadataProvider)), new IModelBinder[] { binder.Object }, new IModelValidatorProvider[0], new IValueProviderFactory[0], diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs index 1a0ac9c3da..ba732e6b29 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs @@ -251,7 +251,7 @@ namespace Microsoft.AspNet.Mvc.Controllers services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) .Returns(metadataProvider); services.Setup(s => s.GetService(typeof(IObjectModelValidator))) - .Returns(new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider)); + .Returns(new DefaultObjectValidator(metadataProvider)); return services.Object; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs index e3c043c894..b8653bf2fb 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataTest.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Xml; using Moq; using Xunit; @@ -605,6 +606,103 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata Assert.False(isReadOnly); } + [Theory] + [InlineData(typeof(int))] // Primitive + [InlineData(typeof(int?))] // Nullable + [InlineData(typeof(Guid))] // TypeConverter + [InlineData(typeof(Guid?))] // Nullable + TypeConverter + [InlineData(typeof(string))] + public void ValidateChildren_SimpleTypes(Type modelType) + { + // Arrange + var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); + var provider = new DefaultModelMetadataProvider(detailsProvider); + + var key = ModelMetadataIdentity.ForType(modelType); + var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); + + var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); + + // Act + var validateChildren = metadata.ValidateChildren; + + // Assert + Assert.False(validateChildren); + } + + [Theory] + [InlineData(typeof(int[]))] + [InlineData(typeof(List))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(Dictionary))] + [InlineData(typeof(KeyValuePair))] + [InlineData(typeof(KeyValuePair?))] + [InlineData(typeof(TypeWithProperties))] + [InlineData(typeof(List))] + public void ValidateChildren_ComplexAndEnumerableTypes(Type modelType) + { + // Arrange + var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); + var provider = new DefaultModelMetadataProvider(detailsProvider); + + var key = ModelMetadataIdentity.ForType(typeof(TypeWithProperties)); + var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); + + var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); + + // Act + var validateChildren = metadata.ValidateChildren; + + // Assert + Assert.True(validateChildren); + } + + [Fact] + public void ValidateChildren_OverrideTrue() + { + // Arrange + var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); + var provider = new DefaultModelMetadataProvider(detailsProvider); + + var key = ModelMetadataIdentity.ForType(typeof(int)); + var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); + cache.ValidationMetadata = new ValidationMetadata() + { + ValidateChildren = true, + }; + + var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); + + // Act + var validateChildren = metadata.ValidateChildren; + + // Assert + Assert.True(validateChildren); + } + + [Fact] + public void ValidateChildren_OverrideFalse() + { + // Arrange + var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); + var provider = new DefaultModelMetadataProvider(detailsProvider); + + var key = ModelMetadataIdentity.ForType(typeof(XmlDocument)); + var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); + cache.ValidationMetadata = new ValidationMetadata() + { + ValidateChildren = false, + }; + + var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); + + // Act + var validateChildren = metadata.ValidateChildren; + + // Assert + Assert.False(validateChildren); + } + private void ActionMethod(string input) { } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs index 2d4c631195..94b9aff083 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs @@ -95,7 +95,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], modelMetadataProvider), + new DefaultObjectValidator(modelMetadataProvider), validator); // Assert @@ -136,7 +136,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider), + new DefaultObjectValidator(metadataProvider), validator); // Assert @@ -221,7 +221,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider), + new DefaultObjectValidator(metadataProvider), validator, includePredicate); @@ -305,7 +305,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider), + new DefaultObjectValidator(metadataProvider), validator, m => m.IncludedProperty, m => m.MyProperty); @@ -357,7 +357,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider), + new DefaultObjectValidator(metadataProvider), validator); // Assert @@ -568,9 +568,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator( - new IExcludeTypeValidationFilter[0], - metadataProvider), + new DefaultObjectValidator(metadataProvider), validator, includePredicate); @@ -645,9 +643,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator( - new IExcludeTypeValidationFilter[0], - metadataProvider), + new DefaultObjectValidator(metadataProvider), validator); // Assert @@ -679,9 +675,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding GetCompositeBinder(binder.Object), Mock.Of(), new List(), - new DefaultObjectValidator( - new IExcludeTypeValidationFilter[0], - metadataProvider), + new DefaultObjectValidator(metadataProvider), Mock.Of(), includePredicate)); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs index c63b5f99ff..7cdf56a6fd 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/DefaultObjectValidatorTests.cs @@ -8,6 +8,7 @@ using System.Linq; #if DNXCORE50 using System.Reflection; #endif +using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Microsoft.AspNet.Testing; using Moq; using Xunit; @@ -655,13 +656,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Profession"), error.ErrorMessage); } + public static TheoryData ValidCollectionData + { + get + { + return new TheoryData() + { + { new int[] { 1, 2, 3 }, typeof(int[]) }, + { new string[] { "Foo", "Bar", "Baz" }, typeof(string[]) }, + { new List { "Foo", "Bar", "Baz" }, typeof(IList)}, + { new HashSet { "Foo", "Bar", "Baz" }, typeof(string[]) }, + { + new List + { + DateTime.Parse("1/1/14"), + DateTime.Parse("2/1/14"), + DateTime.Parse("3/1/14"), + }, + typeof(ICollection) + }, + { + new HashSet + { + new Uri("http://example.com/1"), + new Uri("http://example.com/2"), + new Uri("http://example.com/3"), + }, + typeof(HashSet) + }, + }; + } + } + [Theory] - [InlineData(new[] { "Foo", "Bar", "Baz" }, typeof(string[]))] - [InlineData(new[] { 1, 2, 3 }, typeof(int[]))] - [InlineData(new[] { "Foo", "Bar", "Baz" }, typeof(IList))] - [InlineData(new[] { "Foo", "Bar", "Baz" }, typeof(HashSet))] - [InlineData(new[] { "1/1/14", "2/2/14", "3/3/14" }, typeof(ICollection))] - [InlineData(new[] { "Foo", "Bar", "Baz" }, typeof(HashSet))] + [MemberData(nameof(ValidCollectionData))] public void Validate_IndexedCollectionTypes_Valid(object model, Type type) { // Arrange @@ -670,7 +698,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var modelState = actionContext.ModelState; var validationState = new ValidationStateDictionary(); - var validator = CreateValidator(new SimpleTypesExcludeFilter()); + var validator = CreateValidator(); modelState.Add("items[0]", new ModelStateEntry()); modelState.Add("items[1]", new ModelStateEntry()); @@ -712,7 +740,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var modelState = actionContext.ModelState; var validationState = new ValidationStateDictionary(); - var validator = CreateValidator(new SimpleTypesExcludeFilter()); + var validator = CreateValidator(); var model = new Dictionary() { @@ -734,19 +762,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation AssertKeysEqual(modelState, "items[0].Key", "items[0].Value", "items[1].Key", "items[1].Value"); var entry = modelState["items[0].Key"]; - Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); Assert.Empty(entry.Errors); entry = modelState["items[0].Value"]; - Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); Assert.Empty(entry.Errors); entry = modelState["items[1].Key"]; - Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); Assert.Empty(entry.Errors); entry = modelState["items[1].Value"]; - Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); Assert.Empty(entry.Errors); } @@ -857,7 +885,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation } [Fact] - public void Validate_ForExcludedType_PropertiesMarkedAsSkipped() + public void Validate_ForExcludedComplexType_PropertiesMarkedAsSkipped() { // Arrange var validatorProvider = CreateValidatorProvider(); @@ -890,6 +918,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation Assert.Empty(entry.Errors); } + [Fact] + public void Validate_ForExcludedCollectionType_PropertiesMarkedAsSkipped() + { + // Arrange + var validatorProvider = CreateValidatorProvider(); + var actionContext = new ActionContext(); + var modelState = actionContext.ModelState; + var validationState = new ValidationStateDictionary(); + + var validator = CreateValidator(typeof(List)); + + var model = new List() + { + "15", + }; + + modelState.SetModelValue("userIds[0]", "15", "15"); + validationState.Add(model, new ValidationStateEntry() { Key = "userIds", }); + + // Act + validator.Validate(actionContext, validatorProvider, validationState, "userIds", model); + + // Assert + Assert.Equal(ModelValidationState.Valid, modelState.ValidationState); + AssertKeysEqual(modelState, "userIds[0]"); + + var entry = modelState["userIds[0]"]; + Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Empty(entry.Errors); + } + private static IModelValidatorProvider CreateValidatorProvider() { return TestModelValidatorProvider.CreateDefaultProvider(); @@ -897,23 +956,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation private static DefaultObjectValidator CreateValidator(Type excludedType) { - var excludeFilters = new List(); + var excludeFilters = new List(); if (excludedType != null) { - var excludeFilter = new Mock(); - excludeFilter - .Setup(o => o.IsTypeExcluded(It.IsAny())) - .Returns(t => t.IsAssignableFrom(excludedType)); - - excludeFilters.Add(excludeFilter.Object); + excludeFilters.Add(new ValidationExcludeFilter(excludedType)); } - return new DefaultObjectValidator(excludeFilters, TestModelMetadataProvider.CreateDefaultProvider()); + var provider = TestModelMetadataProvider.CreateDefaultProvider(excludeFilters.ToArray()); + return new DefaultObjectValidator(provider); } - private static DefaultObjectValidator CreateValidator(params IExcludeTypeValidationFilter[] excludeFilters) + private static DefaultObjectValidator CreateValidator(params IMetadataDetailsProvider[] providers) { - return new DefaultObjectValidator(excludeFilters, TestModelMetadataProvider.CreateDefaultProvider()); + var provider = TestModelMetadataProvider.CreateDefaultProvider(providers); + return new DefaultObjectValidator(provider); } private static void AssertKeysEqual(ModelStateDictionary modelState, params string[] keys) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ExcludeTypeValidationFilterCollectionTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ExcludeTypeValidationFilterCollectionTest.cs deleted file mode 100644 index 8141b53dba..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/ExcludeTypeValidationFilterCollectionTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) .NET Foundation. 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.ModelBinding.Validation; -using Xunit; - -namespace Microsoft.AspNet.Mvc.ModelBinding -{ - public class ExcludeTypeValidationFilterCollectionTest - { - [Fact] - public void AddFilter_ByType() - { - // Arrange - var type = typeof(BaseType); - var collection = new ExcludeTypeValidationFilterCollection(); - - // Act - collection.Add(type); - - // Assert - var filter = Assert.IsType(Assert.Single(collection)); - Assert.Equal(type, filter.ExcludedType); - } - - [Fact] - public void AddFilter_ByTypeName() - { - // Arrange - var type = typeof(BaseType); - var collection = new ExcludeTypeValidationFilterCollection(); - - // Act - collection.Add(type.FullName); - - // Assert - var filter = Assert.IsType(Assert.Single(collection)); - Assert.Equal(type.FullName, filter.ExcludedTypeName); - } - - private class BaseType - { - } - } -} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/SimpleTypeExcludeFilterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/SimpleTypeExcludeFilterTest.cs deleted file mode 100644 index 1c29ac3c50..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/Validation/SimpleTypeExcludeFilterTest.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Validation -{ - public class SimpleTypeExcludeFilterTest - { - [Theory] - [MemberData(nameof(ExcludedTypes))] - public void SimpleTypeExcludeFilter_ExcludedTypes(Type type) - { - // Arrange - var filter = new SimpleTypesExcludeFilter(); - - // Act & Assert - Assert.True(filter.IsTypeExcluded(type)); - } - - [Theory] - [MemberData(nameof(IncludedTypes))] - public void SimpleTypeExcludeFilter_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(DateTime), - - // Nullable types - typeof(int?), - - // KeyValue types - typeof(KeyValuePair) - }; - } - } - - public static TheoryData IncludedTypes - { - get - { - return new TheoryData() - { - // Enumerable types - typeof(int[]), - typeof(List), - typeof(SortedSet), - typeof(ICollection), - typeof(int?[]), - typeof(SortedSet), - typeof(HashSet), - typeof(HashSet), - typeof(IList), - typeof(Dictionary), - typeof(IReadOnlyDictionary), - - // Complex types - typeof(TestType), - typeof(TestType[]), - typeof(SortedSet), - typeof(Dictionary), - typeof(Dictionary), - typeof(Dictionary), - typeof(KeyValuePair) - }; - } - } - } -} diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs index c2de2c5210..52033086ec 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs @@ -129,14 +129,12 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[low].Key").Value; Assert.Equal("key0", entry.AttemptedValue); Assert.Equal("key0", entry.RawValue); - - // Key and Value are skipped if they have simple types. - Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[low].Value").Value; Assert.Equal("10", entry.AttemptedValue); Assert.Equal("10", entry.RawValue); - Assert.Equal(ModelValidationState.Skipped, entry.ValidationState); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); } [Theory] diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs index 4b523d75f4..5e587ba58a 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/KeyValuePairModelBinderIntegrationTest.cs @@ -104,9 +104,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests binding.ModelBindingMessageProvider.MissingKeyOrValueAccessor = () => $"Hurts when nothing is provided."; })); - var argumentBinder = new DefaultControllerActionArgumentBinder( - metadataProvider, - ModelBindingTestHelper.GetObjectValidator()); + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); var parameter = new ParameterDescriptor { Name = "parameter", @@ -188,9 +186,8 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests // A real details provider could customize message based on BindingMetadataProviderContext. binding.ModelBindingMessageProvider.MissingKeyOrValueAccessor = () => $"Hurts when nothing is provided."; })); - var argumentBinder = new DefaultControllerActionArgumentBinder( - metadataProvider, - ModelBindingTestHelper.GetObjectValidator()); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); var parameter = new ParameterDescriptor { diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs index 082d9aa16f..6e94035f33 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -42,20 +42,28 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests public static DefaultControllerActionArgumentBinder GetArgumentBinder(MvcOptions options = null) { - var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - return new DefaultControllerActionArgumentBinder( - metadataProvider, - GetObjectValidator(options)); + if (options == null) + { + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + return GetArgumentBinder(metadataProvider); + } + else + { + var metadataProvider = TestModelMetadataProvider.CreateProvider(options.ModelMetadataDetailsProviders); + return GetArgumentBinder(metadataProvider); + } } - public static IObjectModelValidator GetObjectValidator(MvcOptions options = null) + public static DefaultControllerActionArgumentBinder GetArgumentBinder(IModelMetadataProvider metadataProvider) { - options = options ?? new TestMvcOptions().Value; - options.MaxModelValidationErrors = 5; - var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - return new DefaultObjectValidator( - options.ValidationExcludeFilters, - metadataProvider); + return new DefaultControllerActionArgumentBinder( + metadataProvider, + GetObjectValidator(metadataProvider)); + } + + public static IObjectModelValidator GetObjectValidator(IModelMetadataProvider metadataProvider) + { + return new DefaultObjectValidator(metadataProvider); } private static HttpContext GetHttpContext( diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs index 972e6bb8d3..288baf0800 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs @@ -1424,9 +1424,8 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests binding.ModelBindingMessageProvider.MissingBindRequiredValueAccessor = name => $"Hurts when '{ name }' is not provided."; })); - var argumentBinder = new DefaultControllerActionArgumentBinder( - metadataProvider, - ModelBindingTestHelper.GetObjectValidator()); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); var parameter = new ParameterDescriptor() { Name = "parameter", diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index a3d76f0fd6..760403dbc2 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -312,9 +312,8 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests binding.ModelBindingMessageProvider.ValueMustNotBeNullAccessor = value => $"Hurts when '{ value }' is provided."; })); - var argumentBinder = new DefaultControllerActionArgumentBinder( - metadataProvider, - ModelBindingTestHelper.GetObjectValidator()); + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); + var parameter = new ParameterDescriptor { Name = "Parameter1", diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs index 508e7ddcef..36a13f73a1 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/TryUpdateModelIntegrationTest.cs @@ -1109,7 +1109,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests operationContext.ModelBinder, operationContext.ValueProvider, operationContext.InputFormatters, - ModelBindingTestHelper.GetObjectValidator(), + ModelBindingTestHelper.GetObjectValidator(operationContext.MetadataProvider), operationContext.ValidatorProvider); } } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs index 0f525cfed0..08f161cf7c 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ValidationIntegrationTests.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; +using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.AspNet.Mvc.IntegrationTests @@ -1129,7 +1131,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests }, options => { - options.ValidationExcludeFilters.Add(typeof(Address)); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(Address))); testOptions = options; }); @@ -1167,6 +1169,55 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Assert.Equal(ModelValidationState.Valid, entry.ValidationState); } + [Fact] + public async Task FromBody_JToken_ExcludedFromValidation() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(new TestMvcOptions().Value); + var parameter = new ParameterDescriptor + { + Name = "Parameter1", + BindingInfo = new BindingInfo + { + BinderModelName = "CustomParameter", + BindingSource = BindingSource.Body + }, + ParameterType = typeof(JToken) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ message: \"Hello\" }")); + request.ContentType = "application/json"; + }); + + var httpContext = operationContext.HttpContext; + var modelState = operationContext.ActionContext.ModelState; + + // We need to add another model state entry which should get marked as skipped so + // we can prove that the JObject was skipped. + modelState.SetModelValue("message", "Hello", "Hello"); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + Assert.NotNull(modelBindingResult.Model); + var message = Assert.IsType(modelBindingResult.Model).GetValue("message").Value(); + Assert.Equal("Hello", message); + + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Count); + + var entry = Assert.Single(modelState, kvp => kvp.Key == string.Empty); + Assert.Equal(ModelValidationState.Valid, entry.Value.ValidationState); + + entry = Assert.Single(modelState, kvp => kvp.Key == "message"); + Assert.Equal(ModelValidationState.Skipped, entry.Value.ValidationState); + } + private static void AssertRequiredError(string key, ModelError error) { // Mono issue - https://github.com/aspnet/External/issues/19 diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs index 295c162393..98b682eba1 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs @@ -19,6 +19,9 @@ using Moq; using Newtonsoft.Json.Linq; using Xunit; using Microsoft.Extensions.Logging; +using System.Threading; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding.Metadata; namespace Microsoft.AspNet.Mvc { @@ -134,7 +137,7 @@ namespace Microsoft.AspNet.Mvc } [Fact] - public void Setup_SetsUpExcludeFromValidationDelegates() + public void Setup_SetsUpMetadataDetailsProviders() { // Arrange & Act var options = GetOptions(services => @@ -144,48 +147,40 @@ namespace Microsoft.AspNet.Mvc }); // Assert - Assert.Equal(8, options.ValidationExcludeFilters.Count); + var providers = options.ModelMetadataDetailsProviders; + Assert.Equal(12, providers.Count); var i = 0; - // Verify if the delegates registered by default exclude the given types. - Assert.IsType(typeof(SimpleTypesExcludeFilter), options.ValidationExcludeFilters[i++]); + Assert.IsType(providers[i++]); + Assert.IsType(providers[i++]); - Assert.IsType(typeof(DefaultTypeBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var typeFilter - = Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(typeFilter.ExcludedType, typeof(Type)); + var excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(Type), excludeFilter.Type); - Assert.IsType(typeof(DefaultTypeBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var cancellationTokenFilter - = Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(cancellationTokenFilter.ExcludedType, typeof(System.Threading.CancellationToken)); + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(Uri), excludeFilter.Type); - Assert.IsType(typeof(DefaultTypeBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var formFileFilter - = Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(formFileFilter.ExcludedType, typeof(Http.IFormFile)); + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(CancellationToken), excludeFilter.Type); - Assert.IsType( - typeof(DefaultTypeBasedExcludeFilter), - options.ValidationExcludeFilters[i]); - var formCollectionFilter - = Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(formCollectionFilter.ExcludedType, typeof(Http.IFormCollection)); + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(IFormFile), excludeFilter.Type); - Assert.IsType(typeof(DefaultTypeBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var jTokenFilter - = Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(jTokenFilter.ExcludedType, typeof(JToken)); + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(IFormCollection), excludeFilter.Type); - Assert.IsType(typeof(DefaultTypeNameBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var xObjectFilter - = Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(xObjectFilter.ExcludedTypeName, typeof(XObject).FullName); + Assert.IsType(providers[i++]); - Assert.IsType(typeof(DefaultTypeNameBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var xmlNodeFilter = - Assert.IsType(options.ValidationExcludeFilters[i++]); - Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode"); + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(JToken), excludeFilter.Type); + + Assert.IsType(providers[i++]); + + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal(typeof(XObject).FullName, excludeFilter.FullTypeName); + + excludeFilter = Assert.IsType(providers[i++]); + Assert.Equal("System.Xml.XmlNode", excludeFilter.FullTypeName); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs index 0c0d8dd61f..79301eab25 100644 --- a/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestModelMetadataProvider.cs @@ -26,6 +26,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return new DefaultModelMetadataProvider(compositeDetailsProvider); } + public static IModelMetadataProvider CreateDefaultProvider(IList providers) + { + var detailsProviders = new List() + { + new DefaultBindingMetadataProvider(CreateMessageProvider()), + new DefaultValidationMetadataProvider(), + new DataAnnotationsMetadataProvider(), + new DataMemberRequiredBindingMetadataProvider(), + }; + + detailsProviders.AddRange(providers); + + var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); + return new DefaultModelMetadataProvider(compositeDetailsProvider); + } + + public static IModelMetadataProvider CreateProvider(IList providers) + { + var detailsProviders = new List(); + if (providers != null) + { + detailsProviders.AddRange(providers); + } + + var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); + return new DefaultModelMetadataProvider(compositeDetailsProvider); + } + private readonly TestModelMetadataDetailsProvider _detailsProvider; public TestModelMetadataProvider() diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs index b53c21a42d..2f665da687 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs @@ -1873,7 +1873,7 @@ namespace Microsoft.AspNet.Mvc.Test { ControllerContext = controllerContext, MetadataProvider = metadataProvider, - ObjectValidator = new DefaultObjectValidator(new IExcludeTypeValidationFilter[0], metadataProvider), + ObjectValidator = new DefaultObjectValidator(metadataProvider), TempData = tempData, ViewData = viewData, }; diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs index 06c8cc0a2e..8af87b82f0 100644 --- a/test/WebSites/FormatterWebSite/Startup.cs +++ b/test/WebSites/FormatterWebSite/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; namespace FormatterWebSite @@ -13,8 +14,8 @@ namespace FormatterWebSite { services.AddMvc(options => { - options.ValidationExcludeFilters.Add(typeof(Developer)); - options.ValidationExcludeFilters.Add(typeof(Supplier)); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(Developer))); + options.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(Supplier))); options.InputFormatters.Add(new StringInputFormatter()); }) diff --git a/test/WebSites/ModelBindingWebSite/Startup.cs b/test/WebSites/ModelBindingWebSite/Startup.cs index 920fe42870..e02921be9b 100644 --- a/test/WebSites/ModelBindingWebSite/Startup.cs +++ b/test/WebSites/ModelBindingWebSite/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; using ModelBindingWebSite.Models; using ModelBindingWebSite.Services; @@ -20,7 +21,7 @@ namespace ModelBindingWebSite m.MaxModelValidationErrors = 8; m.ModelBinders.Insert(0, new TestBindingSourceModelBinder()); - m.ValidationExcludeFilters.Add(typeof(Address)); + m.ModelMetadataDetailsProviders.Add(new ValidationExcludeFilter(typeof(Address))); // ModelMetadataController relies on additional values AdditionalValuesMetadataProvider provides. m.ModelMetadataDetailsProviders.Add(new AdditionalValuesMetadataProvider());