Shortcircuit validation when using default validator providers and no validation metadata is discovered

Fixes https://github.com/aspnet/Mvc/issues/5887
This commit is contained in:
Pranav K 2018-10-04 16:58:42 -07:00
parent a40c1f2d02
commit fb57810f29
35 changed files with 1992 additions and 39 deletions

View File

@ -132,6 +132,13 @@ namespace BasicApi.Controllers
return new CreatedAtRouteResult("FindPetById", new { id = pet.Id }, pet);
}
[Authorize("pet-store-writer")]
[HttpPost("add-pet")]
public ActionResult<Pet> AddPetWithoutDb(Pet pet)
{
return pet;
}
[Authorize("pet-store-writer")]
[HttpPut]
public IActionResult EditPet(Pet pet)

View File

@ -44,5 +44,11 @@
"Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/postJsonWithToken.lua"
},
"Path": "/pet"
},
"BasicApi.PostWithoutDb": {
"Path": "/pet/add-pet",
"ClientProperties": {
"Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/postJsonWithToken.lua"
}
}
}

View File

@ -0,0 +1,75 @@
// 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.Collections.Generic;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Performance
{
public abstract class ValidationVisitorBenchmarkBase
{
protected const int Iterations = 4;
protected static readonly IModelValidatorProvider[] ValidatorProviders = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
null),
};
protected static readonly CompositeModelValidatorProvider CompositeModelValidatorProvider = new CompositeModelValidatorProvider(ValidatorProviders);
public abstract object Model { get; }
public ModelMetadataProvider BaselineModelMetadataProvider { get; private set; }
public ModelMetadataProvider ModelMetadataProvider { get; private set; }
public ModelMetadata BaselineModelMetadata { get; private set; }
public ModelMetadata ModelMetadata { get; private set; }
public ActionContext ActionContext { get; private set; }
public ValidatorCache ValidatorCache { get; private set; }
[GlobalSetup]
public void Setup()
{
BaselineModelMetadataProvider = CreateModelMetadataProvider(addHasValidatorsProvider: false);
ModelMetadataProvider = CreateModelMetadataProvider(addHasValidatorsProvider: true);
BaselineModelMetadata = BaselineModelMetadataProvider.GetMetadataForType(Model.GetType());
ModelMetadata = ModelMetadataProvider.GetMetadataForType(Model.GetType());
ActionContext = GetActionContext();
ValidatorCache = new ValidatorCache();
}
protected static ModelMetadataProvider CreateModelMetadataProvider(bool addHasValidatorsProvider)
{
var detailsProviders = new List<IMetadataDetailsProvider>
{
new DefaultValidationMetadataProvider(),
};
if (addHasValidatorsProvider)
{
detailsProviders.Add(new HasValidatorsValidationMetadataProvider(ValidatorProviders));
}
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider, Options.Create(new MvcOptions()));
}
protected static ActionContext GetActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}
}
}

View File

@ -0,0 +1,42 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.Performance
{
public class ValidationVisitorByteArrayBenchmark : ValidationVisitorBenchmarkBase
{
public override object Model { get; } = new byte[30];
[Benchmark(Baseline = true, Description = "validation for byte arrays baseline", OperationsPerInvoke = Iterations)]
public void Baseline()
{
// Baseline for validating a byte array of size 30, without the ModelMetadata.HasValidators optimization.
// This is the behavior as of 2.1.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
BaselineModelMetadataProvider,
new ValidationStateDictionary());
validationVisitor.Validate(BaselineModelMetadata, "key", Model);
}
[Benchmark(Description = "validation for byte arrays", OperationsPerInvoke = Iterations)]
public void HasValidators()
{
// Validating a byte array of size 30, with the ModelMetadata.HasValidators optimization.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
ModelMetadataProvider,
new ValidationStateDictionary());
validationVisitor.Validate(ModelMetadata, "key", Model);
}
}
}

View File

@ -0,0 +1,92 @@
// 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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.Performance
{
public class ValidationVisitorModelWithValidatedProperties : ValidationVisitorBenchmarkBase
{
public class Person
{
[Required]
public int Id { get; set; }
[Required]
[StringLength(20)]
public string Name { get; set; }
public string Description { get; set; }
public IList<Address> Address { get; set; }
}
public class Address
{
[Required]
public string Street { get; set; }
public string Street2 { get; set; }
public string Type { get; set; }
[Required]
public string Zip { get; set; }
}
public override object Model { get; } = new Person
{
Id = 10,
Name = "Test",
Address = new List<Address>
{
new Address
{
Street = "1 Microsoft Way",
Type = "Work",
Zip = "98056",
},
new Address
{
Street = "15701 NE 39th St",
Type = "Home",
Zip = "98052",
}
},
};
[Benchmark(Baseline = true, Description = "validation for a model with some validated properties - baseline", OperationsPerInvoke = Iterations)]
public void Visit_TypeWithSomeValidatedProperties_Baseline()
{
// Baseline for validating a typical model with some properties that require validation.
// This executes without the ModelMetadata.HasValidators optimization.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
BaselineModelMetadataProvider,
new ValidationStateDictionary());
validationVisitor.Validate(BaselineModelMetadata, "key", Model);
}
[Benchmark(Description = "validation for a model with some validated properties", OperationsPerInvoke = Iterations)]
public void Visit_TypeWithSomeValidatedProperties()
{
// Validating a typical model with some properties that require validation.
// This executes with the ModelMetadata.HasValidators optimization.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
ModelMetadataProvider,
new ValidationStateDictionary());
validationVisitor.Validate(ModelMetadata, "key", Model);
}
}
}

View File

@ -325,6 +325,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public abstract bool ValidateChildren { get; }
/// <summary>
/// Gets a value that indicates if the model, or one of it's properties, or elements has associatated validators.
/// </summary>
/// <remarks>
/// When <see langword="false"/>, validation can be assume that the model is valid (<see cref="ModelValidationState.Valid"/>) without
/// inspecting the object graph.
/// </remarks>
public virtual bool? HasValidators { get; }
/// <summary>
/// Gets a collection of metadata items for validators.
/// </summary>

View File

@ -146,6 +146,8 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcOptionsConfigureCompatibilityOptions>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
services.TryAddEnumerable(

View File

@ -8,24 +8,25 @@ using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Internal
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Sets up default options for <see cref="MvcOptions"/>.
/// </summary>
public class MvcCoreMvcOptionsSetup : IConfigureOptions<MvcOptions>
internal class MvcCoreMvcOptionsSetup : IConfigureOptions<MvcOptions>, IPostConfigureOptions<MvcOptions>
{
private readonly IHttpRequestStreamReaderFactory _readerFactory;
private readonly ILoggerFactory _loggerFactory;
// Used in tests
public MvcCoreMvcOptionsSetup(IHttpRequestStreamReaderFactory readerFactory)
: this(readerFactory, NullLoggerFactory.Instance)
{
@ -83,6 +84,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
options.ModelValidatorProviders.Add(new DefaultModelValidatorProvider());
}
public void PostConfigure(string name, MvcOptions options)
{
// HasValidatorsValidationMetadataProvider uses the results of other ValidationMetadataProvider to determine if a model requires
// validation. It is imperative that this executes later than all other metadata provider. We'll register it as part of PostConfigure.
// This should ensure it appears later than all of the details provider registered by MVC and user configured details provider registered
// as part of ConfigureOptions.
options.ModelMetadataDetailsProviders.Add(new HasValidatorsValidationMetadataProvider(options.ModelValidatorProviders));
}
internal static void ConfigureAdditionalModelMetadataDetailsProviders(IList<IMetadataDetailsProvider> modelMetadataDetailsProviders)
{
// Don't bind the Type class by default as it's expensive. A user can override this behavior

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@ -29,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private bool? _isRequired;
private ModelPropertyCollection _properties;
private bool? _validateChildren;
private bool? _hasValidators;
private ReadOnlyCollection<object> _validatorMetadata;
/// <summary>
@ -427,6 +429,84 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
}
}
/// <inheritdoc />
public override bool? HasValidators
{
get
{
if (!_hasValidators.HasValue)
{
var visited = new HashSet<DefaultModelMetadata>();
_hasValidators = CalculateHasValidators(visited, this);
}
return _hasValidators.Value;
}
}
internal static bool CalculateHasValidators(HashSet<DefaultModelMetadata> visited, ModelMetadata metadata)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
if (metadata?.GetType() != typeof(DefaultModelMetadata))
{
// The calculation is valid only for DefaultModelMetadata instances. Null, other ModelMetadata instances
// or subtypes of DefaultModelMetadata will be treated as always requiring validation.
return true;
}
var defaultModelMetadata = (DefaultModelMetadata)metadata;
if (defaultModelMetadata._hasValidators.HasValue)
{
// Return a previously calculated value if available.
return defaultModelMetadata._hasValidators.Value;
}
if (defaultModelMetadata.ValidationMetadata.HasValidators != false)
{
// Either the ModelMetadata instance has some validators (HasValidators = true) or it is non-deterministic (HasValidators = null).
// In either case, assume it has validators.
return true;
}
// Before inspecting properties or elements of a collection, ensure we do not have a cycle.
// Consider a model like so
//
// Employee { BusinessDivision Division; int Id; string Name; }
// BusinessDivision { int Id; List<Employee> Employees }
//
// If we get to the Employee element from Employee.Division.Employees, we can return false for that instance
// and allow other properties of BusinessDivision and Employee to determine if it has validators.
if (!visited.Add(defaultModelMetadata))
{
return false;
}
// We have inspected the current element. Inspect properties or elements that may contribute to this value.
if (defaultModelMetadata.IsEnumerableType)
{
if (CalculateHasValidators(visited, defaultModelMetadata.ElementMetadata))
{
return true;
}
}
else if (defaultModelMetadata.IsComplexType)
{
foreach (var property in defaultModelMetadata.Properties)
{
if (CalculateHasValidators(visited, property))
{
return true;
}
}
}
// We've come this far. The ModelMetadata does not have any validation
return false;
}
/// <inheritdoc />
public override IReadOnlyList<object> ValidatorMetadata
{

View File

@ -0,0 +1,53 @@
// 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.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
internal class HasValidatorsValidationMetadataProvider : IValidationMetadataProvider
{
private readonly bool _hasOnlyMetadataBasedValidators;
private readonly IMetadataBasedModelValidatorProvider[] _validatorProviders;
public HasValidatorsValidationMetadataProvider(IList<IModelValidatorProvider> modelValidatorProviders)
{
if (modelValidatorProviders.Count > 0 && modelValidatorProviders.All(p => p is IMetadataBasedModelValidatorProvider))
{
_hasOnlyMetadataBasedValidators = true;
_validatorProviders = modelValidatorProviders.Cast<IMetadataBasedModelValidatorProvider>().ToArray();
}
}
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!_hasOnlyMetadataBasedValidators)
{
return;
}
for (var i = 0; i < _validatorProviders.Length; i++)
{
var provider = _validatorProviders[i];
if (provider.HasValidators(context.Key.ModelType, context.ValidationMetadata.ValidatorMetadata))
{
context.ValidationMetadata.HasValidators = true;
return;
}
}
if (context.ValidationMetadata.HasValidators == null)
{
context.ValidationMetadata.HasValidators = false;
}
}
}
}

View File

@ -41,5 +41,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// in this list, to be consumed later by an <see cref="Validation.IModelValidatorProvider"/>.
/// </remarks>
public IList<object> ValidatorMetadata { get; } = new List<object>();
/// <summary>
/// Gets a value that indicates if the model has validators .
/// </summary>
public bool? HasValidators { get; set; }
}
}

View File

@ -1,9 +1,10 @@
// 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.AspNetCore.Mvc.ModelBinding.Validation;
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Mvc.Internal
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
/// <summary>
/// A default <see cref="IModelValidatorProvider"/>.
@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
/// The <see cref="DefaultModelValidatorProvider"/> provides validators from <see cref="IModelValidator"/>
/// instances in <see cref="ModelBinding.ModelMetadata.ValidatorMetadata"/>.
/// </remarks>
public class DefaultModelValidatorProvider : IModelValidatorProvider
internal sealed class DefaultModelValidatorProvider : IMetadataBasedModelValidatorProvider
{
/// <inheritdoc />
public void CreateValidators(ModelValidatorProviderContext context)
@ -28,13 +29,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
continue;
}
var validator = validatorItem.ValidatorMetadata as IModelValidator;
if (validator != null)
if (validatorItem.ValidatorMetadata is IModelValidator validator)
{
validatorItem.Validator = validator;
validatorItem.IsReusable = true;
}
}
}
public bool HasValidators(Type modelType, IList<object> validatorMetadata)
{
for (var i = 0; i < validatorMetadata.Count; i++)
{
if (validatorMetadata[i] is IModelValidator)
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,30 @@
// 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 Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
/// <summary>
/// An <see cref="IModelValidatorProvider" /> that provides <see cref="IModelValidator"/> instances
/// exclusively using values in <see cref="ModelMetadata.ValidatorMetadata"/> or the model type.
/// <para>
/// <see cref="IMetadataBasedModelValidatorProvider" /> can be used to statically determine if a given
/// <see cref="ModelMetadata"/> instance can incur any validation. The value for <see cref="ModelMetadata.HasValidators"/>
/// can be calculated if all instances in <see cref="MvcOptions.ModelValidatorProviders"/> are <see cref="IMetadataBasedModelValidatorProvider" />.
/// </para>
/// </summary>
public interface IMetadataBasedModelValidatorProvider : IModelValidatorProvider
{
/// <summary>
/// Gets a value that determines if the <see cref="IModelValidatorProvider"/> can
/// produce any validators given the <paramref name="modelType"/> and <paramref name="modelType"/>.
/// </summary>
/// <param name="modelType">The <see cref="Type"/> of the model.</param>
/// <param name="validatorMetadata">The list of metadata items for validators. <seealso cref="ValidationMetadata.ValidatorMetadata"/>.</param>
/// <returns></returns>
bool HasValidators(Type modelType, IList<object> validatorMetadata);
}
}

View File

@ -265,6 +265,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
CurrentPath.Pop(model);
return true;
}
// If the metadata indicates that no validators exist AND the aggregate state for the key says that the model graph
// is not invalid (i.e. is one of Unvalidated, Valid, or Skipped) we can safely mark the graph as valid.
else if (metadata.HasValidators == false &&
ModelState.GetFieldValidationState(key) != ModelValidationState.Invalid)
{
// No validators will be created for this graph of objects. Mark it as valid if it wasn't previously validated.
var entries = ModelState.FindKeysWithPrefix(key);
foreach (var item in entries)
{
if (item.Value.ValidationState == ModelValidationState.Unvalidated)
{
item.Value.ValidationState = ModelValidationState.Valid;
}
}
CurrentPath.Pop(model);
return true;
}
using (StateManager.Recurse(this, key ?? string.Empty, metadata, model, strategy))
{

View File

@ -2,19 +2,21 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
namespace Microsoft.AspNetCore.Mvc.DataAnnotations
{
/// <summary>
/// An implementation of <see cref="IModelValidatorProvider"/> which provides validators
/// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides
/// a validator for types which implement <see cref="IValidatableObject"/>.
/// </summary>
public class DataAnnotationsModelValidatorProvider : IModelValidatorProvider
internal sealed class DataAnnotationsModelValidatorProvider : IMetadataBasedModelValidatorProvider
{
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory;
@ -66,8 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
continue;
}
var attribute = validatorItem.ValidatorMetadata as ValidationAttribute;
if (attribute == null)
if (!(validatorItem.ValidatorMetadata is ValidationAttribute attribute))
{
continue;
}
@ -98,5 +99,23 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
});
}
}
public bool HasValidators(Type modelType, IList<object> validatorMetadata)
{
if (typeof(IValidatableObject).IsAssignableFrom(modelType))
{
return true;
}
for (var i = 0; i < validatorMetadata.Count; i++)
{
if (validatorMetadata[i] is ValidationAttribute)
{
return true;
}
}
return false;
}
}
}

View File

@ -4,3 +4,9 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.DataAnnotations.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Core.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Core.TestCommon, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.ViewFeatures.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -248,6 +248,7 @@ namespace Microsoft.AspNetCore.Mvc
new Type[]
{
typeof(MvcOptionsConfigureCompatibilityOptions),
typeof(MvcCoreMvcOptionsSetup),
}
},
{

View File

@ -1170,11 +1170,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var modelState = actionContext.ModelState;
var validationState = new ValidationStateDictionary();
var validator = CreateValidator(typeof(List<string>));
var validator = CreateValidator(typeof(List<ValidatedModel>));
var model = new List<string>()
var model = new List<ValidatedModel>()
{
"15",
new ValidatedModel { Value = "15" },
};
modelState.SetModelValue("userIds[0]", "15", "15");
@ -1192,6 +1192,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Empty(entry.Errors);
}
private class ValidatedModel
{
[Required]
public string Value { get; set; }
}
[Fact]
public void Validate_SuppressesValidation_ForExcludedType_Stream()
{
@ -1317,7 +1323,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ model, new ValidationStateEntry() }
};
var method = GetType().GetMethod(nameof(Validate_Throws_ForTopLevelMetadataData), BindingFlags.NonPublic | BindingFlags.Instance);
var metadata = MetadataProvider.GetMetadataForParameter(method.GetParameters()[0]);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
@ -1325,6 +1330,102 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.NotNull(ex.HelpLink);
}
[Fact]
public void Validate_TypeWithoutValidators()
{
var actionContext = new ActionContext();
var validator = CreateValidator();
var model = new ModelWithoutValidation();
var validationState = new ValidationStateDictionary
{
{ model, new ValidationStateEntry() }
};
actionContext.ModelState.SetModelValue("Property1", new ValueProviderResult("value1"));
actionContext.ModelState.SetModelValue("Property2", new ValueProviderResult("value2"));
// Act
validator.Validate(actionContext, validationState, string.Empty, model);
// Assert
var modelState = actionContext.ModelState;
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
Assert.True(modelState.IsValid);
var entry = modelState["Property1"];
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = modelState["Property2"];
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
[Fact]
public void Validate_TypeWithoutValidators_DoesNotUpdateValidationState()
{
var actionContext = new ActionContext();
var validator = CreateValidator();
var model = new ModelWithoutValidation();
var validationState = new ValidationStateDictionary
{
{ model, new ValidationStateEntry() }
};
var modelState = actionContext.ModelState;
modelState.SetModelValue("Property1", new ValueProviderResult("value1"));
modelState.SetModelValue("Property2", new ValueProviderResult("value2"));
modelState["Property2"].ValidationState = ModelValidationState.Skipped;
// Act
validator.Validate(actionContext, validationState, string.Empty, model);
// Assert
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
Assert.True(modelState.IsValid);
var entry = modelState["Property1"];
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = modelState["Property2"];
Assert.Equal(ModelValidationState.Skipped, entry.ValidationState);
}
[Fact]
public void Validate_TypeWithoutValidators_DoesNotResetInvalidState()
{
var actionContext = new ActionContext();
var validator = CreateValidator();
var model = new ModelWithoutValidation();
var validationState = new ValidationStateDictionary
{
{ model, new ValidationStateEntry() }
};
var modelState = actionContext.ModelState;
modelState.SetModelValue("Property1", new ValueProviderResult("value1"));
modelState.SetModelValue("Property2", new ValueProviderResult("value2"));
modelState["Property2"].ValidationState = ModelValidationState.Invalid;
// Act
validator.Validate(actionContext, validationState, string.Empty, model);
// Assert
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
Assert.False(modelState.IsValid);
var entry = modelState["Property1"];
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = modelState["Property2"];
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
}
private class ModelWithoutValidation
{
public string Property1 { get; set; }
public string Property2 { get; set; }
}
private static DefaultObjectValidator CreateValidator(Type excludedType)
{
var excludeFilters = new List<SuppressChildValidationMetadataProvider>();
@ -1352,6 +1453,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private class ThrowingProperty
{
[Required]
public string WatchOut
{
get
@ -1520,6 +1622,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Depth = depth;
}
[Range(-10, 400)]
public int Depth { get; }
public int MaxAllowedDepth { get; }

View File

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Xml;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -909,6 +910,351 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
metadataProvider.VerifyAll();
}
[Fact]
public void CalculateHasValidators_ParameterMetadata_TypeHasNoValidators()
{
// Arrange
var parameter = GetType()
.GetMethod(nameof(CalculateHasValidators_ParameterMetadata_TypeHasNoValidatorsMethod), BindingFlags.Static | BindingFlags.NonPublic)
.GetParameters()[0];
var modelIdentity = ModelMetadataIdentity.ForParameter(parameter);
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
private static void CalculateHasValidators_ParameterMetadata_TypeHasNoValidatorsMethod(string model) { }
[Fact]
public void CalculateHasValidators_PropertyMetadata_TypeHasNoValidators()
{
// Arrange
var property = GetType()
.GetProperty(nameof(CalculateHasValidators_PropertyMetadata_TypeHasNoValidatorsProperty), BindingFlags.Static | BindingFlags.NonPublic);
var modelIdentity = ModelMetadataIdentity.ForProperty(property.PropertyType, property.Name, GetType());
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
private static int CalculateHasValidators_PropertyMetadata_TypeHasNoValidatorsProperty { get; set; }
[Fact]
public void CalculateHasValidators_TypeWithoutProperties_TypeHasNoValidators()
{
// Arrange
var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), false);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
[Fact]
public void CalculateHasValidators_SimpleType_TypeHasValidators()
{
// Arrange
var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), true);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_ReturnsTrue_SimpleType_TypeHasNonDeterministicValidators()
{
// Arrange
var modelIdentity = ModelMetadataIdentity.ForType(typeof(string));
var modelMetadata = CreateModelMetadata(modelIdentity, Mock.Of<IModelMetadataProvider>(), null);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_TypeWithProperties_PropertyIsNotDefaultModelMetadata()
{
// Arrange
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var propertyIdentity = ModelMetadataIdentity.ForProperty(typeof(int), nameof(TypeWithProperties.PublicGetPublicSetProperty), typeof(string));
var propertyMetadata = new Mock<ModelMetadata>(propertyIdentity);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata.Object, })
.Verifiable();
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_TypeWithProperties_HasValidatorForAnyPropertyIsTrue()
{
// Arrange
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var property1Identity = ModelMetadataIdentity.ForProperty(typeof(int), nameof(TypeWithProperties.PublicGetPublicSetProperty), typeof(string));
var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, false);
var property2Identity = ModelMetadataIdentity.ForProperty(typeof(int), nameof(TypeWithProperties.PublicGetProtectedSetProperty), typeof(string));
var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, true);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { property1Metadata, property2Metadata })
.Verifiable();
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_TypeWithProperties_HasValidatorsForPropertyIsNotDeterminstic()
{
// Arrange
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var propertyIdentity = ModelMetadataIdentity.ForProperty(typeof(int), nameof(TypeWithProperties.PublicGetPublicSetProperty), typeof(string));
var propertyMetadata = CreateModelMetadata(propertyIdentity, metadataProvider.Object, null);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { propertyMetadata, })
.Verifiable();
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_TypeWithProperties_HasValidatorForAllPropertiesIsFalse()
{
// Arrange
var modelType = typeof(TypeWithProperties);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var property1Identity = ModelMetadataIdentity.ForProperty(typeof(int), nameof(TypeWithProperties.PublicGetPublicSetProperty), modelType);
var property1Metadata = CreateModelMetadata(property1Identity, metadataProvider.Object, false);
var property2Identity = ModelMetadataIdentity.ForProperty(typeof(int), nameof(TypeWithProperties.PublicGetProtectedSetProperty), modelType);
var property2Metadata = CreateModelMetadata(property2Identity, metadataProvider.Object, false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { property1Metadata, property2Metadata })
.Verifiable();
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
[Fact]
public void CalculateHasValidators_SelfReferencingType_HasValidatorOnNestedProperty()
{
// Arrange
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(Employee.Id), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeUnit = ModelMetadataIdentity.ForProperty(typeof(BusinessUnit), nameof(Employee.Unit), modelType);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
var employeeManager = ModelMetadataIdentity.ForProperty(typeof(Employee), nameof(Employee.Unit), modelType);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
var employeeEmployees = ModelMetadataIdentity.ForProperty(typeof(List<Employee>), nameof(Employee.Employees), modelType);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
var unitHead = ModelMetadataIdentity.ForProperty(typeof(Employee), nameof(BusinessUnit.Head), modelType);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, false);
var unitId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(BusinessUnit.Id), modelType);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, true); // BusinessUnit.Id has validators.
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { employeeIdMetadata, employeeUnitMetadata, employeeManagerMetadata, employeeEmployeesMetadata, })
.Verifiable();
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(typeof(BusinessUnit)))
.Returns(new[] { unitHeadMetadata, unitIdMetadata, })
.Verifiable();
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_SelfReferencingType_HasValidatorOnSelfReferencedProperty()
{
// Arrange
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(Employee.Id), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeUnit = ModelMetadataIdentity.ForProperty(typeof(BusinessUnit), nameof(Employee.Unit), modelType);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
var employeeManager = ModelMetadataIdentity.ForProperty(typeof(Employee), nameof(Employee.Unit), modelType);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
var employeeEmployees = ModelMetadataIdentity.ForProperty(typeof(List<Employee>), nameof(Employee.Employees), modelType);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
var unitHead = ModelMetadataIdentity.ForProperty(typeof(Employee), nameof(BusinessUnit.Head), modelType);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, true); // BusinessUnit.Head has validators
var unitId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(BusinessUnit.Id), modelType);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { employeeIdMetadata, employeeUnitMetadata, employeeManagerMetadata, employeeEmployeesMetadata, });
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(typeof(BusinessUnit)))
.Returns(new[] { unitHeadMetadata, unitIdMetadata, });
metadataProvider
.Setup(mp => mp.GetMetadataForType(modelType))
.Returns(modelMetadata);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_CollectionElementHasValidators()
{
// Arrange
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(Employee.Id), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeEmployees = ModelMetadataIdentity.ForProperty(typeof(List<Employee>), nameof(Employee.Employees), modelType);
var employeeEmployeesMetadata = CreateModelMetadata(employeeEmployees, metadataProvider.Object, false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { employeeIdMetadata, employeeEmployeesMetadata, });
metadataProvider
.Setup(mp => mp.GetMetadataForType(modelType))
.Returns(CreateModelMetadata(modelIdentity, metadataProvider.Object, true)); // Employees.Employee has validators
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.True(result);
}
[Fact]
public void CalculateHasValidators_SelfReferencingType_NoValidatorsInGraph()
{
// Arrange
var modelType = typeof(Employee);
var modelIdentity = ModelMetadataIdentity.ForType(modelType);
var metadataProvider = new Mock<IModelMetadataProvider>();
var modelMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(Employee.Id), modelType);
var employeeIdMetadata = CreateModelMetadata(modelIdentity, metadataProvider.Object, false);
var employeeUnit = ModelMetadataIdentity.ForProperty(typeof(BusinessUnit), nameof(Employee.Unit), modelType);
var employeeUnitMetadata = CreateModelMetadata(employeeUnit, metadataProvider.Object, false);
var employeeManager = ModelMetadataIdentity.ForProperty(typeof(Employee), nameof(Employee.Unit), modelType);
var employeeManagerMetadata = CreateModelMetadata(employeeManager, metadataProvider.Object, false);
var employeeEmployeesId = ModelMetadataIdentity.ForProperty(typeof(List<Employee>), nameof(Employee.Employees), modelType);
var employeeEmployeesIdMetadata = CreateModelMetadata(employeeEmployeesId, metadataProvider.Object, false);
var unitHead = ModelMetadataIdentity.ForProperty(typeof(Employee), nameof(BusinessUnit.Head), modelType);
var unitHeadMetadata = CreateModelMetadata(unitHead, metadataProvider.Object, false);
var unitId = ModelMetadataIdentity.ForProperty(typeof(int), nameof(BusinessUnit.Id), modelType);
var unitIdMetadata = CreateModelMetadata(unitId, metadataProvider.Object, false);
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(modelType))
.Returns(new[] { employeeIdMetadata, employeeUnitMetadata, employeeManagerMetadata, employeeEmployeesIdMetadata, });
metadataProvider
.Setup(mp => mp.GetMetadataForProperties(typeof(BusinessUnit)))
.Returns(new[] { unitHeadMetadata, unitIdMetadata, });
metadataProvider
.Setup(mp => mp.GetMetadataForType(modelType))
.Returns(modelMetadata);
// Act
var result = DefaultModelMetadata.CalculateHasValidators(new HashSet<DefaultModelMetadata>(), modelMetadata);
// Assert
Assert.False(result);
}
private static DefaultModelMetadata CreateModelMetadata(
ModelMetadataIdentity modelIdentity,
IModelMetadataProvider metadataProvider,
bool? hasValidators)
{
return new DefaultModelMetadata(
metadataProvider,
new SetHasValidatorsCompositeMetadataDetailsProvider { HasValidators = hasValidators },
new DefaultMetadataDetails(modelIdentity, new ModelAttributes(new object[0], new object[0], new object[0])));
}
private void ActionMethod(string input)
{
}
@ -921,5 +1267,41 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
public int PublicGetPublicSetProperty { get; set; }
}
public class Employee
{
public int Id { get; set; }
public BusinessUnit Unit { get; set; }
public Employee Manager { get; set; }
public List<Employee> Employees { get; set; }
}
public class BusinessUnit
{
public Employee Head { get; set; }
public int Id { get; set; }
}
private class SetHasValidatorsCompositeMetadataDetailsProvider : ICompositeMetadataDetailsProvider
{
public bool? HasValidators { get; set; }
public void CreateBindingMetadata(BindingMetadataProviderContext context)
{
}
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
}
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
context.ValidationMetadata.HasValidators = HasValidators;
}
}
}
}

View File

@ -0,0 +1,131 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
public class HasValidatorsValidationMetadataProviderTest
{
[Fact]
public void CreateValidationMetadata_DoesNotSetHasValidators_IfNonMetadataBasedProviderExists()
{
// Arrange
var validationProviders = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
Mock.Of<IModelValidatorProvider>(),
};
var metadataProvider = new HasValidatorsValidationMetadataProvider(validationProviders);
var key = ModelMetadataIdentity.ForType(typeof(object));
var modelAttributes = new ModelAttributes(new object[0], new object[0], new object[0]);
var context = new ValidationMetadataProviderContext(key, modelAttributes);
// Act
metadataProvider.CreateValidationMetadata(context);
// Assert
Assert.Null(context.ValidationMetadata.HasValidators);
}
[Fact]
public void CreateValidationMetadata_DoesNotSetHasValidators_IfProviderIsConfigured()
{
// Arrange
var validationProviders = new IModelValidatorProvider[0];
var metadataProvider = new HasValidatorsValidationMetadataProvider(validationProviders);
var key = ModelMetadataIdentity.ForType(typeof(object));
var modelAttributes = new ModelAttributes(new object[0], new object[0], new object[0]);
var context = new ValidationMetadataProviderContext(key, modelAttributes);
// Act
metadataProvider.CreateValidationMetadata(context);
// Assert
Assert.Null(context.ValidationMetadata.HasValidators);
}
[Fact]
public void CreateValidationMetadata_SetsHasValidatorsToTrue_IfProviderReturnsTrue()
{
// Arrange
var metadataBasedModelValidatorProvider = new Mock<IMetadataBasedModelValidatorProvider>();
metadataBasedModelValidatorProvider.Setup(p => p.HasValidators(typeof(object), It.IsAny<IList<object>>()))
.Returns(true)
.Verifiable();
var validationProviders = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
metadataBasedModelValidatorProvider.Object,
};
var metadataProvider = new HasValidatorsValidationMetadataProvider(validationProviders);
var key = ModelMetadataIdentity.ForType(typeof(object));
var modelAttributes = new ModelAttributes(new object[0], new object[0], new object[0]);
var context = new ValidationMetadataProviderContext(key, modelAttributes);
// Act
metadataProvider.CreateValidationMetadata(context);
// Assert
Assert.True(context.ValidationMetadata.HasValidators);
metadataBasedModelValidatorProvider.Verify();
}
[Fact]
public void CreateValidationMetadata_SetsHasValidatorsToFalse_IfNoProviderReturnsTrue()
{
// Arrange
var provider = Mock.Of<IMetadataBasedModelValidatorProvider>(p => p.HasValidators(typeof(object), It.IsAny<IList<object>>()) == false);
var validationProviders = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
provider,
};
var metadataProvider = new HasValidatorsValidationMetadataProvider(validationProviders);
var key = ModelMetadataIdentity.ForType(typeof(object));
var modelAttributes = new ModelAttributes(new object[0], new object[0], new object[0]);
var context = new ValidationMetadataProviderContext(key, modelAttributes);
// Act
metadataProvider.CreateValidationMetadata(context);
// Assert
Assert.False(context.ValidationMetadata.HasValidators);
}
[Fact]
public void CreateValidationMetadata_DoesNotOverrideExistingHasValidatorsValue()
{
// Arrange
var provider = Mock.Of<IMetadataBasedModelValidatorProvider>(p => p.HasValidators(typeof(object), It.IsAny<IList<object>>()) == false);
var validationProviders = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
provider,
};
var metadataProvider = new HasValidatorsValidationMetadataProvider(validationProviders);
var key = ModelMetadataIdentity.ForType(typeof(object));
var modelAttributes = new ModelAttributes(new object[0], new object[0], new object[0]);
var context = new ValidationMetadataProviderContext(key, modelAttributes);
// Initialize this value.
context.ValidationMetadata.HasValidators = true;
// Act
metadataProvider.CreateValidationMetadata(context);
// Assert
Assert.True(context.ValidationMetadata.HasValidators);
}
}
}

View File

@ -6,11 +6,9 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
// Integration tests for the default configuration of ModelMetadata and Validation providers
public class DefaultModelValidatorProviderTest
@ -145,6 +143,34 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Single(validatorItems, v => ((DataAnnotationsModelValidator)v.Validator).Attribute is StringLengthAttribute);
}
[Fact]
public void HasValidators_ReturnsTrue_IfMetadataIsIModelValidator()
{
// Arrange
var validatorProvider = new DefaultModelValidatorProvider();
var attributes = new object[] { new RequiredAttribute(), new CustomModelValidatorAttribute(), new BindRequiredAttribute(), };
// Act
var result = validatorProvider.HasValidators(typeof(object), attributes);
// Assert
Assert.True(result);
}
[Fact]
public void HasValidators_ReturnsFalse_IfNoMetadataIsIModelValidator()
{
// Arrange
var validatorProvider = new DefaultModelValidatorProvider();
var attributes = new object[] { new RequiredAttribute(), new BindRequiredAttribute(), };
// Act
var result = validatorProvider.HasValidators(typeof(object), attributes);
// Assert
Assert.False(result);
}
private static IList<ValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
return metadata.ValidatorMetadata.Select(v => new ValidatorItem(v)).ToList();

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Xunit;
@ -37,6 +38,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
MvcCoreMvcOptionsSetup.ConfigureAdditionalModelMetadataDetailsProviders(detailsProviders);
var validationProviders = TestModelValidatorProvider.CreateDefaultProvider();
detailsProviders.Add(new HasValidatorsValidationMetadataProvider(validationProviders.ValidatorProviders));
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider, Options.Create(new MvcOptions()));
}
@ -57,6 +61,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
detailsProviders.AddRange(providers);
var validationProviders = TestModelValidatorProvider.CreateDefaultProvider();
detailsProviders.Add(new HasValidatorsValidationMetadataProvider(validationProviders.ValidatorProviders));
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider, Options.Create(new MvcOptions()));
}

View File

@ -3,8 +3,6 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;

View File

@ -1,16 +1,18 @@
// 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.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
namespace Microsoft.AspNetCore.Mvc.DataAnnotations
{
public class DataAnnotationsModelValidatorProviderTest
{
@ -110,6 +112,56 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
Assert.Single(providerContext.Results);
}
[Fact]
public void HasValidators_ReturnsTrue_IfModelIsIValidatableObject()
{
// Arrange
var provider = GetProvider();
var mockValidatable = Mock.Of<IValidatableObject>();
// Act
var result = provider.HasValidators(mockValidatable.GetType(), Array.Empty<object>());
// Assert
Assert.True(result);
}
[Fact]
public void HasValidators_ReturnsTrue_IfMetadataContainsValidationAttribute()
{
// Arrange
var provider = GetProvider();
var attributes = new object[] { new BindNeverAttribute(), new DummyValidationAttribute() };
// Act
var result = provider.HasValidators(typeof(object), attributes);
// Assert
Assert.True(result);
}
[Fact]
public void HasValidators_ReturnsFalse_IfNoDataAnnotationsValidationIsAvailable()
{
// Arrange
var provider = GetProvider();
var attributes = new object[] { new BindNeverAttribute(), };
// Act
var result = provider.HasValidators(typeof(object), attributes);
// Assert
Assert.False(result);
}
private static DataAnnotationsModelValidatorProvider GetProvider()
{
return new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
stringLocalizerFactory: null);
}
private IList<ValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
var items = new List<ValidatorItem>(metadata.ValidatorMetadata.Count);

View File

@ -262,5 +262,95 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => Client.SendAsync(requestMessage));
Assert.Equal(expected, ex.Message);
}
[Fact]
public async Task ErrorsDeserializingMalformedJson_AreReportedForModelsWithoutAnyValidationAttributes()
{
// This test verifies that for a model with ModelMetadata.HasValidators = false, we continue to get an invalid ModelState + validation
// errors from json serialization errors
// Arrange
var input = "{Id = \"This string is incomplete";
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "TestApi/PostBookWithNoValidation")
{
Content = new StringContent(input, Encoding.UTF8, "application/json"),
};
// Act
var response = await Client.SendAsync(requestMessage);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var responseContent = await response.Content.ReadAsStringAsync();
var validationProblemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(responseContent);
Assert.Collection(
validationProblemDetails.Errors,
error =>
{
Assert.Empty(error.Key);
Assert.Equal(new[] { "Invalid character after parsing property name. Expected ':' but got: =. Path '', line 1, position 4." }, error.Value);
});
}
[Fact]
public async Task JsonValidationErrors_AreReportedForModelsWithoutAnyValidationAttributes()
{
// This test verifies that for a model with ModelMetadata.HasValidators = false, we continue to get an invalid ModelState + validation
// errors from json serialization errors
// Arrange
var input = "{Id: \"0c92bb85-cfaf-4344-8a9d-f92e88716861\"}";
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "TestApi/PostBookWithNoValidation")
{
Content = new StringContent(input, Encoding.UTF8, "application/json"),
};
// Act
var response = await Client.SendAsync(requestMessage);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var responseContent = await response.Content.ReadAsStringAsync();
var validationProblemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(responseContent);
Assert.Collection(
validationProblemDetails.Errors,
error =>
{
Assert.Empty(error.Key);
Assert.Equal(new[] { "Required property 'isbn' not found in JSON. Path '', line 1, position 44." }, error.Value);
});
}
[Fact]
public async Task ErrorsDeserializingMalformedXml_AreReportedForModelsWithoutAnyValidationAttributes()
{
// This test verifies that for a model with ModelMetadata.HasValidators = false, we continue to get an invalid ModelState + validation
// errors from json serialization errors
// Arrange
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<BookModelWithNoValidation xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite.Models\">" +
"<Id>Incomplete element" +
"</BookModelWithNoValidation>";
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "TestApi/PostBookWithNoValidation")
{
Content = new StringContent(input, Encoding.UTF8, "application/xml"),
};
// Act
var response = await Client.SendAsync(requestMessage);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var responseContent = await response.Content.ReadAsStringAsync();
var validationProblemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(responseContent);
Assert.Collection(
validationProblemDetails.Errors,
error =>
{
Assert.Empty(error.Key);
Assert.Equal(new[] { "An error occurred while deserializing input data." }, error.Value);
});
}
}
}

View File

@ -111,13 +111,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Read-only collection should not be updated.
Assert.Empty(boundModel.Address);
// ModelState (data is can't be validated).
Assert.False(modelState.IsValid);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
Assert.Equal("Address[0].Street", entry.Key);
var state = entry.Value;
Assert.NotNull(state);
Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
Assert.Equal("SomeStreet", state.RawValue);
Assert.Equal("SomeStreet", state.AttemptedValue);
}
@ -292,12 +291,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Empty(boundModel.Address);
// ModelState (data cannot be validated).
Assert.False(modelState.IsValid);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
Assert.Equal("prefix.Address[0].Street", entry.Key);
var state = entry.Value;
Assert.NotNull(state);
Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
Assert.Equal("SomeStreet", state.AttemptedValue);
Assert.Equal("SomeStreet", state.RawValue);
}

View File

@ -0,0 +1,53 @@
// 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.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
{
public class HasValidatorsValidationMetadataProviderIntegrationTest
{
[Fact]
public void HasValidatorsValidationMetadataProvider_IsRegisteredAfterOtherMetadataProviders()
{
// HasValidatorsValidationMetadataProvider uses values populated by other details providers to query validator providers
// This test ensures all other detail providers have had an opportunity to modify validation metadata first.
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();
serviceCollection.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
serviceCollection.AddMvc();
var services = serviceCollection.BuildServiceProvider();
// Act
var options = services.GetRequiredService<IOptions<MvcOptions>>();
Assert.IsType<HasValidatorsValidationMetadataProvider>(options.Value.ModelMetadataDetailsProviders.Last());
}
[Fact]
public void HasValidatorsValidationMetadataProvider_IsRegisteredAfterUserSpecifiedMetadataProvider()
{
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();
serviceCollection.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
serviceCollection.AddMvc(mvcOptions =>
{
mvcOptions.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(IQueryable)));
});
var services = serviceCollection.BuildServiceProvider();
// Act
var options = services.GetRequiredService<IOptions<MvcOptions>>();
Assert.IsType<HasValidatorsValidationMetadataProvider>(options.Value.ModelMetadataDetailsProviders.Last());
}
}
}

View File

@ -373,15 +373,15 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var result = await TryUpdateModelAsync(model, string.Empty, testContext);
// Assert
Assert.False(result);
Assert.True(result);
// ModelState
Assert.False(modelState.IsValid);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
Assert.Equal("Address[0].Street", entry.Key);
var state = entry.Value;
Assert.NotNull(state);
Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
Assert.Equal("SomeStreet", state.RawValue);
Assert.Equal("SomeStreet", state.AttemptedValue);
}
@ -402,15 +402,15 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var result = await TryUpdateModelAsync(model, "prefix", testContext);
// Assert
Assert.False(result);
Assert.True(result);
// ModelState
Assert.False(modelState.IsValid);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState);
Assert.Equal("prefix.Address[0].Street", entry.Key);
var state = entry.Value;
Assert.NotNull(state);
Assert.Equal(ModelValidationState.Unvalidated, state.ValidationState);
Assert.Equal(ModelValidationState.Valid, state.ValidationState);
Assert.Equal("SomeStreet", state.RawValue);
Assert.Equal("SomeStreet", state.AttemptedValue);
}

View File

@ -158,7 +158,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(2, modelStateErrors.Count);
AssertErrorEquals("Property", modelStateErrors["Message"]);
AssertErrorEquals("Model", modelStateErrors[""]);
}
[Fact]
@ -183,7 +182,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var modelStateErrors = GetModelStateErrors(modelState);
Assert.Single(modelStateErrors); // single error from the required attribute
AssertErrorEquals("Property", modelStateErrors.Single().Value);
}
[ModelLevelError]

View File

@ -2,9 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -14,6 +18,7 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
@ -1488,6 +1493,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
public string Control { get; set; }
[ValidateSometimes(nameof(Control))]
[Range(0, 10)]
public int ControlLength => Control.Length;
}
@ -1571,6 +1577,53 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
});
}
// This type has a IPropertyValidationFilter declared on a property, but no validators.
// We should expect validation to short-circuit
private class ValidateSomePropertiesSometimesWithoutValidation
{
public string Control { get; set; }
[ValidateSometimes(nameof(Control))]
public int ControlLength => Control.Length;
}
[Fact]
public async Task PropertyToSometimesSkip_IsNotValidated_IfNoValidationAttributesExistButPropertyValidationFilterExists()
{
// Arrange
var parameter = new ParameterDescriptor
{
Name = "parameter",
ParameterType = typeof(ValidateSomePropertiesSometimesWithoutValidation),
};
var testContext = ModelBindingTestHelper.GetTestContext();
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var modelState = testContext.ModelState;
// Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
modelState.SetModelValue(
nameof(ValidateSomePropertiesSometimes.ControlLength),
rawValue: null,
attemptedValue: null);
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(result.IsModelSet);
var model = Assert.IsType<ValidateSomePropertiesSometimesWithoutValidation>(result.Model);
Assert.Null(model.Control);
// Note this Exception is not thrown earlier.
Assert.Throws<NullReferenceException>(() => model.ControlLength);
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal(nameof(ValidateSomePropertiesSometimesWithoutValidation.ControlLength), kvp.Key);
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
}
private class Order11
{
public IEnumerable<Address> ShippingAddresses { get; set; }
@ -1838,6 +1891,550 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
public string Message { get; set; }
}
[Fact]
public async Task Validation_NoAttributeInGraphOfObjects_WithDefaultValidatorProviders()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Order12),
BindingInfo = new BindingInfo
{
BindingSource = BindingSource.Body
},
};
var input = new Order12
{
Id = 10,
OrderFile = new byte[40],
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(input)));
request.ContentType = "application/json";
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Order12>(modelBindingResult.Model);
Assert.Equal(input.Id, model.Id);
Assert.Equal(input.OrderFile, model.OrderFile);
Assert.Null(model.RelatedOrders);
Assert.Empty(modelState);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
}
private class Order12
{
public int Id { get; set; }
public byte[] OrderFile { get; set; }
public IList<Order12> RelatedOrders { get; set; }
}
[Fact]
public async Task Validation_ListOfType_NoValidatorOnParameter()
{
// Arrange
var parameterInfo = GetType().GetMethod(nameof(Validation_ListOfType_NoValidatorOnParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
.GetParameters()
.First();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var parameter = new ParameterDescriptor()
{
Name = parameterInfo.Name,
ParameterType = parameterInfo.ParameterType,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?[0]=1&[1]=2");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<List<int>>(modelBindingResult.Model);
Assert.Equal(new[] { 1, 2 }, model);
Assert.False(modelMetadata.HasValidators);
Assert.True(modelState.IsValid);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "[0]").Value;
Assert.Equal("1", entry.AttemptedValue);
Assert.Equal("1", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "[1]").Value;
Assert.Equal("2", entry.AttemptedValue);
Assert.Equal("2", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
private static void Validation_ListOfType_NoValidatorOnParameterTestMethod(List<int> parameter) { }
[Fact]
public async Task Validation_ListOfType_ValidatorOnParameter()
{
// Arrange
var parameterInfo = GetType().GetMethod(nameof(Validation_ListOfType_ValidatorOnParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
.GetParameters()
.First();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var parameter = new ParameterDescriptor()
{
Name = parameterInfo.Name,
ParameterType = parameterInfo.ParameterType,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?[0]=1&[1]=2");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<List<int>>(modelBindingResult.Model);
Assert.Equal(new[] { 1, 2 }, model);
Assert.True(modelMetadata.HasValidators);
Assert.False(modelState.IsValid);
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "").Value;
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "[0]").Value;
Assert.Equal("1", entry.AttemptedValue);
Assert.Equal("1", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "[1]").Value;
Assert.Equal("2", entry.AttemptedValue);
Assert.Equal("2", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
private static void Validation_ListOfType_ValidatorOnParameterTestMethod([ConsistentMinLength(3)] List<int> parameter) { }
private class ConsistentMinLength : ValidationAttribute
{
private readonly int _length;
public ConsistentMinLength(int length)
{
_length = length;
}
public override bool IsValid(object value)
{
return value is ICollection collection && collection.Count >= _length;
}
}
[Fact]
public async Task Validation_CollectionOfType_ValidatorOnElement()
{
// Arrange
var parameterInfo = GetType().GetMethod(nameof(Validation_CollectionOfType_ValidatorOnElementTestMethod), BindingFlags.NonPublic | BindingFlags.Static)
.GetParameters()
.First();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var parameter = new ParameterDescriptor()
{
Name = parameterInfo.Name,
ParameterType = parameterInfo.ParameterType,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?p[0].Id=1&p[1].Id=2");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Collection<InvalidEvenIds>>(modelBindingResult.Model);
Assert.Equal(1, model[0].Id);
Assert.Equal(2, model[1].Id);
Assert.True(modelMetadata.HasValidators);
Assert.False(modelState.IsValid);
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "p[0].Id").Value;
Assert.Equal("1", entry.AttemptedValue);
Assert.Equal("1", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "p[1]").Value;
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "p[1].Id").Value;
Assert.Equal("2", entry.AttemptedValue);
Assert.Equal("2", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
private static void Validation_CollectionOfType_ValidatorOnElementTestMethod(Collection<InvalidEvenIds> p) { }
public class InvalidEvenIds : IValidatableObject
{
public int Id { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Id % 2 == 0)
{
yield return new ValidationResult("Failed validation");
}
}
}
[Fact]
public async Task Validation_DictionaryType_NoValidators()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(IDictionary<string, int>)
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value=10");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
Assert.Collection(
model.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("key0", kvp.Key);
Assert.Equal(10, kvp.Value);
});
Assert.True(modelState.IsValid);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value;
Assert.Equal("10", entry.AttemptedValue);
Assert.Equal("10", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
[Fact]
public async Task Validation_DictionaryType_ValueHasValidators()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Dictionary<string, NeverValid>)
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value.NeverValidProperty=value0");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, NeverValid>>(modelBindingResult.Model);
Assert.Collection(
model.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("key0", kvp.Key);
Assert.Equal("value0", kvp.Value.NeverValidProperty);
});
Assert.False(modelState.IsValid);
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "parameter[0].Key").Value;
Assert.Equal("key0", entry.AttemptedValue);
Assert.Equal("key0", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value.NeverValidProperty").Value;
Assert.Equal("value0", entry.AttemptedValue);
Assert.Equal("value0", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "parameter[0].Value").Value;
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
Assert.Single(entry.Errors);
}
[Fact]
public async Task Validation_TopLevelProperty_NoValidation()
{
// Arrange
var modelType = typeof(Validation_TopLevelPropertyController);
var propertyInfo = modelType.GetProperty(nameof(Validation_TopLevelPropertyController.Model));
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, propertyInfo.PropertyType);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var parameter = new ParameterDescriptor()
{
Name = propertyInfo.Name,
ParameterType = propertyInfo.PropertyType,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?Model.Id=12");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Validation_TopLevelPropertyModel>(modelBindingResult.Model);
Assert.Equal(12, model.Id);
Assert.False(modelMetadata.HasValidators);
Assert.True(modelState.IsValid);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "Model.Id").Value;
Assert.Equal("12", entry.AttemptedValue);
Assert.Equal("12", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
public class Validation_TopLevelPropertyModel
{
public int Id { get; set; }
}
private class Validation_TopLevelPropertyController
{
public Validation_TopLevelPropertyModel Model { get; set; }
}
[Fact]
public async Task Validation_TopLevelProperty_ValidationOnProperty()
{
// Arrange
var modelType = typeof(Validation_TopLevelProperty_ValidationOnPropertyController);
var propertyInfo = modelType.GetProperty(nameof(Validation_TopLevelProperty_ValidationOnPropertyController.Model));
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, propertyInfo.PropertyType);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var parameter = new ParameterDescriptor()
{
Name = propertyInfo.Name,
ParameterType = propertyInfo.PropertyType,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?Model.Id=12");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Validation_TopLevelPropertyModel>(modelBindingResult.Model);
Assert.Equal(12, model.Id);
Assert.True(modelMetadata.HasValidators);
Assert.False(modelState.IsValid);
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "Model.Id").Value;
Assert.Equal("12", entry.AttemptedValue);
Assert.Equal("12", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
entry = Assert.Single(modelState, e => e.Key == "Model").Value;
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
}
public class Validation_TopLevelProperty_ValidationOnPropertyController
{
[CustomValidation(typeof(Validation_TopLevelProperty_ValidationOnPropertyController), nameof(Validate))]
public Validation_TopLevelPropertyModel Model { get; set; }
public static ValidationResult Validate(ValidationContext context)
{
return new ValidationResult("Invalid result");
}
}
[Fact]
public async Task Validation_InfinitelyRecursiveType_NoValidators()
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(RecursiveModel)
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?Property1=8");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<RecursiveModel>(modelBindingResult.Model);
Assert.Equal(8, model.Property1);
Assert.True(modelState.IsValid);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "Property1").Value;
Assert.Equal("8", entry.AttemptedValue);
Assert.Equal("8", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
public class RecursiveModel
{
public int Property1 { get; set; }
public RecursiveModel Property2 { get; set; }
public RecursiveModel Property3 => new RecursiveModel { Property1 = Property1 };
}
[Fact]
public async Task Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameter()
{
// Arrange
var parameterInfo = GetType().GetMethod(nameof(Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod), BindingFlags.NonPublic | BindingFlags.Static)
.GetParameters()
.First();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var parameter = new ParameterDescriptor()
{
Name = parameterInfo.Name,
ParameterType = parameterInfo.ParameterType,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.QueryString = new QueryString("?Property1=8");
});
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<RecursiveModel>(modelBindingResult.Model);
Assert.Equal(8, model.Property1);
Assert.True(modelState.IsValid);
Assert.Equal(ModelValidationState.Valid, modelState.ValidationState);
var entry = Assert.Single(modelState, e => e.Key == "Property1").Value;
Assert.Equal("8", entry.AttemptedValue);
Assert.Equal("8", entry.RawValue);
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
}
private static void Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod([Required] RecursiveModel model) { }
private static void AssertRequiredError(string key, ModelError error)
{
Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage(key), error.ErrorMessage);

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
@ -251,7 +252,8 @@ namespace Microsoft.AspNetCore.Mvc
{
var excludeFilter = Assert.IsType<SuppressChildValidationMetadataProvider>(provider);
Assert.Equal(typeof(XmlNode).FullName, excludeFilter.FullTypeName);
});
},
provider => Assert.IsType<HasValidatorsValidationMetadataProvider>(provider));
}
private static T GetOptions<T>(Action<IServiceCollection> action = null)

View File

@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters.Json;
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
@ -381,14 +382,15 @@ namespace Microsoft.AspNetCore.Mvc
typeof(IPostConfigureOptions<MvcOptions>),
new[]
{
typeof(MvcOptions).Assembly.GetType("Microsoft.AspNetCore.Mvc.Infrastructure.MvcOptionsConfigureCompatibilityOptions", throwOnError: true),
typeof(MvcOptionsConfigureCompatibilityOptions),
typeof(MvcCoreMvcOptionsSetup),
}
},
{
typeof(IPostConfigureOptions<RazorPagesOptions>),
new[]
{
typeof(RazorPagesOptions).Assembly.GetType("Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptionsConfigureCompatibilityOptions", throwOnError: true),
typeof(RazorPagesOptionsConfigureCompatibilityOptions),
}
},
{

View File

@ -0,0 +1,16 @@
// 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 FormatterWebSite.Models;
using Microsoft.AspNetCore.Mvc;
namespace FormatterWebSite.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class TestApiController : ControllerBase
{
[HttpPost]
public IActionResult PostBookWithNoValidation(BookModelWithNoValidation bookModel) => Ok();
}
}

View File

@ -0,0 +1,20 @@
// 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.Runtime.Serialization;
using Newtonsoft.Json;
namespace FormatterWebSite.Models
{
public class BookModelWithNoValidation
{
public Guid Id { get; set; }
public string Title { get; set; }
[JsonRequired]
[DataMember(IsRequired = true)]
public string ISBN { get; set; }
}
}

View File

@ -1,10 +1,14 @@
// 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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace FormatterWebSite
{
// A System.Security.Principal.SecurityIdentifier like type that works on xplat
public class RecursiveIdentifier
public class RecursiveIdentifier : IValidatableObject
{
public RecursiveIdentifier(string identifier)
{
@ -14,5 +18,10 @@ namespace FormatterWebSite
public string Value { get; }
public RecursiveIdentifier AccountIdentifier => new RecursiveIdentifier(Value);
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return Enumerable.Empty<ValidationResult>();
}
}
}